37 Commits
develop ... 1.1

Author SHA1 Message Date
Chris Hunt
68bc35fd49 Mention updating build_all_branches in the 1.0 branch as well 2024-09-12 14:19:19 +01:00
Chris Hunt
3fff8d24d8 Update readme and use previous size with update script 2024-08-21 12:47:52 +01:00
Chris Hunt
b86eb26acb Add versions.json 2024-08-20 11:26:36 +01:00
Chris Hunt
a420ec4c28 Update text to use 1.16 2024-08-20 11:06:48 +01:00
Chris Hunt
aa0da08392 Add details about the compatibility property 2024-08-20 10:36:06 +01:00
Chris Hunt
53465c78b9 Change workspace count 2024-08-14 13:45:49 +01:00
Chris Hunt
39bac68494 Updates to files and upgrade advice 2024-08-13 17:37:46 +01:00
Chris Hunt
225926bdc1 Add missing variable 2024-08-13 11:25:38 +01:00
Chris Hunt
687738b75b Update defaults 2024-08-13 11:05:25 +01:00
Chris Hunt
5278b3914d Update readme 2024-08-13 10:40:21 +01:00
Chris Hunt
8ddd8aabe0 Add new files and put static version back to 1.0 2024-08-13 10:31:16 +01:00
Chris Hunt
4476528522 Add 1.1 support 2024-08-09 14:12:12 +01:00
Chris Hunt
1beed4e80a Add compatibility filter 2023-08-21 14:22:30 +01:00
Chris Hunt
926c525eb4 Add 1.14.x compatibility to example workspace 2023-08-15 10:43:24 +01:00
Chris Hunt
47e464fb4d Update README.md 2023-04-13 14:05:50 +00:00
Chris Hunt
ef1ecca0ca Automatically set branch version
Better branch support
2023-04-03 17:19:48 +01:00
Chris Hunt
5e8f854b79 Allow for feature branches 2023-04-03 15:45:58 +01:00
Chris Hunt
5104a182a2 Pass basepath as part of the link for the home link 2023-03-29 15:10:53 +01:00
Chris Hunt
48f4f661e0 Try a link instead 2023-03-29 14:27:56 +01:00
Chris Hunt
f76ee59bcb Update new path 2023-03-29 14:18:17 +01:00
Chris Hunt
b596a031f3 change uncompressed size 2023-03-28 08:42:21 +01:00
Chris Hunt
d4024d847b Copy favicon to base 2023-03-27 10:03:51 +01:00
Chris Hunt
b9e5dd3639 Fix typo 2023-03-17 15:01:49 +00:00
Chris Hunt
d9ee2767a8 Add required fields 2023-03-17 13:49:44 +00:00
Chris Hunt
856edf32f6 Remove unused dark declaration 2023-02-15 10:27:09 +00:00
Chris Hunt
4aa0bf7c5d Workspace count
change gradient color
add footer
2023-02-15 10:24:54 +00:00
Chris Hunt
5d09790329 Library link as a href, Link wont add slash 2023-02-13 11:44:39 +00:00
Chris Hunt
9bf7937414 Add chromium as an example 2023-02-10 11:30:18 +00:00
Chris Hunt
1256e268b2 Fix bug caused by using workspaces everywhere
Updated default values
2023-01-19 12:38:57 +00:00
Chris Hunt
8a4c513331 Change Applications to Workspaces 2023-01-19 11:18:02 +00:00
Chris Hunt
3b61336e30 Github specific CI changes 2023-01-13 19:24:46 +00:00
Chris Hunt
12a4b49b9d Add missing npm install 2023-01-13 18:46:28 +00:00
Chris Hunt
c40fe4e72c Update basePath 2023-01-13 18:42:16 +00:00
Chris Hunt
834cca0108 Update readme 2023-01-13 18:38:14 +00:00
Chris Hunt
81f87b8e02 Ignore gh-pages in branch list 2023-01-13 18:28:53 +00:00
Chris Hunt
7fd210c506 Remove schema 2023-01-13 18:21:11 +00:00
Chris Hunt
1e3adcef59 Change build logic 2023-01-13 16:59:58 +00:00
24 changed files with 1103 additions and 1691 deletions

View File

@@ -16,26 +16,11 @@ jobs:
- name: Build # Have to run processing first so the list.json exists to be included in the the deploy
run: |
npm ci --prefix processing
node processing
npm ci --prefix site
npm run deploy --prefix site
- name: Generate App List # Have to run it again because the deploy wipes the file and folders out
run: |
npm ci --prefix processing
node processing
- name: Move to branch
if: github.ref != 'refs/heads/develop'
run: |
mv public ${{ github.head_ref || github.ref_name }}
mkdir public
mv ${{ github.head_ref || github.ref_name }} public
./build_all_branches.sh
- name: Deploy
uses: JamesIves/github-pages-deploy-action@v4
with:
branch: gh-pages
folder: public
force: false
clean: false

351
README.md
View File

