mirror of
https://github.com/kasmtech/kasm-install-wizard.git
synced 2026-07-18 00:45:04 +00:00
Compare commits
37 commits
1.13.0.607
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2a3b4f1a3c | ||
|
|
15f1b4aa17 | ||
|
|
ce72001ce5 | ||
|
|
cc5e786633 | ||
|
|
eb0218f612 | ||
|
|
fbf2e5ff48 | ||
|
|
d7d5cd8f12 | ||
|
|
9bae28c7d4 | ||
|
|
a0ec262f83 | ||
|
|
0411125ffa | ||
|
|
633e3d9c87 | ||
|
|
f3b8319807 | ||
|
|
032df175de | ||
|
|
c66581a6ac | ||
|
|
1ffc4f955f | ||
|
|
34e43330c1 | ||
|
|
f34553f5b7 | ||
|
|
b0536f4a29 | ||
|
|
d73ddacefb | ||
|
|
92b30e8d25 | ||
|
|
7924f882db | ||
|
|
d4b7db69f4 | ||
|
|
502bc2f530 | ||
|
|
d8a0bcb5e5 | ||
|
|
1e8cd14fd2 | ||
|
|
b5e5ef15ff | ||
|
|
63ac91b82e | ||
|
|
9f542df2a9 | ||
|
|
cc2c34c758 | ||
|
|
cfaa944c84 | ||
|
|
f4356863d3 | ||
|
|
457214bf4a | ||
|
|
daa089b1d5 | ||
|
|
07fbb9d873 | ||
|
|
81279dcf88 | ||
|
|
60e9c3a231 | ||
|
|
ff09276100 |
11 changed files with 580 additions and 147 deletions
|
|
@ -1,21 +1,38 @@
|
|||
image: docker
|
||||
services:
|
||||
- docker:dind
|
||||
image: alpine
|
||||
|
||||
variables:
|
||||
DOCKER_HOST: tcp://docker:2375
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
DOCKER_AUTH_CONFIG: ${_DOCKER_AUTH_CONFIG}
|
||||
|
||||
default:
|
||||
tags:
|
||||
- oci-fixed-amd
|
||||
|
||||
stages:
|
||||
- upload
|
||||
- update
|
||||
|
||||
before_script:
|
||||
- export SANITIZED_BRANCH="$(echo $CI_COMMIT_REF_NAME | sed -r 's#^release/##' | sed 's/\//_/g')"
|
||||
|
||||
push_to_s3:
|
||||
stage: upload
|
||||
update_list_json:
|
||||
stage: update
|
||||
rules:
|
||||
- if: '$CI_COMMIT_REF_NAME =~ /^release\//'
|
||||
when: on_success
|
||||
script:
|
||||
- apk add aws-cli curl gzip tar
|
||||
- export S3_URL="https://${S3_BUCKET}.s3.amazonaws.com/wizard/${CI_COMMIT_SHA}.tar.gz"
|
||||
- tar -czf ${CI_COMMIT_SHA}.tar.gz *
|
||||
- aws s3 cp ${CI_COMMIT_SHA}.tar.gz s3://${S3_BUCKET}/wizard/
|
||||
- aws s3 cp ${CI_COMMIT_SHA}.tar.gz s3://${S3_BUCKET}/wizard/${SANITIZED_BRANCH}.tar.gz
|
||||
- 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}"
|
||||
tags:
|
||||
- aws-autoscale
|
||||
- apk add curl git
|
||||
- |
|
||||
if curl -fsSL https://registry.kasmweb.com/1.1/list.json -o list.json.tmp; then
|
||||
mv list.json.tmp list.json
|
||||
git config user.email "ci@gitlab"
|
||||
git config user.name "GitLab CI"
|
||||
git remote set-url origin "https://gitlab-ci-token:${CI_JOB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
|
||||
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
|
||||
|
|
|
|||
|
|
@ -36,3 +36,8 @@ As configured files ingested from a current Kasm Workspaces installer are needed
|
|||
/kasm_release/
|
||||
└── 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
333
index.js
|
|
@ -1,34 +1,238 @@
|
|||
// Imports
|
||||
var Docker = require('dockerode');
|
||||
var socketIO = require('socket.io');
|
||||
var pty = require("node-pty");
|
||||
var fsw = require('fs').promises;
|
||||
var fs = require('fs');
|
||||
var os = require('os');
|
||||
var yaml = require('js-yaml');
|
||||
var si = require('systeminformation');
|
||||
var express = require('express');
|
||||
var app = require('express')();
|
||||
var privateKey = fs.readFileSync('/opt/kasm/certs/kasm_wizard.key', 'utf8');
|
||||
var certificate = fs.readFileSync('/opt/kasm/certs/kasm_wizard.crt', 'utf8');
|
||||
var credentials = {key: privateKey, cert: certificate};
|
||||
var https = require('https').Server(credentials, app);
|
||||
var baserouter = express.Router();
|
||||
var docker = new Docker({socketPath: '/var/run/docker.sock'});
|
||||
var arch = os.arch().replace('x64', 'amd64');
|
||||
var baseUrl = process.env.SUBFOLDER || '/';
|
||||
var version = process.env.VERSION || 'stable';
|
||||
var port = process.env.KASM_PORT || '443';
|
||||
var EULA;
|
||||
var images;
|
||||
const Docker = require('dockerode');
|
||||
const socketIO = require('socket.io');
|
||||
const pty = require("node-pty");
|
||||
const fsw = require('fs').promises;
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const yaml = require('js-yaml');
|
||||
const _ = require('lodash');
|
||||
const si = require('systeminformation');
|
||||
const express = require('express');
|
||||
const app = require('express')();
|
||||
const privateKey = fs.readFileSync('/opt/kasm/certs/kasm_wizard.key', 'utf8');
|
||||
const certificate = fs.readFileSync('/opt/kasm/certs/kasm_wizard.crt', 'utf8');
|
||||
const credentials = {key: privateKey, cert: certificate};
|
||||
const https = require('https').Server(credentials, app);
|
||||
const baserouter = express.Router();
|
||||
const docker = new Docker({socketPath: '/var/run/docker.sock'});
|
||||
const arch = os.arch().replace('x64', 'amd64');
|
||||
const baseUrl = process.env.SUBFOLDER || '/';
|
||||
const port = process.env.KASM_PORT || '443';
|
||||
const { spawn } = require('node:child_process');
|
||||
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
|
||||
async function installerBlobs() {
|
||||
EULA = await fsw.readFile('/kasm_release/licenses/LICENSE.txt', 'utf8');
|
||||
let imagesText = await fsw.readFile('/wizard/default_images_' + arch + '.yaml', 'utf8');
|
||||
images = yaml.load(imagesText);
|
||||
try {
|
||||
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();
|
||||
|
||||
// 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 ////
|
||||
baserouter.use('/public', express.static(__dirname + '/public'));
|
||||
baserouter.get("/", function (req, res) {
|
||||
|
|
@ -41,35 +245,36 @@ app.use(baseUrl, baserouter);
|
|||
https.listen(3000);
|
||||
|
||||
//// socketIO comms ////
|
||||
io = socketIO(https, {path: baseUrl + 'socket.io'});
|
||||
const io = socketIO(https, {path: baseUrl + 'socket.io'});
|
||||
io.on('connection', async function (socket) {
|
||||
// Run bash install with our custom flags
|
||||
async function install(data) {
|
||||
// Determine install settings
|
||||
installSettings = data[0];
|
||||
let imagesI = data[1];
|
||||
let imagesD = images;
|
||||
installFlags = ['/kasm_release/install.sh', '-B' ,'-H', '-e', '-L', port, '-P', installSettings.adminPass, '-U', installSettings.userPass];
|
||||
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)) {
|
||||
let installFlags = ['/kasm_release/install.sh', '-W', '-B' ,'-H', '-e', '-L', port, '-P', installSettings.adminPass, '-U', installSettings.userPass];
|
||||
if (imagesI && typeof imagesI === 'object' && Array.isArray(imagesI.images) && imagesI.images.length < 10) {
|
||||
installFlags.push('-b');
|
||||
}
|
||||
// Flag the images properly based on selection
|
||||
for await (let image of Object.keys(images.images)) {
|
||||
if ((imagesI.hasOwnProperty('images')) && (imagesI.images.hasOwnProperty(image))) {
|
||||
imagesD.images[image].enabled = true;
|
||||
imagesD.images[image].hidden = false;
|
||||
} else {
|
||||
imagesD.images[image].enabled = false;
|
||||
imagesD.images[image].hidden = true;
|
||||
}
|
||||
|
||||
// GPU yaml merge
|
||||
if (installSettings.forceGpu !== 'disabled' && imagesI && imagesI.images) {
|
||||
imagesI = await setGpu(imagesI);
|
||||
}
|
||||
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);
|
||||
cmd.on('data', function(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
|
||||
async function renderLanding() {
|
||||
let containers = await docker.listContainers();
|
||||
// This is a running system
|
||||
if (containers.length !== 0) {
|
||||
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['cpu'] = await si.cpu();
|
||||
dashinfo['mem'] = await si.mem();
|
||||
dashinfo['cpuPercent'] = await si.currentLoad();
|
||||
dashinfo['port'] = port;
|
||||
socket.emit('renderdash', dashinfo);
|
||||
socket.emit('renderdash', [dashinfo, images]);
|
||||
// Render installer
|
||||
} else {
|
||||
socket.emit('renderinstall', [EULA, images]);
|
||||
socket.emit('renderinstall', [EULA, images, gpuInfo, currentVersion, sourceLocal]);
|
||||
}
|
||||
}
|
||||
// Disable wizard
|
||||
|
|
@ -104,5 +342,6 @@ io.on('connection', async function (socket) {
|
|||
//// Incoming requests ////
|
||||
socket.on('renderlanding', renderLanding);
|
||||
socket.on('install', install);
|
||||
socket.on('upgrade', upgrade);
|
||||
socket.on('nowizard', noWizard);
|
||||
});
|
||||
|
|
|
|||
1
list.json
Normal file
1
list.json
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -8,10 +8,14 @@
|
|||
},
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"dependencies": {
|
||||
"dockerode": "^3.3.2",
|
||||
"dockerode": "^4.0.10",
|
||||
"express": "^4.18.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"node-pty": "^0.10.1",
|
||||
"socket.io": "^4.5.1",
|
||||
"systeminformation": "^5.11.16"
|
||||
|
|
|
|||
|
|
@ -2,17 +2,17 @@
|
|||
display: none;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: calc(100% - 50px);
|
||||
height: calc(100% - 65px);
|
||||
left: 0;
|
||||
top: 50px;
|
||||
top: 65px;
|
||||
}
|
||||
|
||||
#container {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
height: calc(100% - 50px);
|
||||
height: calc(100% - 65px);
|
||||
left: 0;
|
||||
top: 50px;
|
||||
top: 65px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
|
@ -25,12 +25,12 @@
|
|||
top: 10%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -10%);
|
||||
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
|
||||
max-width: 740px;
|
||||
}
|
||||
|
||||
#EULAButton {
|
||||
position: absolute;
|
||||
top: 90%;
|
||||
bottom: 0%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -90%);
|
||||
box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2);
|
||||
|
|
@ -38,10 +38,10 @@
|
|||
|
||||
#titleBar {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background: #595959;
|
||||
height: 65px;
|
||||
color: white;
|
||||
display: inline-block;
|
||||
border-bottom: 1px solid #ffffff26;
|
||||
}
|
||||
|
||||
form {
|
||||
|
|
@ -52,16 +52,16 @@ form {
|
|||
|
||||
#title {
|
||||
float: right;
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
height: 65px;
|
||||
line-height: 65px;
|
||||
text-align: center;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
#logo {
|
||||
width: 138px;
|
||||
height: 40px;
|
||||
margin: 5px;
|
||||
width: 170px;
|
||||
height: 35px;
|
||||
margin: 15px;
|
||||
}
|
||||
|
||||
.center {
|
||||
|
|
@ -115,3 +115,38 @@ form {
|
|||
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
34
public/img/logo.svg
Normal 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 |
|
|
@ -13,7 +13,7 @@
|
|||
</head>
|
||||
<body>
|
||||
<div id="titleBar">
|
||||
<img src="public/img/logo.png" id="logo">
|
||||
<img src="public/img/logo.svg" id="logo">
|
||||
<div id="title"></div>
|
||||
</div>
|
||||
<div id="spacing"></div>
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
// Variables
|
||||
var EULA;
|
||||
var images;
|
||||
var term;
|
||||
var installImages = [];
|
||||
var installSettings = {};
|
||||
var selected = false;
|
||||
let EULA;
|
||||
let images;
|
||||
let gpus;
|
||||
let term;
|
||||
let installImages = [];
|
||||
const installSettings = {};
|
||||
const upgradeSettings = {};
|
||||
let currentVersion = '';
|
||||
let isUpgrade = false;
|
||||
let selected = false;
|
||||
|
||||
// Socket.io connection
|
||||
var host = window.location.hostname;
|
||||
var port = window.location.port;
|
||||
var protocol = window.location.protocol;
|
||||
var path = window.location.pathname;
|
||||
var socket = io(protocol + '//' + host + ':' + port, { path: path + 'socket.io'});
|
||||
const host = window.location.hostname;
|
||||
const port = window.location.port;
|
||||
const protocol = window.location.protocol;
|
||||
const path = window.location.pathname;
|
||||
const socket = io(protocol + '//' + host + ':' + port, { path: path + 'socket.io'});
|
||||
|
||||
//// Page Functions ////
|
||||
|
||||
|
|
@ -25,20 +29,32 @@ async function install() {
|
|||
showTerminal()
|
||||
titleChange('Installing');
|
||||
// Create new object based on image selection
|
||||
let selectedImages = {images: {}};
|
||||
let selectedImages = {alembic_version: images.alembic_version, images: [], group_images: []};
|
||||
if (installImages.length == 0) {
|
||||
socket.emit('install', [installSettings, false]);
|
||||
} else {
|
||||
for await (let image of installImages) {
|
||||
if (images.images[image].hasOwnProperty('enabled')) {
|
||||
images.images[image].enabled = true;
|
||||
for (let image of installImages) {
|
||||
let srcImage = images.images.find(x => x.friendly_name === image);
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
// Execute upgrade
|
||||
async function upgrade() {
|
||||
isUpgrade = true;
|
||||
showTerminal();
|
||||
titleChange('Upgrading');
|
||||
socket.emit('upgrade', [upgradeSettings, false]);
|
||||
}
|
||||
|
||||
// Show page container
|
||||
function showContainer() {
|
||||
$('#terminal').empty();
|
||||
|
|
@ -55,7 +71,7 @@ function showTerminal() {
|
|||
$('#terminal').empty();
|
||||
$('#terminal').css('display', 'block');
|
||||
term = new Terminal();
|
||||
fitaddon = new FitAddon.FitAddon();
|
||||
let fitaddon = new FitAddon.FitAddon();
|
||||
term.loadAddon(fitaddon);
|
||||
term.open($('#terminal')[0]);
|
||||
fitaddon.fit();
|
||||
|
|
@ -67,12 +83,18 @@ function titleChange(value) {
|
|||
$('#title').text(value);
|
||||
}
|
||||
|
||||
function versionChange(ver, local) {
|
||||
document.title = 'Kasm Wizard ' + ver + (local ? ' (local)' : '');
|
||||
}
|
||||
|
||||
// Render landing as installer page
|
||||
function renderInstall(data) {
|
||||
showContainer();
|
||||
titleChange('EULA');
|
||||
EULA = data[0];
|
||||
images = data[1];
|
||||
gpus = data[2] || {};
|
||||
versionChange(data[3], data[4]);
|
||||
let EULADiv = $('<div>', {id: 'EULA'}).text(EULA);
|
||||
$('#container').append(EULADiv);
|
||||
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
|
||||
async function renderDash(info) {
|
||||
async function renderDash(data) {
|
||||
showContainer();
|
||||
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
|
||||
containersTable = $('<tbody>');
|
||||
let containersTable = $('<tbody>');
|
||||
containersTable.append(
|
||||
$('<tr>').append([
|
||||
$('<th>').text('Web URL'),
|
||||
$('<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(
|
||||
$('<tr>').append([
|
||||
$('<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'),
|
||||
$('<table>').append(containersTable)
|
||||
]);
|
||||
|
|
@ -107,7 +150,7 @@ async function renderDash(info) {
|
|||
let usedmem = (info.mem.active/info.mem.total)*100;
|
||||
let totalmem = parseFloat(info.mem.total/1000000000).toFixed(2);
|
||||
let diskbuffer = parseFloat(info.mem.buffcache/1000000000).toFixed(2);
|
||||
sysinfoTable = $('<tbody>').append([
|
||||
let sysinfoTable = $('<tbody>').append([
|
||||
$('<tr>').append([
|
||||
$('<th>').text('CPU'),
|
||||
$('<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'),
|
||||
$('<table>').append(sysinfoTable)
|
||||
]);
|
||||
|
|
@ -185,24 +228,28 @@ async function pickSettings() {
|
|||
]);
|
||||
let userPass = $('<div>', {class: 'form-group'}).append([
|
||||
$('<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([
|
||||
$('<label>', {for: 'useRolling'}).text('Use Rolling Images: '),
|
||||
$('<input>', {name: 'useRolling', id: 'useRolling', type: 'checkbox'})
|
||||
]);
|
||||
let noDownload = $('<div>', {class: 'form-group'}).append([
|
||||
$('<label>', {for: 'noDownload'}).text('Skip Image Download: '),
|
||||
$('<input>', {name: 'noDownload', id: 'noDownload', type: 'checkbox'})
|
||||
let gpuOptions = [$('<option>', {value: 'disabled'}).text('Disabled')];
|
||||
for (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 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'})
|
||||
]);
|
||||
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([
|
||||
passHint,
|
||||
adminPass,
|
||||
userPass,
|
||||
useRolling,
|
||||
noDownload,
|
||||
forceGpu,
|
||||
submit
|
||||
]);
|
||||
form.append(fieldset);
|
||||
|
|
@ -210,36 +257,77 @@ async function pickSettings() {
|
|||
// Grab data and move to image selection
|
||||
form.on('submit', function (e) {
|
||||
e.preventDefault();
|
||||
installSettings.adminPass = $('#adminPass').val();
|
||||
installSettings.userPass = $('#userPass').val();
|
||||
installSettings.useRolling = $('#useRolling').is(":checked");
|
||||
installSettings.noDownload = $('#noDownload').is(":checked");
|
||||
pickImages();
|
||||
const passRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!#$%^&*@]).{8,}$/;
|
||||
const adminPassVal = $('#adminPass').val();
|
||||
const userPassVal = $('#userPass').val();
|
||||
if (!passRegex.test(adminPassVal) || !passRegex.test(userPassVal)) {
|
||||
$('#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
|
||||
async function pickImages() {
|
||||
async function pickImages(upgrade) {
|
||||
let installText;
|
||||
let installFunction;
|
||||
if (upgrade) {
|
||||
installText = 'Upgrade';
|
||||
installFunction = 'upgrade()';
|
||||
} else {
|
||||
installText = 'Install';
|
||||
installFunction = 'install()';
|
||||
}
|
||||
showContainer();
|
||||
titleChange('Image Selection');
|
||||
let imagesDiv = $('<div>', {class: 'cardcontainer', id: 'images'});
|
||||
$('#container').append(imagesDiv);
|
||||
for await (let image of Object.keys(images.images).sort(Intl.Collator().compare)) {
|
||||
let imageName = $('<p>').text(image);
|
||||
for (let image of images.images) {
|
||||
let imageName = $('<p>').text(image.friendly_name);
|
||||
let imageDiv = $('<div>', {
|
||||
class: 'card',
|
||||
id: image.replace(new RegExp(' ', 'g'), '_'),
|
||||
title: images.images[image].description,
|
||||
onclick: 'selectImage(\'' + image.replace(new RegExp(' ', 'g'), '_') + '\')'
|
||||
}).append(imageName).css('filter', 'grayscale(100%)')
|
||||
let thumb = $('<img>', {class: 'thumb', src: 'public/' + images.images[image].image_src});
|
||||
id: image.friendly_name.replace(new RegExp(' ', 'g'), '_').replace('.', '-'),
|
||||
title: image.description,
|
||||
'data-friendly-name': image.friendly_name
|
||||
}).append(imageName).css('filter', 'grayscale(100%)');
|
||||
imageDiv.on('click', function() {
|
||||
selectImage($(this).data('friendly-name'));
|
||||
});
|
||||
let thumb = $('<img>', {class: 'thumb', src: image.image_src, loading: 'lazy'});
|
||||
imageDiv.append(thumb);
|
||||
$('#images').append(imageDiv);
|
||||
}
|
||||
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([
|
||||
allButton,
|
||||
installButton
|
||||
|
|
@ -247,19 +335,20 @@ async function pickImages() {
|
|||
}
|
||||
|
||||
// Select an individual image
|
||||
function selectImage(image) {
|
||||
let imageKey = image.replace(new RegExp('_', 'g'), ' ');
|
||||
if (installImages.includes(imageKey)) {
|
||||
installImages = installImages.filter(e => e !== imageKey)
|
||||
$('#' + image).css({
|
||||
filter: 'grayscale(100%)',
|
||||
function selectImage(friendlyName) {
|
||||
let elemId = friendlyName.replace(new RegExp(' ', 'g'), '_').replace('.', '-');
|
||||
let safeSelectorId = $.escapeSelector(elemId); // Escape the ID for jQuery
|
||||
if (installImages.includes(friendlyName)) {
|
||||
installImages = installImages.filter(e => e !== friendlyName);
|
||||
$('#' + safeSelectorId).css({
|
||||
filter: 'grayscale(100%) brightness(0.7)',
|
||||
background: ''
|
||||
});
|
||||
} else {
|
||||
installImages.push(imageKey);
|
||||
$('#' + image).css({
|
||||
installImages.push(friendlyName);
|
||||
$('#' + safeSelectorId).css({
|
||||
filter: '',
|
||||
background: '#89cff0'
|
||||
background: '#30426c'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -269,19 +358,21 @@ function selectAll() {
|
|||
installImages = [];
|
||||
if (selected) {
|
||||
selected = false;
|
||||
for (let image of Object.keys(images.images)) {
|
||||
let imageElem = image.replace(new RegExp(' ', 'g'), '_');
|
||||
$('#' + imageElem).css({
|
||||
for (let image of images.images) {
|
||||
let imageElem = image.friendly_name.replace(new RegExp(' ', 'g'), '_').replace('.', '-');
|
||||
let safeSelectorId = $.escapeSelector(imageElem); // Escape the ID for jQuery
|
||||
$('#' + safeSelectorId).css({
|
||||
filter: 'grayscale(100%)',
|
||||
background: ''
|
||||
});
|
||||
}
|
||||
} else {
|
||||
selected = true;
|
||||
for (let image of Object.keys(images.images)) {
|
||||
let imageElem = image.replace(new RegExp(' ', 'g'), '_');
|
||||
installImages.push(image);
|
||||
$('#' + imageElem).css({
|
||||
for (let image of images.images) {
|
||||
let imageElem = image.friendly_name.replace(new RegExp(' ', 'g'), '_').replace('.', '-');
|
||||
let safeSelectorId = $.escapeSelector(imageElem); // Escape the ID for jQuery
|
||||
installImages.push(image.friendly_name);
|
||||
$('#' + safeSelectorId).css({
|
||||
filter: '',
|
||||
background: '#89cff0'
|
||||
});
|
||||
|
|
@ -290,17 +381,24 @@ function selectAll() {
|
|||
}
|
||||
|
||||
// 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();
|
||||
titleChange('Complete');
|
||||
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('Your installation is available on port ' + port));
|
||||
$('#container').append(titleBar);
|
||||
setTimeout(function(){
|
||||
location.reload(true);
|
||||
}, 5000);
|
||||
}, 5000);
|
||||
}
|
||||
|
||||
//// Socket events ////
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue