Switch shared environment to Mithril component

This commit is contained in:
Peter Stockings
2025-11-30 14:29:55 +11:00
parent e864a9f5f3
commit c65a64f81d
2 changed files with 539 additions and 450 deletions

View File

@@ -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"
)
])
])
])
);
}
};