feat(streamlit): Add ZIP upload with selective tree extraction
- apps.js: ZIP file upload with tree view file selection - Client-side ZIP parsing for file list preview - Interactive tree with checkboxes for file selection - Select All / Deselect All / Python Only buttons - Supports both .py and .zip file uploads - api.js: Added previewZip() and uploadZip() RPC methods - luci.streamlit RPCD: - preview_zip: List ZIP contents with file sizes - upload_zip: Extract selected files to app directory - Automatic main .py file detection and registration Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a8ca00b04d
commit
d2805c35bd
@ -110,6 +110,20 @@ var callUploadApp = rpc.declare({
|
||||
expect: { result: {} }
|
||||
});
|
||||
|
||||
var callUploadZip = rpc.declare({
|
||||
object: 'luci.streamlit',
|
||||
method: 'upload_zip',
|
||||
params: ['name', 'content', 'selected_files'],
|
||||
expect: { result: {} }
|
||||
});
|
||||
|
||||
var callPreviewZip = rpc.declare({
|
||||
object: 'luci.streamlit',
|
||||
method: 'preview_zip',
|
||||
params: ['content'],
|
||||
expect: { result: {} }
|
||||
});
|
||||
|
||||
var callGetInstallProgress = rpc.declare({
|
||||
object: 'luci.streamlit',
|
||||
method: 'get_install_progress',
|
||||
@ -234,6 +248,14 @@ return baseclass.extend({
|
||||
return callUploadApp(name, content);
|
||||
},
|
||||
|
||||
uploadZip: function(name, content, selectedFiles) {
|
||||
return callUploadZip(name, content, selectedFiles);
|
||||
},
|
||||
|
||||
previewZip: function(content) {
|
||||
return callPreviewZip(content);
|
||||
},
|
||||
|
||||
getInstallProgress: function() {
|
||||
return callGetInstallProgress();
|
||||
},
|
||||
|
||||
@ -231,14 +231,14 @@ return view.extend({
|
||||
}, [
|
||||
E('div', { 'class': 'st-upload-icon' }, '\uD83D\uDCC1'),
|
||||
E('div', { 'class': 'st-upload-text' }, [
|
||||
E('p', {}, _('Drop your .py file here or click to browse')),
|
||||
E('p', { 'style': 'font-size: 12px; color: #64748b;' }, _('Supported: Python (.py) files'))
|
||||
E('p', {}, _('Drop your .py or .zip file here or click to browse')),
|
||||
E('p', { 'style': 'font-size: 12px; color: #64748b;' }, _('Supported: Python (.py) or ZIP archive (.zip)'))
|
||||
])
|
||||
]),
|
||||
E('input', {
|
||||
'type': 'file',
|
||||
'id': 'upload-file',
|
||||
'accept': '.py',
|
||||
'accept': '.py,.zip',
|
||||
'style': 'display: none;',
|
||||
'change': function(e) {
|
||||
if (e.target.files.length > 0) {
|
||||
@ -319,15 +319,19 @@ return view.extend({
|
||||
handleFileSelect: function(file) {
|
||||
var self = this;
|
||||
|
||||
if (!file.name.endsWith('.py')) {
|
||||
ui.addNotification(null, E('p', {}, _('Please select a Python (.py) file')), 'error');
|
||||
// Check for valid file types
|
||||
var isPy = file.name.endsWith('.py');
|
||||
var isZip = file.name.endsWith('.zip');
|
||||
|
||||
if (!isPy && !isZip) {
|
||||
ui.addNotification(null, E('p', {}, _('Please select a Python (.py) or ZIP (.zip) file')), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
var nameInput = document.getElementById('upload-name');
|
||||
if (!nameInput.value) {
|
||||
// Auto-fill name from filename
|
||||
nameInput.value = file.name.replace('.py', '');
|
||||
nameInput.value = file.name.replace(/\.(py|zip)$/, '');
|
||||
}
|
||||
|
||||
var statusDiv = document.getElementById('upload-status');
|
||||
@ -336,12 +340,342 @@ return view.extend({
|
||||
statusDiv.appendChild(E('p', { 'style': 'color: #0ff;' },
|
||||
_('Selected: ') + file.name + ' (' + this.formatSize(file.size) + ')'));
|
||||
|
||||
// Add upload button
|
||||
statusDiv.appendChild(E('button', {
|
||||
'class': 'st-btn st-btn-success',
|
||||
'style': 'margin-top: 10px;',
|
||||
'click': function() { self.uploadFile(file); }
|
||||
}, [E('span', {}, '\uD83D\uDCE4'), ' ' + _('Upload App')]));
|
||||
if (isZip) {
|
||||
// For ZIP files, show tree selection dialog
|
||||
statusDiv.appendChild(E('button', {
|
||||
'class': 'st-btn st-btn-success',
|
||||
'style': 'margin-top: 10px;',
|
||||
'click': function() { self.showZipTreeDialog(file); }
|
||||
}, [E('span', {}, '\uD83D\uDCC2'), ' ' + _('Select Files from ZIP')]));
|
||||
} else {
|
||||
// For .py files, direct upload
|
||||
statusDiv.appendChild(E('button', {
|
||||
'class': 'st-btn st-btn-success',
|
||||
'style': 'margin-top: 10px;',
|
||||
'click': function() { self.uploadFile(file); }
|
||||
}, [E('span', {}, '\uD83D\uDCE4'), ' ' + _('Upload App')]));
|
||||
}
|
||||
},
|
||||
|
||||
pendingZipFile: null,
|
||||
pendingZipContent: null,
|
||||
zipTreeData: [],
|
||||
|
||||
showZipTreeDialog: function(file) {
|
||||
var self = this;
|
||||
|
||||
ui.showModal(_('Loading ZIP...'), [
|
||||
E('p', { 'class': 'spinning' }, _('Reading ZIP archive contents...'))
|
||||
]);
|
||||
|
||||
var reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
var bytes = new Uint8Array(e.target.result);
|
||||
var binary = '';
|
||||
for (var i = 0; i < bytes.byteLength; i++) {
|
||||
binary += String.fromCharCode(bytes[i]);
|
||||
}
|
||||
var content = btoa(binary);
|
||||
self.pendingZipContent = content;
|
||||
self.pendingZipFile = file;
|
||||
|
||||
// Call backend to preview ZIP contents
|
||||
api.previewZip(content).then(function(result) {
|
||||
ui.hideModal();
|
||||
if (result && result.files) {
|
||||
self.zipTreeData = result.files;
|
||||
self.renderZipTreeModal(result.files);
|
||||
} else if (result && result.error) {
|
||||
ui.addNotification(null, E('p', {}, _('Failed to read ZIP: ') + result.error), 'error');
|
||||
} else {
|
||||
// Fallback: parse ZIP client-side using JSZip-like approach
|
||||
self.parseZipClientSide(content);
|
||||
}
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
// Fallback: try client-side parsing
|
||||
self.parseZipClientSide(content);
|
||||
});
|
||||
};
|
||||
reader.readAsArrayBuffer(file);
|
||||
},
|
||||
|
||||
parseZipClientSide: function(content) {
|
||||
var self = this;
|
||||
// Basic ZIP parsing - extract file list from central directory
|
||||
try {
|
||||
var raw = atob(content);
|
||||
var files = [];
|
||||
var offset = 0;
|
||||
|
||||
// Find End of Central Directory
|
||||
var eocdOffset = raw.lastIndexOf('PK\x05\x06');
|
||||
if (eocdOffset === -1) {
|
||||
throw new Error('Invalid ZIP file');
|
||||
}
|
||||
|
||||
// Read number of entries
|
||||
var numEntries = raw.charCodeAt(eocdOffset + 10) | (raw.charCodeAt(eocdOffset + 11) << 8);
|
||||
var cdOffset = raw.charCodeAt(eocdOffset + 16) | (raw.charCodeAt(eocdOffset + 17) << 8) |
|
||||
(raw.charCodeAt(eocdOffset + 18) << 16) | (raw.charCodeAt(eocdOffset + 19) << 24);
|
||||
|
||||
offset = cdOffset;
|
||||
for (var i = 0; i < numEntries && offset < eocdOffset; i++) {
|
||||
if (raw.substr(offset, 4) !== 'PK\x01\x02') break;
|
||||
|
||||
var nameLen = raw.charCodeAt(offset + 28) | (raw.charCodeAt(offset + 29) << 8);
|
||||
var extraLen = raw.charCodeAt(offset + 30) | (raw.charCodeAt(offset + 31) << 8);
|
||||
var commentLen = raw.charCodeAt(offset + 32) | (raw.charCodeAt(offset + 33) << 8);
|
||||
var compSize = raw.charCodeAt(offset + 20) | (raw.charCodeAt(offset + 21) << 8) |
|
||||
(raw.charCodeAt(offset + 22) << 16) | (raw.charCodeAt(offset + 23) << 24);
|
||||
var uncompSize = raw.charCodeAt(offset + 24) | (raw.charCodeAt(offset + 25) << 8) |
|
||||
(raw.charCodeAt(offset + 26) << 16) | (raw.charCodeAt(offset + 27) << 24);
|
||||
|
||||
var fileName = raw.substr(offset + 46, nameLen);
|
||||
var isDir = fileName.endsWith('/');
|
||||
|
||||
files.push({
|
||||
path: fileName,
|
||||
size: uncompSize,
|
||||
is_dir: isDir,
|
||||
selected: fileName.endsWith('.py') || isDir
|
||||
});
|
||||
|
||||
offset += 46 + nameLen + extraLen + commentLen;
|
||||
}
|
||||
|
||||
self.zipTreeData = files;
|
||||
self.renderZipTreeModal(files);
|
||||
} catch (err) {
|
||||
ui.addNotification(null, E('p', {}, _('Failed to parse ZIP: ') + err.message), 'error');
|
||||
}
|
||||
},
|
||||
|
||||
renderZipTreeModal: function(files) {
|
||||
var self = this;
|
||||
var nameInput = document.getElementById('upload-name');
|
||||
var appName = nameInput ? nameInput.value.trim() : 'app';
|
||||
|
||||
// Build tree structure
|
||||
var tree = this.buildFileTree(files);
|
||||
var treeHtml = this.renderTreeNode(tree, '', 0);
|
||||
|
||||
ui.showModal(_('Select Files to Deploy'), [
|
||||
E('div', { 'style': 'margin-bottom: 16px;' }, [
|
||||
E('p', {}, _('Select which files to extract from the ZIP archive:')),
|
||||
E('div', { 'style': 'margin-top: 8px;' }, [
|
||||
E('button', {
|
||||
'class': 'st-btn',
|
||||
'style': 'padding: 4px 8px; font-size: 11px; margin-right: 8px;',
|
||||
'click': function() { self.selectAllZipFiles(true); }
|
||||
}, _('Select All')),
|
||||
E('button', {
|
||||
'class': 'st-btn',
|
||||
'style': 'padding: 4px 8px; font-size: 11px; margin-right: 8px;',
|
||||
'click': function() { self.selectAllZipFiles(false); }
|
||||
}, _('Deselect All')),
|
||||
E('button', {
|
||||
'class': 'st-btn',
|
||||
'style': 'padding: 4px 8px; font-size: 11px;',
|
||||
'click': function() { self.selectPythonFiles(); }
|
||||
}, _('Python Only'))
|
||||
])
|
||||
]),
|
||||
E('div', {
|
||||
'id': 'zip-tree-container',
|
||||
'style': 'max-height: 400px; overflow-y: auto; background: #0f172a; border: 1px solid #334155; border-radius: 4px; padding: 12px; font-family: monospace; font-size: 13px;'
|
||||
}, treeHtml),
|
||||
E('div', { 'style': 'margin-top: 16px; padding: 12px; background: #1e293b; border-radius: 4px;' }, [
|
||||
E('span', { 'id': 'zip-selected-count', 'style': 'color: #94a3b8;' },
|
||||
this.getSelectedCount() + ' ' + _('files selected'))
|
||||
]),
|
||||
E('div', { 'class': 'right', 'style': 'margin-top: 16px;' }, [
|
||||
E('button', {
|
||||
'class': 'btn',
|
||||
'click': function() {
|
||||
ui.hideModal();
|
||||
self.pendingZipFile = null;
|
||||
self.pendingZipContent = null;
|
||||
}
|
||||
}, _('Cancel')),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button-positive',
|
||||
'style': 'margin-left: 8px;',
|
||||
'click': function() { self.uploadSelectedZipFiles(); }
|
||||
}, ['\uD83D\uDCE4 ', _('Deploy Selected')])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
buildFileTree: function(files) {
|
||||
var root = { name: '', children: {}, files: [] };
|
||||
|
||||
files.forEach(function(file) {
|
||||
var parts = file.path.split('/').filter(function(p) { return p; });
|
||||
var current = root;
|
||||
|
||||
for (var i = 0; i < parts.length; i++) {
|
||||
var part = parts[i];
|
||||
var isLast = i === parts.length - 1;
|
||||
|
||||
if (isLast && !file.is_dir) {
|
||||
current.files.push({
|
||||
name: part,
|
||||
path: file.path,
|
||||
size: file.size,
|
||||
selected: file.selected !== false
|
||||
});
|
||||
} else {
|
||||
if (!current.children[part]) {
|
||||
current.children[part] = { name: part, children: {}, files: [], path: file.path };
|
||||
}
|
||||
current = current.children[part];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return root;
|
||||
},
|
||||
|
||||
renderTreeNode: function(node, indent, level) {
|
||||
var self = this;
|
||||
var elements = [];
|
||||
|
||||
// Render subdirectories
|
||||
var dirs = Object.keys(node.children).sort();
|
||||
dirs.forEach(function(dirName) {
|
||||
var dir = node.children[dirName];
|
||||
var dirPath = dir.path || (indent ? indent + '/' + dirName : dirName);
|
||||
|
||||
elements.push(E('div', {
|
||||
'class': 'zip-tree-dir',
|
||||
'style': 'padding: 4px 0; padding-left: ' + (level * 16) + 'px;',
|
||||
'data-path': dirPath
|
||||
}, [
|
||||
E('span', {
|
||||
'class': 'zip-tree-toggle',
|
||||
'style': 'cursor: pointer; color: #60a5fa; margin-right: 4px;',
|
||||
'click': function(e) {
|
||||
var next = this.parentNode.nextSibling;
|
||||
while (next && next.classList && next.classList.contains('zip-tree-child-' + level)) {
|
||||
next.style.display = next.style.display === 'none' ? 'block' : 'none';
|
||||
next = next.nextSibling;
|
||||
}
|
||||
this.textContent = this.textContent === '\u25BC' ? '\u25B6' : '\u25BC';
|
||||
}
|
||||
}, '\u25BC'),
|
||||
E('span', { 'style': 'color: #fbbf24;' }, '\uD83D\uDCC1 '),
|
||||
E('span', { 'style': 'color: #f1f5f9;' }, dirName + '/')
|
||||
]));
|
||||
|
||||
// Render children
|
||||
var childElements = self.renderTreeNode(dir, dirPath, level + 1);
|
||||
childElements.forEach(function(el) {
|
||||
el.classList.add('zip-tree-child-' + level);
|
||||
elements.push(el);
|
||||
});
|
||||
});
|
||||
|
||||
// Render files
|
||||
node.files.sort(function(a, b) { return a.name.localeCompare(b.name); });
|
||||
node.files.forEach(function(file) {
|
||||
var isPy = file.name.endsWith('.py');
|
||||
var icon = isPy ? '\uD83D\uDC0D' : '\uD83D\uDCC4';
|
||||
var color = isPy ? '#22c55e' : '#94a3b8';
|
||||
|
||||
elements.push(E('div', {
|
||||
'class': 'zip-tree-file',
|
||||
'style': 'padding: 4px 0; padding-left: ' + (level * 16) + 'px;'
|
||||
}, [
|
||||
E('label', { 'style': 'cursor: pointer; display: flex; align-items: center;' }, [
|
||||
E('input', {
|
||||
'type': 'checkbox',
|
||||
'class': 'zip-file-checkbox',
|
||||
'data-path': file.path,
|
||||
'checked': file.selected,
|
||||
'style': 'margin-right: 8px;',
|
||||
'change': function() { self.updateSelectedCount(); }
|
||||
}),
|
||||
E('span', { 'style': 'margin-right: 4px;' }, icon),
|
||||
E('span', { 'style': 'color: ' + color + ';' }, file.name),
|
||||
E('span', { 'style': 'color: #64748b; margin-left: 8px; font-size: 11px;' },
|
||||
'(' + self.formatSize(file.size) + ')')
|
||||
])
|
||||
]));
|
||||
});
|
||||
|
||||
return elements;
|
||||
},
|
||||
|
||||
selectAllZipFiles: function(select) {
|
||||
var checkboxes = document.querySelectorAll('.zip-file-checkbox');
|
||||
checkboxes.forEach(function(cb) { cb.checked = select; });
|
||||
this.updateSelectedCount();
|
||||
},
|
||||
|
||||
selectPythonFiles: function() {
|
||||
var checkboxes = document.querySelectorAll('.zip-file-checkbox');
|
||||
checkboxes.forEach(function(cb) {
|
||||
cb.checked = cb.dataset.path.endsWith('.py');
|
||||
});
|
||||
this.updateSelectedCount();
|
||||
},
|
||||
|
||||
getSelectedCount: function() {
|
||||
var checkboxes = document.querySelectorAll('.zip-file-checkbox:checked');
|
||||
return checkboxes ? checkboxes.length : 0;
|
||||
},
|
||||
|
||||
updateSelectedCount: function() {
|
||||
var countEl = document.getElementById('zip-selected-count');
|
||||
if (countEl) {
|
||||
countEl.textContent = this.getSelectedCount() + ' ' + _('files selected');
|
||||
}
|
||||
},
|
||||
|
||||
uploadSelectedZipFiles: function() {
|
||||
var self = this;
|
||||
var nameInput = document.getElementById('upload-name');
|
||||
var name = nameInput ? nameInput.value.trim() : '';
|
||||
|
||||
if (!name) {
|
||||
ui.addNotification(null, E('p', {}, _('Please enter an app name')), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Collect selected files
|
||||
var selectedFiles = [];
|
||||
var checkboxes = document.querySelectorAll('.zip-file-checkbox:checked');
|
||||
checkboxes.forEach(function(cb) {
|
||||
selectedFiles.push(cb.dataset.path);
|
||||
});
|
||||
|
||||
if (selectedFiles.length === 0) {
|
||||
ui.addNotification(null, E('p', {}, _('Please select at least one file')), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
ui.hideModal();
|
||||
ui.showModal(_('Deploying...'), [
|
||||
E('p', { 'class': 'spinning' }, _('Extracting and deploying selected files...'))
|
||||
]);
|
||||
|
||||
api.uploadZip(name, self.pendingZipContent, selectedFiles).then(function(result) {
|
||||
ui.hideModal();
|
||||
if (result && result.success) {
|
||||
ui.addNotification(null, E('p', {}, _('App deployed successfully: ') + name), 'success');
|
||||
nameInput.value = '';
|
||||
document.getElementById('upload-status').style.display = 'none';
|
||||
self.pendingZipFile = null;
|
||||
self.pendingZipContent = null;
|
||||
self.refreshData();
|
||||
} else {
|
||||
ui.addNotification(null, E('p', {}, result.message || result.error || _('Deploy failed')), 'error');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', {}, _('Deploy failed: ') + err.message), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
uploadFile: function(file) {
|
||||
|
||||
@ -604,6 +604,114 @@ disable_instance() {
|
||||
json_success "Instance disabled: $id"
|
||||
}
|
||||
|
||||
# Preview ZIP contents
|
||||
preview_zip() {
|
||||
read -r input
|
||||
local content
|
||||
content=$(echo "$input" | jsonfilter -e '@.content' 2>/dev/null)
|
||||
|
||||
if [ -z "$content" ]; then
|
||||
json_error "Missing content"
|
||||
return
|
||||
fi
|
||||
|
||||
# Write to temp file and list contents
|
||||
local tmpzip="/tmp/preview_$$.zip"
|
||||
echo "$content" | base64 -d > "$tmpzip" 2>/dev/null
|
||||
|
||||
if [ ! -f "$tmpzip" ]; then
|
||||
json_error "Failed to decode ZIP"
|
||||
return
|
||||
fi
|
||||
|
||||
json_init_obj
|
||||
json_add_array "files"
|
||||
|
||||
# Use unzip to list contents
|
||||
unzip -l "$tmpzip" 2>/dev/null | tail -n +4 | head -n -2 | while read -r size date time name; do
|
||||
[ -z "$name" ] && continue
|
||||
local is_dir=0
|
||||
echo "$name" | grep -q '/$' && is_dir=1
|
||||
|
||||
json_add_object ""
|
||||
json_add_string "path" "$name"
|
||||
json_add_int "size" "$size"
|
||||
json_add_boolean "is_dir" "$is_dir"
|
||||
json_close_object
|
||||
done
|
||||
|
||||
json_close_array
|
||||
json_close_obj
|
||||
|
||||
rm -f "$tmpzip"
|
||||
}
|
||||
|
||||
# Upload ZIP with selected files
|
||||
upload_zip() {
|
||||
read -r input
|
||||
local name content selected_files
|
||||
name=$(echo "$input" | jsonfilter -e '@.name' 2>/dev/null)
|
||||
content=$(echo "$input" | jsonfilter -e '@.content' 2>/dev/null)
|
||||
selected_files=$(echo "$input" | jsonfilter -e '@.selected_files' 2>/dev/null)
|
||||
|
||||
if [ -z "$name" ] || [ -z "$content" ]; then
|
||||
json_error "Missing name or content"
|
||||
return
|
||||
fi
|
||||
|
||||
local data_path
|
||||
config_load "$CONFIG"
|
||||
config_get data_path main data_path "/srv/streamlit"
|
||||
|
||||
local app_dir="$data_path/apps/$name"
|
||||
local tmpzip="/tmp/upload_$$.zip"
|
||||
|
||||
# Decode ZIP
|
||||
echo "$content" | base64 -d > "$tmpzip" 2>/dev/null
|
||||
if [ ! -f "$tmpzip" ]; then
|
||||
json_error "Failed to decode ZIP"
|
||||
return
|
||||
fi
|
||||
|
||||
mkdir -p "$app_dir"
|
||||
|
||||
# Extract selected files or all if none specified
|
||||
if [ -n "$selected_files" ] && [ "$selected_files" != "[]" ]; then
|
||||
# Parse selected files array and extract each
|
||||
echo "$selected_files" | jsonfilter -e '@[*]' 2>/dev/null | while read -r filepath; do
|
||||
[ -z "$filepath" ] && continue
|
||||
unzip -o "$tmpzip" "$filepath" -d "$app_dir" 2>/dev/null
|
||||
done
|
||||
else
|
||||
# Extract all
|
||||
unzip -o "$tmpzip" -d "$app_dir" 2>/dev/null
|
||||
fi
|
||||
|
||||
rm -f "$tmpzip"
|
||||
|
||||
# Find main .py file for registration
|
||||
local main_py
|
||||
main_py=$(find "$app_dir" -maxdepth 2 -name "*.py" -type f | head -1)
|
||||
|
||||
if [ -n "$main_py" ]; then
|
||||
# Register in UCI
|
||||
uci set "${CONFIG}.${name}=app"
|
||||
uci set "${CONFIG}.${name}.name=$name"
|
||||
uci set "${CONFIG}.${name}.path=$main_py"
|
||||
uci set "${CONFIG}.${name}.enabled=1"
|
||||
uci commit "$CONFIG"
|
||||
|
||||
json_init_obj
|
||||
json_add_boolean "success" 1
|
||||
json_add_string "message" "App deployed: $name"
|
||||
json_add_string "path" "$app_dir"
|
||||
json_add_string "main_file" "$main_py"
|
||||
json_close_obj
|
||||
else
|
||||
json_error "No Python files found in extracted archive"
|
||||
fi
|
||||
}
|
||||
|
||||
# Check install progress
|
||||
get_install_progress() {
|
||||
local log_file="/var/log/streamlit-install.log"
|
||||
@ -678,6 +786,8 @@ case "$1" in
|
||||
"remove_app": {"name": "str"},
|
||||
"set_active_app": {"name": "str"},
|
||||
"upload_app": {"name": "str", "content": "str"},
|
||||
"preview_zip": {"content": "str"},
|
||||
"upload_zip": {"name": "str", "content": "str", "selected_files": []},
|
||||
"get_install_progress": {},
|
||||
"list_instances": {},
|
||||
"add_instance": {"id": "str", "name": "str", "app": "str", "port": 8501},
|
||||
@ -737,6 +847,12 @@ case "$1" in
|
||||
upload_app)
|
||||
upload_app
|
||||
;;
|
||||
preview_zip)
|
||||
preview_zip
|
||||
;;
|
||||
upload_zip)
|
||||
upload_zip
|
||||
;;
|
||||
get_install_progress)
|
||||
get_install_progress
|
||||
;;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user