Add version history for shared environments

This commit is contained in:
Peter Stockings
2025-11-30 19:50:29 +11:00
parent b1fc0ae2f5
commit a7dfc28a8b
2 changed files with 184 additions and 1 deletions

View File

@@ -415,3 +415,85 @@ def unlink_function(env_id):
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
@shared_env.route('/<int:env_id>/history', methods=['GET'])
@login_required
def history(env_id):
"""Get version history for a shared environment"""
try:
# Verify ownership
existing = db.execute(
'SELECT id, name FROM shared_environments WHERE id=%s AND user_id=%s',
[env_id, current_user.id],
one=True
)
if not existing:
return jsonify({'status': 'error', 'message': 'Shared environment not found'}), 404
# Fetch all versions
versions = db.execute('''
SELECT version_number, environment, versioned_at
FROM shared_environment_versions
WHERE shared_env_id = %s
ORDER BY version_number DESC
''', [env_id])
# Convert datetime objects to ISO format strings
for version in versions or []:
version['versioned_at'] = version['versioned_at'].isoformat() if version.get('versioned_at') else None
return jsonify({
'status': 'success',
'env_name': existing['name'],
'versions': versions if versions else []
})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
@shared_env.route('/<int:env_id>/restore', methods=['POST'])
@login_required
def restore(env_id):
"""Restore a shared environment to a previous version"""
try:
version_number = request.json.get('version_number')
if not version_number:
return jsonify({'status': 'error', 'message': 'Version number is required'}), 400
# Verify ownership
existing = db.execute(
'SELECT id, name FROM shared_environments WHERE id=%s AND user_id=%s',
[env_id, current_user.id],
one=True
)
if not existing:
return jsonify({'status': 'error', 'message': 'Shared environment not found'}), 404
# Fetch the selected version's environment data
version_data = db.execute(
'SELECT environment FROM shared_environment_versions WHERE shared_env_id=%s AND version_number=%s',
[env_id, version_number],
one=True
)
if not version_data:
return jsonify({'status': 'error', 'message': 'Version not found'}), 404
# Update the shared environment with the old version's data
# This will trigger the versioning function to create a new version
db.execute(
'UPDATE shared_environments SET environment=%s, updated_at=NOW() WHERE id=%s',
[json.dumps(version_data['environment']), env_id],
commit=True
)
return jsonify({
'status': 'success',
'message': f'Restored to version {version_number}'
})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500

View File

