Add version history for shared environments
This commit is contained in:
@@ -415,3 +415,85 @@ def unlink_function(env_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
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: '',
|
selectedHttp: '',
|
||||||
selectedTimer: ''
|
selectedTimer: ''
|
||||||
},
|
},
|
||||||
|
historyModal: {
|
||||||
|
isOpen: false,
|
||||||
|
envId: null,
|
||||||
|
envName: '',
|
||||||
|
versions: []
|
||||||
|
},
|
||||||
expandedEnvs: new Set(),
|
expandedEnvs: new Set(),
|
||||||
|
|
||||||
oninit: function(vnode) {
|
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() {
|
view: function() {
|
||||||
return m("div.container.mx-auto.p-6.max-w-6xl", [
|
return m("div.container.mx-auto.p-6.max-w-6xl", [
|
||||||
// Header
|
// Header
|
||||||
@@ -271,7 +317,10 @@ const SharedEnvironments = {
|
|||||||
SharedEnvironments.modal.isOpen ? SharedEnvironments.renderModal() : null,
|
SharedEnvironments.modal.isOpen ? SharedEnvironments.renderModal() : null,
|
||||||
|
|
||||||
// Link Modal
|
// 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("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]",
|
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) },
|
{ onclick: () => SharedEnvironments.openEditModal(env) },
|
||||||
m("svg.w-5.h-5[fill=none][stroke=currentColor][viewBox=0 0 24 24]",
|
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