diff --git a/static/js/mithril/sharedEnvironments.js b/static/js/mithril/sharedEnvironments.js new file mode 100644 index 0000000..cd9986e --- /dev/null +++ b/static/js/mithril/sharedEnvironments.js @@ -0,0 +1,534 @@ +// Shared Environments Mithril Component +const SharedEnvironments = { + environments: [], + linkedFunctions: {}, + envEditor: null, + modal: { + isOpen: false, + type: null, // 'create', 'edit', or 'link' + envId: null, + envName: '', + envDescription: '', + envData: {} + }, + linkModal: { + isOpen: false, + envId: null, + envName: '', + availableHttp: [], + availableTimer: [], + selectedHttp: '', + selectedTimer: '' + }, + expandedEnvs: new Set(), + + oninit: function(vnode) { + // Use initial data if provided, otherwise load from API + if (vnode.attrs.initialEnvironments && vnode.attrs.initialEnvironments.length >= 0) { + SharedEnvironments.environments = vnode.attrs.initialEnvironments; + // Load linked functions for each environment + for (const env of SharedEnvironments.environments) { + SharedEnvironments.loadLinkedFunctions(env.id); + } + } else { + this.loadEnvironments(); + } + }, + + initAceEditor: function() { + // Initialize Ace editor if not already done + if (!SharedEnvironments.envEditor) { + setTimeout(() => { + const editorEl = document.getElementById("envEditor"); + if (editorEl && !SharedEnvironments.envEditor) { + SharedEnvironments.envEditor = ace.edit("envEditor"); + SharedEnvironments.envEditor.setTheme("ace/theme/github_dark"); + SharedEnvironments.envEditor.session.setMode("ace/mode/json"); + SharedEnvironments.envEditor.setOptions({ + fontSize: "14px", + showPrintMargin: false + }); + } + }, 100); + } + }, + + loadEnvironments: async function() { + try { + const response = await m.request({ + method: "GET", + url: "/shared_env/list" + }); + SharedEnvironments.environments = response.environments || []; + + // Load linked functions for each environment + for (const env of SharedEnvironments.environments) { + await SharedEnvironments.loadLinkedFunctions(env.id); + } + m.redraw(); + } catch (err) { + console.error('Failed to load environments:', err); + } + }, + + loadLinkedFunctions: async function(envId) { + try { + const response = await m.request({ + method: "GET", + url: `/shared_env/${envId}/linked-functions` + }); + SharedEnvironments.linkedFunctions[envId] = response; + } catch (err) { + console.error(`Failed to load linked functions for env ${envId}:`, err); + } + }, + + openCreateModal: function() { + SharedEnvironments.modal = { + isOpen: true, + type: 'create', + envId: null, + envName: '', + envDescription: '', + envData: {} + }; + SharedEnvironments.initAceEditor(); + setTimeout(() => { + if (SharedEnvironments.envEditor) { + SharedEnvironments.envEditor.setValue('{\n \n}', -1); + } + }, 150); + }, + + openEditModal: function(env) { + SharedEnvironments.modal = { + isOpen: true, + type: 'edit', + envId: env.id, + envName: env.name, + envDescription: env.description || '', + envData: env.environment + }; + SharedEnvironments.initAceEditor(); + setTimeout(() => { + if (SharedEnvironments.envEditor) { + SharedEnvironments.envEditor.setValue(JSON.stringify(env.environment, null, 2), -1); + } + }, 150); + }, + + closeModal: function() { + SharedEnvironments.modal.isOpen = false; + }, + + saveEnvironment: async function(e) { + e.preventDefault(); + + const envJson = SharedEnvironments.envEditor.getValue().trim(); + let parsedEnv; + try { + parsedEnv = JSON.parse(envJson); + } catch (err) { + alert('Invalid JSON: ' + err.message); + return; + } + + const url = SharedEnvironments.modal.type === 'edit' + ? `/shared_env/${SharedEnvironments.modal.envId}` + : '/shared_env/new'; + const method = SharedEnvironments.modal.type === 'edit' ? 'PUT' : 'POST'; + + try { + await m.request({ + method: method, + url: url, + body: { + name: SharedEnvironments.modal.envName, + description: SharedEnvironments.modal.envDescription, + environment: envJson + } + }); + SharedEnvironments.closeModal(); + await SharedEnvironments.loadEnvironments(); + } catch (err) { + alert('Error: ' + (err.message || 'Failed to save')); + } + }, + + deleteEnvironment: async function(env) { + if (!confirm(`Are you sure you want to delete "${env.name}"?\n\nThis will remove it from all linked functions.`)) { + return; + } + + try { + await m.request({ + method: 'DELETE', + url: `/shared_env/${env.id}` + }); + await SharedEnvironments.loadEnvironments(); + } catch (err) { + alert('Error: ' + (err.message || 'Failed to delete')); + } + }, + + toggleLinkedFunctions: function(envId) { + if (SharedEnvironments.expandedEnvs.has(envId)) { + SharedEnvironments.expandedEnvs.delete(envId); + } else { + SharedEnvironments.expandedEnvs.add(envId); + } + }, + + openLinkModal: async function(envId, envName) { + try { + const response = await m.request({ + method: 'GET', + url: `/shared_env/${envId}/available-functions` + }); + SharedEnvironments.linkModal = { + isOpen: true, + envId: envId, + envName: envName, + availableHttp: response.http_functions || [], + availableTimer: response.timer_functions || [], + selectedHttp: '', + selectedTimer: '' + }; + } catch (err) { + alert('Error loading available functions: ' + err.message); + } + }, + + closeLinkModal: function() { + SharedEnvironments.linkModal.isOpen = false; + }, + + linkFunction: async function() { + const { envId, selectedHttp, selectedTimer } = SharedEnvironments.linkModal; + + if (!selectedHttp && !selectedTimer) { + alert('Please select a function to link'); + return; + } + + const functionId = selectedHttp || selectedTimer; + const functionType = selectedHttp ? 'http' : 'timer'; + + try { + await m.request({ + method: 'POST', + url: `/shared_env/${envId}/link-function`, + body: { function_id: functionId, function_type: functionType } + }); + SharedEnvironments.closeLinkModal(); + await SharedEnvironments.loadLinkedFunctions(envId); + } catch (err) { + alert('Error: ' + (err.message || 'Failed to link')); + } + }, + + unlinkFunction: async function(envId, functionId, functionType) { + try { + await m.request({ + method: 'POST', + url: `/shared_env/${envId}/unlink-function`, + body: { function_id: functionId, function_type: functionType } + }); + await SharedEnvironments.loadLinkedFunctions(envId); + } catch (err) { + alert('Error: ' + (err.message || 'Failed to unlink')); + } + }, + + view: function() { + return m("div.container.mx-auto.p-6.max-w-6xl", [ + // Header + m("div.flex.items-center.justify-between.mb-6", [ + m("div", [ + m("h1.text-3xl.font-bold.text-gray-900.dark:text-white", "Shared Environments"), + m("p.text-gray-600.dark:text-gray-400.mt-1", + "Manage reusable environment configurations that can be injected into your functions") + ]), + m("button.bg-blue-600.hover:bg-blue-700.text-white.px-4.py-2.rounded-lg.flex.items-center.space-x-2", + { onclick: () => SharedEnvironments.openCreateModal() }, + [ + 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 4v16m8-8H4]") + ), + m("span", "New Shared Environment") + ] + ) + ]), + + // Environments list + SharedEnvironments.environments.length > 0 + ? m("div.grid.gap-4", SharedEnvironments.environments.map(env => + SharedEnvironments.renderEnvironmentCard(env) + )) + : SharedEnvironments.renderEmptyState(), + + // Create/Edit Modal + SharedEnvironments.modal.isOpen ? SharedEnvironments.renderModal() : null, + + // Link Modal + SharedEnvironments.linkModal.isOpen ? SharedEnvironments.renderLinkModal() : null + ]); + }, + + renderEnvironmentCard: function(env) { + const linked = SharedEnvironments.linkedFunctions[env.id] || { http_functions: [], timer_functions: [] }; + const totalLinked = (linked.http_functions?.length || 0) + (linked.timer_functions?.length || 0); + const isExpanded = SharedEnvironments.expandedEnvs.has(env.id); + + return m("div.bg-white.dark:bg-gray-800.rounded-lg.shadow.border.border-gray-200.dark:border-gray-700.p-6", [ + m("div.flex.items-start.justify-between", [ + m("div.flex-1", [ + m("h3.text-xl.font-semibold.text-gray-900.dark:text-white.mb-1", env.name), + env.description ? m("p.text-gray-600.dark:text-gray-400.text-sm.mb-3", env.description) : null, + m("div.bg-gray-50.dark:bg-gray-900.rounded.p-3.font-mono.text-sm.overflow-x-auto", + m("pre.text-gray-800.dark:text-gray-200", JSON.stringify(env.environment, null, 2)) + ), + m("div.mt-3.text-xs.text-gray-500.dark:text-gray-400", [ + `Created: ${new Date(env.created_at).toLocaleString()}`, + env.updated_at && env.updated_at !== env.created_at + ? ` | Updated: ${new Date(env.updated_at).toLocaleString()}` + : '' + ]), + + // Linked Functions Section + m("div.mt-4.pt-4.border-t.border-gray-200.dark:border-gray-700", [ + m("div.flex.items-center.justify-between.mb-2", [ + m("button.flex.items-center.space-x-2.text-sm.font-medium.text-gray-700.dark:text-gray-300.hover:text-blue-600.dark:hover:text-blue-400.transition-colors", + { onclick: () => SharedEnvironments.toggleLinkedFunctions(env.id) }, + [ + m(`svg.w-4.h-4.transition-transform${isExpanded ? '.rotate-90' : ''}[fill=none][stroke=currentColor][viewBox=0 0 24 24]`, + m("path[stroke-linecap=round][stroke-linejoin=round][stroke-width=2][d=M9 5l7 7-7 7]") + ), + m("span", `🔗 Linked Functions (${totalLinked})`) + ] + ), + m("button.text-sm.text-blue-600.hover:text-blue-800.dark:text-blue-400.dark:hover:text-blue-300.flex.items-center.space-x-1", + { onclick: () => SharedEnvironments.openLinkModal(env.id, env.name) }, + [ + m("svg.w-4.h-4[fill=none][stroke=currentColor][viewBox=0 0 24 24]", + m("path[stroke-linecap=round][stroke-linejoin=round][stroke-width=2][d=M12 4v16m8-8H4]") + ), + m("span", "Link Function") + ] + ) + ]), + isExpanded ? SharedEnvironments.renderLinkedFunctions(env.id, linked) : null + ]) + ]), + m("div.flex.space-x-2.ml-4", [ + 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]", + m("path[stroke-linecap=round][stroke-linejoin=round][stroke-width=2][d=M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z]") + ) + ), + m("button.text-red-600.hover:text-red-800.dark:text-red-400.dark:hover:text-red-300.p-2[title=Delete]", + { onclick: () => SharedEnvironments.deleteEnvironment(env) }, + 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=M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16]") + ) + ) + ]) + ]) + ]); + }, + + renderLinkedFunctions: function(envId, linked) { + const httpFuncs = linked.http_functions || []; + const timerFuncs = linked.timer_functions || []; + + if (httpFuncs.length === 0 && timerFuncs.length === 0) { + return m("div.mt-3.text-sm.text-gray-500.dark:text-gray-400.italic", "No functions linked yet"); + } + + return m("div.mt-3.space-y-1", [ + ...httpFuncs.map(func => + m("div.flex.items-center.justify-between.bg-gray-100.dark:bg-gray-700.rounded.px-3.py-2", [ + m("div.flex.items-center.space-x-2", [ + m("svg.w-4.h-4.text-blue-500[fill=none][stroke=currentColor][viewBox=0 0 24 24]", + m("path[stroke-linecap=round][stroke-linejoin=round][stroke-width=2][d=M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9]") + ), + m("span.text-sm.font-medium.text-gray-900.dark:text-white", func.name), + m("span.text-xs.text-gray-500.dark:text-gray-400", "(HTTP)") + ]), + m("button.text-red-600.hover:text-red-800.dark:text-red-400.dark:hover:text-red-300", + { onclick: () => SharedEnvironments.unlinkFunction(envId, func.id, 'http') }, + m("svg.w-4.h-4[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]") + ) + ) + ]) + ), + ...timerFuncs.map(func => + m("div.flex.items-center.justify-between.bg-gray-100.dark:bg-gray-700.rounded.px-3.py-2", [ + m("div.flex.items-center.space-x-2", [ + m("svg.w-4.h-4.text-green-500[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("span.text-sm.font-medium.text-gray-900.dark:text-white", func.name), + m("span.text-xs.text-gray-500.dark:text-gray-400", "(Timer)") + ]), + m("button.text-red-600.hover:text-red-800.dark:text-red-400.dark:hover:text-red-300", + { onclick: () => SharedEnvironments.unlinkFunction(envId, func.id, 'timer') }, + m("svg.w-4.h-4[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]") + ) + ) + ]) + ) + ]); + }, + + renderEmptyState: function() { + return m("div.text-center.py-12.bg-gray-50.dark:bg-gray-800.rounded-lg.border-2.border-dashed.border-gray-300.dark:border-gray-700", [ + m("svg.mx-auto.h-12.w-12.text-gray-400[fill=none][stroke=currentColor][viewBox=0 0 24 24]", + m("path[stroke-linecap=round][stroke-linejoin=round][stroke-width=2][d=M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4]") + ), + m("h3.mt-2.text-sm.font-medium.text-gray-900.dark:text-white", "No shared environments"), + m("p.mt-1.text-sm.text-gray-500.dark:text-gray-400", "Get started by creating a new shared environment"), + m("div.mt-6", + m("button.inline-flex.items-center.px-4.py-2.border.border-transparent.shadow-sm.text-sm.font-medium.rounded-md.text-white.bg-blue-600.hover:bg-blue-700", + { onclick: () => SharedEnvironments.openCreateModal() }, + [ + m("svg.-ml-1.mr-2.h-5.w-5[fill=none][stroke=currentColor][viewBox=0 0 24 24]", + m("path[stroke-linecap=round][stroke-linejoin=round][stroke-width=2][d=M12 4v16m8-8H4]") + ), + "New Shared Environment" + ] + ) + ) + ]); + }, + + renderModal: 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.closeModal() }, + m("div.relative.top-20.mx-auto.p-5.border.w-full.max-w-2xl.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", + SharedEnvironments.modal.type === 'edit' ? 'Edit Shared Environment' : 'New Shared Environment' + ), + m("button.text-gray-400.hover:text-gray-600.dark:hover:text-gray-300", + { onclick: () => SharedEnvironments.closeModal() }, + 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]") + ) + ) + ]), + m("form.space-y-4", { onsubmit: SharedEnvironments.saveEnvironment }, [ + m("div", [ + m("label.block.text-sm.font-medium.text-gray-700.dark:text-gray-300.mb-1", [ + "Name ", + m("span.text-red-500", "*") + ]), + m("input.w-full.px-3.py-2.border.border-gray-300.dark:border-gray-600.rounded-md.dark:bg-gray-700.dark:text-white[type=text][required][placeholder=e.g., apiConfig, database]", + { + value: SharedEnvironments.modal.envName, + oninput: (e) => SharedEnvironments.modal.envName = e.target.value + } + ), + m("p.mt-1.text-xs.text-gray-500", "This becomes the namespace key (e.g., environment.apiConfig)") + ]), + m("div", [ + m("label.block.text-sm.font-medium.text-gray-700.dark:text-gray-300.mb-1", "Description"), + m("input.w-full.px-3.py-2.border.border-gray-300.dark:border-gray-600.rounded-md.dark:bg-gray-700.dark:text-white[type=text][placeholder=Brief description of this shared environment]", + { + value: SharedEnvironments.modal.envDescription, + oninput: (e) => SharedEnvironments.modal.envDescription = e.target.value + } + ) + ]), + m("div", [ + m("label.block.text-sm.font-medium.text-gray-700.dark:text-gray-300.mb-1", [ + "Environment JSON ", + m("span.text-red-500", "*") + ]), + m("div#envEditor.border.border-gray-300.dark:border-gray-600.rounded-md[style=height: 300px]") + ]), + m("div.flex.justify-end.space-x-3.pt-4", [ + m("button.px-4.py-2.border.border-gray-300.dark:border-gray-600.rounded-md.text-gray-700.dark:text-gray-300.hover:bg-gray-50.dark:hover:bg-gray-700[type=button]", + { onclick: () => SharedEnvironments.closeModal() }, + "Cancel" + ), + m("button.px-4.py-2.bg-blue-600.text-white.rounded-md.hover:bg-blue-700[type=submit]", + SharedEnvironments.modal.type === 'edit' ? 'Update' : 'Create' + ) + ]) + ]) + ]) + ); + }, + + renderLinkModal: 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.closeLinkModal() }, + m("div.relative.top-20.mx-auto.p-5.border.w-full.max-w-lg.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", [ + "Link Function to ", + m("span.text-blue-600.dark:text-blue-400", SharedEnvironments.linkModal.envName) + ]), + m("button.text-gray-400.hover:text-gray-600.dark:hover:text-gray-300", + { onclick: () => SharedEnvironments.closeLinkModal() }, + 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]") + ) + ) + ]), + m("div.space-y-4", [ + m("div", [ + m("label.block.text-sm.font-medium.text-gray-700.dark:text-gray-300.mb-2", "HTTP Functions"), + m("select.w-full.px-3.py-2.border.border-gray-300.dark:border-gray-600.rounded-md.dark:bg-gray-700.dark:text-white", + { + value: SharedEnvironments.linkModal.selectedHttp, + onchange: (e) => { + SharedEnvironments.linkModal.selectedHttp = e.target.value; + SharedEnvironments.linkModal.selectedTimer = ''; + } + }, + [ + m("option[value=]", "Select an HTTP function..."), + ...SharedEnvironments.linkModal.availableHttp.map(func => + m("option", { value: func.id }, func.name) + ) + ] + ) + ]), + m("div", [ + m("label.block.text-sm.font-medium.text-gray-700.dark:text-gray-300.mb-2", "Timer Functions"), + m("select.w-full.px-3.py-2.border.border-gray-300.dark:border-gray-600.rounded-md.dark:bg-gray-700.dark:text-white", + { + value: SharedEnvironments.linkModal.selectedTimer, + onchange: (e) => { + SharedEnvironments.linkModal.selectedTimer = e.target.value; + SharedEnvironments.linkModal.selectedHttp = ''; + } + }, + [ + m("option[value=]", "Select a timer function..."), + ...SharedEnvironments.linkModal.availableTimer.map(func => + m("option", { value: func.id }, func.name) + ) + ] + ) + ]), + m("div.flex.justify-end.space-x-3.pt-4", [ + m("button.px-4.py-2.border.border-gray-300.dark:border-gray-600.rounded-md.text-gray-700.dark:text-gray-300.hover:bg-gray-50.dark:hover:bg-gray-700", + { onclick: () => SharedEnvironments.closeLinkModal() }, + "Cancel" + ), + m("button.px-4.py-2.bg-blue-600.text-white.rounded-md.hover:bg-blue-700", + { onclick: () => SharedEnvironments.linkFunction() }, + "Link" + ) + ]) + ]) + ]) + ); + } +}; diff --git a/templates/dashboard/shared_environments/index.html b/templates/dashboard/shared_environments/index.html index e03903e..9eaa421 100644 --- a/templates/dashboard/shared_environments/index.html +++ b/templates/dashboard/shared_environments/index.html @@ -1,458 +1,13 @@ {% extends 'dashboard.html' %} {% block page %} -
-
-
-

Shared Environments

-

Manage reusable environment configurations that can be - injected into your functions

-
- -
+
- {% if environments and environments|length > 0 %} -
- {% for env in environments %} -
-
-
-

{{ env.name }}

- {% if env.description %} -

{{ env.description }}

- {% endif %} -
-
{{ env.environment | tojson(indent=2) }}
-
-
- Created: {{ env.created_at.strftime('%Y-%m-%d %H:%M') if env.created_at else 'Unknown' }} - {% if env.updated_at and env.updated_at != env.created_at %} - | Updated: {{ env.updated_at.strftime('%Y-%m-%d %H:%M') }} - {% endif %} -
- - -
-
- - -
- -
-
-
- - -
-
-
- {% endfor %} -
- {% else %} -
- - - - -

No shared environments

-

Get started by creating a new shared environment

-
- -
-
- {% endif %} -
- - - - - - - - + {% endblock %} \ No newline at end of file