@@ -20,6 +20,12 @@ const SharedEnvironments = {
selectedHttp: '',
selectedTimer: ''
},
historyModal: {
isOpen: false,
envId: null,
envName: '',
versions: []
},
expandedEnvs: new Set(),
oninit: function(vnode) {
@@ -240,6 +246,46 @@ const SharedEnvironments = {
}
},
openHistoryModal: async function(envId, envName) {
try {
const response = await m.request({
method: 'GET',
url: `/shared_env/${envId}/history`
});
SharedEnvironments.historyModal = {
isOpen: true,
envId: envId,
envName: envName,
versions: response.versions || []
};
} catch (err) {
alert('Error loading version history: ' + err.message);
}
},
closeHistoryModal: function() {
SharedEnvironments.historyModal.isOpen = false;
},
restoreVersion: async function(versionNumber) {
if (!confirm(`Restore to version ${versionNumber}?\n\nThis will update the environment to match version ${versionNumber} (creating a new version).`)) {
return;
}
try {
await m.request({
method: 'POST',
url: `/shared_env/${SharedEnvironments.historyModal.envId}/restore`,
body: { version_number: versionNumber }
});
SharedEnvironments.closeHistoryModal();
await SharedEnvironments.loadEnvironments();
alert('Version restored successfully!');
} catch (err) {
alert('Error: ' + (err.message || 'Failed to restore'));
}
},
view: function() {
return m("div.container.mx-auto.p-6.max-w-6xl", [
// Header
@@ -271,7 +317,10 @@ const SharedEnvironments = {
SharedEnvironments.modal.isOpen ? SharedEnvironments.renderModal() : null,
// Link Modal
SharedEnvironments.linkModal.isOpen ? SharedEnvironments.renderLinkModal() : null
SharedEnvironments.linkModal.isOpen ? SharedEnvironments.renderLinkModal() : null,
// History Modal
SharedEnvironments.historyModal.isOpen ? SharedEnvironments.renderHistoryModal() : null
]);
},
@@ -321,6 +370,12 @@ const SharedEnvironments = {
])
]),
m("div.flex.space-x-2.ml-4", [
m("button.text-purple-600.hover:text-purple-800.dark:text-purple-400.dark:hover:text-purple-300.p-2[title=History]",
{ onclick: () => SharedEnvironments.openHistoryModal(env.id, env.name) },
m("svg.w-5.h-5[fill=none][stroke=currentColor][viewBox=0 0 24 24]",
m("path[stroke-linecap=round][stroke-linejoin=round][stroke-width=2][d=M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z]")
)
),
m("button.text-blue-600.hover:text-blue-800.dark:text-blue-400.dark:hover:text-blue-300.p-2[title=Edit]",
{ onclick: () => SharedEnvironments.openEditModal(env) },
m("svg.w-5.h-5[fill=none][stroke=currentColor][viewBox=0 0 24 24]",
@@ -530,5 +585,51 @@ const SharedEnvironments = {
])
])
);
},
renderHistoryModal: function() {
return m("div.fixed.inset-0.bg-gray-600.bg-opacity-50.overflow-y-auto.h-full.w-full.z-50",
{ onclick: (e) => e.target === e.currentTarget && SharedEnvironments.closeHistoryModal() },
m("div.relative.top-20.mx-auto.p-5.border.w-full.max-w-3xl.shadow-lg.rounded-md.bg-white.dark:bg-gray-800", [
m("div.flex.items-center.justify-between.mb-4", [
m("h3.text-lg.font-medium.text-gray-900.dark:text-white", [
"Version History: ",
m("span.text-purple-600.dark:text-purple-400", SharedEnvironments.historyModal.envName)
]),
m("button.text-gray-400.hover:text-gray-600.dark:hover:text-gray-300",
{ onclick: () => SharedEnvironments.closeHistoryModal() },
m("svg.w-6.h-6[fill=none][stroke=currentColor][viewBox=0 0 24 24]",
m("path[stroke-linecap=round][stroke-linejoin=round][stroke-width=2][d=M6 18L18 6M6 6l12 12]")
)
)
]),
SharedEnvironments.historyModal.versions.length > 0
? m("div.space-y-3.max-h-96.overflow-y-auto",
SharedEnvironments.historyModal.versions.map(version =>
m("div.border.border-gray-300.dark:border-gray-600.rounded-lg.p-4.hover:bg-gray-50.dark:hover:bg-gray-700.transition-colors", [
m("div.flex.items-center.justify-between.mb-2", [
m("div.flex.items-center.space-x-2", [
m("span.text-sm.font-semibold.text-purple-600.dark:text-purple-400", `Version ${version.version_number}`),
m("span.text-xs.text-gray-500.dark:text-gray-400",
new Date(version.versioned_at).toLocaleString()
)
]),
m("button.px-3.py-1.text-sm.bg-purple-600.hover:bg-purple-700.text-white.rounded-md",
{ onclick: () => SharedEnvironments.restoreVersion(version.version_number) },
"Restore"
)
]),
m("div.bg-gray-100.dark:bg-gray-900.rounded.p-3.font-mono.text-xs.overflow-x-auto",
m("pre.text-gray-800.dark:text-gray-200",
JSON.stringify(version.environment, null, 2)
)
)
])
)
)
: m("div.text-center.py-8.text-gray-500.dark:text-gray-400", "No version history available")
])
);
}
};