Compare commits

...

37 commits

Author SHA1 Message Date
Richard Koliser
2a3b4f1a3c Merge branch 'bugfix/KASM-8472-redirect-kasm-install-wizard-issues-to-central-issue-tracker' into 'develop'
KASM-8472 redirect kasm-install-wizard issues to central issue tracker

Closes KASM-8472

See merge request kasm-technologies/internal/kasm_install_wizard!5
2026-07-15 19:37:43 +00:00
Teja Swaroop Pothala
15f1b4aa17
KASM-8472 redirect kasm-install-wizard issues to central issue tracker 2026-05-26 15:25:57 +00:00
Adam Beardwood
ce72001ce5 Merge branch 'bugfix/ci-job-token' into 'develop'
Add CI_JOB_TOKEN to release pipeline

See merge request kasm-technologies/internal/kasm_install_wizard!4
2026-04-16 14:04:00 +01:00
Chris Hunt
cc5e786633
Add CI_JOB_TOKEN to release pipeline 2026-04-16 13:21:41 +01:00
Adam Beardwood
eb0218f612 Merge branch 'feature/KASM-8264-registry-based-wizard-installer' into 'develop'
KASM-8264 Registry based wizard installer

Closes KASM-8264

See merge request kasm-technologies/internal/kasm_install_wizard!2
2026-04-16 10:31:45 +01:00
Chris Hunt
fbf2e5ff48
KASM-8264 set node to a min of 18, fixed a potential gpu issue, remove typo in currentVersion 2026-04-15 14:53:17 +01:00
Chris Hunt
d7d5cd8f12
KASM-8264 fixed implicit globals, update vars, change loops, fix vulns 2026-04-15 10:35:14 +01:00
Chris Hunt
9bae28c7d4
KASM-8264 Fix for potential crash on gpu selection 2026-04-15 10:12:21 +01:00
Chris Hunt
a0ec262f83
KASM-8264 - Fix for friendly name determination 2026-04-15 10:09:09 +01:00
Chris Hunt
0411125ffa
KASM-8264 Guard against no images selected breaking the install 2026-04-15 10:05:12 +01:00
Chris Hunt
633e3d9c87
KASM-8264 Swet max width 2026-04-14 18:13:42 +01:00
Chris Hunt
f3b8319807
KASM-8264 Small styling updates 2026-04-14 18:13:42 +01:00
Chris Hunt
032df175de
KASM-8264 Add password complexity 2026-04-14 18:13:42 +01:00
Chris Hunt
c66581a6ac
KASM-8264 Update repository on release 2026-04-14 18:13:42 +01:00
Chris Hunt
1ffc4f955f
KASM-8264 Retrieve updated list.json when merging to releases 2026-04-14 18:12:49 +01:00
Chris Hunt
34e43330c1
KASM-8264 Remove unused flag and update tags displayed on final screen 2026-04-14 18:12:49 +01:00
Chris Hunt
f34553f5b7
KASM-8264 Update upgrade 2026-04-14 18:12:49 +01:00
Chris Hunt
b0536f4a29
KASM-8264 Update path 2026-04-14 18:12:49 +01:00
Chris Hunt
d73ddacefb
KASM-8264 Add rolling service images 2026-04-14 18:12:49 +01:00
Chris Hunt
92b30e8d25
KASM-8264 Update tags and alembic version 2026-04-14 18:12:49 +01:00
Chris Hunt
7924f882db
KASM-8264 Don't call setGpu when there aren't any images 2026-04-14 18:12:49 +01:00
Chris Hunt
d4b7db69f4
KASM-8264 Registry based wizard installer 2026-04-14 18:12:49 +01:00
Richard Koliser
502bc2f530
Add escaping for the JQuery selector on image friendly name 2026-04-14 18:12:49 +01:00
thelamer
d8a0bcb5e5
disable lossless 2026-04-14 18:12:49 +01:00
thelamer
1e8cd14fd2
nvidia runtime updates for 116 release with compatibility 2026-04-14 18:12:49 +01:00
ryan.kuba
b5e5ef15ff
fix old bug if no images are selected install and upgrade bombs out 2026-04-14 18:12:49 +01:00
thelamer
63ac91b82e
KASM-4200 restructure code to handle new yaml format and update gpu settings for non NVIDIA cards to use DRI3 2026-04-14 18:12:49 +01:00
ryan.kuba
9f542df2a9
KASM-3699 have a fallback mode for gpuinfo script to simply list the cards 2026-04-14 18:12:49 +01:00
ryan.kuba
cc2c34c758
KASM-2903 need globals for install and upgrade settings 2026-04-14 18:12:49 +01:00
ryan.kuba
cfaa944c84
KASM-2903 add initial upgrade support 2026-04-14 18:12:49 +01:00
ryan.kuba
f4356863d3
KASM-3512 nvidia bugfix 2026-04-14 18:12:49 +01:00
ryan.kuba
457214bf4a
KASM-3512 add support for nvidia GPUS 2026-04-14 18:12:49 +01:00
ryan.kuba
daa089b1d5
KASM-3512 make lossless install default 2026-04-14 18:12:49 +01:00
ryan.kuba
07fbb9d873
KASM-3512 use deep merge on the exec and run configs to load the default values alongside the gpu modifications 2026-04-14 18:12:49 +01:00
ryan.kuba
81279dcf88
KASM-3512 adding select dropdown during installation to force a GPU on all containers 2026-04-14 18:12:49 +01:00
Richard Koliser
60e9c3a231 Merge branch 'feature/QA-21_switch_runners_to_OCI' into 'develop'
QA-21 switched runners from AWS to OCI