@@ -1,40 +1,337 @@
# THIS IS CURRENTLY IN DEVELOPMENT AND NOT READY FOR PRIME TIME YET, WE'LL ANNOUNCE WHEN IT'S INCLUDED IN BUILDS. FEEL FREE TO PLAY WITH IT THOUGH AND GIVE FEEDBACK.
<h1 align="center">
<br>
<img width="150" src="https://user-images.githubusercontent.com/5698566/230345149-ef757e51-6eb9-479d-94f5-a13e4ad33b03.png">
<br>
Kasm Workspaces Registry
<br>
</h1>
# Kasm Apps
<p align="center">This repository is a template you can use to create your own registry that will work with Kasm Workspaces. A front end website is automatically generated for you and will look similar to the one below.</p>
This is a repository of the apps supported by Kasm. The apps list is automatically generated and can be used when creating new workspaces or using the 1 click installer
![image](https://user-images.githubusercontent.com/5698566/230064289-9e8967a1-4ff9-43f3-8495-f4170c23a80f.png)
## Create your own app store
## Contents
We have tried to make it as simple as possible for people to create their own app stores that work with Kasm, the easiest way to do that is to follow these steps:
1. [Create your own repository](#1-create-your-own-repository)
- [Use this template](#use-this-template)
- [Configure repository](#configure-repository)
1. [Check workflows are running](#2-check-workflows-are-running)
1. [Edit the config variables](#3-edit-the-config-variables)
- [Modify next.config.js](#modify-nextconfigjs)
- [Settings definitions](#settings-definitions)
- [Commit changes](#commit-changes)
1. [Set up GitHub pages](#4-set-up-github-pages)
- [Initial setup](#initial-setup)
- [Visit the site](#visit-the-site)
- [Check build progress](#check-build-progress)
- [Checking it works](#checking-it-works)
1. [Creating Workspaces](#5-creating-workspaces)
- [Folder structure](#folder-structure)
- [Schema](#schema)
- [New schema version](#new-schema-version)
1. [Discovery](#6-dicovery)
1. Click on "Use this template", select Create a new repository
1. Select a Repository name, you will need to use this name later in the process as well, tick the "Include all branches" checkbox, then click on the "Create repository from template" button
1. Click on the actions tab and check whether workflows need enabling, if they do, enable them, otherwise you should be good to go.
1. Go to `/site/next.config.js` and edit the `env` section with the relevant details.
* name - The name you want to display
* description - A short description to display when a store's information button is pressed.
* icon - The image to display for your registry. You can upload an image to `/site/public/` and reference that by https://domain.com/image.png or if you aren't using a {sub}domain by referencing it from https://username.github.io/repositoryname/image.png where image.png is the name of the image you uploaded.
* listUrl - The link to the JSON. This will follow the same format as the icon above but ends in list.json, for example https://username.github.io/repositoryname/list.json
* contactUrl - A link users can use to contact you on.
If you are using a domain or a subdomain, you need to completely remove the `basePath: 'kasm-app',` line, otherwise change the value to match what you chose for the repository name in step 2.
1. Upload your apps to the /apps folder
1. Go to Settings then Pages and select Branch - gh-pages and click Save
1. Check progress in Actions
1. Once complete go back to Settings / Pages and you should have a live site. Click on the Visit Site button.
1. You should now have a working site which includes any apps you added
[![](https://cdn.loom.com/sessions/thumbnails/256fac3d2bbb422b8e779ac1c8244d33-00001.gif)](https://www.loom.com/share/256fac3d2bbb422b8e779ac1c8244d33 "")
&nbsp;
## New schema version
## 1. Create your own repository
If a new schema version comes out, this is what you will need to do.
### Use this template
1. Create a new branch with the schema version as the branch name (for example, 1.1)
1. Open `site/next.config.js` and change `env.schema` to match the version number, add the version number to the `env.listUrl` and append the version number to the `basePath` as well.
<img width="800" src="https://user-images.githubusercontent.com/5698566/230080486-08d4c678-a0bd-4f30-863b-5f7f0bb9182f.png" />
## Discovery
1. Click on the green **Use this template** button.
2. Select **Create a new repository** from the dropdown.
The tag below will hopefully make it easier for people to find your App Registry by clicking on [this github search link](https://github.com/search?q=in%3Areadme+sort%3Aupdated+-user%3Akasmtech+%22KASM-REGISTRY-DISCOVERY-IDENTIFIER%22&type=repositories). If you want to make it harder to find your repository for some reason, just remove this section.
### Configure repository
![use-template](https://user-images.githubusercontent.com/5698566/230080115-e81a5663-5752-44fc-94a5-ea7721e5745b.gif)
> **Note**
> If you set the **Repository name** to `kasm-registry` you wont have to make any changes to the `baseUrl` later, unless you want to use a (sub)domain.
1. Select a **Repository name**, this name will also be used later for the `baseUrl`,
2. Make sure it's set as a **Public** repository
3. Tick the **Include all branches** checkbox,
4. Click on the **Create repository from template** button.
&nbsp;
## 2. Check workflows are running
<img width="600" src="https://user-images.githubusercontent.com/5698566/230056251-09d722e8-41d8-4adf-b166-b623624e19ec.png" />
> **Note**
> If you see something in the table that looks similar to the above then it's working. If it needs enabling this section will probably be blank with a message about workflows needing to be enabled.
Click on the **Actions** tab in the top menu and check whether workflows need enabling, if they do, enable them, otherwise you should be good to go.
&nbsp;
## 3. Edit the config variables
### Modify next.config.js
![edit-conf](https://user-images.githubusercontent.com/5698566/230075839-ec698589-1276-4163-bd5e-34642a108e7f.gif)
1. Go back to the `Code` tab
1. Click the `site` folder
2. Click on the `next.config.js` file
1. Click the edit button.
2. Fill in the `env` section with the relevant information and change the basePath if needed (details below).
### Settings definitions
<img width="600" src="https://user-images.githubusercontent.com/5698566/230057592-a6fbde4d-2bda-44ca-b77d-62c58ed97cc4.png" />
| Property | Description |
| --------- | ----------- |
| env.name | The name you want to display for your registry. |
| env.description | A short description to display when a store's information button is pressed. |
| env.icon | The image to display for your registry. You can upload an image to `/site/public/` and reference that by https://domain.com/1.1/image.png or if you aren't using a {sub}domain by referencing it from https://username.github.io/repositoryname/1.1/image.png where image.png is the name of the image you uploaded. Alternatively just put the url of an image available on the web. If you just want to get the registry up and working, leave the default value in place until later. |
| env.listUrl | The link to the root of your site. For example https://username.github.io/repositoryname/ it should always include a trailing slash. |
| env.contactUrl | A link users can use to contact you on, such as your github issues page (right click the **Issues** tab in the top menu - next to the **Code** tab - and select `copy link address` and paste that in). |
| basePath | If you are using a domain or a subdomain, your basePath will just be `basePath: '/1.0',`, otherwise change the value to include what you chose for the repository name in step 2 `basePath: '/repositoryname/1.0',`. **The `1.0` will be replaced with the branch name automatically, so you should always keep it as 1.0.** |
### Commit changes
<img width="600" alt="image" src="https://user-images.githubusercontent.com/5698566/230355586-39f6b4a6-9e01-482d-bab1-f0c1a292de24.png">
1. Scroll down to the bottom of the page
2. Enter a commit message (You can also leave this blank)
3. Click the **Commit changes** button.
&nbsp;
## 4. Set up GitHub pages
### Initial setup
![gh-pages](https://user-images.githubusercontent.com/5698566/230077012-938704c4-af44-4d3c-8023-0732c34a78bc.gif)
> **Note**
> If you ticked the "Include all branches" checkbox in step 1, this should all be set up for you, if not, just follow the instructions below
1. Go to the **Settings** top menu tab
2. Click the **Pages** left menu item
3. In the **Build and deployment** section, under the **Branch** heading, make sure the dropdown is set to gh-pages, if not, set it and click **Save**.
### Visit the site
If you see a message like the following:
![image](https://user-images.githubusercontent.com/5698566/230061310-f2883e2f-7279-45c9-8cc5-de56dcc6e03f.png)
Then congratulations, you should have a working site! Just click the **Visit Site** button. **However**, you changes may not have finished building yet, so before clicking the button it's advised to check the build process first.
### Check build progress
If you don't see that button yet, then not to worry, it's likely that you are just too quick (also if you do see the button but it doesn't reflect the changes you made, this next bit is relevant as well)
Check on the CI progress in the **Actions** tab,
<img width="600" alt="image" src="https://user-images.githubusercontent.com/5698566/230061667-63829dbd-46f2-4f7b-96c4-0cab8279e8a6.png" />
Once `Page build and deployment` is finished go back to Settings / Pages and you should have a live site. Click on the Visit Site button.
**You should now have a working site which includes any workspaces you added or the default if you haven't made any changes yet**
<img width="600" alt="image" src="https://user-images.githubusercontent.com/5698566/230064289-9e8967a1-4ff9-43f3-8495-f4170c23a80f.png" />
### Checking it works
![install-registry-800](https://user-images.githubusercontent.com/5698566/230379178-4b2a08c7-3ae1-4000-88a0-ae4b8ab17892.gif)
> **Note**
> If you copy the url from the address bar instead of clicking the button, be sure to remove the branch version from the URL when adding to workspaces, otherwise it wont work.
1. Click on the **Workspace Registry Link** button, this will put the correct url in your clipboard.
2. Go to your Kasm Workspaces instance.
3. Navigate to the Workspaces Registry (Admin / Workspaces / Click on the **Workspaces Registry** button).
4. Click on **Add new** in the registries list.
5. Paste the URL into the text box and click **Add Registry**
6. Click on the mini icon under the registry name to filter by your registries workspaces
&nbsp;
## 5. Creating workspaces
Once you are ready to upload your workspaces, head back to the **Code** tab. You can either continue using the online editor or you might find it easier to clone the repository and work on a local copy, it's up to you. For this example we will continue with the online editor.
### Folder structure
![workspaces-800](https://user-images.githubusercontent.com/5698566/230384525-d8577582-fab7-4850-979d-d75e83503022.gif)
All workspaces reside in the **workspaces** folder
You will need to create a folder and the necessary files using the following format:
```
Workspace Name
- workspace.json
- workspace-name.png
```
![add-workspace-800](https://user-images.githubusercontent.com/5698566/230386427-c2221647-ce30-4c2e-bc92-e83481d1b8ba.gif)
**Folder name** - The folder name can be whatever it needs to be. You probably want to stay clear of special characters to be on the safe side, but spaces should be fine.
**workspace.json** - This is a JSON file with all the parameters you want to be sent to Kasm Workspaces when it builds the container. You can see the valid paramaters in the schema section and whether they are required or not.
```
{
"description": "Visual Studio Code is a code editor redefined and optimized for building and debugging modern web and cloud applications.",
"docker_registry": "https://index.docker.io/v1/",
"image_src": "vs-code.png",
"categories": [
"Development"
],
"friendly_name": "Visual Studio Code",
"architecture": [
"amd64",
"arm64"
],
"compatibility": [
{
"version": "1.16.x",
"image": "kasmweb/vs-code:1.16",
"uncompressed_size_mb": 2428,
"available_tags": [
"develop",
"1.16.0"
]
},
{
"version": "1.17.x",
"image": "kasmweb/vs-code:1.17",
"uncompressed_size_mb": 2528,
"available_tags": [
"develop",
"1.17.0"
]
}
]
}
```
**Image file** - The image can be `.png` or `.svg` and ideally will be square and at least 50 x 50px. If you use the workspace builder on your registry store front it will try to normalise everything to make it simpler.
Don't forget to commit your changes!
### Schema
**Version** 1.1
| Property | Required | Type | Description |
| --------------------- | -------- | --- | --- |
| friendly_name | True | String | The name to show |
| description | True | String | A short description of the workspace |
| image_src | True | String | The name of the workspace icon used |
| architecture | True | Array | Json list containing either "amd64", "arm64" or both |
| compatability | True | Array | A list of Kasm versions the workspace should work with |
| categories | False | Array | Json list containing the categories the workspace belongs too. This should be limited to a max of 3. |
| docker_registry | False | String | Which docker registry to use |
| run_config | False | Object | Any additional parameters to add to the run config |
| exec_config | False | Object | Any additional parameters to add to the exec config |
| notes | False | String | Notes about running the workspace, such as if it requires libseccomp. |
| cores | False | Integer | Specify the amount of cores to use for this workspace |
| memory | False | Integer | Specify the amount of memory to use for this workspace |
| gpu_count | False | Integer | Specify the amount of GPUs to use for this workspace |
| cpu_allocation_method | False | String | What CPU allocation method to use for this workspace. Can be either "Inherit", "Quotas" or "Shares" |
The compatibility property is an array of objects and needs a bit more explanation
```json
"compatibility": [
{
"version": "1.16.x",
"image": "kasmweb/chromium:1.16.0-rolling-daily",
"uncompressed_size_mb": 2643,
"available_tags": [
"develop",
"1.16.0",
"1.16.0-rolling-weekly",
"1.16.0-rolling-daily"
]
}
]
```
* **version** - This is the version of kasm the entry is compatible with
* **image** - The docker image. The tag is included for things like estimating the size and is used if there are no available_tags.
* **uncompressed_size_mb** - Integer of the approximate size of the workspace when its uncompressed in MB. This doesnt take into account layers. For example if an image is 2.46GB you would enter 2460
* **available_tags** - These values are what will determine the available "channels" on the front end. If you don't want/need channels, remove the available_tags section completely. You shouldn't mix and match though, if you specify available_tags for 1 workspace, it should be specified for all of them. That doesn't mean every workspace has to have all the same tags, if a workspace only has develop tags then it will only show when develop is the selected channel.
Head to the **Actions** tab to check your progress and once `Page build and deployment` is complete, your site should be ready.
### New Kasm Workspaces version
When a new version of Kasm is released then a new entry needs to be added to the compatibility list to support it. If you have a lot of workspaces defined then there are a couple of scripts included that can help.
Go to the processing folder and edit add_next_version.js changing the `baseversion` to match the new version. Also make sure the rest of the file matches your setup, if you anre't using channels then completely remove the `available_tags` section.
Then in a terminal run
```
cd processing
npm install
node add_next_version.js
```
This will add a new entry for every single workspace, but the size will be set to 0, this is so you can run the `get_image_sizes.js` script. This will loop through each `image` that has an uncompressed_size_mb of 0 and will pull the image, get the size, update the workspace json and remove the image.
This can take a long time if you have a lot of workspaces and dependng on their sizes, but if the script crashes out, you can just start it agin and it will carry on from where it left off.
```
node add_next_version.js
```
### New schema version
When a new schema version comes out, you just need to create a new branch that reflects the new schema, for example `1.2` and make it the default branch.
In the new branch, make any updates that are needed, when the changes are committed a new version will be built.
Kasm Workspaces will automatically pull the version of the schema that it understands.
If only the latest version is building (so 1.1 works but 1.0 doesn't), open build_all_branches.sh, search for `echo "All branches:` and check if there is `git fetch --all` on the line underneath, if not, add it, this will need to be added to the 1.0 branch as well if it's missing, otherwise if you make a change to 1.0 (for kasm versions 1.12.x - 1.15.x) it won't build all the branches.
**Updating to 1.16.x support**
1.16.x changed the schema from 1.0 to 1.1, the main changes to this are the compatibility changes from a simple array to an array of objects, this allows us to tie the image used and the image size to the kasm version.
In addition the top level name is removed as is top level uncompessed_size_mb as these are now available in the compatibility matrix (name is called image).
If you have an older version you will probably need to update the following files in your 1.1 branch:
* build_all_branches.sh
* processing/processjson.js
* site/components/Workspace.js
* site/pages/index.js
* site/pages/new/[[..workspace]].js
If you have a lot of workspaces (or just want an easier way to update your workspaces), there is an update file in processing called `update_1_0_to_1_1.js` copy that across to your own install, edit it and make sure the tags etc match your install. If you don't want to use channels, you can remove the available_tags section entirely.
Then to use it, create a 1.1 branch from your current 1.0 source, then in a terminal:
```
cd processing
npm install
node update_1_0_to_1_1.js
```
This will convert your existing workspaces to a 1.1 compatible format.
## Channels
Schema 1.1 added the concept of channels. Each registry can specify the channels they support, these are defined by the tags an image has. For example you might have develop, 1.16.0 and 1.16.0-rolling-daily. When the registry json is built it loops through all the workspaces and generates a list of all the possible "Channels" (tags) that are listed in compatibility.available_tags. Available tags is an optional list, if you don't include it on any of the workspaces then your registry will work as before without presenting the end user with a channels option. You shouldn't mix and match though, if you add available tags to 1 workspace, you should add available tags to all workspaces.
If you are using channels, update processing/processjson.js and specify the `default_channel` such as `'develop'`. If you aren't using channels you don't need to do anything, it will automatically detect there are no channels and set the correct value.
&nbsp;
## 6. Discovery
The tag below will hopefully make it easier for people to find your Workspace Registry by clicking on [this github search link](https://github.com/search?q=in%3Areadme+sort%3Aupdated+-user%3Akasmtech+%22KASM-REGISTRY-DISCOVERY-IDENTIFIER%22&type=repositories). If you want to make it harder to find your repository for some reason, just remove this section.
If you are the one doing the searching, click on the **site** folder, then click on **next.config.js** and the url can be found under **env.listUrl**
![search-600](https://user-images.githubusercontent.com/5698566/230614274-2976b4d7-074f-4e6d-9e58-e4d2512a3d2a.gif)
KASM-REGISTRY-DISCOVERY-IDENTIFIER

View File

@@ -1,7 +0,0 @@
# Apps directory
This directory is for storing all the files needed for your apps store, they should be stored with the following structure:
* apps/Application Name
* apps/Application Name/app.json
* apps/Application Name/application-name.png

30
build_all_branches.sh Executable file
View File

@@ -0,0 +1,30 @@
#!/bin/sh
DEFAULT=$(git remote show origin | sed -n '/HEAD branch/s/.*: //p')
mkdir base
cat > base/index.html << EOF
<meta http-equiv="refresh" content="0; url=./$DEFAULT/">
EOF
touch base/.nojekyll
# Generating documentation for each other branch in a subdirectory
echo "All branches:"
git fetch --all
echo "$(git branch --remotes --format '%(refname:lstrip=3)' | grep -Ev '^(HEAD|develop|gh-pages)$')"
for BRANCH in $(git branch --remotes --format '%(refname:lstrip=3)' | grep -Ev '^(HEAD|develop|gh-pages)$'); do
SANITIZED_BRANCH="$(echo $BRANCH | sed 's/\//_/g')"
echo "$SANITIZED_BRANCH" >> base/versions.txt
git checkout $BRANCH
node processing
cp -a public/. process
sed -i "s/1.0/$SANITIZED_BRANCH/" site/next.config.js
npm run deploy --prefix site
cp -a process/. public/ # Have to run it again because the deploy wipes the file and folders out
rm -rf process
sed -i "s/$SANITIZED_BRANCH/1.0/" site/next.config.js # Set it back to 1.0 so it can be changed again on the next loop
mv public base/$SANITIZED_BRANCH
cp base/$SANITIZED_BRANCH/favicon.ico base/favicon.ico
done
mv base public

View File

@@ -0,0 +1,48 @@
const fs = require("fs");
const glob = require("glob");
const baseversion = '1.16'
const tag = ':develop'
const version = baseversion + '.x'
const tagversion = baseversion + '.0'
glob("../workspaces/**/workspace.json", async function (err, files) {
if (err) {
console.log(
"cannot read the folder, something goes wrong with glob",
err
);
}
for (const file of files) {
let filedata = fs.readFileSync(file);
let parsed = JSON.parse(filedata);
const current = parsed.compatibility[parsed.compatibility.length - 1]
const image = current.image.split(':')[0]
const exists = parsed.compatibility.findIndex(el => el.version === version)
let details = {
version,
image: image + tag,
uncompressed_size_mb: 0,
available_tags: [
'develop',
tagversion,
tagversion + '-rolling-weekly',
tagversion + '-rolling-daily'
]
}
if (exists === -1) {
parsed.compatibility.push(details)
fs.writeFileSync(file, JSON.stringify(parsed, null, 2));
}
}
});

View File

@@ -0,0 +1,45 @@
const fs = require("fs");
const glob = require("glob");
const { execSync } = require('child_process');
glob("../workspaces/**/workspace.json", function (err, files) {
if (err) {
console.log(
"cannot read the folder, something goes wrong with glob",
err
);
}
let total = 0
for (const file of files) {
let filedata = fs.readFileSync(file);
let parsed = JSON.parse(filedata);
parsed.compatibility.forEach((element, index) => {
total++
if (element.uncompressed_size_mb === 0) {
execSync('docker image prune -a -f')
execSync('docker system prune --all --force --volumes')
let pull = execSync('docker pull ' + element.image)
// console.log(pull)
let inspect = execSync('docker inspect -f "{{ .Size }}" ' + element.image)
let size = Math.round(inspect / 1000000)
let remove = execSync('docker rmi ' + element.image)
console.log(remove)
parsed.compatibility[index].uncompressed_size_mb = size
console.log('Write file: ' + parsed.friendly_name + ' - ' + element.version + ': ' + size)
fs.writeFileSync(file, JSON.stringify(parsed, null, 2));
} else {
console.log(parsed.friendly_name + ' - ' + element.version + ': skipped')
}
})
}
console.log(total + ' entries processed')
});

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@ if (!fs.existsSync(dir + "/icons")) {
fs.mkdirSync(dir + "/icons");
}
glob("**/app.json", async function (err, files) {
glob("**/workspace.json", async function (err, files) {
if (err) {
console.log(
"cannot read the folder, something goes wrong with glob",
@@ -20,8 +20,8 @@ glob("**/app.json", async function (err, files) {
);
}
let apptotal = files.length;
let apps = [];
let workspacetotal = files.length;
let workspaces = [];
let promises = [];
const options = {
@@ -29,18 +29,31 @@ glob("**/app.json", async function (err, files) {
encoding: "hex",
};
let channels = new Set()
let versions = new Set()
for (const file of files) {
//files.forEach(async function(file) {
let folder = file.replace("/app.json", "");
let folder = file.replace("/workspace.json", "");
let hash = await hashElement(folder, options);
let filedata = fs.readFileSync(file);
let parsed = JSON.parse(filedata);
parsed.sha = hash.hash;
console.log(parsed.name + ' added')
apps.push(parsed);
console.log(parsed.friendly_name + ' added')
parsed.compatibility.forEach((element, index) => {
if ('available_tags' in element) {
element.available_tags.forEach((el) => {
channels.add(el)
})
}
if ('version' in element) {
versions.add(element.version)
}
})
workspaces.push(parsed);
if (fs.existsSync(folder + "/" + parsed.image_src)) {
let imagedata = fs.readFileSync(folder + "/" + parsed.image_src);
@@ -53,17 +66,26 @@ glob("**/app.json", async function (err, files) {
let json = {
name: nextConfig.env.name || 'Unknown store',
appcount: apptotal,
schema_version: nextConfig.env.schema || '1.0',
workspacecount: workspacetotal,
icon: nextConfig.env.icon || null,
description: nextConfig.env.description || null,
list_url: nextConfig.env.listUrl || null,
contact_url: nextConfig.env.contactUrl || null,
modified: Date.now(),
apps: apps,
workspaces: workspaces,
channels: [...channels],
default_channel: 'develop'
};
if (channels.size === 0) {
json.default_channel = null
}
let data = JSON.stringify(json);
fs.writeFileSync(dir + "/list.json", data);
fs.writeFileSync(dir + "/versions.json", JSON.stringify({
versions: [...versions]
}));
});

View File

@@ -0,0 +1,41 @@
const fs = require("fs");
const glob = require("glob");
glob("../workspaces/**/workspace.json", async function (err, files) {
if (err) {
console.log(
"cannot read the folder, something goes wrong with glob",
err
);
}
for (const file of files) {
let filedata = fs.readFileSync(file);
let parsed = JSON.parse(filedata);
delete parsed.compatibility
parsed.compatibility = []
let details = {
version: '1.16.x',
image: parsed.name.split(':')[0] + ':1.16.0-rolling-daily',
uncompressed_size_mb: parsed.uncompressed_size_mb,
available_tags: [
'develop',
'1.16.0',
'1.16.0-rolling-weekly',
'1.16.0-rolling-daily'
]
}
parsed.compatibility.push(details)
delete parsed.uncompressed_size_mb
delete parsed.name
fs.writeFileSync(file, JSON.stringify(parsed, null, 2));
}
});

View File

@@ -1 +0,0 @@
NEXT_PUBLIC_APPURL=apps.kasmweb.com/list.json

View File

@@ -1,30 +0,0 @@
import { useRouter } from 'next/router'
function App({ Component, pageProps, app }) {
const router = useRouter()
const viewexample = (app) => {
router.push({
pathname: '/addapp/[app]',
query: { app: btoa(app.name)}
})
}
return (
<div onClick={() => viewexample(app)} className="w-[245px] h-[88px] transition-all relative cursor-pointer group flex p-2 items-center justify-center bg-slate-100/90 dark:bg-slate-900/90 shadow rounded hover:shadow-xl hover:bg-gradient-to-r hover:from-slate-900 hover:to-cyan-800 hover:text-white">
<div className="w-full h-full">
<div className="show-grid flex h-full items-center">
<div className="kasmcard-img flex h-full mx-4 items-center justify-center">
<img className="w-[50px] max-h-[66px]" src={ 'icons/' + app.image_src} />
</div>
<div className="kasmcard-detail settingPad">
<h5 className="text-base">{ app.friendly_name }</h5>
<p className="text-xs opacity-50">{ app.categories && app.categories[0] || 'Unknown' }</p>
</div>
</div>
</div>
</div>
)
}
export default App

View File

@@ -0,0 +1,30 @@
import { useRouter } from 'next/router'
function Workspace({ Component, pageProps, workspace }) {
const router = useRouter()
const viewexample = (workspace) => {
router.push({
pathname: '/new/[workspace]',
query: { workspace: btoa(workspace.friendly_name)}
})
}
return (
<div onClick={() => viewexample(workspace)} className="w-[245px] h-[88px] transition-all relative cursor-pointer group flex p-2 items-center justify-center bg-slate-100/90 shadow rounded hover:shadow-xl hover:bg-gradient-to-r hover:from-[#162d48] hover:to-[#2980b9] hover:text-white">
<div className="w-full h-full">
<div className="show-grid flex h-full items-center">
<div className="kasmcard-img flex h-full mx-4 items-center justify-center">
<img className="w-[50px] max-h-[66px]" src={ 'icons/' + workspace.image_src} />
</div>
<div className="kasmcard-detail settingPad">
<h5 className="text-base">{ workspace.friendly_name }</h5>
<p className="text-xs opacity-50">{ workspace.categories && workspace.categories[0] || 'Unknown' }</p>
</div>
</div>
</div>
</div>
)
}
export default Workspace

View File

@@ -1,5 +1,7 @@
export default function Footer() {
return (
<footer></footer>
<footer className="flex justify-center items-center p-5 bg-gradient-to-tr text-white/80 text-sm from-[#162d48] to-[#2980b9]">
This registry is intended to work in conjuction with Kasm Workspaces.&nbsp;<a className="underline" href="https://kasmweb.com">Click here to find out about Kasm Workspaces</a>
</footer>
)
}

View File

@@ -16,45 +16,44 @@ export default function Header({ searchText, changeSearch }) {
}
const listUrl = process.env.listUrl;
const router = useRouter();
const getLink = (path) => `${router.basePath}${path}`;
return (
<header className="relative font-light overflow-hidden bg-gradient-to-tr from-slate-900 to-cyan-800 p-8 xl:px-32 text-white gap-5 md:gap-0 flex flex-wrap justify-center items-center">
<header className="relative font-light overflow-hidden bg-gradient-to-tr from-[#162d48] to-[#2980b9] p-8 xl:px-32 text-white gap-5 md:gap-0 flex flex-wrap justify-center items-center">
<Bubbles />
<div className='relative z-10'>
<div className="text-3xl">{process.env.name}</div>
<div className="text-sm uppercase w-full flex justify-between">
<span className='opacity-70'>A</span>
<span className='opacity-70'>p</span>
<span className='opacity-70'>p</span>
<span className='opacity-70'>l</span>
<span className='opacity-70'>i</span>
<span className='opacity-70'>c</span>
<span className='opacity-70'>a</span>
<span className='opacity-70'>t</span>
<span className='opacity-70'>i</span>
<span className='opacity-70'>W</span>
<span className='opacity-70'>o</span>
<span className='opacity-70'>n</span>
<span className='opacity-70'>r</span>
<span className='opacity-70'>k</span>
<span className='opacity-70'>s</span>
<span className='opacity-70'>p</span>
<span className='opacity-70'>a</span>
<span className='opacity-70'>c</span>
<span className='opacity-70'>e</span>
<span>&nbsp;</span>
<span className='opacity-40'>D</span>
<span className='opacity-40'>a</span>
<span className='opacity-40'>t</span>
<span className='opacity-40'>a</span>
<span className='opacity-40'>b</span>
<span className='opacity-40'>a</span>
<span className='opacity-40'>s</span>
<span className='opacity-40'>R</span>
<span className='opacity-40'>e</span>
<span className='opacity-40'>g</span>
<span className='opacity-40'>i</span>
<span className='opacity-40'>s</span>
<span className='opacity-40'>t</span>
<span className='opacity-40'>r</span>
<span className='opacity-40'>y</span>
</div>
</div>
<nav className='relative z-10 mx-12'>
<Link href="/" className={'p-4 inline-block rounded-full border border-solid' + (router.pathname == "/" ? ' border-white/30' : ' border-transparent')}>Library</Link>
<Link href="/addapp" className={'p-4 inline-block rounded-full border border-solid' + (router.pathname.startsWith("/addapp") ? ' bg-black/10 border-white/30' : ' border-transparent')}>Add App</Link>
<a href={getLink("/")} className={'p-4 inline-block rounded-full border border-solid' + (router.pathname == "/" ? ' border-white/30' : ' border-transparent')}>Library</a>
<Link href="/new/" className={'p-4 inline-block rounded-full border border-solid' + (router.pathname.startsWith("/new") ? ' bg-black/10 border-white/30' : ' border-transparent')}>New</Link>
</nav>
<div className="grow flex justify-center relative z-10">
<div className='bg-black/10 shadow border border-1 border-white/30 rounded flex w-full max-w-md'>
<input
name="search"
className='bg-transparent shadow-inner text-lg font-light w-full p-4 placeholder:text-white/40'
placeholder='Search for application'
placeholder='Search for workspace'
type="text"
value={searchText}
onChange={changeSearch}
@@ -63,8 +62,8 @@ export default function Header({ searchText, changeSearch }) {
</div>
</div>
<button className='p-4 relative z-10 px-5 bg-cyan-700 border-t border-white/20 border-solid hover:bg-slate-900 transition shadow-lg m-2 rounded items-center text-white/70 flex cursor-pointer' onClick={() => { copyToClipboard() }}>
<span className="mr-3">App Registry Link</span>
<button className='p-4 relative z-10 px-5 bg-[#162d48]/70 border-t border-white/20 border-solid hover:bg-slate-900 transition shadow-lg m-2 rounded items-center text-white/70 flex cursor-pointer' onClick={() => { copyToClipboard() }}>
<span className="mr-3">Workspace Registry Link</span>
<svg style={{ height: '14px', fill: '#fff' }} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M224 0c-35.3 0-64 28.7-64 64V288c0 35.3 28.7 64 64 64H448c35.3 0 64-28.7 64-64V64c0-35.3-28.7-64-64-64H224zM64 160c-35.3 0-64 28.7-64 64V448c0 35.3 28.7 64 64 64H288c35.3 0 64-28.7 64-64V384H288v64H64V224h64V160H64z" /></svg>
</button>
</header >

View File

@@ -7,11 +7,11 @@ import { NotificationContainer } from 'react-notifications';
export default function Layout({ children, searchText, changeSearch }) {
return (
<>
<div className='flex flex-col min-h-screen'>
<Header searchText={searchText} changeSearch={changeSearch} />
<main>{children}</main>
<main className="grow">{children}</main>
<Footer />
<NotificationContainer/>
</>
</div>
)
}

View File

@@ -3,15 +3,14 @@
const nextConfig = {
env: {
name: 'Kasm Technologies',
description: 'The official store for Kasm supported applications.',
description: 'The official store for Kasm supported workspaces.',
icon: '/img/logo.svg',
listUrl: 'https://registry.kasmweb.com/list.json',
listUrl: 'https://registry.kasmweb.com/',
contactUrl: 'https://kasmweb.com/support',
schema: '1.0',
},
reactStrictMode: true,
swcMinify: true,
basePath: '/kasm-apps',
basePath: '/kasm-registry/1.0',
trailingSlash: true,
images: {
unoptimized: true,

View File

@@ -1,24 +1,52 @@
import { useState, useEffect } from 'react'
import Head from 'next/head'
import App from '../components/App'
import Workspace from '../components/Workspace'
import styles from '../styles/Home.module.css'
export default function Home({ searchText }) {
const [apps, setApps] = useState(null)
const [workspaces, setWorkspaces] = useState(null)
const [versions, setVersions] = useState(null)
const [version, setVersion] = useState(null)
useEffect(() => {
let currentVersion = localStorage.getItem("version") || null
fetch('list.json')
.then((res) => res.json())
.then((apps) => {
setApps(apps)
.then((workspaces) => {
let wsversions = []
workspaces.workspaces.forEach((workspace) => {
if(workspace.compatibility) {
workspace.compatibility.forEach((v) => {
const value = parseFloat(v.version)
if(wsversions.indexOf(value) === -1) {
wsversions.push(value)
}
})
}
})
const sorted = wsversions.sort((a,b) => a-b).reverse()
setVersions(sorted)
if (currentVersion === null) {
currentVersion = sorted[0]
localStorage.setItem("version", currentVersion);
}
setVersion(currentVersion)
setWorkspaces(workspaces)
})
}, [])
let filteredapps = apps && apps.apps && apps.apps.length > 0 ? [...apps.apps] : [];
const updateVersion = (version) => {
localStorage.setItem("version", version);
setVersion(version)
}
let filteredworkspaces = workspaces && workspaces.workspaces && workspaces.workspaces.length > 0 ? [...workspaces.workspaces] : [];
filteredworkspaces = filteredworkspaces.filter((v) => v.compatibility.some((el) => el.version === version + '.x'))
const lowerSearch = searchText && searchText.toLowerCase();
if (searchText && searchText !== "") {
filteredapps = filteredapps.filter((i) => {
filteredworkspaces = filteredworkspaces.filter((i) => {
const category = (i.categories && i.categories.length > 0) ? i.categories.filter((i) =>
i.toLowerCase().includes(lowerSearch)
) : [];
@@ -33,20 +61,31 @@ export default function Home({ searchText }) {
return (
<div className="">
<Head>
<title>Kasm Apps</title>
<meta name="description" content="List of apps for Kasm Webspaces" />
<title>Kasm Workspaces</title>
<meta name="description" content="List of workspaces for Kasm Webspaces" />
<link rel="icon" href="/favicon.ico" />
</Head>
<main className="p-20">
<h1 className='flex text-2xl justify-center mb-10'>Applications: <span className=''>{apps && apps.appcount}</span></h1>
<main className="p-8 py-10 xl:px-20">
<h1 className='flex flex-wrap-reverse uppercase tracking-widest justify-center mb-10 gap-5'>
<span className='flex items-center text-lg bg-slate-100/90 rounded overflow-hidden shadow'>
<span className='flex px-3 text-xs opacity-100'>Workspaces</span>
<span className='text-white p-3 py-1 flex bg-[#2980b9]'>{filteredworkspaces && filteredworkspaces.length}</span>
</span>
<span className='flex items-center text-lg bg-slate-100/90 rounded overflow-hidden shadow'>
<span className='flex px-3 text-xs opacity-100'>Kasm Version</span>
<span className='text-white gap-3 p-3 py-1 flex items-center bg-[#2980b9]'>{versions && versions.map((v) => (
<div className={'cursor-pointer ' + (+v === +version ? 'text-white' : 'text-white/50 text-xs')} key={v} onClick={() => updateVersion(v)}>{v}</div>
))}</span>
</span>
</h1>
<div className="flex flex-wrap gap-1 justify-center">
{filteredapps && filteredapps.length > 0 && filteredapps.map(function (app, i) {
return <App key={app.sha} app={app} />
{filteredworkspaces && filteredworkspaces.length > 0 && filteredworkspaces.map(function (workspace, i) {
return <Workspace key={workspace.sha} workspace={workspace} />
})}
{filteredapps && filteredapps.length === 0 && (
<p>No applications found {searchText !== '' && ('matching "' + searchText + '"')}</p>
{filteredworkspaces && filteredworkspaces.length === 0 && (
<p>No workspaces found {searchText !== '' && ('matching "' + searchText + '"')}</p>
)}
</div>

View File

@@ -4,17 +4,17 @@ import { saveAs } from 'file-saver';
import CreatableSelect from 'react-select/creatable';
import Select from 'react-select';
import { useRouter } from 'next/router'
import allapps from '../../../public/list.json'
import allworkspaces from '../../../public/list.json'
export async function getStaticPaths() {
let paths = allapps.apps.map(app => ({
let paths = allworkspaces.workspaces.map(workspace => ({
params: {
app: [btoa(app.name)]
workspace: [btoa(workspace.friendly_name)]
}
}))
paths.push({
params: { app: null }
params: { workspace: null }
})
return {
paths,
@@ -24,47 +24,14 @@ export async function getStaticPaths() {
// `getStaticPaths` requires using `getStaticProps`
export async function getStaticProps({ params }) {
const app = params.app
const workspace = params.workspace
return {
// Passed to the page component as props
props: { app: app ?? null },
props: { workspace: workspace ?? null },
}
}
export default function AddApp({ app }) {
function friendlyUrl(url) {
// make the url lowercase
var encodedUrl = url.toString().toLowerCase();
// replace & with and
encodedUrl = encodedUrl.split(/\&+/).join("-and-")
// remove invalid characters
encodedUrl = encodedUrl.split(/[^a-z0-9]/).join("-");
// remove duplicates
encodedUrl = encodedUrl.split(/-+/).join("-");
// trim leading & trailing characters
encodedUrl = encodedUrl.trim('-');
return encodedUrl;
}
const downloadZip = () => {
var JSZip = require("jszip");
const zip = new JSZip()
const folder = zip.folder(application.friendly_name)
folder.file('app.json', JSON.stringify(application, null, 2))
if (icon) {
folder.file(application.image_src, icon.file)
}
else if (inlineImage) {
const promise = fetch(inlineImage).then(response => response.blob())
folder.file(application.image_src, promise)
}
zip.generateAsync({ type: "blob" })
.then(function (content) {
// Force down of the Zip file
saveAs(content, friendlyUrl(application.friendly_name) + '.zip');
});
}
export default function New({ workspace }) {
const name = useRef(null);
const friendly_name = useRef(null);
@@ -80,9 +47,8 @@ export default function AddApp({ app }) {
friendly_name: null,
image_src: null,
description: null,
name: null,
cores: 1,
memory: 1024,
cores: 2,
memory: 2768,
gpu_count: 0,
cpu_allocation_method: "Inherit",
docker_registry: "https://index.docker.io/v1/",
@@ -92,58 +58,57 @@ export default function AddApp({ app }) {
image_type: 'Container',
}
const [application, setApplication] = useState(defaultState)
const [combined, setCombined] = useState(defaultState)
const router = useRouter()
// const { app } = router.query
// const { workspace } = router.query
useEffect(() => {
console.log(app)
if(app === null) {
if(workspace === null) {
description.current.value = ''
name.current.value = ''
friendly_name.current.value = ''
setCategories(null)
setArchitecture(null)
setIcon(null)
setApplication(defaultState)
setCombined(defaultState)
}
else if (app && app[0]) {
const appDetails = allapps.apps.find(el => el.name === atob(app[0]))
delete appDetails['sha']
description.current.value = appDetails.description
name.current.value = appDetails.name
friendly_name.current.value = appDetails.friendly_name
if (appDetails.categories) {
else if (workspace && workspace[0]) {
const workspaceDetails = allworkspaces.workspaces.find(el => el.friendly_name === atob(workspace[0]))
delete workspaceDetails['sha']
description.current.value = workspaceDetails.description
name.current.value = workspaceDetails.name
friendly_name.current.value = workspaceDetails.friendly_name
if (workspaceDetails.categories) {
let catMap = []
appDetails.categories.map((e) => catMap.push({
workspaceDetails.categories.map((e) => catMap.push({
label: e,
value: e,
}))
setCategories(catMap)
}
if (appDetails.architecture) {
if (workspaceDetails.architecture) {
let archMap = []
appDetails.architecture.map((e) => archMap.push({
workspaceDetails.architecture.map((e) => archMap.push({
label: e,
value: e,
}))
setArchitecture(archMap)
}
setInlineImage('../../icons/' + appDetails.image_src)
setInlineImage('../../icons/' + workspaceDetails.image_src)
setApplication({
...application,
...appDetails
setCombined({
...combined,
...workspaceDetails
})
}
}, [app])
}, [workspace])
const displayApplication = () => {
const displayWorkspace = () => {
return {
...application,
// categories: JSON.stringify(application.categories)
...combined,
// categories: JSON.stringify(combined.categories)
}
}
@@ -163,23 +128,23 @@ export default function AddApp({ app }) {
}
useEffect(() => {
if (application && application.friendly_name) {
const updateapp = {
...application
if (combined && combined.friendly_name) {
const updateWorkspace = {
...combined
}
updateapp.image_src = friendlyUrl(updateapp.friendly_name) + '.' + ext
setApplication(updateapp)
updateWorkspace.image_src = friendlyUrl(updateWorkspace.friendly_name) + '.' + ext
setCombined(updateWorkspace)
}
}, [ext])
const updateCategories = (items) => {
const updateapp = {
...application
const updateWorkspace = {
...combined
}
updateapp.categories = items.map(cat => cat.value)
setApplication(updateapp)
updateWorkspace.categories = items.map(cat => cat.value)
setCombined(updateWorkspace)
let catMap = []
updateapp.categories.map((e) => catMap.push({
updateWorkspace.categories.map((e) => catMap.push({
label: e,
value: e,
}))
@@ -187,27 +152,59 @@ export default function AddApp({ app }) {
}
const updateArchitecture = (items) => {
const updateapp = {
...application
const updateWorkspace = {
...combined
}
updateapp.architecture = items.map(arch => arch.value)
setApplication(updateapp)
updateWorkspace.architecture = items.map(arch => arch.value)
setCombined(updateWorkspace)
let archMap = []
updateapp.architecture.map((e) => archMap.push({
updateWorkspace.architecture.map((e) => archMap.push({
label: e,
value: e,
}))
setArchitecture(archMap)
}
function friendlyUrl(url) {
// make the url lowercase
var encodedUrl = url.toString().toLowerCase();
// replace & with and
encodedUrl = encodedUrl.split(/\&+/).join("-and-")
// remove invalid characters
encodedUrl = encodedUrl.split(/[^a-z0-9]/).join("-");
// remove duplicates
encodedUrl = encodedUrl.split(/-+/).join("-");
// trim leading & trailing characters
encodedUrl = encodedUrl.trim('-');
return encodedUrl;
}
const downloadZip = () => {
var JSZip = require("jszip");
const zip = new JSZip()
const folder = zip.folder(combined.friendly_name)
folder.file('workspace.json', JSON.stringify(combined, null, 2))
if (icon) {
folder.file(combined.image_src, icon.file)
}
else if (inlineImage) {
const promise = fetch(inlineImage).then(response => response.blob())
folder.file(combined.image_src, promise)
}
zip.generateAsync({ type: "blob" })
.then(function (content) {
// Force down of the Zip file
saveAs(content, friendlyUrl(combined.friendly_name) + '.zip');
});
}
const handleChange = (event) => {
const updateapp = {
...application
const updateWorkspace = {
...combined
}
updateapp[event.target.name] = event.target.value
updateWorkspace[event.target.name] = event.target.value
if (event.target.name === 'icon') {
delete updateapp.icon
delete updateWorkspace.icon
setIcon({
value: event.target.value,
file: event.target.files[0]
@@ -217,11 +214,11 @@ export default function AddApp({ app }) {
// return
}
if (updateapp.friendly_name) {
updateapp.image_src = friendlyUrl(updateapp.friendly_name) + '.' + ext
if (updateWorkspace.friendly_name) {
updateWorkspace.image_src = friendlyUrl(updateWorkspace.friendly_name) + '.' + ext
}
setApplication(updateapp)
setCombined(updateWorkspace)
}
const options = [
@@ -240,15 +237,15 @@ export default function AddApp({ app }) {
return (
<div className="">
<Head>
<title>Kasm Apps</title>
<meta name="description" content="List of apps for Kasm Webspaces" />
<title>Kasm Workspaces</title>
<meta name="description" content="List of workspaces for Kasm Webspaces" />
<link rel="icon" href="/favicon.ico" />
</Head>
<div className='flex flex-col lg:flex-row w-full my-20 max-w-6xl text-sm rounded-xl overflow-hidden mx-auto'>
<div className='w-full lg:w-1/2 p-16 bg-slate-300'>
<h1 className='text-2xl font-medium mb-2'>Add Application</h1>
<h1 className='text-2xl font-medium mb-2'>Add Workspace</h1>
<div className='flex flex-col'>
<p className='mb-8 opacity-70'>This will help you generate the JSON file you need to upload to the App directory.</p>
<p className='mb-8 opacity-70'>This page is designed to allow admins to generate the JSON they need to upload to the "workspaces" directory. It also allows end users to see what settings are needed if they want to manually copy them into a new workspace.</p>
<label className='mb-2 font-medium'>Icon</label>
<input type="file" name="icon" onChange={handleChange} className='mb-2 p-2 rounded-lg bg-slate-100 border border-solid border-slate-400' />
@@ -272,7 +269,7 @@ export default function AddApp({ app }) {
<label className='mb-2 font-medium'>Description</label>
<input ref={description} name="description" onChange={handleChange} className='mb-2 p-2 rounded-lg bg-slate-100 border border-solid border-slate-400' />
<p className='mb-6 opacity-70'>A short description about the application</p>
<p className='mb-6 opacity-70'>A short description about the workspace</p>
<label className='mb-2 font-medium'>Docker Image</label>
<input ref={name} name="name" onChange={handleChange} className='mb-2 p-2 rounded-lg bg-slate-100 border border-solid border-slate-400' />
@@ -296,9 +293,9 @@ export default function AddApp({ app }) {
</div>
</div>
<div className='w-full lg:w-1/2 p-16 bg-slate-100'>
<App app={application} icon={icon} inlineImage={inlineImage} />
<pre className='my-8 overflow-y-auto text-xs'>{JSON.stringify(displayApplication(), null, 2)}</pre>
<button onClick={downloadZip} className='p-4 relative z-10 px-5 bg-cyan-700 border-t border-white/20 border-solid hover:bg-slate-900 transition m-2 rounded items-center text-white/70 flex cursor-pointer'>Download</button>
<Workspace workspace={combined} icon={icon} inlineImage={inlineImage} />
<pre className='my-8 overflow-y-auto text-xs'>{JSON.stringify(displayWorkspace(), null, 2)}</pre>
<button onClick={downloadZip} className='p-4 relative z-10 px-5 bg-[#2980b9] border-t border-white/20 border-solid hover:bg-slate-900 transition m-2 rounded items-center text-white/70 flex cursor-pointer'>Download</button>
</div>
</div>
</div>
@@ -307,7 +304,7 @@ export default function AddApp({ app }) {
}
function App({ app, icon, inlineImage }) {
function Workspace({ workspace, icon, inlineImage }) {
const [showDescription, setShowDescription] = useState(false);
@@ -316,7 +313,7 @@ function App({ app, icon, inlineImage }) {
if (icon) {
const blob = new Blob([icon.file])
srcBlob = URL.createObjectURL(blob);
app.image_src = srcBlob
workspace.image_src = srcBlob
}
const installButton = () => {
@@ -329,30 +326,30 @@ function App({ app, icon, inlineImage }) {
return
}
const appExists = false
const workspaceExists = false
return (
<div className={"rounded-xl group w-full shadow max-w-xs relative overflow-hidden h-[100px] border border-solid flex flex-col justify-between bg-slate-300 border-slate-400/50"}>
<div className={"absolute top-0 left-0 right-0 h-[200px] transition-all" + (showDescription ? ' -translate-y-1/2' : '')}>
<div onClick={() => setShowDescription(true)} className={"h-[100px] p-4 relative overflow-hidden cursor-pointer"}>
<img className="h-[90px] group-hover:scale-150 transition-all absolute left-2 top-1" src={app.image_src} onError={(e) => {
if ( inlineImage !== null) { e.target.src = inlineImage }}} alt={app.friendly_name} />
<img className="h-[90px] group-hover:scale-150 transition-all absolute left-2 top-1" src={workspace.image_src} onError={(e) => {
if ( inlineImage !== null) { e.target.src = inlineImage }}} alt={workspace.friendly_name} />
<div className="flex-col pl-28">
<div className="font-bold">{app.friendly_name || 'Friendly Name'}</div>
<div className="text-xs mb-2 flex gap-2">{process.env.name || 'Unknown'} <span>{official()}</span></div>
<div className="font-bold">{workspace.friendly_name || 'Friendly Name'}</div>
<div className="text-xs mb-2 flex gap-2">{process.env.name || 'Manual'} <span>{official()}</span></div>
<div className=" h-8"></div>
</div>
<div className="absolute bottom-0 left-0 right-0 bg-slate-400/20 h-8 text-[10px] flex items-center justify-center">
{app.architecture && app.architecture.map((arch, index) => (
{workspace.architecture && workspace.architecture.map((arch, index) => (
<span key={'arch' + index} className="p-2 py-0 m-[1px] inline-block rounded bg-slate-400/70">{arch}</span>
))}
{app.categories.map((cat, index) => (
{workspace.categories.map((cat, index) => (
<span key={'cat' + index} className="p-2 py-0 m-[1px] inline-block rounded bg-slate-300/90">{cat}</span>
))}
</div>
{appExists && appExists.enabled === true && appExists.available === false && (
{workspaceExists && workspaceExists.enabled === true && workspaceExists.available === false && (
<div className="absolute inset-0 flex justify-center items-center bg-slate-600/70 text-white"><i className="fa fa-spinner fa-spin mr-3"></i> Installing</div>
)}
</div>
@@ -360,7 +357,7 @@ function App({ app, icon, inlineImage }) {
<button className="absolute right-2 top-2 bg-slate-100 rounded-full flex justify-center items-center h-6 w-6" onClick={() => setShowDescription(false)}>
<svg style={{ height: '14px' }} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><path d="M310.6 150.6c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0L160 210.7 54.6 105.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L114.7 256 9.4 361.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0L160 301.3 265.4 406.6c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3L205.3 256 310.6 150.6z" /></svg>
</button>
<div className="flex flex-col flex-grow"><div className="font-bold">{app.friendly_name}</div> {app.description}</div>
<div className="flex flex-col flex-grow"><div className="font-bold">{workspace.friendly_name}</div> {workspace.description}</div>
<div className="flex flex-col justify-end gap-1">
{editButton()}
{installButton()}

View File

@@ -1 +1 @@
{"appcount":1,"apps":[{"name":"Chromium","icon":"chromium.png","description":"Chromium is a free and open-source browser, primarily developed and maintained by Google.","image":"kasmweb/chromium:develop","cores":2,"memory":2768,"gpu_count":0,"cpu_allocation":"inherit","docker_registry":"https://index.docker.io/v1/","volume_mappings":{},"config_override":{"hostname":"kasm"},"exec_config":{"go":{"cmd":"bash -c '/dockerstartup/custom_startup.sh --go --url \"$KASM_URL\"'"},"assign":{"cmd":"bash -c '/dockerstartup/custom_startup.sh --assign --url \"$KASM_URL\"'"}},"categories":["Browser"],"sha":"13126dde5f5338398a728debe459dd1106548aef"}]}
{"workspacecount":1,"workspaces":[{"name":"Chromium","icon":"chromium.png","description":"Chromium is a free and open-source browser, primarily developed and maintained by Google.","image":"kasmweb/chromium:develop","cores":2,"memory":2768,"gpu_count":0,"cpu_allocation":"inherit","docker_registry":"https://index.docker.io/v1/","volume_mappings":{},"config_override":{"hostname":"kasm"},"exec_config":{"go":{"cmd":"bash -c '/dockerstartup/custom_startup.sh --go --url \"$KASM_URL\"'"},"assign":{"cmd":"bash -c '/dockerstartup/custom_startup.sh --assign --url \"$KASM_URL\"'"}},"categories":["Browser"],"sha":"13126dde5f5338398a728debe459dd1106548aef"}]}

View File

@@ -5,8 +5,8 @@
@tailwind utilities;
body {
@apply bg-gradient-to-tr from-slate-500 to-slate-300 min-h-[100vh] text-slate-700;
font-family: 'Roboto', sans-serif;
@apply bg-gradient-to-tr from-slate-300 to-slate-300 min-h-screen text-slate-700;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
font-weight: 400;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -0,0 +1,34 @@
{
"description": "Chromium is a free and open-source browser, primarily developed and maintained by Google.",
"docker_registry": "https://index.docker.io/v1/",
"image_src": "chromium.png",
"run_config": {
"hostname": "kasm"
},
"exec_config": {
"go": {
"cmd": "bash -c '/dockerstartup/custom_startup.sh --go --url \"$KASM_URL\"'"
},
"assign": {
"cmd": "bash -c '/dockerstartup/custom_startup.sh --assign --url \"$KASM_URL\"'"
}
},
"categories": [
"Browser"
],
"friendly_name": "Chromium",
"architecture": [
"amd64",
"arm64"
],
"compatibility": [
{
"version": "1.17.x",
"image": "kasmweb/chromium:develop",
"uncompressed_size_mb": 2170,
"available_tags": [
"develop"
]
}
]
}

7
workspaces/README.md Normal file
View File

@@ -0,0 +1,7 @@
# Workspaces directory
This directory is for storing all the files needed for your workspaces store, they should be stored with the following structure:
* workspaces/Workspace Name
* workspaces/Workspace Name/workspace.json
* workspaces/Workspace Name/workspace-name.png