Development

This commit is contained in:
Matthew Grotke 2026-06-03 00:10:52 -04:00
parent 91b11c618b
commit 4a9110cc4c
3 changed files with 12 additions and 142 deletions

View file

@ -24,7 +24,6 @@ COMMON_JS_FILE = os.path.join(DATA_DIR, 'common.js')
LEVEL_RANK = {'nothing': 0, 'viewer': 1, 'administrator': 2, 'manager': 3} LEVEL_RANK = {'nothing': 0, 'viewer': 1, 'administrator': 2, 'manager': 3}
STANDARD_INPUT_TYPES = {'text', 'password', 'number', 'checkbox', 'select', 'textarea'}
VALIDATION_FLAGS = { VALIDATION_FLAGS = {
'VALIDATION_IPV4_FORMAT': 1 << 0, 'VALIDATION_IPV4_FORMAT': 1 << 0,
@ -114,30 +113,6 @@ def apply_tokens(text, tokens):
return re.sub(r'%([A-Z_]+)%', lambda m: str(tokens.get(m.group(1), m.group(0))), text) return re.sub(r'%([A-Z_]+)%', lambda m: str(tokens.get(m.group(1), m.group(0))), text)
def expand_fields(obj, tokens):
"""Recursively apply token substitution to a field-definition object.
String values that resolve to a JSON array or object are parsed back into
Python structures so they serialize correctly into data-fields JSON."""
if isinstance(obj, list):
return [expand_fields(item, tokens) for item in obj]
if isinstance(obj, dict):
out = {}
for k, v in obj.items():
if isinstance(v, str):
s = apply_tokens(v, tokens)
if s != v and s[:1] in ('[', '{'):
try:
out[k] = json.loads(s)
continue
except Exception:
pass
out[k] = s
else:
out[k] = expand_fields(v, tokens)
return out
return obj
def js_str(value): def js_str(value):
return json.dumps(str(value)) return json.dumps(str(value))
@ -156,18 +131,6 @@ def parse_validation(s):
return result return result
def _encode_field_validations(fields):
out = []
for f in fields:
f2 = dict(f)
raw = f2.get('validate', '')
if not raw and f2.get('input_type') == 'number':
raw = 'VALIDATION_RANGE_INT'
if raw and isinstance(raw, str):
f2['validate'] = parse_validation(raw)
out.append(f2)
return out
def build_big_validate(): def build_big_validate():
body = r""" body = r"""
@ -248,12 +211,6 @@ return _ok();"""
return f'function bigValidate(value,validation,collisions,dedup,arg1,arg2){{{body}\n}}' return f'function bigValidate(value,validation,collisions,dedup,arg1,arg2){{{body}\n}}'
def get_worker_id(datasource):
for prefix in ('config:', 'live:'):
if datasource.startswith(prefix):
return datasource[len(prefix):]
return ''
def table_token_key(spec): def table_token_key(spec):
return 'TABLE_' + re.sub(r'[^A-Z0-9]', '_', spec.upper()) return 'TABLE_' + re.sub(r'[^A-Z0-9]', '_', spec.upper())
@ -920,58 +877,6 @@ def build_editable_list(item, tokens):
f'</div>{hint_html}</div>' f'</div>{hint_html}</div>'
) )
# Table worker script =================================================
def build_table_worker_script(item, expanded_ra_fields):
"""Emit a <script> registering a table worker for any non-standard inline_edit field types.
Returns empty string when all fields are standard types."""
if not expanded_ra_fields:
return ''
worker_id = get_worker_id(item.get('datasource', ''))
if not worker_id:
return ''
nonstandard = set()
for fields in expanded_ra_fields.values():
for f in fields:
it = f.get('input_type', 'text')
if it not in STANDARD_INPUT_TYPES:
nonstandard.add(it)
if not nonstandard:
return ''
if nonstandard == {'credentials'}:
return (
f'<script>document.addEventListener("DOMContentLoaded",function(){{registerTableWorker({js_str(worker_id)},(function(){{\n'
' function _buildCreds(provider, data) {\n'
' if (provider === \'noip\') {\n'
' return \'<div class="cred-field"><span class="cred-label">U:</span>\' +\n'
' \'<input type="text" name="username" value="\' + htmlEsc(data.username||\'\')'
' + \'" class="form-input inline-edit-input"/></div>\' +\n'
' \'<div class="cred-field"><span class="cred-label">P:</span>\' +\n'
' \'<input type="password" name="password" value="\' + htmlEsc(data.password||\'\')'
' + \'" class="form-input inline-edit-input"/></div>\';\n'
' }\n'
' return \'<input type="text" name="api_token" value="\' + htmlEsc(data.api_token||\'\')'
' + \'" class="form-input inline-edit-input" placeholder="API Token"/>\';\n'
' }\n'
' return {\n'
' renderCell: function(fDef, td, val, row) {\n'
' if (fDef.input_type !== \'credentials\') return false;\n'
' td.innerHTML = _buildCreds(row.provider || \'noip\', row);\n'
' return true;\n'
' },\n'
' afterRowOpen: function(tr, row) {\n'
' var provSel = tr.querySelector(\'td[data-field="provider"] select\');\n'
' var credTd = tr.querySelector(\'td[data-field="credentials"]\');\n'
' if (!provSel || !credTd) return;\n'
' provSel.addEventListener(\'change\', function() {\n'
' credTd.innerHTML = _buildCreds(this.value, row);\n'
' });\n'
' }\n'
' };\n'
'}()));});</script>\n'
)
return f'<script>document.addEventListener("DOMContentLoaded",function(){{registerTableWorker({js_str(worker_id)},{{}});}});</script>\n'
# Table cell renderer ================================================= # Table cell renderer =================================================
def build_table_cell(value, render_fn, col_class='', field='', row_idx=None, def build_table_cell(value, render_fn, col_class='', field='', row_idx=None,
@ -1102,12 +1007,6 @@ def build_table(item, tokens, rows, inherited_req=None):
if row_actions: if row_actions:
thead += '<th></th>' thead += '<th></th>'
expanded_ra_fields = {
i: _encode_field_validations(expand_fields(ra.get('fields', []), tokens))
for i, ra in enumerate(row_actions)
if ra.get('method', 'post').lower() == 'inline_edit'
}
if not rows: if not rows:
colspan = len(columns) + (1 if row_actions else 0) colspan = len(columns) + (1 if row_actions else 0)
tbody = f'<tr><td colspan="{colspan}" class="table-empty">{empty}</td></tr>' tbody = f'<tr><td colspan="{colspan}" class="table-empty">{empty}</td></tr>'
@ -1157,39 +1056,21 @@ def build_table(item, tokens, rows, inherited_req=None):
row_json = e(json.dumps(row)) row_json = e(json.dumps(row))
btns += ( btns += (
f'<button type="button" class="btn {cls} row-edit-btn"' f'<button type="button" class="btn {cls} row-edit-btn"'
f' data-edit-mode="reveal"'
f' data-row-index="{idx}" data-row="{row_json}"' f' data-row-index="{idx}" data-row="{row_json}"'
f' data-target="{target}">{text}</button>' f' data-target="{target}">{text}</button>'
) )
elif method == 'inline_edit':
expanded = expanded_ra_fields.get(ra_i, [])
fields_json = e(json.dumps(expanded))
row_json = e(json.dumps(row))
worker_id = get_worker_id(item.get('datasource', ''))
has_nonstandard = any(
f.get('input_type', 'text') not in STANDARD_INPUT_TYPES
for f in expanded
)
worker_attr = f' data-worker-id="{e(worker_id)}"' if has_nonstandard and worker_id else ''
btns += (
f'<button type="button" class="btn {cls} row-edit-btn"'
f' data-edit-mode="inline"'
f' data-row-index="{idx}" data-row="{row_json}"'
f' data-action="{action}" data-fields="{fields_json}"{worker_attr}>{text}</button>'
)
else: else:
btns += f'<a href="{action}?row_index={idx}" class="btn {cls}">{text}</a>' btns += f'<a href="{action}?row_index={idx}" class="btn {cls}">{text}</a>'
cells += f'<td class="col-actions">{btns}</td>' cells += f'<td class="col-actions">{btns}</td>'
tbody += f'<tr>{cells}</tr>' tbody += f'<tr>{cells}</tr>'
worker_script = build_table_worker_script(item, expanded_ra_fields)
return ( return (
f'{toolbar_html}' f'{toolbar_html}'
'<div class="table-wrapper">' '<div class="table-wrapper">'
'<table class="data-table">' '<table class="data-table">'
f'<thead><tr>{thead}</tr></thead>' f'<thead><tr>{thead}</tr></thead>'
f'<tbody>{tbody}</tbody>' f'<tbody>{tbody}</tbody>'
f'</table></div>{worker_script}' f'</table></div>'
) )
# Main dispatcher ===================================================== # Main dispatcher =====================================================

View file

@ -282,7 +282,7 @@ def peers_edit():
flash('Invalid request.', 'error') flash('Invalid request.', 'error')
return redirect(f'/{_PAGE}') return redirect(f'/{_PAGE}')
peer_name = sanitize.name(request.form.get('name', '')) peer_name = sanitize.name(request.form.get('peer_name', ''))
split_tunnel = request.form.get('split_tunnel') in ('true', '1', 'on', 'yes') split_tunnel = request.form.get('split_tunnel') in ('true', '1', 'on', 'yes')
enabled = request.form.get('enabled') not in ('false', '0', '') enabled = request.form.get('enabled') not in ('false', '0', '')

View file

@ -88,27 +88,10 @@
"row_actions": [ "row_actions": [
{ {
"client_requirement": "client_is_administrator+", "client_requirement": "client_is_administrator+",
"action": "/action/vpn/peers_edit", "method": "js_edit",
"method": "inline_edit", "target": "add-form",
"text": "Edit", "text": "Edit",
"class": "btn-ghost btn-sm", "class": "btn-ghost btn-sm"
"fields": [
{
"col": "name",
"input_type": "text",
"validate": "VALIDATION_DASH_NAME"
},
{
"col": "split_tunnel",
"input_type": "checkbox",
"checkbox_label": "Enabled"
},
{
"col": "enabled",
"input_type": "checkbox",
"checkbox_label": "Enabled"
}
]
}, },
{ {
"client_requirement": "client_is_administrator+", "client_requirement": "client_is_administrator+",
@ -128,6 +111,7 @@
}, },
{ {
"type": "card", "type": "card",
"id": "add-form",
"label": "Add Peer", "label": "Add Peer",
"client_requirement": "client_is_administrator+", "client_requirement": "client_is_administrator+",
"items": [ "items": [
@ -136,6 +120,11 @@
"action": "/action/vpn/addpeer_add", "action": "/action/vpn/addpeer_add",
"method": "post", "method": "post",
"items": [ "items": [
{
"type": "hidden",
"name": "row_index",
"value": ""
},
{ {
"type": "field", "type": "field",
"label": "Name", "label": "Name",
@ -273,4 +262,4 @@
] ]
} }
] ]
} }