Add version history for shared environments
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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")
|
||||
])
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user