Compare commits

...

19 commits

Author SHA1 Message Date
Richard Koliser
9b173217d1
Add escaping for the JQuery selector on image friendly name 2025-05-23 13:26:58 -04:00
j-travis
8622084a02
Merge pull request #7 from kasmtech/116-updates
nvidia runtime updates for 116 release with compatibility
2024-07-15 12:39:25 -04:00
thelamer
08280190ea disable lossless 2024-07-10 14:50:23 -04:00
thelamer
84f2bfdd48 nvidia runtime updates for 116 release with compatibility 2024-07-08 19:07:44 -04:00
j-travis
b5c356997f
Merge pull request #5 from kasmtech/114-updates
fix old bug if no images are selected install and upgrade bombs out
2023-08-21 17:07:02 -04:00
ryan.kuba
58775a6491 fix old bug if no images are selected install and upgrade bombs out 2023-08-21 15:59:23 -04:00
j-travis
e599398710
Merge pull request #4 from kasmtech/feature/KASM-4200-113-updates
KASM-4200 Updates for 1.13 release
2023-03-28 08:27:34 -04:00
thelamer
0c654da05a KASM-4200 restructure code to handle new yaml format and update gpu settings for non NVIDIA cards to use DRI3 2023-03-27 19:06:13 -07:00
j-travis
3e9264d25d
Merge pull request #3 from kasmtech/bugfix/KASM-3699-fix-gpu-info-error
KASM-3699 have a fallback mode for gpuinfo script to simply list the cards
2022-12-05 17:58:44 -05:00
ryan.kuba
a8735cf7e3 KASM-3699 have a fallback mode for gpuinfo script to simply list the cards 2022-12-05 14:18:18 -08:00
j-travis
bfacaa3734
Merge pull request #2 from kasmtech/feature/KASM-2903-upgrade-support
Feature/kasm 2903 upgrade support
2022-11-30 17:54:56 -05:00
ryan.kuba
ee1ea94b59 KASM-2903 need globals for install and upgrade settings 2022-11-28 09:46:28 -05:00
ryan.kuba
a5c2f0635c KASM-2903 add initial upgrade support 2022-11-27 20:27:27 -05:00
j-travis
675ca12955
Merge pull request #1 from kasmtech/feature/KASM-3512-gpu-option
Feature/kasm 3512 gpu option
2022-11-23 11:05:23 -05:00
ryan.kuba
c51cbe881d KASM-3512 nvidia bugfix 2022-11-17 17:05:13 -05:00
ryan.kuba
0aa9746d3b KASM-3512 add support for nvidia GPUS 2022-11-17 16:33:15 -05:00
ryan.kuba
37dbb06b8c KASM-3512 make lossless install default 2022-11-16 16:05:03 -08:00
ryan.kuba
64bd739f1d KASM-3512 use deep merge on the exec and run configs to load the default values alongside the gpu modifications 2022-11-05 14:57:43 -07:00
ryan.kuba
1e5706208c KASM-3512 adding select dropdown during installation to force a GPU on all containers 2022-11-01 15:44:15 -07:00
3 changed files with 259 additions and 60 deletions

156
index.js
View file

