Switch shared environment to Mithril component
This commit is contained in:
534
static/js/mithril/sharedEnvironments.js
Normal file
534
static/js/mithril/sharedEnvironments.js
Normal 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"
|
||||
)
|
||||
])
|
||||
])
|
||||
])
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -1,458 +1,13 @@
|
||||
{% extends 'dashboard.html' %}
|
||||
|
||||
{% block page %}
|
||||
<div class="container mx-auto p-6 max-w-6xl">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">Shared Environments</h1>
|
||||
<p class="text-gray-600 dark:text-gray-400 mt-1">Manage reusable environment configurations that can be
|
||||
injected into your functions</p>
|
||||
</div>
|
||||
<button onclick="openCreateModal()"
|
||||
class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg flex items-center space-x-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
<span>New Shared Environment</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="shared-env-root"></div>
|
||||
|
||||
{% if environments and environments|length > 0 %}
|
||||
<div class="grid gap-4">
|
||||
{% for env in environments %}
|
||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white mb-1">{{ env.name }}</h3>
|
||||
{% if env.description %}
|
||||
<p class="text-gray-600 dark:text-gray-400 text-sm mb-3">{{ env.description }}</p>
|
||||
{% endif %}
|
||||
<div class="bg-gray-50 dark:bg-gray-900 rounded p-3 font-mono text-sm overflow-x-auto">
|
||||
<pre class="text-gray-800 dark:text-gray-200">{{ env.environment | tojson(indent=2) }}</pre>
|
||||
</div>
|
||||
<div class="mt-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
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 %}
|
||||
</div>
|
||||
|
||||
<!-- Linked Functions Section -->
|
||||
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<button onclick="toggleLinkedFunctions({{ env.id }})"
|
||||
class="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">
|
||||
<svg id="expand-icon-{{ env.id }}" class="w-4 h-4 transition-transform" fill="none"
|
||||
stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M9 5l7 7-7 7"></path>
|
||||
</svg>
|
||||
<span id="linked-count-{{ env.id }}">🔗 Linked Functions (loading...)</span>
|
||||
</button>
|
||||
<button onclick="openLinkModal({{ env.id }}, '{{ env.name }}')"
|
||||
class="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 flex items-center space-x-1">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
<span>Link Function</span>
|
||||
</button>
|
||||
</div>
|
||||
<div id="linked-functions-{{ env.id }}" class="hidden mt-3 space-y-2">
|
||||
<div class="text-sm text-gray-500 dark:text-gray-400">Loading...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2 ml-4">
|
||||
<button
|
||||
onclick="editEnvironment({{ env.id }}, '{{ env.name }}', {{ env.environment | tojson }}, '{{ env.description or '' }}')"
|
||||
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300 p-2"
|
||||
title="Edit">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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">
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
<button onclick="deleteEnvironment({{ env.id }}, '{{ env.name }}')"
|
||||
class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300 p-2"
|
||||
title="Delete">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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">
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div
|
||||
class="text-center py-12 bg-gray-50 dark:bg-gray-800 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-700">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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">
|
||||
</path>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">No shared environments</h3>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Get started by creating a new shared environment</p>
|
||||
<div class="mt-6">
|
||||
<button onclick="openCreateModal()"
|
||||
class="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">
|
||||
<svg class="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
||||
</svg>
|
||||
New Shared Environment
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit Modal -->
|
||||
<div id="envModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-full max-w-2xl shadow-lg rounded-md bg-white dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 id="modalTitle" class="text-lg font-medium text-gray-900 dark:text-white">New Shared Environment</h3>
|
||||
<button onclick="closeModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12">
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<form id="envForm" class="space-y-4">
|
||||
<input type="hidden" id="envId" value="">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Name <span
|
||||
class="text-red-500">*</span></label>
|
||||
<input type="text" id="envName" required placeholder="e.g., apiConfig, database"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md dark:bg-gray-700 dark:text-white">
|
||||
<p class="mt-1 text-xs text-gray-500">This becomes the namespace key (e.g., environment.apiConfig)</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
|
||||
<input type="text" id="envDescription" placeholder="Brief description of this shared environment"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md dark:bg-gray-700 dark:text-white">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Environment JSON <span
|
||||
class="text-red-500">*</span></label>
|
||||
<div id="envEditor" class="border border-gray-300 dark:border-gray-600 rounded-md"
|
||||
style="height: 300px;"></div>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3 pt-4">
|
||||
<button type="button" onclick="closeModal()"
|
||||
class="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">Cancel</button>
|
||||
<button type="submit" class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700"><span
|
||||
id="submitText">Create</span></button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Link Function Modal -->
|
||||
<div id="linkModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-full max-w-lg shadow-lg rounded-md bg-white dark:bg-gray-800">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-white">
|
||||
Link Function to <span id="linkEnvName" class="text-blue-600 dark:text-blue-400"></span>
|
||||
</h3>
|
||||
<button onclick="closeLinkModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12">
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<input type="hidden" id="linkEnvId" value="">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">HTTP Functions</label>
|
||||
<select id="httpFunctionSelect"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md dark:bg-gray-700 dark:text-white">
|
||||
<option value="">Select an HTTP function...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Timer Functions</label>
|
||||
<select id="timerFunctionSelect"
|
||||
class="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md dark:bg-gray-700 dark:text-white">
|
||||
<option value="">Select a timer function...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3 pt-4">
|
||||
<button onclick="closeLinkModal()"
|
||||
class="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">Cancel</button>
|
||||
<button onclick="linkSelectedFunction()"
|
||||
class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700">Link</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/ace.js"></script>
|
||||
<script src="{{ url_for('static', filename='js/mithril/sharedEnvironments.js') }}"></script>
|
||||
<script>
|
||||
let envEditor;
|
||||
let isEditMode = false;
|
||||
let linkedFunctionsCache = {};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Initialize Ace Editor
|
||||
envEditor = ace.edit("envEditor");
|
||||
envEditor.setTheme("ace/theme/github_dark");
|
||||
envEditor.session.setMode("ace/mode/json");
|
||||
envEditor.setValue('{\n \n}', -1);
|
||||
envEditor.setOptions({
|
||||
fontSize: "14px",
|
||||
showPrintMargin: false
|
||||
});
|
||||
|
||||
// Load linked functions for all environments
|
||||
{% for env in environments %}
|
||||
loadLinkedFunctions({{ env.id }});
|
||||
{% endfor %}
|
||||
});
|
||||
|
||||
// Environment CRUD Functions
|
||||
function openCreateModal() {
|
||||
isEditMode = false;
|
||||
document.getElementById('modalTitle').textContent = 'New Shared Environment';
|
||||
document.getElementById('submitText').textContent = 'Create';
|
||||
document.getElementById('envId').value = '';
|
||||
document.getElementById('envName').value = '';
|
||||
document.getElementById('envDescription').value = '';
|
||||
envEditor.setValue('{\n \n}', -1);
|
||||
document.getElementById('envModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function editEnvironment(id, name, environment, description) {
|
||||
isEditMode = true;
|
||||
document.getElementById('modalTitle').textContent = 'Edit Shared Environment';
|
||||
document.getElementById('submitText').textContent = 'Update';
|
||||
document.getElementById('envId').value = id;
|
||||
document.getElementById('envName').value = name;
|
||||
document.getElementById('envDescription').value = description;
|
||||
envEditor.setValue(JSON.stringify(environment, null, 2), -1);
|
||||
document.getElementById('envModal').classList.remove('hidden');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
document.getElementById('envModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
document.getElementById('envForm').addEventListener('submit', async function (e) {
|
||||
e.preventDefault();
|
||||
const id = document.getElementById('envId').value;
|
||||
const name = document.getElementById('envName').value.trim();
|
||||
const description = document.getElementById('envDescription').value.trim();
|
||||
const environment = envEditor.getValue().trim();
|
||||
|
||||
try {
|
||||
JSON.parse(environment);
|
||||
} catch (err) {
|
||||
alert('Invalid JSON: ' + err.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const url = isEditMode ? `/shared_env/${id}` : '/shared_env/new';
|
||||
const method = isEditMode ? 'PUT' : 'POST';
|
||||
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, description, environment })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
}
|
||||
// Pass server-rendered data to Mithril component
|
||||
m.mount(document.getElementById('shared-env-root'), {
|
||||
view: () => m(SharedEnvironments, { initialEnvironments: {{ environments | tojson | safe }}})
|
||||
});
|
||||
|
||||
async function deleteEnvironment(id, name) {
|
||||
if (!confirm(`Are you sure you want to delete "${name}"?\n\nThis will remove it from all linked functions.`)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`/shared_env/${id}`, {
|
||||
method: 'DELETE',
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Linked Functions UI
|
||||
async function toggleLinkedFunctions(envId) {
|
||||
const container = document.getElementById(`linked-functions-${envId}`);
|
||||
const icon = document.getElementById(`expand-icon-${envId}`);
|
||||
|
||||
if (container.classList.contains('hidden')) {
|
||||
container.classList.remove('hidden');
|
||||
icon.style.transform = 'rotate(90deg)';
|
||||
await loadLinkedFunctions(envId);
|
||||
} else {
|
||||
container.classList.add('hidden');
|
||||
icon.style.transform = 'rotate(0deg)';
|
||||
}
|
||||
}
|
||||
|
||||
async function loadLinkedFunctions(envId) {
|
||||
try {
|
||||
const response = await fetch(`/shared_env/${envId}/linked-functions`);
|
||||
const data = await response.json();
|
||||
|
||||
linkedFunctionsCache[envId] = data;
|
||||
|
||||
const httpFuncs = data.http_functions || [];
|
||||
const timerFuncs = data.timer_functions || [];
|
||||
const total = httpFuncs.length + timerFuncs.length;
|
||||
|
||||
// Update count
|
||||
document.getElementById(`linked-count-${envId}`).textContent = `🔗 Linked Functions (${total})`;
|
||||
|
||||
// Update list
|
||||
const container = document.getElementById(`linked-functions-${envId}`);
|
||||
if (total === 0) {
|
||||
container.innerHTML = '<div class="text-sm text-gray-500 dark:text-gray-400 italic">No functions linked yet</div>';
|
||||
} else {
|
||||
let html = '<div class="space-y-1">';
|
||||
httpFuncs.forEach(func => {
|
||||
html += `<div class="flex items-center justify-between bg-gray-100 dark:bg-gray-700 rounded px-3 py-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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"></path>
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">${func.name}</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">(HTTP)</span>
|
||||
</div>
|
||||
<button onclick="unlinkFunction(${envId}, ${func.id}, 'http')" class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>`;
|
||||
});
|
||||
timerFuncs.forEach(func => {
|
||||
html += `<div class="flex items-center justify-between bg-gray-100 dark:bg-gray-700 rounded px-3 py-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<svg class="w-4 h-4 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<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"></path>
|
||||
</svg>
|
||||
<span class="text-sm font-medium text-gray-900 dark:text-white">${func.name}</span>
|
||||
<span class="text-xs text-gray-500 dark:text-gray-400">(Timer)</span>
|
||||
</div>
|
||||
<button onclick="unlinkFunction(${envId}, ${func.id}, 'timer')" class="text-red-600 hover:text-red-800 dark:text-red-400 dark:hover:text-red-300">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
container.innerHTML = html;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load linked functions:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Link Modal Functions
|
||||
async function openLinkModal(envId, envName) {
|
||||
document.getElementById('linkEnvId').value = envId;
|
||||
document.getElementById('linkEnvName').textContent = envName;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/shared_env/${envId}/available-functions`);
|
||||
const data = await response.json();
|
||||
|
||||
const httpSelect = document.getElementById('httpFunctionSelect');
|
||||
const timerSelect = document.getElementById('timerFunctionSelect');
|
||||
|
||||
httpSelect.innerHTML = '<option value="">Select an HTTP function...</option>';
|
||||
(data.http_functions || []).forEach(func => {
|
||||
httpSelect.innerHTML += `<option value="${func.id}">${func.name}</option>`;
|
||||
});
|
||||
|
||||
timerSelect.innerHTML = '<option value="">Select a timer function...</option>';
|
||||
(data.timer_functions || []).forEach(func => {
|
||||
timerSelect.innerHTML += `<option value="${func.id}">${func.name}</option>`;
|
||||
});
|
||||
|
||||
document.getElementById('linkModal').classList.remove('hidden');
|
||||
} catch (err) {
|
||||
alert('Error loading available functions: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
function closeLinkModal() {
|
||||
document.getElementById('linkModal').classList.add('hidden');
|
||||
document.getElementById('httpFunctionSelect').value = '';
|
||||
document.getElementById('timerFunctionSelect').value = '';
|
||||
}
|
||||
|
||||
async function linkSelectedFunction() {
|
||||
const envId = document.getElementById('linkEnvId').value;
|
||||
const httpFuncId = document.getElementById('httpFunctionSelect').value;
|
||||
const timerFuncId = document.getElementById('timerFunctionSelect').value;
|
||||
|
||||
if (!httpFuncId && !timerFuncId) {
|
||||
alert('Please select a function to link');
|
||||
return;
|
||||
}
|
||||
|
||||
const functionId = httpFuncId || timerFuncId;
|
||||
const functionType = httpFuncId ? 'http' : 'timer';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/shared_env/${envId}/link-function`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ function_id: functionId, function_type: functionType })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') {
|
||||
closeLinkModal();
|
||||
await loadLinkedFunctions(envId);
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function unlinkFunction(envId, functionId, functionType) {
|
||||
try {
|
||||
const response = await fetch(`/shared_env/${envId}/unlink-function`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ function_id: functionId, function_type: functionType })
|
||||
});
|
||||
const data = await response.json();
|
||||
if (data.status === 'success') {
|
||||
await loadLinkedFunctions(envId);
|
||||
} else {
|
||||
alert('Error: ' + data.message);
|
||||
}
|
||||
} catch (err) {
|
||||
alert('Error: ' + err.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user