Closes QA-21

See merge request kasm-technologies/internal/kasm_install_wizard!1
2025-06-04 03:55:15 +00:00
Huan Truong
ff09276100
QA-21 switched runners from AWS to OCI 2025-06-03 15:47:17 -05:00
11 changed files with 580 additions and 147 deletions

View file

@ -1,21 +1,38 @@
image: docker image: alpine
services:
- docker:dind variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_AUTH_CONFIG: ${_DOCKER_AUTH_CONFIG}
default:
tags:
- oci-fixed-amd
stages: stages:
- upload - update
before_script: update_list_json:
- export SANITIZED_BRANCH="$(echo $CI_COMMIT_REF_NAME | sed -r 's#^release/##' | sed 's/\//_/g')" stage: update
rules:
push_to_s3: - if: '$CI_COMMIT_REF_NAME =~ /^release\//'
stage: upload when: on_success
script: script:
- apk add aws-cli curl gzip tar - apk add curl git
- export S3_URL="https://${S3_BUCKET}.s3.amazonaws.com/wizard/${CI_COMMIT_SHA}.tar.gz" - |
- tar -czf ${CI_COMMIT_SHA}.tar.gz * if curl -fsSL https://registry.kasmweb.com/1.1/list.json -o list.json.tmp; then
- aws s3 cp ${CI_COMMIT_SHA}.tar.gz s3://${S3_BUCKET}/wizard/ mv list.json.tmp list.json
- aws s3 cp ${CI_COMMIT_SHA}.tar.gz s3://${S3_BUCKET}/wizard/${SANITIZED_BRANCH}.tar.gz git config user.email "ci@gitlab"
- curl --request POST --header "PRIVATE-TOKEN:${GITLAB_API_TOKEN}" "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/statuses/${CI_COMMIT_SHA}?state=success&name=build-url&target_url=${S3_URL}" git config user.name "GitLab CI"
tags: git remote set-url origin "https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
- aws-autoscale git add list.json
if ! git diff --cached --quiet; then
git commit -m "chore: update list.json from registry [skip ci]"
git push origin HEAD:${CI_COMMIT_REF_NAME}
else
echo "list.json unchanged, nothing to commit"
fi
else
echo "Failed to fetch list.json, skipping update"
rm -f list.json.tmp
fi

View file

@ -36,3 +36,8 @@ As configured files ingested from a current Kasm Workspaces installer are needed
/kasm_release/ /kasm_release/
└── Full Kasm workspaces installer └── Full Kasm workspaces installer
``` ```
## Reporting Issues
To report any issues for this repository, please use our central issue tracker: **[Kasm Workspaces Issue Tracker](https://github.com/kasmtech/workspaces-issues/issues)**

333
index.js
View file

@ -1,34 +1,238 @@
// Imports // Imports
var Docker = require('dockerode'); const Docker = require('dockerode');
var socketIO = require('socket.io'); const socketIO = require('socket.io');
var pty = require("node-pty"); const pty = require("node-pty");
var fsw = require('fs').promises; const fsw = require('fs').promises;
var fs = require('fs'); const fs = require('fs');
var os = require('os'); const os = require('os');
var yaml = require('js-yaml'); const yaml = require('js-yaml');
var si = require('systeminformation'); const _ = require('lodash');
var express = require('express'); const si = require('systeminformation');
var app = require('express')(); const express = require('express');
var privateKey = fs.readFileSync('/opt/kasm/certs/kasm_wizard.key', 'utf8'); const app = require('express')();
var certificate = fs.readFileSync('/opt/kasm/certs/kasm_wizard.crt', 'utf8'); const privateKey = fs.readFileSync('/opt/kasm/certs/kasm_wizard.key', 'utf8');
var credentials = {key: privateKey, cert: certificate}; const certificate = fs.readFileSync('/opt/kasm/certs/kasm_wizard.crt', 'utf8');
var https = require('https').Server(credentials, app); const credentials = {key: privateKey, cert: certificate};
var baserouter = express.Router(); const https = require('https').Server(credentials, app);
var docker = new Docker({socketPath: '/var/run/docker.sock'}); const baserouter = express.Router();
var arch = os.arch().replace('x64', 'amd64'); const docker = new Docker({socketPath: '/var/run/docker.sock'});
var baseUrl = process.env.SUBFOLDER || '/'; const arch = os.arch().replace('x64', 'amd64');
var version = process.env.VERSION || 'stable'; const baseUrl = process.env.SUBFOLDER || '/';
var port = process.env.KASM_PORT || '443'; const port = process.env.KASM_PORT || '443';
var EULA; const { spawn } = require('node:child_process');
var images; let EULA;
let images;
let currentVersion;
let sourceLocal = false;
let gpuInfo;
let installSettings = {};
let upgradeSettings = {};
// Find the best matching compatibility entry for the current version.
// Exact version match takes precedence over wildcard (e.g. "1.18.x").
function matchVersion(currentVer, compatibilityList) {
if (!compatibilityList || compatibilityList.length === 0) return null;
const parts = currentVer.split('.');
const major = parts[0];
const minor = parts[1] !== undefined ? parts[1] : '0';
// Exact match (most precise)
let match = compatibilityList.find(c => c.version === currentVer);
if (match) return match;
// Major.minor wildcard, e.g. "1.18.x"
match = compatibilityList.find(c => c.version === `${major}.${minor}.x`);
if (match) return match;
return null;
}
// Build the image tag for a given compat entry.
// Returns e.g. "1.18.1-rolling-weekly", falling back to "develop".
function buildImageTag(compat) {
const compatTag = compat.image.split(':')[1] || '';
const versionPrefix = compatTag.replace(/-[^-]+-[^-]+$/, ''); // strip last two dash-segments
const candidate = versionPrefix + '-rolling-weekly';
return (compat.available_tags && compat.available_tags.includes(candidate)) ? candidate : 'develop';
}
// Fetch the registry workspace list, with local fallback.
async function fetchListData() {
try {
const response = await fetch('https://registry.kasmweb.com/1.1/list.json');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return await response.json();
} catch (err) {
console.error('Failed to fetch workspace list, falling back to local list.json:', err.message);
sourceLocal = true;
return JSON.parse(await fsw.readFile('/wizard/list.json', 'utf8'));
}
}
// Return the rolling-weekly tag string for the current version by finding
// the first compatible workspace in the registry (e.g. "1.18.1-rolling-weekly").
async function getRollingWeeklyTag(currentVer, archName) {
const listData = await fetchListData();
const ws = (listData.workspaces || []).find(
w => w.architecture && w.architecture.includes(archName) && matchVersion(currentVer, w.compatibility || [])
);
if (!ws) return currentVer + '-rolling-weekly';
return buildImageTag(matchVersion(currentVer, ws.compatibility));
}
// Fetch workspace list from registry and convert to the images YAML structure.
// Falls back to local list.json if the remote is unavailable.
async function fetchWorkspaceList(currentVer, archName) {
const listData = await fetchListData();
const filtered = (listData.workspaces || []).filter(
ws => ws.architecture && ws.architecture.includes(archName)
);
let imageIdx = 1;
const imagesList = [];
for (const ws of filtered) {
const compat = matchVersion(currentVer, ws.compatibility || []);
if (!compat) continue;
const imageBase = compat.image.split(':')[0];
const imageTag = buildImageTag(compat);
const imageName = imageBase + ':' + imageTag;
imagesList.push({
categories: ws.categories,
cores: 2.0,
cpu_allocation_method: 'Inherit',
description: ws.description,
docker_registry: ws.docker_registry,
enabled: true,
exec_config: {},
friendly_name: ws.friendly_name,
gpu_count: 0,
hidden: false,
image_id: '${uuid:image_id:' + imageIdx + '}',
image_src: 'https://registry.kasmweb.com/1.1/icons/' + ws.image_src,
image_type: 'Container',
launch_config: {},
memory: 2768000000,
name: imageName,
notes: ws.notes || null,
run_config: {},
uncompressed_size_mb: compat.uncompressed_size_mb,
zone_id: null
});
imageIdx++;
}
let alembicVersion = 'e3900d8a4fee';
try {
const propsText = await fsw.readFile('/kasm_release/conf/database/seed_data/default_properties.yaml', 'utf8');
const props = yaml.load(propsText);
if (props && props.alembic_version) alembicVersion = props.alembic_version;
} catch (err) {
console.error('Could not read default_properties.yaml, using hardcoded alembic_version:', err.message);
}
return { alembic_version: alembicVersion, images: imagesList };
}
// 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'); try {
images = yaml.load(imagesText); currentVersion = fs.readFileSync('/version.txt', 'utf8').replace(/(\r\n|\n|\r)/gm,'');
} catch (err) {
currentVersion = '1.18.1';
}
images = await fetchWorkspaceList(currentVersion, arch);
gpuInfo = {};
await new Promise((resolve) => {
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";
}
}
}
resolve();
});
});
} }
installerBlobs(); installerBlobs();
// GPU image yaml merging
async function setGpu(imagesI) {
if (upgradeSettings['forceGpu'] !== undefined) {
installSettings = upgradeSettings;
}
const forceGpu = installSettings.forceGpu;
if (!forceGpu || forceGpu === 'disabled' || !forceGpu.includes('|')) {
console.error('setGpu: invalid or missing forceGpu value:', forceGpu);
return imagesI;
}
const [gpu, gpuName] = forceGpu.split('|');
if (!gpu || !gpuName) {
console.error('setGpu: could not parse GPU path or name from forceGpu:', forceGpu);
return imagesI;
}
const card = gpu.slice(-1);
const render = (Number(card) + 128).toString();
// Handle NVIDIA Gpus
let baseRun;
if (gpuName.includes('NVIDIA')) {
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 (let i=0; i<imagesI.images.length; i++) {
console.log(imagesI.images[i]['run_config']);
let finalRun = _.merge(imagesI.images[i]['run_config'], baseRun)
let finalExec = _.merge(imagesI.images[i]['exec_config'], baseExec)
imagesI.images[i]['run_config'] = finalRun;
imagesI.images[i]['exec_config'] = finalExec;
}
return imagesI;
}
// For rolling-weekly installs, append -rolling to service image tags in docker conf yamls.
// Prevents -rolling-rolling by only modifying tags that don't already end with -rolling.
async function appendRollingToServiceImages() {
const confDir = '/kasm_release/docker';
let entries;
try {
entries = await fsw.readdir(confDir);
} catch (err) {
return;
}
for (const file of entries.filter(f => f.endsWith('.yaml'))) {
const filePath = confDir + '/' + file;
const content = await fsw.readFile(filePath, 'utf8');
const updated = content.split('\n').map(line => {
if (/ image:/.test(line) && /"$/.test(line) && !/-rolling"$/.test(line) && !/develop"$/.test(line)) {
return line.replace(/"$/, '-rolling"');
}
return line;
}).join('\n');
await fsw.writeFile(filePath, updated);
}
}
//// 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) {
@ -41,35 +245,36 @@ app.use(baseUrl, baserouter);
https.listen(3000); https.listen(3000);
//// socketIO comms //// //// socketIO comms ////
io = socketIO(https, {path: baseUrl + 'socket.io'}); const 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]; let imagesI = data[1];
let imagesD = images; let 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 && typeof imagesI === 'object' && Array.isArray(imagesI.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' && imagesI && imagesI.images) {
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);
await appendRollingToServiceImages();
}
// 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 +285,52 @@ io.on('connection', async function (socket) {
} }
}); });
} }
// Run bash upgrade with our custom flags
async function upgrade(data) {
let upgradeFlags = ['/kasm_release/upgrade.sh', '-L', port];
// Patch service image tags for rolling builds
await appendRollingToServiceImages();
// Run upgrade
let cmd = pty.spawn('/bin/bash', upgradeFlags);
cmd.on('data', function(data) {
socket.emit('term', data);
});
cmd.on('exit', async function(code, signal) {
if (code == 0) {
await fsw.copyFile('/version.txt', '/opt/version.txt');
const rollingTag = await getRollingWeeklyTag(currentVersion, arch);
socket.emit('done', {port, rollingTag});
}
});
}
// 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['sourceLocal'] = sourceLocal;
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, currentVersion, sourceLocal]);
} }
} }
// Disable wizard // Disable wizard
@ -104,5 +342,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);
}); });

1
list.json Normal file

File diff suppressed because one or more lines are too long

View file

@ -8,10 +8,14 @@
}, },
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"engines": {
"node": ">=18"
},
"dependencies": { "dependencies": {
"dockerode": "^3.3.2", "dockerode": "^4.0.10",
"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

@ -2,17 +2,17 @@
display: none; display: none;
position: fixed; position: fixed;
width: 100%; width: 100%;
height: calc(100% - 50px); height: calc(100% - 65px);
left: 0; left: 0;
top: 50px; top: 65px;
} }
#container { #container {
position: fixed; position: fixed;
width: 100%; width: 100%;
height: calc(100% - 50px); height: calc(100% - 65px);
left: 0; left: 0;
top: 50px; top: 65px;
overflow: auto; overflow: auto;
} }
@ -25,12 +25,12 @@
top: 10%; top: 10%;
left: 50%; left: 50%;
transform: translate(-50%, -10%); transform: translate(-50%, -10%);
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); max-width: 740px;
} }
#EULAButton { #EULAButton {
position: absolute; position: absolute;
top: 90%; bottom: 0%;
left: 50%; left: 50%;
transform: translate(-50%, -90%); transform: translate(-50%, -90%);
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
@ -38,10 +38,10 @@
#titleBar { #titleBar {
width: 100%; width: 100%;
height: 50px; height: 65px;
background: #595959;
color: white; color: white;
display: inline-block; display: inline-block;
border-bottom: 1px solid #ffffff26;
} }
form { form {
@ -52,16 +52,16 @@ form {
#title { #title {
float: right; float: right;
height: 50px; height: 65px;
line-height: 50px; line-height: 65px;
text-align: center; text-align: center;
margin-right: 10px; margin-right: 10px;
} }
#logo { #logo {
width: 138px; width: 170px;
height: 40px; height: 35px;
margin: 5px; margin: 15px;
} }
.center { .center {
@ -115,3 +115,38 @@ form {
height: 100px; height: 100px;
} }
fieldset {
background: #2b3352b3;
border: 1px solid #334155b3;
border-radius: 8px;
}
input[type=email], input[type=number], input[type=password], input[type=search], input[type=text] {
border-radius: 0.25rem;
border: 1px solid #ffffff26;
border-radius: 5px;
background: #0f172ab3;
width: 100%;
padding: 0.75rem;
font-size: 1rem;
}
input::placeholder {
color: var(--font-color)!important;
opacity: 0.7; /* Firefox reduces opacity by default */
}
label {
font-weight: bold;
margin-bottom:0.5rem;
display: inline-block;
}
legend {
font-size: 1.125rem;
background: #2b3352b3;
border: 1px solid #334155b3;
border-radius: 8px;
padding: 5px 20px;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

34
public/img/logo.svg Normal file
View file

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="4458.45" height="919.08" viewBox="0 0 4458.45 919.08">
<defs>
<style>
.cls-1 {
fill: #fff;
}
.cls-2 {
fill: #185f85;
}
.cls-3 {
fill: #2a2d31;
}
.cls-4 {
fill: #1a9bd7;
}
.cls-5 {
fill: #0d84be;
}
</style>
</defs>
<polygon class="cls-1" points="952.91 0 925.94 0 925.94 919.08 952.91 919.08 952.91 0"/>
<path class="cls-1" d="M1601.72,654.51h-128.28l-258.01-311.95v311.95h-107.14V0h107.14V293.73L1464.7,0h127.55l-264.57,318.51,274.05,336Zm411.07-467.92h101.31v467.92h-101.31v-67.78c-37.9,53.21-92.56,79.44-163.26,79.44-64.14,0-119.53-23.32-164.72-71.43-45.19-47.38-68.51-105.68-68.51-174.2s23.32-127.55,68.51-174.92c45.19-47.37,100.58-71.43,164.72-71.43,70.7,0,125.36,26.24,163.26,78.72v-66.33Zm-252.91,341.1h0c27.7,28.43,63.41,42.27,105.68,42.27s77.26-13.85,105.68-42.27c27.7-28.42,41.54-64.14,41.54-107.14s-13.85-78.72-41.54-107.14c-28.42-28.43-63.41-42.27-105.68-42.27s-77.99,13.85-105.68,42.27c-28.42,28.42-42.27,64.14-42.27,107.14s13.85,78.72,42.27,107.14Zm580.16-212.82h0c0,15.31,8.75,27.7,26.24,36.44,16.76,9.47,37.9,17.49,61.95,24.78,24.05,7.29,48.83,15.31,74.34,23.32,25.51,7.29,46.65,21.87,63.41,43,16.76,21.14,25.51,48.1,25.51,80.17,0,44.46-17.49,80.17-51.75,105.68-34.99,25.51-77.99,37.9-130.46,37.9-45.92,0-86-9.47-118.8-28.42-32.8-19.68-56.85-46.65-72.16-80.9l87.46-50.29c16.03,44.46,50.29,67.05,103.5,67.05s79.44-17.49,79.44-52.48c0-14.58-8.75-26.97-25.51-36.44-17.49-8.75-37.9-17.49-61.95-24.78-24.78-8.02-49.56-15.31-74.34-23.32-25.51-7.29-46.65-21.87-64.14-41.54-16.76-20.41-25.51-45.92-25.51-77.26,0-43,16.04-77.99,48.83-104.22,32.8-26.24,73.61-39.36,122.45-39.36,38.63,0,72.89,8.75,102.77,26.24,30.61,16.76,53.93,40.82,69.97,71.43l-85.28,47.37c-16.03-34.98-45.19-53.21-87.46-53.21-19.68,0-35.71,4.37-48.83,12.39-13.12,8.75-19.68,20.41-19.68,36.44Zm870.98-140.67h0c53.21,0,96.21,17.49,128.28,51.75,32.8,34.26,48.83,80.17,48.83,138.48v290.08h-101.31V370.98c0-32.8-8.02-57.58-24.05-75.07-16.03-18.22-38.63-26.97-66.32-26.97-31.34,0-56.12,10.2-74.34,30.61-18.22,21.14-27.7,51.75-27.7,92.56v262.39h-101.31V370.98c0-32.8-7.29-57.58-22.59-75.07-15.31-18.22-37.17-26.97-64.87-26.97-30.61,0-55.39,10.2-75.07,31.34-18.95,21.14-29.15,51.75-29.15,91.84v262.39h-100.58V186.59h100.58v56.12c29.88-45.19,75.07-68.51,134.11-68.51s104.23,24.78,132.65,74.34c31.34-49.56,78.72-74.34,142.85-74.34Z"/>
<path class="cls-1" d="M1149.1,916.16l-40.82-150.14h25.51l32.07,117.34,34.26-117.34h22.59l33.53,117.34,31.34-117.34h24.78l-40.82,150.14h-28.42l-32.07-110.06-32.07,110.06h-29.88Zm403.05-19.68h0c-15.31,15.31-33.53,22.59-55.39,22.59s-40.09-7.29-55.39-22.59c-15.31-15.31-22.59-33.53-22.59-55.39s7.29-40.09,22.59-54.66c15.31-15.31,33.53-23.32,55.39-23.32s40.09,8.02,55.39,23.32c14.58,14.58,22.59,32.8,22.59,54.66s-8.02,40.09-22.59,55.39Zm-93.29-16.76h0c10.2,10.2,22.59,15.31,37.9,15.31s27.7-5.1,37.9-15.31c10.2-10.2,15.31-23.32,15.31-38.63s-5.1-28.42-15.31-38.63c-10.2-10.2-23.32-15.31-37.9-15.31-15.31,0-27.7,5.1-37.9,15.31-10.2,10.2-15.31,23.32-15.31,38.63s5.1,28.43,15.31,38.63Zm323.61,36.44h0l-32.07-54.66h-29.15v54.66h-24.78v-150.14h59.77c13.85,0,25.51,5.1,34.26,14.58,9.47,8.75,14.58,20.41,14.58,33.53,0,10.2-2.92,18.95-8.75,26.97-5.1,7.29-12.39,13.12-21.14,16.76l34.26,58.31h-26.97Zm-61.22-126.82h0v50.29h34.98c7.29,0,12.39-2.19,16.76-7.29,5.1-5.1,7.29-10.93,7.29-18.22,0-6.56-2.19-12.39-7.29-17.49-4.37-5.1-9.47-7.29-16.76-7.29h-34.98Zm319.24,126.82h-29.15l-59.04-71.43v71.43h-24.78v-150.14h24.78v67.78l56.85-67.78h29.15l-60.49,73.61,62.68,76.53Zm157.43,2.92h0c-14.58,0-26.97-2.92-37.17-9.47-9.47-6.56-16.76-16.03-21.14-26.97l21.87-13.12c5.83,17.49,18.22,25.51,37.17,25.51,8.75,0,16.04-1.46,20.41-5.1,5.1-3.64,7.29-8.75,7.29-14.58s-2.19-10.93-7.29-13.85c-5.1-2.92-13.85-6.56-26.97-10.93-6.56-2.19-12.39-4.37-16.76-5.83-5.1-2.19-9.48-4.37-14.58-8.02-5.1-2.91-8.75-7.29-10.93-12.39-2.19-5.1-3.64-10.93-3.64-17.49,0-13.12,4.37-24.05,13.85-32.07,9.48-7.29,21.14-11.66,34.26-11.66,12.39,0,22.59,2.92,31.34,9.48,9.47,5.83,16.03,13.85,21.14,24.05l-21.14,11.66c-5.83-13.85-16.76-21.14-31.34-21.14-7.29,0-13.12,2.19-16.76,5.1-4.37,3.64-6.56,8.02-6.56,13.85s2.19,9.48,6.56,13.12c4.37,2.92,12.39,6.56,24.05,9.48,5.1,2.19,9.48,3.64,11.66,5.1,2.91,.73,6.56,2.19,11.66,4.37,5.1,1.46,8.75,3.64,10.93,5.83,2.19,1.46,5.1,4.37,7.29,7.29,2.92,2.92,5.1,6.56,5.83,10.2,1.46,4.37,2.19,8.75,2.19,13.12,0,13.85-5.1,24.78-14.58,32.8-10.2,8.02-22.59,11.66-38.63,11.66Zm228.13-153.06h0c13.85,0,26.24,5.1,35.71,14.58,9.47,9.48,13.85,21.14,13.85,34.99s-4.37,25.51-13.85,34.98c-9.48,9.47-21.87,14.58-35.71,14.58h-30.61v51.02h-24.78v-150.14h55.39Zm0,75.8h0c7.29,0,13.12-2.92,18.22-7.29,4.37-5.1,6.56-11.66,6.56-18.95s-2.19-13.85-6.56-18.95c-5.1-5.1-10.93-7.29-18.22-7.29h-30.61v52.48h30.61Zm253.64,74.34h0l-10.93-29.88h-63.41l-10.93,29.88h-26.97l54.66-150.14h29.88l53.93,150.14h-26.24Zm-66.32-53.21h47.37l-23.32-67.78-24.05,67.78Zm274.05,56.12h0c-22.59,0-41.54-7.29-56.12-22.59-15.31-14.58-22.59-32.8-22.59-55.39s7.29-40.09,22.59-55.39c14.58-14.58,33.53-22.59,56.12-22.59,13.12,0,26.24,3.64,37.17,10.2,11.66,6.56,20.41,14.58,26.97,26.24l-21.14,12.39c-4.37-8.02-10.2-13.85-17.49-18.22-8.02-4.37-16.03-6.56-25.51-6.56-16.04,0-29.15,5.1-39.36,15.31-9.48,10.2-14.58,23.32-14.58,38.63s5.1,28.43,14.58,38.63c10.2,10.2,23.32,15.31,39.36,15.31,9.47,0,17.49-2.19,25.51-6.56,8.02-4.37,13.85-10.2,17.49-17.49l21.14,12.39c-5.83,10.93-15.31,19.68-26.24,26.24-11.66,6.56-24.05,9.47-37.9,9.47Zm209.18-26.24h66.32v23.32h-91.11v-150.14h89.65v24.05h-64.87v38.63h59.77v23.32h-59.77v40.82Zm238.33,26.24h0c-14.58,0-26.97-2.92-37.17-9.47-9.47-6.56-16.76-16.03-21.14-26.97l21.87-13.12c5.83,17.49,18.22,25.51,37.17,25.51,9.48,0,16.04-1.46,20.41-5.1,5.1-3.64,7.29-8.75,7.29-14.58s-2.19-10.93-7.29-13.85c-5.1-2.92-13.85-6.56-26.97-10.93-6.56-2.19-12.39-4.37-16.76-5.83-5.1-2.19-9.48-4.37-14.58-8.02-5.1-2.91-8.75-7.29-10.93-12.39-2.19-5.1-3.64-10.93-3.64-17.49,0-13.12,4.37-24.05,13.85-32.07,9.48-7.29,21.14-11.66,34.26-11.66,12.39,0,22.59,2.92,31.34,9.48,9.47,5.83,16.03,13.85,21.14,24.05l-21.14,11.66c-5.83-13.85-16.76-21.14-31.34-21.14-7.29,0-12.39,2.19-16.76,5.1-4.37,3.64-6.56,8.02-6.56,13.85s2.19,9.48,6.56,13.12c4.37,2.92,12.39,6.56,24.05,9.48,5.1,2.19,9.48,3.64,11.66,5.1,2.91,.73,6.56,2.19,11.66,4.37,5.1,1.46,8.75,3.64,10.93,5.83,2.19,1.46,5.1,4.37,8.02,7.29,2.19,2.92,4.37,6.56,5.83,10.2,.73,4.37,1.46,8.75,1.46,13.12,0,13.85-5.1,24.78-14.58,32.8-10.2,8.02-22.59,11.66-38.63,11.66Z"/>
<polygon class="cls-1" points="33.6 235.37 421.24 13.5 807.32 237.72 807.32 695.77 421.24 916.16 33.6 698.38 33.6 235.37"/>
<polygon class="cls-2" points="260.79 678.63 260.79 707.58 260.79 746.57 363 805.06 387.81 819.83 421.48 839.33 445.11 796.79 455.16 751.3 421.48 721.76 260.79 628.42 260.79 678.63"/>
<path class="cls-5" d="M743.46,653.82V282.22l-67.35-2.36-14.18,21.86-20.68,39.58v253.45l-219.77,127.02v117.57c107.52-62.03,214.46-124.06,321.98-185.51Z"/>
<polygon class="cls-3" points="201.71 409.23 201.71 223.14 100.1 282.22 100.1 653.82 201.71 712.31 201.71 526.8 421.48 653.82 421.48 653.82 421.48 535.66 303.92 467.72 421.48 399.78 421.48 282.22 201.71 409.23"/>
<path class="cls-4" d="M421.48,96.71c107.52,62.03,214.46,123.47,321.98,185.51l-59.08,34.27-43.13,24.81-219.77-127.02-160.69,92.75v-117.57l160.69-92.75Z"/>
</svg>

After

Width:  |  Height:  |  Size: 7.3 KiB

View file

@ -13,7 +13,7 @@
</head> </head>
<body> <body>
<div id="titleBar"> <div id="titleBar">
<img src="public/img/logo.png" id="logo"> <img src="public/img/logo.svg" id="logo">
<div id="title"></div> <div id="title"></div>
</div> </div>
<div id="spacing"></div> <div id="spacing"></div>

View file

@ -1,17 +1,21 @@
// Variables // Variables
var EULA; let EULA;
var images; let images;
var term; let gpus;
var installImages = []; let term;
var installSettings = {}; let installImages = [];
var selected = false; const installSettings = {};
const upgradeSettings = {};
let currentVersion = '';
let isUpgrade = false;
let selected = false;
// Socket.io connection // Socket.io connection
var host = window.location.hostname; const host = window.location.hostname;
var port = window.location.port; const port = window.location.port;
var protocol = window.location.protocol; const protocol = window.location.protocol;
var path = window.location.pathname; const path = window.location.pathname;
var socket = io(protocol + '//' + host + ':' + port, { path: path + 'socket.io'}); const socket = io(protocol + '//' + host + ':' + port, { path: path + 'socket.io'});
//// Page Functions //// //// Page Functions ////
@ -25,20 +29,32 @@ 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 (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; if (!srcImage) {
alert('Error: image "' + image + '" not found. Please refresh and try again.');
return;
} }
Object.assign(selectedImages.images, {[image]: images.images[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('install', [installSettings, selectedImages]); socket.emit('install', [installSettings, selectedImages]);
} }
} }
// Execute upgrade
async function upgrade() {
isUpgrade = true;
showTerminal();
titleChange('Upgrading');
socket.emit('upgrade', [upgradeSettings, false]);
}
// Show page container // Show page container
function showContainer() { function showContainer() {
$('#terminal').empty(); $('#terminal').empty();
@ -55,7 +71,7 @@ function showTerminal() {
$('#terminal').empty(); $('#terminal').empty();
$('#terminal').css('display', 'block'); $('#terminal').css('display', 'block');
term = new Terminal(); term = new Terminal();
fitaddon = new FitAddon.FitAddon(); let fitaddon = new FitAddon.FitAddon();
term.loadAddon(fitaddon); term.loadAddon(fitaddon);
term.open($('#terminal')[0]); term.open($('#terminal')[0]);
fitaddon.fit(); fitaddon.fit();
@ -67,12 +83,18 @@ function titleChange(value) {
$('#title').text(value); $('#title').text(value);
} }
function versionChange(ver, local) {
document.title = 'Kasm Wizard ' + ver + (local ? ' (local)' : '');
}
// Render landing as installer page // Render landing as installer page
function renderInstall(data) { function renderInstall(data) {
showContainer(); showContainer();
titleChange('EULA'); titleChange('EULA');
EULA = data[0]; EULA = data[0];
images = data[1]; images = data[1];
gpus = data[2] || {};
versionChange(data[3], data[4]);
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,18 +102,39 @@ 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];
currentVersion = info.currentVersion;
versionChange(info.currentVersion, info.sourceLocal);
// Store GPU info
$('body').data('gpuInfo', info.gpuInfo);
// Upgrade button if needed
let 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>'); let 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 (let container of info.containers) {
containersTable.append( containersTable.append(
$('<tr>').append([ $('<tr>').append([
$('<th>').text(container.Names[0]), $('<th>').text(container.Names[0]),
@ -99,7 +142,7 @@ async function renderDash(info) {
]) ])
); );
} }
dockerCard = $('<div>', {id: 'dockerinfo', class: 'terminal-card'}).append([ let dockerCard = $('<div>', {id: 'dockerinfo', class: 'terminal-card'}).append([
$('<header>').text('Kasm Docker containers'), $('<header>').text('Kasm Docker containers'),
$('<table>').append(containersTable) $('<table>').append(containersTable)
]); ]);
@ -107,7 +150,7 @@ async function renderDash(info) {
let usedmem = (info.mem.active/info.mem.total)*100; let usedmem = (info.mem.active/info.mem.total)*100;
let totalmem = parseFloat(info.mem.total/1000000000).toFixed(2); let totalmem = parseFloat(info.mem.total/1000000000).toFixed(2);
let diskbuffer = parseFloat(info.mem.buffcache/1000000000).toFixed(2); let diskbuffer = parseFloat(info.mem.buffcache/1000000000).toFixed(2);
sysinfoTable = $('<tbody>').append([ let sysinfoTable = $('<tbody>').append([
$('<tr>').append([ $('<tr>').append([
$('<th>').text('CPU'), $('<th>').text('CPU'),
$('<td>').text(info.cpu.vendor + ' ' + info.cpu.brand) $('<td>').text(info.cpu.vendor + ' ' + info.cpu.brand)
@ -147,7 +190,7 @@ async function renderDash(info) {
) )
]) ])
]); ]);
systemCard = $('<div>', {id: 'systeminfo', class: 'terminal-card'}).append([ let systemCard = $('<div>', {id: 'systeminfo', class: 'terminal-card'}).append([
$('<header>').text('System Information'), $('<header>').text('System Information'),
$('<table>').append(sysinfoTable) $('<table>').append(sysinfoTable)
]); ]);
@ -185,24 +228,28 @@ async function pickSettings() {
]); ]);
let userPass = $('<div>', {class: 'form-group'}).append([ let userPass = $('<div>', {class: 'form-group'}).append([
$('<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', required: true, placeholder: 'required'})
]); ]);
let useRolling = $('<div>', {class: 'form-group'}).append([ let gpuOptions = [$('<option>', {value: 'disabled'}).text('Disabled')];
$('<label>', {for: 'useRolling'}).text('Use Rolling Images: '), for (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'})
]); ]);
let passHint = $('<p>', {
id: 'pass-hint',
style: 'color: inherit;'
}).text('Passwords must be at least 8 characters and include uppercase, lowercase, a number, and a special character (!#$%^&*@).');
fieldset.append([ fieldset.append([
passHint,
adminPass, adminPass,
userPass, userPass,
useRolling, forceGpu,
noDownload,
submit submit
]); ]);
form.append(fieldset); form.append(fieldset);
@ -210,36 +257,77 @@ async function pickSettings() {
// Grab data and move to image selection // Grab data and move to image selection
form.on('submit', function (e) { form.on('submit', function (e) {
e.preventDefault(); e.preventDefault();
installSettings.adminPass = $('#adminPass').val(); const passRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!#$%^&*@]).{8,}$/;
installSettings.userPass = $('#userPass').val(); const adminPassVal = $('#adminPass').val();
installSettings.useRolling = $('#useRolling').is(":checked"); const userPassVal = $('#userPass').val();
installSettings.noDownload = $('#noDownload').is(":checked"); if (!passRegex.test(adminPassVal) || !passRegex.test(userPassVal)) {
pickImages(); $('#pass-hint').css('color', '#df2f87');
return;
}
$('#pass-hint').css('color', 'inherit');
installSettings.adminPass = adminPassVal;
installSettings.userPass = userPassVal;
installSettings.forceGpu = $('#forceGpu').val();
pickImages(false);
});
}
// Render upgrade form
function renderUpgrade() {
showContainer();
titleChange('Upgrade Settings');
let fieldset = $('<fieldset>').append($('<legend>').text('Kasm Upgrade Settings'));
fieldset.append($('<p>').text('Are you sure you want to upgrade Kasm to version ' + currentVersion + '?'));
let confirmCheck = $('<div>', {class: 'form-group'}).append([
$('<label>', {for: 'confirmUpgrade'}).text('I confirm I want to upgrade'),
$('<input>', {name: 'confirmUpgrade', id: 'confirmUpgrade', type: 'checkbox'})
]);
fieldset.append(confirmCheck);
let upgradeButton = $('<button>', {
class: 'btn btn-default btn-ghost',
id: 'upgradeButton',
onclick: 'upgrade()',
disabled: true
}).text('Upgrade');
$('#container').append([$('<div>', {class: 'terminal-card', style: 'margin: 20px 0; border: none;'}).append(fieldset), upgradeButton]);
$('#confirmUpgrade').on('change', function() {
$('#upgradeButton').prop('disabled', !this.checked);
}); });
} }
// Render image selection // Render image selection
async function pickImages() { async function pickImages(upgrade) {
let installText;
let 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 (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'), '_') + '\')' 'data-friendly-name': image.friendly_name
}).append(imageName).css('filter', 'grayscale(100%)') }).append(imageName).css('filter', 'grayscale(100%)');
let thumb = $('<img>', {class: 'thumb', src: 'public/' + images.images[image].image_src}); imageDiv.on('click', function() {
selectImage($(this).data('friendly-name'));
});
let thumb = $('<img>', {class: 'thumb', src: image.image_src, loading: 'lazy'});
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
@ -247,19 +335,20 @@ async function pickImages() {
} }
// Select an individual image // Select an individual image
function selectImage(image) { function selectImage(friendlyName) {
let imageKey = image.replace(new RegExp('_', 'g'), ' '); let elemId = friendlyName.replace(new RegExp(' ', 'g'), '_').replace('.', '-');
if (installImages.includes(imageKey)) { let safeSelectorId = $.escapeSelector(elemId); // Escape the ID for jQuery
installImages = installImages.filter(e => e !== imageKey) if (installImages.includes(friendlyName)) {
$('#' + image).css({ installImages = installImages.filter(e => e !== friendlyName);
filter: 'grayscale(100%)', $('#' + safeSelectorId).css({
filter: 'grayscale(100%) brightness(0.7)',
background: '' background: ''
}); });
} else { } else {
installImages.push(imageKey); installImages.push(friendlyName);
$('#' + image).css({ $('#' + safeSelectorId).css({
filter: '', filter: '',
background: '#89cff0' background: '#30426c'
}); });
} }
} }
@ -269,19 +358,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'
}); });
@ -290,11 +381,18 @@ function selectAll() {
} }
// Show finished page // Show finished page
function done(port) { function done(data) {
const port = (typeof data === 'object') ? data.port : data;
const rollingTag = (typeof data === 'object') ? data.rollingTag : null;
showContainer(); showContainer();
titleChange('Complete'); titleChange('Complete');
let titleBar = $('<div>'); let titleBar = $('<div>');
titleBar.append($('<h2>', {class: 'center'}).text('Installation Complete')); if (isUpgrade) {
titleBar.append($('<h2>', {class: 'center'}).text('Upgrade Complete'));
titleBar.append($('<h3>', {class: 'center'}).text('Remember to update your workspace image tags to :' + (rollingTag || (currentVersion + '-rolling-weekly'))));
} else {
titleBar.append($('<h2>', {class: 'center'}).text('Installation Complete'));
}
titleBar.append($('<h3>', {class: 'center'}).text('This page will reload in 5 seconds')); titleBar.append($('<h3>', {class: 'center'}).text('This page will reload in 5 seconds'));
titleBar.append($('<h3>', {class: 'center'}).text('Your installation is available on port ' + port)); titleBar.append($('<h3>', {class: 'center'}).text('Your installation is available on port ' + port));
$('#container').append(titleBar); $('#container').append(titleBar);