@ -6,6 +6,7 @@ var fsw = require('fs').promises;
var fs = require('fs'); var fs = require('fs');
var os = require('os'); var os = require('os');
var yaml = require('js-yaml'); var yaml = require('js-yaml');
var _ = require('lodash');
var si = require('systeminformation'); var si = require('systeminformation');
var express = require('express'); var express = require('express');
var app = require('express')(); var app = require('express')();
@ -19,16 +20,72 @@ var arch = os.arch().replace('x64', 'amd64');
var baseUrl = process.env.SUBFOLDER || '/'; var baseUrl = process.env.SUBFOLDER || '/';
var version = process.env.VERSION || 'stable'; var version = process.env.VERSION || 'stable';
var port = process.env.KASM_PORT || '443'; var port = process.env.KASM_PORT || '443';
const { spawn } = require('node:child_process');
var EULA; var EULA;
var images; var images;
var currentVersion;
var gpuInfo;
var installSettings = {};
var upgradeSettings = {};
// Grab installer variables // Grab installer variables
async function installerBlobs() { async function installerBlobs() {
EULA = await fsw.readFile('/kasm_release/licenses/LICENSE.txt', 'utf8'); EULA = await fsw.readFile('/kasm_release/licenses/LICENSE.txt', 'utf8');
let imagesText = await fsw.readFile('/wizard/default_images_' + arch + '.yaml', 'utf8'); let imagesText = await fsw.readFile('/wizard/default_images_' + arch + '.yaml', 'utf8');
images = yaml.load(imagesText); images = yaml.load(imagesText);
currentVersion = fs.readFileSync('/version.txt', 'utf8').replace(/(\r\n|\n|\r)/gm,'');
let gpuData = [];
let gpuCmd = spawn('/gpuinfo.sh');
gpuCmd.stdout.on('data', function(data) {
gpuData.push(data);
});
gpuCmd.on('close', function(code) {
try {
if (code == 0) {
gpuInfo = JSON.parse(gpuData.join(''));
} else {
gpuInfo = {};
}
} catch (err) {
// Manually backfill GPU info if available
gpuInfo = {};
for (let i = 0; i < 10; i++) {
let num = i.toString();
if (fs.existsSync('/dev/dri/card' + num)) {
gpuInfo['/dev/dri/card' + num] = "Unknown GPU";
}
}
}
});
} }
installerBlobs(); installerBlobs();
// GPU image yaml merging
async function setGpu(imagesI) {
if (upgradeSettings['forceGpu'] !== undefined) {
installSettings = upgradeSettings;
}
let gpu = installSettings.forceGpu.split('|')[0];
let gpuName = installSettings.forceGpu.split('|')[1];
let card = gpu.slice(-1);
let render = (Number(card) + 128).toString();
// Handle NVIDIA Gpus
var baseRun;
if (gpuName.indexOf('NVIDIA') !== -1) {
baseRun = JSON.parse('{"runtime":"nvidia","environment":{"NVIDIA_DRIVER_CAPABILITIES":"all","KASM_EGL_CARD":"/dev/dri/card' + card + '","KASM_RENDERD":"/dev/dri/renderD' + render + '"},"device_requests":[{"driver": "","count": -1,"device_ids": null,"capabilities":[["gpu"]],"options":{}}]}');
} else {
baseRun = JSON.parse('{"environment":{"DRINODE":"/dev/dri/renderD' + render + '", "HW3D": true},"devices":["/dev/dri/card' + card + ':/dev/dri/card' + card + ':rwm","/dev/dri/renderD' + render + ':/dev/dri/renderD' + render + ':rwm"]}');
}
let baseExec = JSON.parse('{"first_launch":{"user":"root","cmd": "bash -c \'chown -R kasm-user:kasm-user /dev/dri/*\'"}}');
for (var i=0; i<imagesI.images.length; i++) {
console.log(imagesI.images[i]['run_config']);
finalRun = _.merge(imagesI.images[i]['run_config'], baseRun)
finalExec = _.merge(imagesI.images[i]['exec_config'], baseExec)
imagesI.images[i]['run_config'] = finalRun;
imagesI.images[i]['exec_config'] = finalExec;
}
return imagesI;
}
//// Http server //// //// Http server ////
baserouter.use('/public', express.static(__dirname + '/public')); baserouter.use('/public', express.static(__dirname + '/public'));
baserouter.get("/", function (req, res) { baserouter.get("/", function (req, res) {
@ -45,31 +102,31 @@ io = socketIO(https, {path: baseUrl + 'socket.io'});
io.on('connection', async function (socket) { io.on('connection', async function (socket) {
// Run bash install with our custom flags // Run bash install with our custom flags
async function install(data) { async function install(data) {
// Determine install settings
installSettings = data[0]; installSettings = data[0];
let imagesI = data[1]; var imagesI = data[1];
let imagesD = images; installFlags = ['/kasm_release/install.sh', '-W', '-B' ,'-H', '-e', '-L', port, '-P', installSettings.adminPass, '-U', installSettings.userPass];
installFlags = ['/kasm_release/install.sh', '-B' ,'-H', '-e', '-L', port, '-P', installSettings.adminPass, '-U', installSettings.userPass]; if ((imagesI.hasOwnProperty('images')) && (imagesI.images.length < 10)) {
if (installSettings.useRolling == true) {
installFlags.push('-O');
}
if ((installSettings.noDownload == true) || (imagesI == false)) {
installFlags.push('-u');
}
if ((imagesI.hasOwnProperty('images')) && (Object.keys(imagesI.images).length < 10)) {
installFlags.push('-b'); installFlags.push('-b');
} }
// Flag the images properly based on selection
for await (let image of Object.keys(images.images)) { // GPU yaml merge
if ((imagesI.hasOwnProperty('images')) && (imagesI.images.hasOwnProperty(image))) { if (installSettings.forceGpu !== 'disabled') {
imagesD.images[image].enabled = true; imagesI = await setGpu(imagesI);
imagesD.images[image].hidden = false;
} else {
imagesD.images[image].enabled = false;
imagesD.images[image].hidden = true;
}
} }
let yamlStr = yaml.dump(imagesD);
await fsw.writeFile('/kasm_release/conf/database/seed_data/default_images_' + arch + '.yaml', yamlStr); // Write finalized image data
let yamlStr = yaml.dump(imagesI);
if (yamlStr.startsWith("false")) {
installFlags = installFlags.filter(function(e) { return e !== '-W' });
} else {
await fsw.writeFile('/kasm_release/conf/database/seed_data/default_images_' + arch + '.yaml', yamlStr);
}
// Copy over version
await fsw.copyFile('/version.txt', '/opt/version.txt');
// Run install
let cmd = pty.spawn('/bin/bash', installFlags); let cmd = pty.spawn('/bin/bash', installFlags);
cmd.on('data', function(data) { cmd.on('data', function(data) {
socket.emit('term', data); socket.emit('term', data);
@ -80,19 +137,71 @@ io.on('connection', async function (socket) {
} }
}); });
} }
// Run bash upgrade with our custom flags
async function upgrade(data) {
// Determine upgrade settings
upgradeSettings = data[0];
var imagesI = data[1];
upgradeFlags = ['/kasm_release/upgrade.sh', '-L', port];
if (upgradeSettings.keepOldImages == true) {
upgradeFlags.push('-K');
} else {
upgradeFlags.push('-U');
}
// GPU yaml merge
if (upgradeSettings.forceGpu !== 'disabled') {
imagesI = await setGpu(imagesI);
}
// Write finalized image data
let yamlStr = yaml.dump(imagesI);
if (yamlStr.startsWith("false")) {
upgradeFlags = upgradeFlags.filter(function(e) { return e !== '-K' });
upgradeFlags = upgradeFlags.filter(function(e) { return e !== '-U' });
} else {
await fsw.writeFile('/kasm_release/conf/database/seed_data/default_images_' + arch + '.yaml', yamlStr);
}
// Copy over version
await fsw.copyFile('/version.txt', '/opt/version.txt');
// Run upgrade
let cmd = pty.spawn('/bin/bash', upgradeFlags);
cmd.on('data', function(data) {
socket.emit('term', data);
});
cmd.on('exit', function(code, signal) {
if (code == 0) {
socket.emit('done', port);
}
});
}
// Render landing page depending on installed status // Render landing page depending on installed status
async function renderLanding() { async function renderLanding() {
let containers = await docker.listContainers(); let containers = await docker.listContainers();
// This is a running system
if (containers.length !== 0) { if (containers.length !== 0) {
let dashinfo = {}; let dashinfo = {};
// Get version information
if (fs.existsSync('/opt/version.txt')) {
dashinfo['localVersion'] = fs.readFileSync('/opt/version.txt', 'utf8').replace(/(\r\n|\n|\r)/gm,'');
} else {
dashinfo['localVersion'] = 'Unknown';
}
dashinfo['currentVersion'] = currentVersion;
dashinfo['gpuInfo'] = gpuInfo;
dashinfo['containers'] = containers; dashinfo['containers'] = containers;
dashinfo['cpu'] = await si.cpu(); dashinfo['cpu'] = await si.cpu();
dashinfo['mem'] = await si.mem(); dashinfo['mem'] = await si.mem();
dashinfo['cpuPercent'] = await si.currentLoad(); dashinfo['cpuPercent'] = await si.currentLoad();
dashinfo['port'] = port; dashinfo['port'] = port;
socket.emit('renderdash', dashinfo); socket.emit('renderdash', [dashinfo, images]);
// Render installer
} else { } else {
socket.emit('renderinstall', [EULA, images]); socket.emit('renderinstall', [EULA, images, gpuInfo]);
} }
} }
// Disable wizard // Disable wizard
@ -104,5 +213,6 @@ io.on('connection', async function (socket) {
//// Incoming requests //// //// Incoming requests ////
socket.on('renderlanding', renderLanding); socket.on('renderlanding', renderLanding);
socket.on('install', install); socket.on('install', install);
socket.on('upgrade', upgrade);
socket.on('nowizard', noWizard); socket.on('nowizard', noWizard);
}); });

View file

@ -12,6 +12,7 @@
"dockerode": "^3.3.2", "dockerode": "^3.3.2",
"express": "^4.18.1", "express": "^4.18.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"node-pty": "^0.10.1", "node-pty": "^0.10.1",
"socket.io": "^4.5.1", "socket.io": "^4.5.1",
"systeminformation": "^5.11.16" "systeminformation": "^5.11.16"

View file

@ -1,9 +1,11 @@
// Variables // Variables
var EULA; var EULA;
var images; var images;
var gpus;
var term; var term;
var installImages = []; var installImages = [];
var installSettings = {}; var installSettings = {};
var upgradeSettings = {};
var selected = false; var selected = false;
// Socket.io connection // Socket.io connection
@ -25,20 +27,39 @@ async function install() {
showTerminal() showTerminal()
titleChange('Installing'); titleChange('Installing');
// Create new object based on image selection // Create new object based on image selection
let selectedImages = {images: {}}; let selectedImages = {alembic_version: images.alembic_version, images: [], group_images: []};
if (installImages.length == 0) { if (installImages.length == 0) {
socket.emit('install', [installSettings, false]); socket.emit('install', [installSettings, false]);
} else { } else {
for await (let image of installImages) { for await (let image of installImages) {
if (images.images[image].hasOwnProperty('enabled')) { let srcImage = images.images.find(x => x.friendly_name === image);
images.images[image].enabled = true; srcImage['enabled'] = true;
} selectedImages.images.push(srcImage);
Object.assign(selectedImages.images, {[image]: images.images[image]}); selectedImages.group_images.push({image_id: srcImage.image_id, group_id: "68d557ac-4cac-42cc-a9f3-1c7c853de0f3"});
} }
socket.emit('install', [installSettings, selectedImages]); socket.emit('install', [installSettings, selectedImages]);
} }
} }
// Execute upgrade
async function upgrade() {
showTerminal()
titleChange('Upgrading');
// Create new object based on image selection
let selectedImages = {alembic_version: images.alembic_version, images: [], group_images: []};
if (installImages.length == 0) {
socket.emit('upgrade', [upgradeSettings, false]);
} else {
for await (let image of installImages) {
let srcImage = images.images.find(x => x.friendly_name === image);
srcImage['enabled'] = true;
selectedImages.images.push(srcImage);
selectedImages.group_images.push({image_id: srcImage.image_id, group_id: "68d557ac-4cac-42cc-a9f3-1c7c853de0f3"});
}
socket.emit('upgrade', [upgradeSettings, selectedImages]);
}
}
// Show page container // Show page container
function showContainer() { function showContainer() {
$('#terminal').empty(); $('#terminal').empty();
@ -73,6 +94,7 @@ function renderInstall(data) {
titleChange('EULA'); titleChange('EULA');
EULA = data[0]; EULA = data[0];
images = data[1]; images = data[1];
gpus = data[2];
let EULADiv = $('<div>', {id: 'EULA'}).text(EULA); let EULADiv = $('<div>', {id: 'EULA'}).text(EULA);
$('#container').append(EULADiv); $('#container').append(EULADiv);
let EULAButton = $('<button>', {id: 'EULAButton', onclick: 'pickSettings()', class: 'btn btn-default btn-ghost'}).text('Accept and continue'); let EULAButton = $('<button>', {id: 'EULAButton', onclick: 'pickSettings()', class: 'btn btn-default btn-ghost'}).text('Accept and continue');
@ -80,15 +102,34 @@ function renderInstall(data) {
} }
// Render Dashboard // Render Dashboard
async function renderDash(info) { async function renderDash(data) {
showContainer(); showContainer();
titleChange('Dashboard'); titleChange('Dashboard');
let info = data[0];
images = data[1];
// Store GPU info
$('body').data('gpuInfo', info.gpuInfo);
// Upgrade button if needed
var upgrade;
if (info.currentVersion !== info.localVersion) {
upgrade = $('<button>', {class: 'btn btn-primary', onclick: 'renderUpgrade()'}).text('Upgrade to ' + info.currentVersion)
} else {
upgrade = info.currentVersion;
}
// Kasm docker containers // Kasm docker containers
containersTable = $('<tbody>'); containersTable = $('<tbody>');
containersTable.append( containersTable.append(
$('<tr>').append([ $('<tr>').append([
$('<th>').text('Web URL'), $('<th>').text('Web URL'),
$('<td>').append($('<a>', {href: 'https://' + host + ':' + info.port, target: '_blank'}).text('https://' + host + ':' + info.port)) $('<td>').append($('<a>', {href: 'https://' + host + ':' + info.port, target: '_blank'}).text('https://' + host + ':' + info.port))
]),
$('<tr>').append([
$('<th>').text('Installed Version'),
$('<td>').text(info.localVersion)
]),
$('<tr>').append([
$('<th>').text('Current Version'),
$('<td>').append(upgrade)
]) ])
); );
for await (let container of info.containers) { for await (let container of info.containers) {
@ -187,13 +228,13 @@ async function pickSettings() {
$('<label>', {for: 'userPass'}).text('user@kasm.local Password: '), $('<label>', {for: 'userPass'}).text('user@kasm.local Password: '),
$('<input>', {name: 'userPass', id: 'userPass', type: 'password', placeholder: 'required'}).prop('required',true) $('<input>', {name: 'userPass', id: 'userPass', type: 'password', placeholder: 'required'}).prop('required',true)
]); ]);
let useRolling = $('<div>', {class: 'form-group'}).append([ let gpuOptions = [$('<option>', {value: 'disabled'}).text('Disabled')];
$('<label>', {for: 'useRolling'}).text('Use Rolling Images: '), for await (let card of Object.keys(gpus)) {
$('<input>', {name: 'useRolling', id: 'useRolling', type: 'checkbox'}) gpuOptions.push($('<option>', {value: card + '|' + gpus[card]}).text(card + ' - ' + gpus[card]));
]); }
let noDownload = $('<div>', {class: 'form-group'}).append([ let forceGpu = $('<div>', {class: 'form-group'}).append([
$('<label>', {for: 'noDownload'}).text('Skip Image Download: '), $('<label>', {for: 'forceGpu'}).text('Use GPU on all images: '),
$('<input>', {name: 'noDownload', id: 'noDownload', type: 'checkbox'}) $('<select>', {name: 'forceGpu', id: 'forceGpu',}).append(gpuOptions)
]); ]);
let submit = $('<div>', {class: 'form-group'}).append([ let submit = $('<div>', {class: 'form-group'}).append([
$('<input>', {name: 'submit', type: 'submit', value: 'Next', class: 'btn btn-default btn-ghost'}) $('<input>', {name: 'submit', type: 'submit', value: 'Next', class: 'btn btn-default btn-ghost'})
@ -201,8 +242,7 @@ async function pickSettings() {
fieldset.append([ fieldset.append([
adminPass, adminPass,
userPass, userPass,
useRolling, forceGpu,
noDownload,
submit submit
]); ]);
form.append(fieldset); form.append(fieldset);
@ -212,34 +252,79 @@ async function pickSettings() {
e.preventDefault(); e.preventDefault();
installSettings.adminPass = $('#adminPass').val(); installSettings.adminPass = $('#adminPass').val();
installSettings.userPass = $('#userPass').val(); installSettings.userPass = $('#userPass').val();
installSettings.useRolling = $('#useRolling').is(":checked"); installSettings.forceGpu = $('#forceGpu').val();
installSettings.noDownload = $('#noDownload').is(":checked"); pickImages(false);
pickImages(); });
}
// Render upgrade form
async function renderUpgrade() {
showContainer();
let gpus = $('body').data('gpuInfo');
titleChange('Upgrade Settings');
let form = $('<form>', {id: 'settingsform'});
let fieldset = $('<fieldset>').append($('<legend>').text('Kasm Upgrade Settings'));
let keepOldImages = $('<div>', {class: 'form-group'}).append([
$('<label>', {for: 'keepOldImages'}).text('Do not purge existing images: '),
$('<input>', {name: 'keepOldImages', id: 'keepOldImages', type: 'checkbox'})
]);
let gpuOptions = [$('<option>', {value: 'disabled'}).text('Disabled')];
for await (let card of Object.keys(gpus)) {
gpuOptions.push($('<option>', {value: card + '|' + gpus[card]}).text(card + ' - ' + gpus[card]));
}
let forceGpu = $('<div>', {class: 'form-group'}).append([
$('<label>', {for: 'forceGpu'}).text('Use GPU on all new images: '),
$('<select>', {name: 'forceGpu', id: 'forceGpu',}).append(gpuOptions)
]);
let submit = $('<div>', {class: 'form-group'}).append([
$('<input>', {name: 'submit', type: 'submit', value: 'Next', class: 'btn btn-default btn-ghost'})
]);
fieldset.append([
keepOldImages,
forceGpu,
submit
]);
form.append(fieldset);
$('#container').append(form);
// Grab data and move to image selection
form.on('submit', function (e) {
e.preventDefault();
upgradeSettings.keepOldImages = $('#keepOldImages').is(":checked");
upgradeSettings.forceGpu = $('#forceGpu').val();
pickImages(true);
}); });
} }
// Render image selection // Render image selection
async function pickImages() { async function pickImages(upgrade) {
var installText;
var installFunction;
if (upgrade) {
installText = 'Upgrade';
installFunction = 'upgrade()';
} else {
installText = 'Install';
installFunction = 'install()';
}
showContainer(); showContainer();
titleChange('Image Selection'); titleChange('Image Selection');
let imagesDiv = $('<div>', {class: 'cardcontainer', id: 'images'}); let imagesDiv = $('<div>', {class: 'cardcontainer', id: 'images'});
$('#container').append(imagesDiv); $('#container').append(imagesDiv);
for await (let image of Object.keys(images.images).sort(Intl.Collator().compare)) { for await (let image of images.images) {
let imageName = $('<p>').text(image); let imageName = $('<p>').text(image.friendly_name);
let imageDiv = $('<div>', { let imageDiv = $('<div>', {
class: 'card', class: 'card',
id: image.replace(new RegExp(' ', 'g'), '_'), id: image.friendly_name.replace(new RegExp(' ', 'g'), '_').replace('.', '-'),
title: images.images[image].description, title: image.description,
onclick: 'selectImage(\'' + image.replace(new RegExp(' ', 'g'), '_') + '\')' onclick: 'selectImage(\'' + image.friendly_name.replace(new RegExp(' ', 'g'), '_').replace('.', '-') + '\')'
}).append(imageName).css('filter', 'grayscale(100%)') }).append(imageName).css('filter', 'grayscale(100%)')
let thumb = $('<img>', {class: 'thumb', src: 'public/' + images.images[image].image_src}); let thumb = $('<img>', {class: 'thumb', src: 'public/' + image.image_src});
imageDiv.append(thumb); imageDiv.append(thumb);
$('#images').append(imageDiv); $('#images').append(imageDiv);
} }
let allButton = $('<button>', {class: 'btn btn-default btn-ghost center', onclick: 'selectAll()'}).text('Select All'); let allButton = $('<button>', {class: 'btn btn-default btn-ghost center', onclick: 'selectAll()'}).text('Select All');
let installButton = $('<button>', {class: 'btn btn-default btn-ghost center', onclick: 'install()'}).text('Install'); let installButton = $('<button>', {class: 'btn btn-default btn-ghost center', onclick: installFunction}).text(installText);
$('#container').append([ $('#container').append([
allButton, allButton,
installButton installButton
@ -248,16 +333,17 @@ async function pickImages() {
// Select an individual image // Select an individual image
function selectImage(image) { function selectImage(image) {
let imageKey = image.replace(new RegExp('_', 'g'), ' '); let imageKey = image.replace(new RegExp('_', 'g'), ' ').replace('-', '.');
let safeSelectorId = $.escapeSelector(image); // Escape the ID for jQuery
if (installImages.includes(imageKey)) { if (installImages.includes(imageKey)) {
installImages = installImages.filter(e => e !== imageKey) installImages = installImages.filter(e => e !== imageKey)
$('#' + image).css({ $('#' + safeSelectorId).css({
filter: 'grayscale(100%)', filter: 'grayscale(100%)',
background: '' background: ''
}); });
} else { } else {
installImages.push(imageKey); installImages.push(imageKey);
$('#' + image).css({ $('#' + safeSelectorId).css({
filter: '', filter: '',
background: '#89cff0' background: '#89cff0'
}); });
@ -269,19 +355,21 @@ function selectAll() {
installImages = []; installImages = [];
if (selected) { if (selected) {
selected = false; selected = false;
for (let image of Object.keys(images.images)) { for (let image of images.images) {
let imageElem = image.replace(new RegExp(' ', 'g'), '_'); let imageElem = image.friendly_name.replace(new RegExp(' ', 'g'), '_').replace('.', '-');
$('#' + imageElem).css({ let safeSelectorId = $.escapeSelector(imageElem); // Escape the ID for jQuery
$('#' + safeSelectorId).css({
filter: 'grayscale(100%)', filter: 'grayscale(100%)',
background: '' background: ''
}); });
} }
} else { } else {
selected = true; selected = true;
for (let image of Object.keys(images.images)) { for (let image of images.images) {
let imageElem = image.replace(new RegExp(' ', 'g'), '_'); let imageElem = image.friendly_name.replace(new RegExp(' ', 'g'), '_').replace('.', '-');
installImages.push(image); let safeSelectorId = $.escapeSelector(imageElem); // Escape the ID for jQuery
$('#' + imageElem).css({ installImages.push(image.friendly_name);
$('#' + safeSelectorId).css({
filter: '', filter: '',
background: '#89cff0' background: '#89cff0'
}); });