feat(streamlit): Add instances management and Gitea integration
- Add Running Instances section with enable/disable/delete actions - Add Instance form to create new instances on different ports - Add Gitea clone functionality to pull apps from repositories - Add Gitea configuration section in Settings page - RPCD handler now supports: - get_gitea_config, save_gitea_config - gitea_clone, gitea_pull, gitea_list_repos - API module exports all new Gitea methods - Upload supports both .py files and .zip archives - Instance status shown with colored indicators Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
5317f37e7a
commit
e07fec6cb4
@ -8,14 +8,14 @@ include $(TOPDIR)/rules.mk
|
|||||||
|
|
||||||
PKG_NAME:=luci-app-streamlit
|
PKG_NAME:=luci-app-streamlit
|
||||||
PKG_VERSION:=1.0.0
|
PKG_VERSION:=1.0.0
|
||||||
PKG_RELEASE:=9
|
PKG_RELEASE:=11
|
||||||
PKG_ARCH:=all
|
PKG_ARCH:=all
|
||||||
|
|
||||||
PKG_LICENSE:=Apache-2.0
|
PKG_LICENSE:=Apache-2.0
|
||||||
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
PKG_MAINTAINER:=CyberMind <contact@cybermind.fr>
|
||||||
|
|
||||||
LUCI_TITLE:=LuCI Streamlit Dashboard
|
LUCI_TITLE:=LuCI Streamlit Dashboard
|
||||||
LUCI_DESCRIPTION:=Modern dashboard for Streamlit Platform management on OpenWrt
|
LUCI_DESCRIPTION:=Multi-instance Streamlit management with Gitea integration
|
||||||
LUCI_DEPENDS:=+luci-base +luci-lib-jsonc +rpcd +rpcd-mod-luci +secubox-app-streamlit
|
LUCI_DEPENDS:=+luci-base +luci-lib-jsonc +rpcd +rpcd-mod-luci +secubox-app-streamlit
|
||||||
|
|
||||||
LUCI_PKGARCH:=all
|
LUCI_PKGARCH:=all
|
||||||
|
|||||||
@ -164,6 +164,39 @@ var callDisableInstance = rpc.declare({
|
|||||||
expect: { result: {} }
|
expect: { result: {} }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
var callGetGiteaConfig = rpc.declare({
|
||||||
|
object: 'luci.streamlit',
|
||||||
|
method: 'get_gitea_config',
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callSaveGiteaConfig = rpc.declare({
|
||||||
|
object: 'luci.streamlit',
|
||||||
|
method: 'save_gitea_config',
|
||||||
|
params: ['enabled', 'url', 'user', 'token'],
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGiteaClone = rpc.declare({
|
||||||
|
object: 'luci.streamlit',
|
||||||
|
method: 'gitea_clone',
|
||||||
|
params: ['name', 'repo'],
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGiteaPull = rpc.declare({
|
||||||
|
object: 'luci.streamlit',
|
||||||
|
method: 'gitea_pull',
|
||||||
|
params: ['name'],
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
|
var callGiteaListRepos = rpc.declare({
|
||||||
|
object: 'luci.streamlit',
|
||||||
|
method: 'gitea_list_repos',
|
||||||
|
expect: { result: {} }
|
||||||
|
});
|
||||||
|
|
||||||
return baseclass.extend({
|
return baseclass.extend({
|
||||||
getStatus: function() {
|
getStatus: function() {
|
||||||
return callGetStatus();
|
return callGetStatus();
|
||||||
@ -282,6 +315,26 @@ return baseclass.extend({
|
|||||||
return callDisableInstance(id);
|
return callDisableInstance(id);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getGiteaConfig: function() {
|
||||||
|
return callGetGiteaConfig();
|
||||||
|
},
|
||||||
|
|
||||||
|
saveGiteaConfig: function(enabled, url, user, token) {
|
||||||
|
return callSaveGiteaConfig(enabled, url, user, token);
|
||||||
|
},
|
||||||
|
|
||||||
|
giteaClone: function(name, repo) {
|
||||||
|
return callGiteaClone(name, repo);
|
||||||
|
},
|
||||||
|
|
||||||
|
giteaPull: function(name) {
|
||||||
|
return callGiteaPull(name);
|
||||||
|
},
|
||||||
|
|
||||||
|
giteaListRepos: function() {
|
||||||
|
return callGiteaListRepos();
|
||||||
|
},
|
||||||
|
|
||||||
getDashboardData: function() {
|
getDashboardData: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
|
|||||||
@ -7,7 +7,9 @@
|
|||||||
return view.extend({
|
return view.extend({
|
||||||
status: {},
|
status: {},
|
||||||
apps: [],
|
apps: [],
|
||||||
|
instances: [],
|
||||||
activeApp: '',
|
activeApp: '',
|
||||||
|
giteaConfig: {},
|
||||||
|
|
||||||
load: function() {
|
load: function() {
|
||||||
return this.refresh();
|
return this.refresh();
|
||||||
@ -17,11 +19,15 @@ return view.extend({
|
|||||||
var self = this;
|
var self = this;
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
api.getStatus(),
|
api.getStatus(),
|
||||||
api.listApps()
|
api.listApps(),
|
||||||
|
api.listInstances(),
|
||||||
|
api.getGiteaConfig().catch(function() { return {}; })
|
||||||
]).then(function(r) {
|
]).then(function(r) {
|
||||||
self.status = r[0] || {};
|
self.status = r[0] || {};
|
||||||
self.apps = (r[1] && r[1].apps) || [];
|
self.apps = (r[1] && r[1].apps) || [];
|
||||||
self.activeApp = (r[1] && r[1].active_app) || '';
|
self.activeApp = (r[1] && r[1].active_app) || '';
|
||||||
|
self.instances = r[2] || [];
|
||||||
|
self.giteaConfig = r[3] || {};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -33,7 +39,7 @@ return view.extend({
|
|||||||
|
|
||||||
var view = E('div', { 'class': 'cbi-map' }, [
|
var view = E('div', { 'class': 'cbi-map' }, [
|
||||||
E('h2', {}, _('Streamlit Platform')),
|
E('h2', {}, _('Streamlit Platform')),
|
||||||
E('div', { 'class': 'cbi-map-descr' }, _('Python data app hosting')),
|
E('div', { 'class': 'cbi-map-descr' }, _('Python data app hosting with multi-instance support')),
|
||||||
|
|
||||||
// Status Section
|
// Status Section
|
||||||
E('div', { 'class': 'cbi-section', 'id': 'status-section' }, [
|
E('div', { 'class': 'cbi-section', 'id': 'status-section' }, [
|
||||||
@ -59,6 +65,18 @@ return view.extend({
|
|||||||
E('div', { 'class': 'cbi-page-actions' }, this.renderControls(installed, running))
|
E('div', { 'class': 'cbi-page-actions' }, this.renderControls(installed, running))
|
||||||
]),
|
]),
|
||||||
|
|
||||||
|
// Instances Section
|
||||||
|
E('div', { 'class': 'cbi-section', 'id': 'instances-section' }, [
|
||||||
|
E('h3', {}, _('Running Instances')),
|
||||||
|
E('div', { 'id': 'instances-table' }, this.renderInstancesTable())
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Add Instance Section
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, _('Add Instance')),
|
||||||
|
this.renderAddInstanceForm()
|
||||||
|
]),
|
||||||
|
|
||||||
// Apps Section
|
// Apps Section
|
||||||
E('div', { 'class': 'cbi-section', 'id': 'apps-section' }, [
|
E('div', { 'class': 'cbi-section', 'id': 'apps-section' }, [
|
||||||
E('h3', {}, _('Applications')),
|
E('h3', {}, _('Applications')),
|
||||||
@ -69,16 +87,23 @@ return view.extend({
|
|||||||
E('div', { 'class': 'cbi-section' }, [
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
E('h3', {}, _('Upload App')),
|
E('h3', {}, _('Upload App')),
|
||||||
E('div', { 'class': 'cbi-value' }, [
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
E('label', { 'class': 'cbi-value-title' }, _('Python File')),
|
E('label', { 'class': 'cbi-value-title' }, _('File')),
|
||||||
E('div', { 'class': 'cbi-value-field' }, [
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
E('input', { 'type': 'file', 'id': 'upload-file', 'accept': '.py' }),
|
E('input', { 'type': 'file', 'id': 'upload-file', 'accept': '.py,.zip' }),
|
||||||
E('button', {
|
E('button', {
|
||||||
'class': 'cbi-button cbi-button-action',
|
'class': 'cbi-button cbi-button-action',
|
||||||
'style': 'margin-left: 8px',
|
'style': 'margin-left: 8px',
|
||||||
'click': function() { self.uploadApp(); }
|
'click': function() { self.uploadApp(); }
|
||||||
}, _('Upload'))
|
}, _('Upload'))
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value-description' }, _('Accepts .py files or .zip archives'))
|
||||||
])
|
])
|
||||||
])
|
]),
|
||||||
|
|
||||||
|
// Gitea Section
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, _('Clone from Gitea')),
|
||||||
|
this.renderGiteaSection()
|
||||||
])
|
])
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -127,6 +152,105 @@ return view.extend({
|
|||||||
return btns;
|
return btns;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderInstancesTable: function() {
|
||||||
|
var self = this;
|
||||||
|
var instances = this.instances;
|
||||||
|
|
||||||
|
if (!instances.length) {
|
||||||
|
return E('em', {}, _('No instances configured. Add one below.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
var rows = instances.map(function(inst) {
|
||||||
|
var statusIcon = inst.enabled ?
|
||||||
|
E('span', { 'style': 'color:#0a0' }, '\u25CF') :
|
||||||
|
E('span', { 'style': 'color:#999' }, '\u25CB');
|
||||||
|
|
||||||
|
return E('tr', {}, [
|
||||||
|
E('td', {}, [
|
||||||
|
E('strong', {}, inst.id),
|
||||||
|
inst.name && inst.name !== inst.id ?
|
||||||
|
E('span', { 'style': 'color:#666; margin-left:4px' }, '(' + inst.name + ')') : ''
|
||||||
|
]),
|
||||||
|
E('td', {}, inst.app || '-'),
|
||||||
|
E('td', {}, ':' + inst.port),
|
||||||
|
E('td', { 'style': 'text-align:center' }, [
|
||||||
|
statusIcon,
|
||||||
|
E('span', { 'style': 'margin-left:4px' }, inst.enabled ? _('Enabled') : _('Disabled'))
|
||||||
|
]),
|
||||||
|
E('td', {}, [
|
||||||
|
inst.enabled ?
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button',
|
||||||
|
'click': function() { self.disableInstance(inst.id); }
|
||||||
|
}, _('Disable')) :
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-positive',
|
||||||
|
'click': function() { self.enableInstance(inst.id); }
|
||||||
|
}, _('Enable')),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-remove',
|
||||||
|
'style': 'margin-left: 4px',
|
||||||
|
'click': function() { self.deleteInstance(inst.id); }
|
||||||
|
}, _('Delete'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return E('table', { 'class': 'table cbi-section-table' }, [
|
||||||
|
E('tr', { 'class': 'tr table-titles' }, [
|
||||||
|
E('th', { 'class': 'th' }, _('ID')),
|
||||||
|
E('th', { 'class': 'th' }, _('App')),
|
||||||
|
E('th', { 'class': 'th' }, _('Port')),
|
||||||
|
E('th', { 'class': 'th', 'style': 'text-align:center' }, _('Status')),
|
||||||
|
E('th', { 'class': 'th' }, _('Actions'))
|
||||||
|
])
|
||||||
|
].concat(rows));
|
||||||
|
},
|
||||||
|
|
||||||
|
renderAddInstanceForm: function() {
|
||||||
|
var self = this;
|
||||||
|
var appOptions = [E('option', { 'value': '' }, _('-- Select App --'))];
|
||||||
|
|
||||||
|
this.apps.forEach(function(app) {
|
||||||
|
appOptions.push(E('option', { 'value': app.name }, app.name));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Calculate next available port
|
||||||
|
var usedPorts = this.instances.map(function(i) { return i.port; });
|
||||||
|
var nextPort = 8501;
|
||||||
|
while (usedPorts.indexOf(nextPort) !== -1) {
|
||||||
|
nextPort++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return E('div', {}, [
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Instance ID')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' },
|
||||||
|
E('input', { 'type': 'text', 'id': 'new-inst-id', 'class': 'cbi-input-text', 'placeholder': _('myapp') })
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('App')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' },
|
||||||
|
E('select', { 'id': 'new-inst-app', 'class': 'cbi-input-select' }, appOptions)
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Port')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' },
|
||||||
|
E('input', { 'type': 'number', 'id': 'new-inst-port', 'class': 'cbi-input-text',
|
||||||
|
'value': nextPort, 'min': '1024', 'max': '65535' })
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-page-actions' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-positive',
|
||||||
|
'click': function() { self.addInstance(); }
|
||||||
|
}, _('Add Instance'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
renderAppsTable: function() {
|
renderAppsTable: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
var apps = this.apps;
|
var apps = this.apps;
|
||||||
@ -166,11 +290,52 @@ return view.extend({
|
|||||||
].concat(rows));
|
].concat(rows));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
renderGiteaSection: function() {
|
||||||
|
var self = this;
|
||||||
|
var cfg = this.giteaConfig;
|
||||||
|
var enabled = cfg.enabled;
|
||||||
|
|
||||||
|
return E('div', {}, [
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Status')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' },
|
||||||
|
enabled ?
|
||||||
|
E('span', { 'style': 'color:#0a0' }, _('Configured')) :
|
||||||
|
E('span', { 'style': 'color:#999' }, _('Not configured'))
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
enabled ? E('div', {}, [
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Repository')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' },
|
||||||
|
E('input', { 'type': 'text', 'id': 'gitea-repo', 'class': 'cbi-input-text',
|
||||||
|
'placeholder': _('owner/repo or full URL') })
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('App Name')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' },
|
||||||
|
E('input', { 'type': 'text', 'id': 'gitea-name', 'class': 'cbi-input-text',
|
||||||
|
'placeholder': _('myapp') })
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
E('div', { 'class': 'cbi-page-actions' }, [
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-positive',
|
||||||
|
'click': function() { self.giteaClone(); }
|
||||||
|
}, _('Clone'))
|
||||||
|
])
|
||||||
|
]) : E('div', { 'class': 'cbi-value-description' },
|
||||||
|
_('Configure Gitea in Settings to enable cloning'))
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
updateStatus: function() {
|
updateStatus: function() {
|
||||||
var s = this.status;
|
var s = this.status;
|
||||||
var statusEl = document.getElementById('svc-status');
|
var statusEl = document.getElementById('svc-status');
|
||||||
var activeEl = document.getElementById('active-app');
|
var activeEl = document.getElementById('active-app');
|
||||||
var appsEl = document.getElementById('apps-table');
|
var appsEl = document.getElementById('apps-table');
|
||||||
|
var instancesEl = document.getElementById('instances-table');
|
||||||
|
|
||||||
if (statusEl) {
|
if (statusEl) {
|
||||||
statusEl.innerHTML = '';
|
statusEl.innerHTML = '';
|
||||||
@ -191,6 +356,11 @@ return view.extend({
|
|||||||
appsEl.innerHTML = '';
|
appsEl.innerHTML = '';
|
||||||
appsEl.appendChild(this.renderAppsTable());
|
appsEl.appendChild(this.renderAppsTable());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (instancesEl) {
|
||||||
|
instancesEl.innerHTML = '';
|
||||||
|
instancesEl.appendChild(this.renderInstancesTable());
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
doStart: function() {
|
doStart: function() {
|
||||||
@ -280,6 +450,91 @@ return view.extend({
|
|||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
addInstance: function() {
|
||||||
|
var self = this;
|
||||||
|
var id = document.getElementById('new-inst-id').value.trim();
|
||||||
|
var app = document.getElementById('new-inst-app').value;
|
||||||
|
var port = parseInt(document.getElementById('new-inst-port').value, 10);
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Please enter an instance ID')), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^[a-zA-Z0-9_]+$/.test(id)) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('ID can only contain letters, numbers, and underscores')), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Please select an app')), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!port || port < 1024 || port > 65535) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Please enter a valid port (1024-65535)')), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
api.addInstance(id, id, app, port).then(function(r) {
|
||||||
|
if (r && r.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Instance added: ') + id), 'success');
|
||||||
|
document.getElementById('new-inst-id').value = '';
|
||||||
|
document.getElementById('new-inst-app').value = '';
|
||||||
|
self.refresh().then(function() { self.updateStatus(); });
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, r.message || _('Failed to add instance')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
enableInstance: function(id) {
|
||||||
|
var self = this;
|
||||||
|
api.enableInstance(id).then(function(r) {
|
||||||
|
if (r && r.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Instance enabled: ') + id), 'success');
|
||||||
|
self.refresh().then(function() { self.updateStatus(); });
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, r.message || _('Failed')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
disableInstance: function(id) {
|
||||||
|
var self = this;
|
||||||
|
api.disableInstance(id).then(function(r) {
|
||||||
|
if (r && r.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Instance disabled: ') + id), 'success');
|
||||||
|
self.refresh().then(function() { self.updateStatus(); });
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, r.message || _('Failed')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteInstance: function(id) {
|
||||||
|
var self = this;
|
||||||
|
ui.showModal(_('Confirm'), [
|
||||||
|
E('p', {}, _('Delete instance: ') + id + '?'),
|
||||||
|
E('div', { 'class': 'right' }, [
|
||||||
|
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, _('Cancel')),
|
||||||
|
E('button', {
|
||||||
|
'class': 'cbi-button cbi-button-negative',
|
||||||
|
'style': 'margin-left: 8px',
|
||||||
|
'click': function() {
|
||||||
|
ui.hideModal();
|
||||||
|
api.removeInstance(id).then(function(r) {
|
||||||
|
if (r && r.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Instance deleted')), 'info');
|
||||||
|
}
|
||||||
|
self.refresh().then(function() { self.updateStatus(); });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, _('Delete'))
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
|
||||||
activateApp: function(name) {
|
activateApp: function(name) {
|
||||||
var self = this;
|
var self = this;
|
||||||
api.setActiveApp(name).then(function(r) {
|
api.setActiveApp(name).then(function(r) {
|
||||||
@ -288,7 +543,7 @@ return view.extend({
|
|||||||
return api.restart();
|
return api.restart();
|
||||||
}
|
}
|
||||||
}).then(function() {
|
}).then(function() {
|
||||||
self.refresh();
|
self.refresh().then(function() { self.updateStatus(); });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -307,7 +562,7 @@ return view.extend({
|
|||||||
if (r && r.success) {
|
if (r && r.success) {
|
||||||
ui.addNotification(null, E('p', {}, _('App deleted')), 'info');
|
ui.addNotification(null, E('p', {}, _('App deleted')), 'info');
|
||||||
}
|
}
|
||||||
self.refresh();
|
self.refresh().then(function() { self.updateStatus(); });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, _('Delete'))
|
}, _('Delete'))
|
||||||
@ -324,22 +579,63 @@ return view.extend({
|
|||||||
}
|
}
|
||||||
|
|
||||||
var file = fileInput.files[0];
|
var file = fileInput.files[0];
|
||||||
var name = file.name.replace(/\.py$/, '');
|
var name = file.name.replace(/\.(py|zip)$/, '');
|
||||||
|
var isZip = file.name.endsWith('.zip');
|
||||||
var reader = new FileReader();
|
var reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = function(e) {
|
reader.onload = function(e) {
|
||||||
var content = btoa(e.target.result);
|
var content = btoa(e.target.result);
|
||||||
api.uploadApp(name, content).then(function(r) {
|
var uploadFn = isZip ? api.uploadZip(name, content, null) : api.uploadApp(name, content);
|
||||||
|
|
||||||
|
uploadFn.then(function(r) {
|
||||||
if (r && r.success) {
|
if (r && r.success) {
|
||||||
ui.addNotification(null, E('p', {}, _('App uploaded: ') + name), 'success');
|
ui.addNotification(null, E('p', {}, _('App uploaded: ') + name), 'success');
|
||||||
fileInput.value = '';
|
fileInput.value = '';
|
||||||
self.refresh();
|
self.refresh().then(function() { self.updateStatus(); });
|
||||||
} else {
|
} else {
|
||||||
ui.addNotification(null, E('p', {}, r.message || _('Upload failed')), 'error');
|
ui.addNotification(null, E('p', {}, r.message || _('Upload failed')), 'error');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isZip) {
|
||||||
|
reader.readAsBinaryString(file);
|
||||||
|
} else {
|
||||||
reader.readAsText(file);
|
reader.readAsText(file);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
giteaClone: function() {
|
||||||
|
var self = this;
|
||||||
|
var repo = document.getElementById('gitea-repo').value.trim();
|
||||||
|
var name = document.getElementById('gitea-name').value.trim();
|
||||||
|
|
||||||
|
if (!repo) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Please enter a repository')), 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name) {
|
||||||
|
// Extract name from repo
|
||||||
|
name = repo.split('/').pop().replace(/\.git$/, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.showModal(_('Cloning'), [
|
||||||
|
E('p', { 'class': 'spinning' }, _('Cloning ') + repo + '...')
|
||||||
|
]);
|
||||||
|
|
||||||
|
api.giteaClone(name, repo).then(function(r) {
|
||||||
|
ui.hideModal();
|
||||||
|
if (r && r.success) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Clone started: ') + name), 'success');
|
||||||
|
document.getElementById('gitea-repo').value = '';
|
||||||
|
document.getElementById('gitea-name').value = '';
|
||||||
|
setTimeout(function() {
|
||||||
|
self.refresh().then(function() { self.updateStatus(); });
|
||||||
|
}, 3000);
|
||||||
|
} else {
|
||||||
|
ui.addNotification(null, E('p', {}, r.message || _('Clone failed')), 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -5,18 +5,24 @@
|
|||||||
|
|
||||||
return view.extend({
|
return view.extend({
|
||||||
config: {},
|
config: {},
|
||||||
|
giteaConfig: {},
|
||||||
|
|
||||||
load: function() {
|
load: function() {
|
||||||
return api.getConfig().then(function(c) {
|
var self = this;
|
||||||
return c || {};
|
return Promise.all([
|
||||||
|
api.getConfig().then(function(c) { return c || {}; }),
|
||||||
|
api.getGiteaConfig().catch(function() { return {}; })
|
||||||
|
]).then(function(r) {
|
||||||
|
self.config = r[0];
|
||||||
|
self.giteaConfig = r[1];
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
render: function(config) {
|
render: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
this.config = config;
|
var main = this.config.main || {};
|
||||||
var main = config.main || {};
|
var server = this.config.server || {};
|
||||||
var server = config.server || {};
|
var gitea = this.giteaConfig || {};
|
||||||
|
|
||||||
return E('div', { 'class': 'cbi-map' }, [
|
return E('div', { 'class': 'cbi-map' }, [
|
||||||
E('h2', {}, _('Streamlit Settings')),
|
E('h2', {}, _('Streamlit Settings')),
|
||||||
@ -113,7 +119,49 @@ return view.extend({
|
|||||||
])
|
])
|
||||||
]),
|
]),
|
||||||
|
|
||||||
// Save button
|
// Gitea Settings
|
||||||
|
E('div', { 'class': 'cbi-section' }, [
|
||||||
|
E('h3', {}, _('Gitea Integration')),
|
||||||
|
E('div', { 'class': 'cbi-section-descr' }, _('Configure Gitea to clone apps from repositories')),
|
||||||
|
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Enabled')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' },
|
||||||
|
E('select', { 'id': 'cfg-gitea-enabled', 'class': 'cbi-input-select' }, [
|
||||||
|
E('option', { 'value': '1', 'selected': gitea.enabled == true || gitea.enabled == '1' }, _('Yes')),
|
||||||
|
E('option', { 'value': '0', 'selected': !gitea.enabled || gitea.enabled == '0' }, _('No'))
|
||||||
|
])
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Gitea URL')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' },
|
||||||
|
E('input', { 'type': 'text', 'id': 'cfg-gitea-url', 'class': 'cbi-input-text',
|
||||||
|
'value': gitea.url || '', 'placeholder': 'http://192.168.255.1:3000' })
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Username')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' },
|
||||||
|
E('input', { 'type': 'text', 'id': 'cfg-gitea-user', 'class': 'cbi-input-text',
|
||||||
|
'value': gitea.user || '', 'placeholder': 'admin' })
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
|
||||||
|
E('div', { 'class': 'cbi-value' }, [
|
||||||
|
E('label', { 'class': 'cbi-value-title' }, _('Access Token')),
|
||||||
|
E('div', { 'class': 'cbi-value-field' }, [
|
||||||
|
E('input', { 'type': 'password', 'id': 'cfg-gitea-token', 'class': 'cbi-input-text',
|
||||||
|
'value': '', 'placeholder': gitea.has_token ? _('(token configured)') : _('Enter token') }),
|
||||||
|
E('div', { 'class': 'cbi-value-description' },
|
||||||
|
_('Generate from Gitea: Settings > Applications > Generate Token'))
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
|
||||||
|
// Save buttons
|
||||||
E('div', { 'class': 'cbi-page-actions' }, [
|
E('div', { 'class': 'cbi-page-actions' }, [
|
||||||
E('button', {
|
E('button', {
|
||||||
'class': 'cbi-button cbi-button-positive',
|
'class': 'cbi-button cbi-button-positive',
|
||||||
@ -124,6 +172,9 @@ return view.extend({
|
|||||||
},
|
},
|
||||||
|
|
||||||
save: function() {
|
save: function() {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Save main config
|
||||||
var cfg = {
|
var cfg = {
|
||||||
enabled: document.getElementById('cfg-enabled').value,
|
enabled: document.getElementById('cfg-enabled').value,
|
||||||
http_port: document.getElementById('cfg-port').value,
|
http_port: document.getElementById('cfg-port').value,
|
||||||
@ -137,12 +188,24 @@ return view.extend({
|
|||||||
theme_primary_color: document.getElementById('cfg-color').value
|
theme_primary_color: document.getElementById('cfg-color').value
|
||||||
};
|
};
|
||||||
|
|
||||||
api.saveConfig(cfg).then(function(r) {
|
// Save Gitea config
|
||||||
|
var giteaEnabled = document.getElementById('cfg-gitea-enabled').value;
|
||||||
|
var giteaUrl = document.getElementById('cfg-gitea-url').value;
|
||||||
|
var giteaUser = document.getElementById('cfg-gitea-user').value;
|
||||||
|
var giteaToken = document.getElementById('cfg-gitea-token').value;
|
||||||
|
|
||||||
|
Promise.all([
|
||||||
|
api.saveConfig(cfg),
|
||||||
|
api.saveGiteaConfig(giteaEnabled, giteaUrl, giteaUser, giteaToken || '')
|
||||||
|
]).then(function(results) {
|
||||||
|
var r = results[0];
|
||||||
if (r && r.success) {
|
if (r && r.success) {
|
||||||
ui.addNotification(null, E('p', {}, _('Settings saved')), 'info');
|
ui.addNotification(null, E('p', {}, _('Settings saved')), 'info');
|
||||||
} else {
|
} else {
|
||||||
ui.addNotification(null, E('p', {}, r.message || _('Save failed')), 'error');
|
ui.addNotification(null, E('p', {}, r.message || _('Save failed')), 'error');
|
||||||
}
|
}
|
||||||
|
}).catch(function(err) {
|
||||||
|
ui.addNotification(null, E('p', {}, _('Save failed: ') + err.message), 'error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -712,6 +712,130 @@ upload_zip() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Get Gitea config
|
||||||
|
get_gitea_config() {
|
||||||
|
config_load "$CONFIG"
|
||||||
|
local enabled url user token
|
||||||
|
|
||||||
|
config_get enabled gitea enabled "0"
|
||||||
|
config_get url gitea url ""
|
||||||
|
config_get user gitea user ""
|
||||||
|
config_get token gitea token ""
|
||||||
|
|
||||||
|
json_init_obj
|
||||||
|
json_add_boolean "enabled" "$( [ "$enabled" = "1" ] && echo 1 || echo 0 )"
|
||||||
|
json_add_string "url" "$url"
|
||||||
|
json_add_string "user" "$user"
|
||||||
|
json_add_boolean "has_token" "$( [ -n "$token" ] && echo 1 || echo 0 )"
|
||||||
|
json_close_obj
|
||||||
|
}
|
||||||
|
|
||||||
|
# Save Gitea config
|
||||||
|
save_gitea_config() {
|
||||||
|
read -r input
|
||||||
|
local enabled url user token
|
||||||
|
|
||||||
|
enabled=$(echo "$input" | jsonfilter -e '@.enabled' 2>/dev/null)
|
||||||
|
url=$(echo "$input" | jsonfilter -e '@.url' 2>/dev/null)
|
||||||
|
user=$(echo "$input" | jsonfilter -e '@.user' 2>/dev/null)
|
||||||
|
token=$(echo "$input" | jsonfilter -e '@.token' 2>/dev/null)
|
||||||
|
|
||||||
|
# Ensure gitea section exists
|
||||||
|
uci -q get "${CONFIG}.gitea" >/dev/null || uci set "${CONFIG}.gitea=gitea"
|
||||||
|
|
||||||
|
[ -n "$enabled" ] && uci set "${CONFIG}.gitea.enabled=$enabled"
|
||||||
|
[ -n "$url" ] && uci set "${CONFIG}.gitea.url=$url"
|
||||||
|
[ -n "$user" ] && uci set "${CONFIG}.gitea.user=$user"
|
||||||
|
[ -n "$token" ] && uci set "${CONFIG}.gitea.token=$token"
|
||||||
|
|
||||||
|
uci commit "$CONFIG"
|
||||||
|
|
||||||
|
json_success "Gitea configuration saved"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Clone app from Gitea
|
||||||
|
gitea_clone() {
|
||||||
|
read -r input
|
||||||
|
local name repo
|
||||||
|
|
||||||
|
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||||
|
repo=$(echo "$input" | jsonfilter -e '@.repo' 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$name" ] || [ -z "$repo" ]; then
|
||||||
|
json_error "Missing name or repo"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run clone in background
|
||||||
|
/usr/sbin/streamlitctl gitea clone "$name" "$repo" >/var/log/streamlit-gitea.log 2>&1 &
|
||||||
|
|
||||||
|
json_init_obj
|
||||||
|
json_add_boolean "success" 1
|
||||||
|
json_add_string "message" "Cloning $repo to $name in background"
|
||||||
|
json_close_obj
|
||||||
|
}
|
||||||
|
|
||||||
|
# Pull app from Gitea
|
||||||
|
gitea_pull() {
|
||||||
|
read -r input
|
||||||
|
local name
|
||||||
|
|
||||||
|
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$name" ]; then
|
||||||
|
json_error "Missing app name"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run pull
|
||||||
|
/usr/sbin/streamlitctl gitea pull "$name" >/var/log/streamlit-gitea.log 2>&1
|
||||||
|
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
json_success "App updated from Gitea: $name"
|
||||||
|
else
|
||||||
|
json_error "Failed to pull app from Gitea"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# List Gitea repositories
|
||||||
|
gitea_list_repos() {
|
||||||
|
config_load "$CONFIG"
|
||||||
|
local enabled url user token
|
||||||
|
|
||||||
|
config_get enabled gitea enabled "0"
|
||||||
|
config_get url gitea url ""
|
||||||
|
config_get user gitea user ""
|
||||||
|
config_get token gitea token ""
|
||||||
|
|
||||||
|
if [ "$enabled" != "1" ] || [ -z "$url" ] || [ -z "$token" ]; then
|
||||||
|
json_error "Gitea not configured"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Call Gitea API to list user repos
|
||||||
|
local api_url="${url}/api/v1/user/repos"
|
||||||
|
local response
|
||||||
|
|
||||||
|
response=$(curl -s -H "Authorization: token $token" "$api_url" 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -z "$response" ]; then
|
||||||
|
json_error "Failed to connect to Gitea"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
json_init_obj
|
||||||
|
json_add_array "repos"
|
||||||
|
|
||||||
|
# Parse JSON response (simple extraction)
|
||||||
|
echo "$response" | jsonfilter -e '@[*].full_name' 2>/dev/null | while read -r repo; do
|
||||||
|
[ -z "$repo" ] && continue
|
||||||
|
json_add_string "" "$repo"
|
||||||
|
done
|
||||||
|
|
||||||
|
json_close_array
|
||||||
|
json_close_obj
|
||||||
|
}
|
||||||
|
|
||||||
# Check install progress
|
# Check install progress
|
||||||
get_install_progress() {
|
get_install_progress() {
|
||||||
local log_file="/var/log/streamlit-install.log"
|
local log_file="/var/log/streamlit-install.log"
|
||||||
@ -793,7 +917,12 @@ case "$1" in
|
|||||||
"add_instance": {"id": "str", "name": "str", "app": "str", "port": 8501},
|
"add_instance": {"id": "str", "name": "str", "app": "str", "port": 8501},
|
||||||
"remove_instance": {"id": "str"},
|
"remove_instance": {"id": "str"},
|
||||||
"enable_instance": {"id": "str"},
|
"enable_instance": {"id": "str"},
|
||||||
"disable_instance": {"id": "str"}
|
"disable_instance": {"id": "str"},
|
||||||
|
"get_gitea_config": {},
|
||||||
|
"save_gitea_config": {"enabled": "str", "url": "str", "user": "str", "token": "str"},
|
||||||
|
"gitea_clone": {"name": "str", "repo": "str"},
|
||||||
|
"gitea_pull": {"name": "str"},
|
||||||
|
"gitea_list_repos": {}
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
;;
|
;;
|
||||||
@ -871,6 +1000,21 @@ case "$1" in
|
|||||||
disable_instance)
|
disable_instance)
|
||||||
disable_instance
|
disable_instance
|
||||||
;;
|
;;
|
||||||
|
get_gitea_config)
|
||||||
|
get_gitea_config
|
||||||
|
;;
|
||||||
|
save_gitea_config)
|
||||||
|
save_gitea_config
|
||||||
|
;;
|
||||||
|
gitea_clone)
|
||||||
|
gitea_clone
|
||||||
|
;;
|
||||||
|
gitea_pull)
|
||||||
|
gitea_pull
|
||||||
|
;;
|
||||||
|
gitea_list_repos)
|
||||||
|
gitea_list_repos
|
||||||
|
;;
|
||||||
*)
|
*)
|
||||||
json_error "Unknown method: $2"
|
json_error "Unknown method: $2"
|
||||||
;;
|
;;
|
||||||
|
|||||||
Binary file not shown.
Loading…
Reference in New Issue
Block a user