Development
This commit is contained in:
parent
8ab88094b3
commit
6ebf7027fd
4 changed files with 65 additions and 23 deletions
|
|
@ -357,7 +357,8 @@ def _db():
|
||||||
parent_path TEXT NOT NULL,
|
parent_path TEXT NOT NULL,
|
||||||
item_key TEXT,
|
item_key TEXT,
|
||||||
item_value TEXT,
|
item_value TEXT,
|
||||||
reverts_group TEXT
|
reverts_group TEXT,
|
||||||
|
reverted INTEGER NOT NULL DEFAULT 0
|
||||||
);
|
);
|
||||||
CREATE TABLE IF NOT EXISTS changes (
|
CREATE TABLE IF NOT EXISTS changes (
|
||||||
group_id TEXT NOT NULL REFERENCES groups(uuid),
|
group_id TEXT NOT NULL REFERENCES groups(uuid),
|
||||||
|
|
@ -510,8 +511,9 @@ def load_all_groups():
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
def revert_group(group_uuid):
|
def revert_group(group_uuid, force=False):
|
||||||
"""Revert a change group. Returns (flash_message, success_bool)."""
|
"""Revert a change group. Returns (flash_message, success_bool).
|
||||||
|
force=True skips the revert-of-revert guard, used by revert_group_chain."""
|
||||||
conn = _db()
|
conn = _db()
|
||||||
try:
|
try:
|
||||||
g = conn.execute('SELECT * FROM groups WHERE uuid=?', (group_uuid,)).fetchone()
|
g = conn.execute('SELECT * FROM groups WHERE uuid=?', (group_uuid,)).fetchone()
|
||||||
|
|
@ -524,7 +526,7 @@ def revert_group(group_uuid):
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
if g['reverts_group']:
|
if g['reverts_group'] and not force:
|
||||||
return 'Cannot revert a revert.', False
|
return 'Cannot revert a revert.', False
|
||||||
|
|
||||||
cfg = load_config()
|
cfg = load_config()
|
||||||
|
|
@ -556,9 +558,45 @@ def revert_group(group_uuid):
|
||||||
inv = [(c['field'], c['after'], c['before'], c['value_type']) for c in changes]
|
inv = [(c['field'], c['after'], c['before'], c['value_type']) for c in changes]
|
||||||
msg = record_group(cfg, parent_path, item_key, item_value, inv,
|
msg = record_group(cfg, parent_path, item_key, item_value, inv,
|
||||||
g['cmd'], reverts_group=group_uuid)
|
g['cmd'], reverts_group=group_uuid)
|
||||||
|
conn = _db()
|
||||||
|
try:
|
||||||
|
conn.execute('UPDATE groups SET reverted=1 WHERE uuid=?', (group_uuid,))
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
return msg, True
|
return msg, True
|
||||||
|
|
||||||
|
|
||||||
|
def revert_group_chain(group_uuid):
|
||||||
|
"""Revert group_uuid and all subsequent groups touching the same item
|
||||||
|
(same parent_path + item_key + item_value), newest first.
|
||||||
|
Returns (error_messages, succeeded_count, failed_count)."""
|
||||||
|
conn = _db()
|
||||||
|
try:
|
||||||
|
g = conn.execute('SELECT * FROM groups WHERE uuid=?', (group_uuid,)).fetchone()
|
||||||
|
if not g:
|
||||||
|
return [f'Snapshot not found for {group_uuid[:8]}.'], 0, 1
|
||||||
|
g = dict(g)
|
||||||
|
chain = [dict(r) for r in conn.execute(
|
||||||
|
'SELECT * FROM groups '
|
||||||
|
'WHERE parent_path=? AND item_key IS ? AND item_value IS ? AND ts >= ? AND reverted=0 '
|
||||||
|
'ORDER BY ts DESC',
|
||||||
|
(g['parent_path'], g['item_key'], g['item_value'], g['ts'])
|
||||||
|
).fetchall()]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
errors, succeeded, failed = [], 0, 0
|
||||||
|
for grp in chain:
|
||||||
|
msg, ok = revert_group(grp['uuid'], force=True)
|
||||||
|
if ok:
|
||||||
|
succeeded += 1
|
||||||
|
else:
|
||||||
|
errors.append(msg)
|
||||||
|
failed += 1
|
||||||
|
return errors, succeeded, failed
|
||||||
|
|
||||||
|
|
||||||
# Misc ==============================================================
|
# Misc ==============================================================
|
||||||
|
|
||||||
def run_apply():
|
def run_apply():
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from pathlib import Path
|
||||||
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, get_dashboard_pending,
|
from config_utils import (flush_pending_to_queue, get_dashboard_pending,
|
||||||
revert_group, queued_msg,
|
revert_group, revert_group_chain, queued_msg,
|
||||||
DASHBOARD_PENDING, _db)
|
DASHBOARD_PENDING, _db)
|
||||||
|
|
||||||
_PAGE = Path(__file__).parent.name
|
_PAGE = Path(__file__).parent.name
|
||||||
|
|
@ -37,17 +37,21 @@ def history_revert():
|
||||||
if not selected_uuids:
|
if not selected_uuids:
|
||||||
flash('No items selected.', 'info')
|
flash('No items selected.', 'info')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
succeeded, failed = 0, 0
|
if len(selected_uuids) != 1:
|
||||||
for uuid in selected_uuids:
|
flash('Please select exactly one change to revert.', 'error')
|
||||||
msg, ok = revert_group(uuid)
|
return redirect(f'/{_PAGE}')
|
||||||
if ok:
|
|
||||||
succeeded += 1
|
behavior = request.form.get('revert_behavior', 'revert_subsequent')
|
||||||
else:
|
errors, succeeded, failed = revert_group_chain(selected_uuids[0])
|
||||||
flash(msg, 'error')
|
|
||||||
failed += 1
|
for msg in errors:
|
||||||
|
flash(msg, 'error')
|
||||||
if succeeded:
|
if succeeded:
|
||||||
plural = 's' if succeeded != 1 else ''
|
s = 's' if succeeded != 1 else ''
|
||||||
flash(f'{succeeded} change{plural} reverted.', 'success')
|
if behavior == 'restore_state':
|
||||||
|
flash(f'Config restored to selected state ({succeeded} change{s} reverted).', 'success')
|
||||||
|
else:
|
||||||
|
flash(f'{succeeded} change{s} reverted.', 'success')
|
||||||
return redirect(f'/{_PAGE}')
|
return redirect(f'/{_PAGE}')
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -110,7 +110,7 @@
|
||||||
"items": [
|
"items": [
|
||||||
{
|
{
|
||||||
"type": "raw_html",
|
"type": "raw_html",
|
||||||
"html": "<label style='display:flex;align-items:center;gap:0.5rem;margin:0;font-size:0.875rem'>Revert behavior:<select name='revert_behavior' class='form-select' style='width:auto'><option value='revert_subsequent'>Revert selected and subsequent tree changes</option><option value='restore_state'>Restore to tree selected state</option></select></label>"
|
"html": "<label style='display:flex;align-items:center;gap:0.5rem;margin:0;font-size:0.875rem;white-space:nowrap'>Revert behavior:<select name='revert_behavior' class='form-select' style='width:auto'><option value='revert_subsequent'>Revert selected and subsequent tree changes</option><option value='restore_state'>Restore tree to selected state</option></select></label>"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "button_secondary",
|
"type": "button_secondary",
|
||||||
|
|
|
||||||
|
|
@ -66,11 +66,7 @@ def collect_tokens(cfg):
|
||||||
done_ts_map = get_done_timestamps()
|
done_ts_map = get_done_timestamps()
|
||||||
if all_groups:
|
if all_groups:
|
||||||
is_manager = client_level() >= LEVEL_RANK['manager']
|
is_manager = client_level() >= LEVEL_RANK['manager']
|
||||||
no_revert = set()
|
no_revert = {g['uuid'] for g, _ in all_groups if g['reverted'] or g['reverts_group']}
|
||||||
for g, _ in all_groups:
|
|
||||||
if g['reverts_group']:
|
|
||||||
no_revert.add(g['uuid'])
|
|
||||||
no_revert.add(g['reverts_group'])
|
|
||||||
hist_rows = ''
|
hist_rows = ''
|
||||||
hist_onclick = (
|
hist_onclick = (
|
||||||
'onclick="if(event.target.type!==\'checkbox\')'
|
'onclick="if(event.target.type!==\'checkbox\')'
|
||||||
|
|
@ -91,7 +87,11 @@ def collect_tokens(cfg):
|
||||||
else:
|
else:
|
||||||
verb = 'Edited'
|
verb = 'Edited'
|
||||||
item = g.get('item_value') or ''
|
item = g.get('item_value') or ''
|
||||||
summary = f'{verb} {g["parent_path"]}: {item}' if item else f'{verb} {g["parent_path"]}'
|
summary_text = f'{verb} {g["parent_path"]}: {item}' if item else f'{verb} {g["parent_path"]}'
|
||||||
|
if g['reverted']:
|
||||||
|
summary = f'<span style="text-decoration:line-through;opacity:0.5">{e(summary_text)}</span> <span class="badge badge-disabled">Superseded</span>'
|
||||||
|
else:
|
||||||
|
summary = e(summary_text)
|
||||||
snap_tag = (
|
snap_tag = (
|
||||||
f'<div class="tag-list"><span class="tag" data-tooltip="{e(uuid)}" data-uuid="{e(uuid)}">'
|
f'<div class="tag-list"><span class="tag" data-tooltip="{e(uuid)}" data-uuid="{e(uuid)}">'
|
||||||
f'<span class="tl-full">{e(uuid[:8])}</span>'
|
f'<span class="tl-full">{e(uuid[:8])}</span>'
|
||||||
|
|
@ -105,7 +105,7 @@ def collect_tokens(cfg):
|
||||||
f'<tr class="row-expandable" data-uuid="{e(uuid)}" {hist_onclick}>'
|
f'<tr class="row-expandable" data-uuid="{e(uuid)}" {hist_onclick}>'
|
||||||
f'<td class="table-cell"><input type="checkbox" name="selected_uuids" value="{e(uuid)}" {cb_attrs}/></td>'
|
f'<td class="table-cell"><input type="checkbox" name="selected_uuids" value="{e(uuid)}" {cb_attrs}/></td>'
|
||||||
f'<td class="table-cell">{e(dt_str)}</td>'
|
f'<td class="table-cell">{e(dt_str)}</td>'
|
||||||
f'<td class="table-cell">{e(summary)}</td>'
|
f'<td class="table-cell">{summary}</td>'
|
||||||
f'<td class="table-cell">{build_snap_val(changes)}</td>'
|
f'<td class="table-cell">{build_snap_val(changes)}</td>'
|
||||||
f'<td class="table-cell">{snap_tag}</td>'
|
f'<td class="table-cell">{snap_tag}</td>'
|
||||||
f'<td class="table-cell">{snap_user}</td>'
|
f'<td class="table-cell">{snap_user}</td>'
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue