Development
This commit is contained in:
parent
59d3d65d18
commit
ac0aa4de22
4 changed files with 98 additions and 113 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
from flask import Blueprint, request, redirect, flash, session
|
from flask import Blueprint, request, redirect, flash, session
|
||||||
from auth import require_level
|
from auth import require_level
|
||||||
from config_utils import (flush_pending_to_queue, flush_selected_to_queue,
|
from config_utils import (flush_pending_to_queue, get_dashboard_pending,
|
||||||
delete_pending_by_uuids, get_dashboard_pending,
|
revert_snapshot_to_core,
|
||||||
_is_locked, _format_timing, _seconds_until_next_run)
|
_is_locked, _format_timing, _seconds_until_next_run)
|
||||||
|
|
||||||
bp = Blueprint('action_actions', __name__)
|
bp = Blueprint('action_actions', __name__)
|
||||||
|
|
@ -36,13 +36,22 @@ def actions_cardpending_applynow():
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/action/actions_cardpending_revertselected', methods=['POST'])
|
@bp.route('/action/actions_cardhistory_revertselected', methods=['POST'])
|
||||||
@require_level('administrator')
|
@require_level('administrator')
|
||||||
def actions_cardpending_revertselected():
|
def actions_cardhistory_revertselected():
|
||||||
selected_uuids = request.form.getlist('selected_uuids')
|
selected_uuids = request.form.getlist('selected_uuids')
|
||||||
if not selected_uuids:
|
if not selected_uuids:
|
||||||
flash('No items selected.', 'info')
|
flash('No items selected.', 'info')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
delete_pending_by_uuids(selected_uuids)
|
succeeded, failed = 0, 0
|
||||||
flash('Selected changes reverted.', 'success')
|
for uuid in selected_uuids:
|
||||||
|
msg, ok = revert_snapshot_to_core(uuid)
|
||||||
|
if ok:
|
||||||
|
succeeded += 1
|
||||||
|
else:
|
||||||
|
flash(msg, 'error')
|
||||||
|
failed += 1
|
||||||
|
if succeeded:
|
||||||
|
plural = 's' if succeeded != 1 else ''
|
||||||
|
flash(f'{succeeded} change{plural} reverted.', 'success')
|
||||||
return redirect(_VIEW)
|
return redirect(_VIEW)
|
||||||
|
|
|
||||||
|
|
@ -181,35 +181,6 @@ def flush_pending_to_queue():
|
||||||
_trim_if_needed()
|
_trim_if_needed()
|
||||||
|
|
||||||
|
|
||||||
def _remove_pending_by_uuids(uuid_set):
|
|
||||||
try:
|
|
||||||
lines = open(DASHBOARD_PENDING).read().splitlines()
|
|
||||||
except Exception:
|
|
||||||
return
|
|
||||||
kept = [l for l in lines if l.strip() and l.split(None, 1)[0] not in uuid_set]
|
|
||||||
with open(DASHBOARD_PENDING, 'w') as f:
|
|
||||||
f.write('\n'.join(kept) + ('\n' if kept else ''))
|
|
||||||
|
|
||||||
|
|
||||||
def flush_selected_to_queue(selected_uuids):
|
|
||||||
if not selected_uuids:
|
|
||||||
return
|
|
||||||
selected_set = set(selected_uuids)
|
|
||||||
items = _read_dashboard_pending()
|
|
||||||
done_set = _load_done_set()
|
|
||||||
existing_ids = {uu for uu, *_ in _read_pending(done_set)}
|
|
||||||
with open(DASHBOARD_QUEUE, 'a') as f:
|
|
||||||
for entry_uuid, entry_ts, entry_cmd, entry_user in items:
|
|
||||||
if entry_uuid in selected_set and entry_uuid not in existing_ids:
|
|
||||||
f.write(f'{entry_uuid} {entry_ts} [{entry_cmd}] ({entry_user})\n')
|
|
||||||
_remove_pending_by_uuids(selected_set)
|
|
||||||
_trim_if_needed()
|
|
||||||
|
|
||||||
|
|
||||||
def delete_pending_by_uuids(selected_uuids):
|
|
||||||
if not selected_uuids:
|
|
||||||
return
|
|
||||||
_remove_pending_by_uuids(set(selected_uuids))
|
|
||||||
|
|
||||||
|
|
||||||
def _queue_pending_command(cmd):
|
def _queue_pending_command(cmd):
|
||||||
|
|
|
||||||
|
|
@ -442,7 +442,7 @@ def _blocklist_stats_html(cfg):
|
||||||
if not rows:
|
if not rows:
|
||||||
return ''
|
return ''
|
||||||
return (
|
return (
|
||||||
'<table class="data-table" style="margin-bottom:1rem">'
|
'<table class="data-table">'
|
||||||
'<thead><tr>'
|
'<thead><tr>'
|
||||||
'<th class="table-header">Blocklist</th>'
|
'<th class="table-header">Blocklist</th>'
|
||||||
'<th class="table-header">Entries</th>'
|
'<th class="table-header">Entries</th>'
|
||||||
|
|
@ -593,55 +593,46 @@ def collect_tokens():
|
||||||
|
|
||||||
pending_items = get_dashboard_pending()
|
pending_items = get_dashboard_pending()
|
||||||
if pending_items:
|
if pending_items:
|
||||||
|
# Group by command; each group = one row in the Pending Actions table.
|
||||||
|
from collections import defaultdict
|
||||||
|
groups = defaultdict(list)
|
||||||
|
for _uuid, _ts, cmd, user in pending_items:
|
||||||
|
groups[cmd].append((_uuid, user))
|
||||||
rows = ''
|
rows = ''
|
||||||
_tr_onclick = (
|
for cmd, entries in groups.items():
|
||||||
'onclick="if(event.target.type!==\'checkbox\')'
|
users = ', '.join(sorted({u for _, u in entries}))
|
||||||
'this.nextElementSibling.hidden=!this.nextElementSibling.hidden"'
|
required_by_parts = []
|
||||||
)
|
for _uuid, _ in entries:
|
||||||
for _uuid, ts, _cmd, user in pending_items:
|
|
||||||
snap = load_snapshot_for_uuid(_uuid)
|
snap = load_snapshot_for_uuid(_uuid)
|
||||||
dt_str = datetime.fromtimestamp(ts).strftime('%Y-%m-%d %H:%M')
|
required_by_parts.append(snap.get('description', _uuid[:8]) if snap else _uuid[:8])
|
||||||
snap_desc = e(snap['description']) if snap else ''
|
required_by = ', '.join(required_by_parts)
|
||||||
before_val = snap.get('before') if snap else None
|
rows += (f'<tr>'
|
||||||
after_val = snap.get('after') if snap else None
|
f'<td class="table-cell">{e(cmd)}</td>'
|
||||||
snap_id = e(_uuid[:8]) if snap else ''
|
f'<td class="table-cell">{e(users)}</td>'
|
||||||
rows += (f'<tr style="cursor:pointer" {_tr_onclick}>'
|
f'<td class="table-cell">{e(required_by)}</td>'
|
||||||
f'<td class="table-cell"><input type="checkbox" name="selected_uuids" value="{e(_uuid)}"/></td>'
|
f'</tr>')
|
||||||
f'<td class="table-cell">{e(dt_str)}</td>'
|
|
||||||
f'<td class="table-cell">{snap_desc}</td>'
|
|
||||||
f'<td class="table-cell">{_render_snap_val(before_val)}</td>'
|
|
||||||
f'<td class="table-cell">{_render_snap_val(after_val)}</td>'
|
|
||||||
f'<td class="table-cell">{snap_id}</td>'
|
|
||||||
f'<td class="table-cell">{e(user)}</td>'
|
|
||||||
f'</tr>'
|
|
||||||
f'{_snap_expand_row(before_val, after_val, 7)}')
|
|
||||||
select_all = (
|
|
||||||
'<input type="checkbox" '
|
|
||||||
'onchange="document.querySelectorAll(\'[name=selected_uuids]\').forEach(c=>c.checked=this.checked)"/>'
|
|
||||||
)
|
|
||||||
pending_html = (
|
pending_html = (
|
||||||
'<table class="data-table" style="margin-bottom:1rem">'
|
'<table class="data-table">'
|
||||||
'<thead><tr>'
|
'<thead><tr>'
|
||||||
f'<th class="table-header">{select_all}</th>'
|
'<th class="table-header">Command</th>'
|
||||||
'<th class="table-header">Time</th>'
|
|
||||||
'<th class="table-header">Description</th>'
|
|
||||||
'<th class="table-header">Before</th>'
|
|
||||||
'<th class="table-header">After</th>'
|
|
||||||
'<th class="table-header">Snapshot</th>'
|
|
||||||
'<th class="table-header">User</th>'
|
'<th class="table-header">User</th>'
|
||||||
|
'<th class="table-header">Required By</th>'
|
||||||
'</tr></thead>'
|
'</tr></thead>'
|
||||||
f'<tbody>{rows}</tbody>'
|
f'<tbody>{rows}</tbody>'
|
||||||
'</table>'
|
'</table>'
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
pending_html = '<p class="text-muted">No pending changes.</p>'
|
pending_html = '<p class="text-muted">No pending actions.</p>'
|
||||||
tokens['PENDING_CHANGES_HTML'] = pending_html
|
tokens['PENDING_ACTIONS_HTML'] = pending_html
|
||||||
tokens['NO_PENDING'] = 'true' if not pending_items else ''
|
tokens['NO_PENDING'] = 'true' if not pending_items else ''
|
||||||
|
|
||||||
done_items = get_dashboard_done()
|
done_items = get_dashboard_done()
|
||||||
if done_items:
|
if done_items:
|
||||||
hist_rows = ''
|
hist_rows = ''
|
||||||
_hist_onclick = 'onclick="this.nextElementSibling.hidden=!this.nextElementSibling.hidden"'
|
_hist_onclick = (
|
||||||
|
'onclick="if(event.target.type!==\'checkbox\')'
|
||||||
|
'this.nextElementSibling.hidden=!this.nextElementSibling.hidden"'
|
||||||
|
)
|
||||||
for _uuid, applied_ts in done_items:
|
for _uuid, applied_ts in done_items:
|
||||||
snap = load_snapshot_for_uuid(_uuid)
|
snap = load_snapshot_for_uuid(_uuid)
|
||||||
if applied_ts:
|
if applied_ts:
|
||||||
|
|
@ -653,7 +644,8 @@ def collect_tokens():
|
||||||
after_val = snap.get('after') if snap else None
|
after_val = snap.get('after') if snap else None
|
||||||
snap_id = e(_uuid[:8]) if snap else ''
|
snap_id = e(_uuid[:8]) if snap else ''
|
||||||
snap_user = e(snap['user']) if snap else ''
|
snap_user = e(snap['user']) if snap else ''
|
||||||
hist_rows += (f'<tr style="cursor:pointer" {_hist_onclick}>'
|
hist_rows += (f'<tr class="row-expandable" {_hist_onclick}>'
|
||||||
|
f'<td class="table-cell"><input type="checkbox" name="selected_uuids" value="{e(_uuid)}"/></td>'
|
||||||
f'<td class="table-cell">{e(dt_str)}</td>'
|
f'<td class="table-cell">{e(dt_str)}</td>'
|
||||||
f'<td class="table-cell">{snap_desc}</td>'
|
f'<td class="table-cell">{snap_desc}</td>'
|
||||||
f'<td class="table-cell">{_render_snap_val(before_val)}</td>'
|
f'<td class="table-cell">{_render_snap_val(before_val)}</td>'
|
||||||
|
|
@ -661,10 +653,15 @@ def collect_tokens():
|
||||||
f'<td class="table-cell">{snap_id}</td>'
|
f'<td class="table-cell">{snap_id}</td>'
|
||||||
f'<td class="table-cell">{snap_user}</td>'
|
f'<td class="table-cell">{snap_user}</td>'
|
||||||
f'</tr>'
|
f'</tr>'
|
||||||
f'{_snap_expand_row(before_val, after_val, 6)}')
|
f'{_snap_expand_row(before_val, after_val, 7)}')
|
||||||
|
select_all = (
|
||||||
|
'<input type="checkbox" '
|
||||||
|
'onchange="document.querySelectorAll(\'[name=selected_uuids]\').forEach(c=>c.checked=this.checked)"/>'
|
||||||
|
)
|
||||||
history_html = (
|
history_html = (
|
||||||
'<table class="data-table" style="margin-bottom:1rem">'
|
'<table class="data-table">'
|
||||||
'<thead><tr>'
|
'<thead><tr>'
|
||||||
|
f'<th class="table-header">{select_all}</th>'
|
||||||
'<th class="table-header">Applied</th>'
|
'<th class="table-header">Applied</th>'
|
||||||
'<th class="table-header">Description</th>'
|
'<th class="table-header">Description</th>'
|
||||||
'<th class="table-header">Before</th>'
|
'<th class="table-header">Before</th>'
|
||||||
|
|
@ -678,6 +675,7 @@ def collect_tokens():
|
||||||
else:
|
else:
|
||||||
history_html = '<p class="text-muted">No change history.</p>'
|
history_html = '<p class="text-muted">No change history.</p>'
|
||||||
tokens['CHANGE_HISTORY_HTML'] = history_html
|
tokens['CHANGE_HISTORY_HTML'] = history_html
|
||||||
|
tokens['NO_HISTORY'] = 'true' if not done_items else ''
|
||||||
|
|
||||||
servers = dns.get('upstream_servers', [])
|
servers = dns.get('upstream_servers', [])
|
||||||
tokens['DNS_STRICT_ORDER'] = 'true' if dns.get('strict_order') else 'false'
|
tokens['DNS_STRICT_ORDER'] = 'true' if dns.get('strict_order') else 'false'
|
||||||
|
|
@ -843,19 +841,16 @@ def _render_snap_val(val):
|
||||||
|
|
||||||
def _snap_expand_row(before_val, after_val, colspan):
|
def _snap_expand_row(before_val, after_val, colspan):
|
||||||
"""Return a hidden <tr> that expands with full before/after content."""
|
"""Return a hidden <tr> that expands with full before/after content."""
|
||||||
pre = ('max-height:200px;overflow-y:auto;white-space:pre-wrap;'
|
|
||||||
'font-size:0.85em;background:#fff;border:1px solid #ddd;'
|
|
||||||
'padding:0.5rem;margin:0.25rem 0')
|
|
||||||
def box(label, val):
|
def box(label, val):
|
||||||
text = _snap_text(val) if val is not None else ''
|
text = _snap_text(val) if val is not None else ''
|
||||||
if isinstance(val, (dict, list)):
|
if isinstance(val, (dict, list)):
|
||||||
text = json.dumps(val, indent=2)
|
text = json.dumps(val, indent=2)
|
||||||
body = e(text) if text else '<em>(none)</em>'
|
body = e(text) if text else '<em class="snap-expand-none">(none)</em>'
|
||||||
return f'<div style="flex:1;min-width:0"><strong>{label}</strong><pre style="{pre}">{body}</pre></div>'
|
return (f'<div class="snap-expand-col">'
|
||||||
inner = f'<div style="display:flex;gap:1rem">{box("Before", before_val)}{box("After", after_val)}</div>'
|
f'<span class="snap-expand-label">{label}</span>'
|
||||||
return (f'<tr hidden>'
|
f'<pre class="snap-expand-pre">{body}</pre></div>')
|
||||||
f'<td colspan="{colspan}" style="padding:0.5rem 1rem;background:#f8f8f8">'
|
inner = f'<div class="snap-expand-cols">{box("Before", before_val)}{box("After", after_val)}</div>'
|
||||||
f'{inner}</td></tr>')
|
return f'<tr hidden><td colspan="{colspan}" class="snap-expand-cell">{inner}</td></tr>'
|
||||||
|
|
||||||
|
|
||||||
def apply_tokens(text, tokens):
|
def apply_tokens(text, tokens):
|
||||||
|
|
@ -944,7 +939,7 @@ def _render_item(item, tokens, inherited_req=None):
|
||||||
formaction = e(apply_tokens(formaction, tokens))
|
formaction = e(apply_tokens(formaction, tokens))
|
||||||
return f'<button type="submit" class="btn {e(cls)}" formaction="{formaction}"{disabled}>{text}</button>'
|
return f'<button type="submit" class="btn {e(cls)}" formaction="{formaction}"{disabled}>{text}</button>'
|
||||||
if item.get('method', '').lower() == 'post':
|
if item.get('method', '').lower() == 'post':
|
||||||
return (f'<form method="post" action="{action}" style="display:inline">'
|
return (f'<form method="post" action="{action}" class="form-inline">'
|
||||||
f'<button type="submit" class="btn {e(cls)}"{disabled}>{text}</button></form>')
|
f'<button type="submit" class="btn {e(cls)}"{disabled}>{text}</button></form>')
|
||||||
return f'<a href="{action}" class="btn {e(cls)}">{text}</a>'
|
return f'<a href="{action}" class="btn {e(cls)}">{text}</a>'
|
||||||
|
|
||||||
|
|
@ -985,7 +980,7 @@ def _render_item(item, tokens, inherited_req=None):
|
||||||
return (
|
return (
|
||||||
f'<div class="{cls}">'
|
f'<div class="{cls}">'
|
||||||
f'<div class="stat-card-label">{label}</div>'
|
f'<div class="stat-card-label">{label}</div>'
|
||||||
f'<div style="display:flex;align-items:center;gap:0.5em">'
|
f'<div class="stat-card-value-row">'
|
||||||
f'<span class="stat-card-value">{value}</span>'
|
f'<span class="stat-card-value">{value}</span>'
|
||||||
f'<button type="button" class="btn btn-ghost btn-sm"'
|
f'<button type="button" class="btn btn-ghost btn-sm"'
|
||||||
f' data-reveal-card="{e(reveal_card_id)}">Edit</button>'
|
f' data-reveal-card="{e(reveal_card_id)}">Edit</button>'
|
||||||
|
|
@ -996,9 +991,9 @@ def _render_item(item, tokens, inherited_req=None):
|
||||||
if edit_action and edit_field:
|
if edit_action and edit_field:
|
||||||
min_attr = f' min="{e(edit_min)}"' if edit_min else ''
|
min_attr = f' min="{e(edit_min)}"' if edit_min else ''
|
||||||
suffix_html = f'<span>{e(edit_suffix)}</span>' if edit_suffix else ''
|
suffix_html = f'<span>{e(edit_suffix)}</span>' if edit_suffix else ''
|
||||||
input_wrap = (f'<div style="display:flex;align-items:center;gap:0.5em">'
|
input_wrap = (f'<div class="stat-card-value-row">'
|
||||||
f'<input type="{e(edit_input_type)}" name="{e(edit_field)}" value="{e(edit_raw)}"'
|
f'<input type="{e(edit_input_type)}" name="{e(edit_field)}" value="{e(edit_raw)}"'
|
||||||
f' data-original="{e(edit_raw)}" class="form-input"{min_attr} style="width:5rem"/>'
|
f' data-original="{e(edit_raw)}" class="form-input stat-card-edit-input"{min_attr}/>'
|
||||||
f'{suffix_html}</div>')
|
f'{suffix_html}</div>')
|
||||||
return (
|
return (
|
||||||
f'<div class="{cls} stat-card-editable">'
|
f'<div class="{cls} stat-card-editable">'
|
||||||
|
|
@ -1007,10 +1002,10 @@ def _render_item(item, tokens, inherited_req=None):
|
||||||
f'<span class="stat-card-value">{value}</span>'
|
f'<span class="stat-card-value">{value}</span>'
|
||||||
f'<button type="button" class="btn btn-ghost btn-sm stat-card-edit-btn">Edit</button>'
|
f'<button type="button" class="btn btn-ghost btn-sm stat-card-edit-btn">Edit</button>'
|
||||||
f'</div>'
|
f'</div>'
|
||||||
f'<form class="stat-card-edit-form" style="display:none" action="{e(edit_action)}" method="post">'
|
f'<form class="stat-card-edit-form hidden" action="{e(edit_action)}" method="post">'
|
||||||
f'<input type="hidden" name="config_hash" value="{e(config_hash())}"/>'
|
f'<input type="hidden" name="config_hash" value="{e(config_hash())}"/>'
|
||||||
f'{input_wrap}'
|
f'{input_wrap}'
|
||||||
f'<div style="margin-top:0.5em;display:flex;gap:0.5em">'
|
f'<div class="stat-card-edit-actions">'
|
||||||
f'<button type="submit" class="btn btn-primary btn-sm" disabled>Save</button>'
|
f'<button type="submit" class="btn btn-primary btn-sm" disabled>Save</button>'
|
||||||
f'<button type="button" class="btn btn-secondary btn-sm stat-card-cancel-btn">Cancel</button>'
|
f'<button type="button" class="btn btn-secondary btn-sm stat-card-cancel-btn">Cancel</button>'
|
||||||
f'</div>'
|
f'</div>'
|
||||||
|
|
@ -1027,10 +1022,10 @@ def _render_item(item, tokens, inherited_req=None):
|
||||||
if t == 'card':
|
if t == 'card':
|
||||||
label = item.get('label', '')
|
label = item.get('label', '')
|
||||||
id_attr = f' id="{e(item["id"])}"' if item.get('id') else ''
|
id_attr = f' id="{e(item["id"])}"' if item.get('id') else ''
|
||||||
style = ' style="display:none"' if item.get('hidden') else ''
|
cls_hidden = ' hidden' if item.get('hidden') else ''
|
||||||
header = f'<div class="card-header"><h2 class="card-title">{e(label)}</h2></div>' if label else ''
|
header = f'<div class="card-header"><h2 class="card-title">{e(label)}</h2></div>' if label else ''
|
||||||
body = render_items(item.get('items', []), tokens, req)
|
body = render_items(item.get('items', []), tokens, req)
|
||||||
return f'<div class="card"{id_attr}{style}>{header}<div class="card-body">{body}</div></div>'
|
return f'<div class="card{cls_hidden}"{id_attr}>{header}<div class="card-body">{body}</div></div>'
|
||||||
|
|
||||||
if t == 'field_status':
|
if t == 'field_status':
|
||||||
label = e(item.get('label', ''))
|
label = e(item.get('label', ''))
|
||||||
|
|
@ -1060,11 +1055,11 @@ def _render_item(item, tokens, inherited_req=None):
|
||||||
psel = e(item.get('provider_select', 'provider'))
|
psel = e(item.get('provider_select', 'provider'))
|
||||||
return (
|
return (
|
||||||
f'<div class="credential-fields" data-provider-select="{psel}">'
|
f'<div class="credential-fields" data-provider-select="{psel}">'
|
||||||
f'<div class="cred-group-token" style="display:none">'
|
f'<div class="cred-group-token hidden">'
|
||||||
f'<div class="form-group"><label class="form-label">API Token</label>'
|
f'<div class="form-group"><label class="form-label">API Token</label>'
|
||||||
f'<input type="text" name="api_token" class="form-input"/></div>'
|
f'<input type="text" name="api_token" class="form-input"/></div>'
|
||||||
f'</div>'
|
f'</div>'
|
||||||
f'<div class="cred-group-noip" style="display:none">'
|
f'<div class="cred-group-noip hidden">'
|
||||||
f'<div class="form-group"><label class="form-label">Username</label>'
|
f'<div class="form-group"><label class="form-label">Username</label>'
|
||||||
f'<input type="text" name="username" class="form-input"/></div>'
|
f'<input type="text" name="username" class="form-input"/></div>'
|
||||||
f'<div class="form-group"><label class="form-label">Password</label>'
|
f'<div class="form-group"><label class="form-label">Password</label>'
|
||||||
|
|
@ -1131,7 +1126,7 @@ def _render_item(item, tokens, inherited_req=None):
|
||||||
f'<input type="number" name="{prefix_name}" value="{pf}" min="1" max="30" class="form-input subnet-prefix-input"/>'
|
f'<input type="number" name="{prefix_name}" value="{pf}" min="1" max="30" class="form-input subnet-prefix-input"/>'
|
||||||
f'<span class="subnet-dotted">{e(dotted)}</span>'
|
f'<span class="subnet-dotted">{e(dotted)}</span>'
|
||||||
f'</div>'
|
f'</div>'
|
||||||
f'<p class="form-hint field-dyn-hint" style="display:none"></p>'
|
f'<p class="form-hint field-dyn-hint hidden"></p>'
|
||||||
f'</div>'
|
f'</div>'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1228,7 +1223,7 @@ def _render_field(item, tokens):
|
||||||
if input_type == 'number':
|
if input_type == 'number':
|
||||||
min_attr = f' min="{item["min"]}"' if 'min' in item else ''
|
min_attr = f' min="{item["min"]}"' if 'min' in item else ''
|
||||||
max_attr = f' max="{item["max"]}"' if 'max' in item else ''
|
max_attr = f' max="{item["max"]}"' if 'max' in item else ''
|
||||||
dyn_hint_html = '<p class="form-hint field-dyn-hint" style="display:none"></p>'
|
dyn_hint_html = '<p class="form-hint field-dyn-hint hidden"></p>'
|
||||||
inp = (f'<input type="number" name="{name}" value="{e(value)}"{min_attr}{max_attr}'
|
inp = (f'<input type="number" name="{name}" value="{e(value)}"{min_attr}{max_attr}'
|
||||||
f' class="form-input{extra_cls}"{readonly}'
|
f' class="form-input{extra_cls}"{readonly}'
|
||||||
f' data-validate="positive_int" />')
|
f' data-validate="positive_int" />')
|
||||||
|
|
@ -1349,7 +1344,7 @@ def _render_field(item, tokens):
|
||||||
|
|
||||||
validate = item.get('validate', '')
|
validate = item.get('validate', '')
|
||||||
validate_attr = f' data-validate="{e(validate)}"' if validate else ''
|
validate_attr = f' data-validate="{e(validate)}"' if validate else ''
|
||||||
dyn_hint = '<p class="form-hint field-dyn-hint" style="display:none"></p>' if (item.get('readonly') or item.get('dyn_hint') or validate) else ''
|
dyn_hint = '<p class="form-hint field-dyn-hint hidden"></p>' if (item.get('readonly') or item.get('dyn_hint') or validate) else ''
|
||||||
return (f'<div class="form-group"><label class="form-label">{label}</label>'
|
return (f'<div class="form-group"><label class="form-label">{label}</label>'
|
||||||
f'<input type="{e(input_type)}" name="{name}" value="{e(value)}"'
|
f'<input type="{e(input_type)}" name="{name}" value="{e(value)}"'
|
||||||
f' placeholder="{placeholder}" class="form-input{extra_cls}"{readonly}{validate_attr}/>{hint_html}{dyn_hint}</div>')
|
f' placeholder="{placeholder}" class="form-input{extra_cls}"{readonly}{validate_attr}/>{hint_html}{dyn_hint}</div>')
|
||||||
|
|
@ -1484,7 +1479,7 @@ def _render_table(item, tokens, inherited_req=None):
|
||||||
if disable_if and row.get(disable_if.get('field')) == disable_if.get('value'):
|
if disable_if and row.get(disable_if.get('field')) == disable_if.get('value'):
|
||||||
btns += f'<button type="button" class="btn {cls}" disabled>{text}</button>'
|
btns += f'<button type="button" class="btn {cls}" disabled>{text}</button>'
|
||||||
continue
|
continue
|
||||||
btns += (f'<form method="post" action="{action}" style="display:inline">'
|
btns += (f'<form method="post" action="{action}" class="form-inline">'
|
||||||
f'<input type="hidden" name="row_index" value="{idx}"/>'
|
f'<input type="hidden" name="row_index" value="{idx}"/>'
|
||||||
f'<input type="hidden" name="config_hash" value="{e(hash_val)}"/>'
|
f'<input type="hidden" name="config_hash" value="{e(hash_val)}"/>'
|
||||||
f'<button type="submit" class="btn {cls}">{text}</button></form>')
|
f'<button type="submit" class="btn {cls}">{text}</button></form>')
|
||||||
|
|
@ -1545,7 +1540,7 @@ def _render_table_cell(value, render_fn, col_class='', field='', row_idx=None,
|
||||||
else:
|
else:
|
||||||
label = 'Disabled'; badge_cls = 'badge-disabled'
|
label = 'Disabled'; badge_cls = 'badge-disabled'
|
||||||
if toggle_action and row_idx is not None and toggle_allowed:
|
if toggle_action and row_idx is not None and toggle_allowed:
|
||||||
inner = (f'<form method="post" action="{e(toggle_action)}" style="display:inline">'
|
inner = (f'<form method="post" action="{e(toggle_action)}" class="form-inline">'
|
||||||
f'<input type="hidden" name="row_index" value="{row_idx}"/>'
|
f'<input type="hidden" name="row_index" value="{row_idx}"/>'
|
||||||
f'<button type="submit" class="btn-badge">'
|
f'<button type="submit" class="btn-badge">'
|
||||||
f'<span class="badge {badge_cls}">{label}</span></button></form>')
|
f'<span class="badge {badge_cls}">{label}</span></button></form>')
|
||||||
|
|
@ -1749,7 +1744,7 @@ def _render_nav_item(item, active_view, level, in_dropdown=False, inherited_req=
|
||||||
is_active = ' active' if map_to and map_to == active_view else ''
|
is_active = ' active' if map_to and map_to == active_view else ''
|
||||||
cls = f'dropdown-item{is_active}' if in_dropdown else f'nav-item{is_active}'
|
cls = f'dropdown-item{is_active}' if in_dropdown else f'nav-item{is_active}'
|
||||||
if action:
|
if action:
|
||||||
return (f'<form method="post" action="/action/{e(action)}" style="display:inline">'
|
return (f'<form method="post" action="/action/{e(action)}" class="form-inline">'
|
||||||
f'<button type="submit" class="{cls}">{label}</button></form>')
|
f'<button type="submit" class="{cls}">{label}</button></form>')
|
||||||
if map_to:
|
if map_to:
|
||||||
return f'<a href="/view/{e(map_to)}" class="{cls}">{label}</a>'
|
return f'<a href="/view/{e(map_to)}" class="{cls}">{label}</a>'
|
||||||
|
|
@ -2222,7 +2217,7 @@ document.addEventListener('click', function(e) {
|
||||||
var maxAttr = fDef.max !== undefined ? ' max="' + esc(String(fDef.max)) + '"' : '';
|
var maxAttr = fDef.max !== undefined ? ' max="' + esc(String(fDef.max)) + '"' : '';
|
||||||
td.innerHTML = '<input type="number" name="' + field + '" value="' + esc(String(val)) +
|
td.innerHTML = '<input type="number" name="' + field + '" value="' + esc(String(val)) +
|
||||||
'"' + minAttr + maxAttr + ' class="form-input inline-edit-input" data-validate="positive_int"/>' +
|
'"' + minAttr + maxAttr + ' class="form-input inline-edit-input" data-validate="positive_int"/>' +
|
||||||
'<p class="form-hint field-dyn-hint" style="display:none"></p>';
|
'<p class="form-hint field-dyn-hint hidden"></p>';
|
||||||
if (typeof validateEl === 'function') validateEl(td.querySelector('input'));
|
if (typeof validateEl === 'function') validateEl(td.querySelector('input'));
|
||||||
} else if (inputType === 'textarea') {
|
} else if (inputType === 'textarea') {
|
||||||
var textVal;
|
var textVal;
|
||||||
|
|
@ -2234,7 +2229,7 @@ document.addEventListener('click', function(e) {
|
||||||
td.innerHTML = buildCredentialsHtml(rowData.provider || 'noip', rowData);
|
td.innerHTML = buildCredentialsHtml(rowData.provider || 'noip', rowData);
|
||||||
} else {
|
} else {
|
||||||
var validateAttr = fDef.validate ? ' data-validate="' + esc(fDef.validate) + '"' : '';
|
var validateAttr = fDef.validate ? ' data-validate="' + esc(fDef.validate) + '"' : '';
|
||||||
var hintHtml = fDef.validate ? '<p class="form-hint field-dyn-hint" style="display:none"></p>' : '';
|
var hintHtml = fDef.validate ? '<p class="form-hint field-dyn-hint hidden"></p>' : '';
|
||||||
td.innerHTML = '<input type="' + inputType + '" name="' + field +
|
td.innerHTML = '<input type="' + inputType + '" name="' + field +
|
||||||
'" value="' + esc(String(val)) + '" class="form-input inline-edit-input"' + validateAttr + '/>' + hintHtml;
|
'" value="' + esc(String(val)) + '" class="form-input inline-edit-input"' + validateAttr + '/>' + hintHtml;
|
||||||
if (fDef.validate && typeof validateEl === 'function') validateEl(td.querySelector('input'));
|
if (fDef.validate && typeof validateEl === 'function') validateEl(td.querySelector('input'));
|
||||||
|
|
|
||||||
|
|
@ -603,7 +603,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "card",
|
"type": "card",
|
||||||
"label": "Pending Changes",
|
"label": "Pending Actions",
|
||||||
"client_requirement": "client_is_administrator+",
|
"client_requirement": "client_is_administrator+",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
|
|
@ -613,22 +613,15 @@
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "raw_html",
|
"type": "raw_html",
|
||||||
"html": "%PENDING_CHANGES_HTML%"
|
"html": "%PENDING_ACTIONS_HTML%"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "button_row",
|
"type": "button_row",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "button_primary",
|
"type": "button_primary",
|
||||||
"formaction": "/action/actions_cardpending_applynow",
|
|
||||||
"text": "Apply Now",
|
"text": "Apply Now",
|
||||||
"disabled": "%NO_PENDING%"
|
"disabled": "%NO_PENDING%"
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "button_secondary",
|
|
||||||
"formaction": "/action/actions_cardpending_revertselected",
|
|
||||||
"text": "Revert Selected",
|
|
||||||
"disabled": "%NO_PENDING%"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -648,7 +641,7 @@
|
||||||
"name": "apply_changes_immediately",
|
"name": "apply_changes_immediately",
|
||||||
"input_type": "checkbox",
|
"input_type": "checkbox",
|
||||||
"value": "%GENERAL_APPLY_ON_SAVE%",
|
"value": "%GENERAL_APPLY_ON_SAVE%",
|
||||||
"hint": "When enabled, saved changes are queued immediately. When disabled, changes accumulate in Pending Changes until you click Apply Now."
|
"hint": "When enabled, saved changes are queued immediately. When disabled, changes accumulate in Pending Actions until you click Apply Now."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "button_row",
|
"type": "button_row",
|
||||||
|
|
@ -673,10 +666,27 @@
|
||||||
"type": "card",
|
"type": "card",
|
||||||
"label": "Change History",
|
"label": "Change History",
|
||||||
"client_requirement": "client_is_administrator+",
|
"client_requirement": "client_is_administrator+",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "form",
|
||||||
|
"action": "/action/actions_cardhistory_revertselected",
|
||||||
|
"method": "post",
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "raw_html",
|
"type": "raw_html",
|
||||||
"html": "%CHANGE_HISTORY_HTML%"
|
"html": "%CHANGE_HISTORY_HTML%"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "button_row",
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"type": "button_danger",
|
||||||
|
"text": "Revert Selected",
|
||||||
|
"disabled": "%NO_HISTORY%"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue