Initial commit
This commit is contained in:
34
site/README.md
Normal file
34
site/README.md
Normal file
@@ -0,0 +1,34 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `pages/index.js`. The page auto-updates as you edit the file.
|
||||
|
||||
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.js`.
|
||||
|
||||
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
18
site/components/Bubbles.js
Normal file
18
site/components/Bubbles.js
Normal file
@@ -0,0 +1,18 @@
|
||||
function Bubbles() {
|
||||
return (
|
||||
<ul className="bg-bubbles">
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
<li></li>
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
|
||||
export default Bubbles
|
||||
30
site/components/Workspace.js
Normal file
30
site/components/Workspace.js
Normal 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
|
||||
7
site/components/footer.js
Normal file
7
site/components/footer.js
Normal file
@@ -0,0 +1,7 @@
|
||||
export default function Footer() {
|
||||
return (
|
||||
<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. <a className="underline" href="https://kasmweb.com">Click here to find out about Kasm Workspaces</a>
|
||||
</footer>
|
||||
)
|
||||
}
|
||||
72
site/components/header.js
Normal file
72
site/components/header.js
Normal file
@@ -0,0 +1,72 @@
|
||||
import Bubbles from '../components/Bubbles'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from "next/router";
|
||||
import { NotificationManager } from 'react-notifications';
|
||||
|
||||
export default function Header({ searchText, changeSearch }) {
|
||||
|
||||
const copyToClipboard = () => {
|
||||
var textField = document.createElement('textarea')
|
||||
textField.innerText = listUrl
|
||||
document.body.appendChild(textField)
|
||||
textField.select()
|
||||
document.execCommand('copy')
|
||||
textField.remove()
|
||||
NotificationManager.info('URL successfully copied to clipboard', 'Copy URL', 4000);
|
||||
}
|
||||
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-[#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'>W</span>
|
||||
<span className='opacity-70'>o</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> </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'>
|
||||
<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 workspace'
|
||||
type="text"
|
||||
value={searchText}
|
||||
onChange={changeSearch}
|
||||
/>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<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 >
|
||||
|
||||
)
|
||||
}
|
||||
17
site/components/layout.js
Normal file
17
site/components/layout.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// components/layout.js
|
||||
|
||||
import Header from './header'
|
||||
import Footer from './footer'
|
||||
import 'react-notifications/lib/notifications.css';
|
||||
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 className="grow">{children}</main>
|
||||
<Footer />
|
||||
<NotificationContainer/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
21
site/next.config.js
Normal file
21
site/next.config.js
Normal file
@@ -0,0 +1,21 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
|
||||
const nextConfig = {
|
||||
output: 'export',
|
||||
distDir: '../public',
|
||||
env: {
|
||||
name: 'Kasm Technologies',
|
||||
description: 'The official store for Kasm supported workspaces.',
|
||||
icon: '/img/logo.svg',
|
||||
listUrl: 'https://registry.kasmweb.com/',
|
||||
contactUrl: 'https://kasmweb.com/support',
|
||||
},
|
||||
reactStrictMode: true,
|
||||
basePath: '/kasm-registry/1.0',
|
||||
trailingSlash: true,
|
||||
images: {
|
||||
unoptimized: true,
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = nextConfig
|
||||
7461
site/package-lock.json
generated
Normal file
7461
site/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
27
site/package.json
Normal file
27
site/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"deploy": "next build && touch ../public/.nojekyll",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"file-saver": "^2.0.5",
|
||||
"jszip": "^3.10.1",
|
||||
"next": "^14.2.27",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"react-notifications": "^1.7.4",
|
||||
"react-select": "^5.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.13",
|
||||
"eslint": "8.26.0",
|
||||
"eslint-config-next": "13.0.0",
|
||||
"postcss": "^8.4.18",
|
||||
"tailwindcss": "^3.2.1"
|
||||
}
|
||||
}
|
||||
20
site/pages/_app.js
Normal file
20
site/pages/_app.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import '../styles/globals.css'
|
||||
import Layout from '../components/layout'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
const [searchText, setSearchText] = useState('')
|
||||
|
||||
const changeSearch = event => {
|
||||
setSearchText(event.target.value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout searchText={searchText} changeSearch={changeSearch}>
|
||||
<Component searchText={searchText} {...pageProps} />
|
||||
</Layout>
|
||||
)
|
||||
}
|
||||
|
||||
export default MyApp
|
||||
99
site/pages/index.js
Normal file
99
site/pages/index.js
Normal file
@@ -0,0 +1,99 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import Head from 'next/head'
|
||||
import Workspace from '../components/Workspace'
|
||||
import styles from '../styles/Home.module.css'
|
||||
|
||||
export default function Home({ searchText }) {
|
||||
|
||||
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((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)
|
||||
})
|
||||
}, [])
|
||||
|
||||
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 !== "") {
|
||||
filteredworkspaces = filteredworkspaces.filter((i) => {
|
||||
const category = (i.categories && i.categories.length > 0) ? i.categories.filter((i) =>
|
||||
i.toLowerCase().includes(lowerSearch)
|
||||
) : [];
|
||||
return (
|
||||
i.name.toLowerCase().includes(lowerSearch) ||
|
||||
category.length > 0
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<Head>
|
||||
<title>Kasm Workspaces</title>
|
||||
<meta name="description" content="List of workspaces for Kasm Webspaces" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
|
||||
<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">
|
||||
{filteredworkspaces && filteredworkspaces.length > 0 && filteredworkspaces.map(function (workspace, i) {
|
||||
return <Workspace key={workspace.sha} workspace={workspace} />
|
||||
})}
|
||||
{filteredworkspaces && filteredworkspaces.length === 0 && (
|
||||
<p>No workspaces found {searchText !== '' && ('matching "' + searchText + '"')}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
<div className={styles.grid}>
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
371
site/pages/new/[[...workspace]].js
Normal file
371
site/pages/new/[[...workspace]].js
Normal file
@@ -0,0 +1,371 @@
|
||||
import Head from 'next/head'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { saveAs } from 'file-saver';
|
||||
import CreatableSelect from 'react-select/creatable';
|
||||
import Select from 'react-select';
|
||||
import { useRouter } from 'next/router'
|
||||
import allworkspaces from '../../../public/list.json'
|
||||
|
||||
|
||||
export async function getStaticPaths() {
|
||||
let paths = allworkspaces.workspaces.map(workspace => ({
|
||||
params: {
|
||||
workspace: [btoa(workspace.friendly_name)]
|
||||
}
|
||||
}))
|
||||
paths.push({
|
||||
params: { workspace: null }
|
||||
})
|
||||
return {
|
||||
paths,
|
||||
fallback: false, // can also be true or 'blocking'
|
||||
}
|
||||
}
|
||||
|
||||
// `getStaticPaths` requires using `getStaticProps`
|
||||
export async function getStaticProps({ params }) {
|
||||
const workspace = params.workspace
|
||||
return {
|
||||
// Passed to the page component as props
|
||||
props: { workspace: workspace ?? null },
|
||||
}
|
||||
}
|
||||
|
||||
export default function New({ workspace }) {
|
||||
|
||||
const name = useRef(null);
|
||||
const friendly_name = useRef(null);
|
||||
const description = useRef(null);
|
||||
|
||||
const [categories, setCategories] = useState(null)
|
||||
const [architecture, setArchitecture] = useState(null)
|
||||
const [icon, setIcon] = useState(null)
|
||||
const [ext, setExt] = useState('png')
|
||||
const [inlineImage, setInlineImage] = useState(null)
|
||||
|
||||
const defaultState = {
|
||||
friendly_name: null,
|
||||
image_src: null,
|
||||
description: null,
|
||||
cores: 2,
|
||||
memory: 2768,
|
||||
gpu_count: 0,
|
||||
cpu_allocation_method: "Inherit",
|
||||
docker_registry: "https://index.docker.io/v1/",
|
||||
categories: [],
|
||||
require_gpu: false,
|
||||
enabled: true,
|
||||
image_type: 'Container',
|
||||
}
|
||||
|
||||
const [combined, setCombined] = useState(defaultState)
|
||||
|
||||
const router = useRouter()
|
||||
// const { workspace } = router.query
|
||||
|
||||
useEffect(() => {
|
||||
if(workspace === null) {
|
||||
description.current.value = ''
|
||||
name.current.value = ''
|
||||
friendly_name.current.value = ''
|
||||
setCategories(null)
|
||||
setArchitecture(null)
|
||||
setIcon(null)
|
||||
setCombined(defaultState)
|
||||
}
|
||||
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 = []
|
||||
workspaceDetails.categories.map((e) => catMap.push({
|
||||
label: e,
|
||||
value: e,
|
||||
}))
|
||||
setCategories(catMap)
|
||||
}
|
||||
if (workspaceDetails.architecture) {
|
||||
let archMap = []
|
||||
workspaceDetails.architecture.map((e) => archMap.push({
|
||||
label: e,
|
||||
value: e,
|
||||
}))
|
||||
setArchitecture(archMap)
|
||||
}
|
||||
|
||||
setInlineImage('../../icons/' + workspaceDetails.image_src)
|
||||
|
||||
setCombined({
|
||||
...combined,
|
||||
...workspaceDetails
|
||||
})
|
||||
}
|
||||
}, [workspace])
|
||||
|
||||
const displayWorkspace = () => {
|
||||
return {
|
||||
...combined,
|
||||
// categories: JSON.stringify(combined.categories)
|
||||
}
|
||||
}
|
||||
|
||||
const customStyles = {
|
||||
control: (base, state) => ({
|
||||
...base,
|
||||
background: "#f1f5f9",
|
||||
borderRadius: '0.5rem',
|
||||
borderColor: "#94a3b8"
|
||||
}),
|
||||
multiValue: (styles, { data }) => {
|
||||
return {
|
||||
...styles,
|
||||
backgroundColor: '#dde6f1',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (combined && combined.friendly_name) {
|
||||
const updateWorkspace = {
|
||||
...combined
|
||||
}
|
||||
updateWorkspace.image_src = friendlyUrl(updateWorkspace.friendly_name) + '.' + ext
|
||||
setCombined(updateWorkspace)
|
||||
}
|
||||
}, [ext])
|
||||
|
||||
const updateCategories = (items) => {
|
||||
const updateWorkspace = {
|
||||
...combined
|
||||
}
|
||||
updateWorkspace.categories = items.map(cat => cat.value)
|
||||
setCombined(updateWorkspace)
|
||||
let catMap = []
|
||||
updateWorkspace.categories.map((e) => catMap.push({
|
||||
label: e,
|
||||
value: e,
|
||||
}))
|
||||
setCategories(catMap)
|
||||
}
|
||||
|
||||
const updateArchitecture = (items) => {
|
||||
const updateWorkspace = {
|
||||
...combined
|
||||
}
|
||||
updateWorkspace.architecture = items.map(arch => arch.value)
|
||||
setCombined(updateWorkspace)
|
||||
let archMap = []
|
||||
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 updateWorkspace = {
|
||||
...combined
|
||||
}
|
||||
updateWorkspace[event.target.name] = event.target.value
|
||||
if (event.target.name === 'icon') {
|
||||
delete updateWorkspace.icon
|
||||
setIcon({
|
||||
value: event.target.value,
|
||||
file: event.target.files[0]
|
||||
})
|
||||
setExt(event.target.value.substr(event.target.value.lastIndexOf('.') + 1))
|
||||
setInlineImage(null)
|
||||
// return
|
||||
}
|
||||
|
||||
if (updateWorkspace.friendly_name) {
|
||||
updateWorkspace.image_src = friendlyUrl(updateWorkspace.friendly_name) + '.' + ext
|
||||
}
|
||||
|
||||
setCombined(updateWorkspace)
|
||||
}
|
||||
|
||||
const options = [
|
||||
{ value: 'Browser', label: 'Browser' },
|
||||
{ value: 'Communication', label: 'Communication' },
|
||||
{ value: 'Desktop', label: 'Desktop' },
|
||||
{ value: 'Development', label: 'Development' },
|
||||
{ value: 'Games', label: 'Games' },
|
||||
{ value: 'Multimedia', label: 'Multimedia' },
|
||||
{ value: 'Office', label: 'Office' },
|
||||
{ value: 'Privacy', label: 'Privacy' },
|
||||
{ value: 'Productivity', label: 'Productivity' },
|
||||
{ value: 'Remote Access', label: 'Remote Access' }
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<Head>
|
||||
<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 Workspace</h1>
|
||||
<div className='flex flex-col'>
|
||||
<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' />
|
||||
<p className='mb-6 opacity-70'>Select the image to use, image will be renamed when it's downloaded.</p>
|
||||
|
||||
<label className='mb-2 font-medium'>Friendly Name</label>
|
||||
<input ref={friendly_name} name="friendly_name" onChange={handleChange} className='mb-2 p-2 rounded-lg bg-slate-100 border border-solid border-slate-400' />
|
||||
<p className='mb-6 opacity-70'>This is the name that will show for users</p>
|
||||
|
||||
<label className='mb-2 font-medium'>Categories</label>
|
||||
<CreatableSelect
|
||||
instanceId="1"
|
||||
name="categories"
|
||||
isMulti
|
||||
options={options}
|
||||
onChange={updateCategories}
|
||||
styles={customStyles}
|
||||
value={categories}
|
||||
/>
|
||||
<p className='mb-6 mt-2 opacity-70'>You can select from the available option or create new ones.</p>
|
||||
|
||||
<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 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' />
|
||||
<p className='mb-6 opacity-70'>The docker image to use, i.e. <code className='text-xs p-1 px-2 rounded bg-white/40'>kasmweb/filezilla:develop</code></p>
|
||||
|
||||
<label className='mb-2 font-medium'>Architecture</label>
|
||||
<Select
|
||||
instanceId="2"
|
||||
name="architecture"
|
||||
isMulti
|
||||
options={[
|
||||
{ value: 'amd64', label: 'amd64' },
|
||||
{ value: 'arm64', label: 'arm64' },
|
||||
]}
|
||||
onChange={updateArchitecture}
|
||||
styles={customStyles}
|
||||
value={architecture}
|
||||
/>
|
||||
<p className='mb-6 mt-2 opacity-70'>You can select from the available option or create new ones.</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className='w-full lg:w-1/2 p-16 bg-slate-100'>
|
||||
<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>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
function Workspace({ workspace, icon, inlineImage }) {
|
||||
|
||||
const [showDescription, setShowDescription] = useState(false);
|
||||
|
||||
let srcBlob = null
|
||||
|
||||
if (icon) {
|
||||
const blob = new Blob([icon.file])
|
||||
srcBlob = URL.createObjectURL(blob);
|
||||
workspace.image_src = srcBlob
|
||||
}
|
||||
|
||||
const installButton = () => {
|
||||
return <button className={"text-xs w-full p-4 py-1 rounded-lg flex justify-center items-center bg-blue-500 font-bold text-white"}>Install</button>
|
||||
}
|
||||
const editButton = () => {
|
||||
return <div className="text-xs text-color w-full p-4 py-1 rounded-lg bg-black/5 flex justify-center items-center">Edit</div>
|
||||
}
|
||||
const official = () => {
|
||||
return
|
||||
}
|
||||
|
||||
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={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">{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">
|
||||
{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>
|
||||
))}
|
||||
|
||||
{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>
|
||||
{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>
|
||||
<div className="h-[100px] text-xs relative p-2 pl-4 flex">
|
||||
<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">{workspace.friendly_name}</div> {workspace.description}</div>
|
||||
<div className="flex flex-col justify-end gap-1">
|
||||
{editButton()}
|
||||
{installButton()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
6
site/postcss.config.js
Normal file
6
site/postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
BIN
site/public/favicon.ico
Normal file
BIN
site/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
1
site/public/list.json
Normal file
1
site/public/list.json
Normal file
@@ -0,0 +1 @@
|
||||
{"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"}]}
|
||||
4
site/public/vercel.svg
Normal file
4
site/public/vercel.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
129
site/styles/Home.module.css
Normal file
129
site/styles/Home.module.css
Normal file
@@ -0,0 +1,129 @@
|
||||
.container {
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.main {
|
||||
min-height: 100vh;
|
||||
padding: 4rem 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
padding: 2rem 0;
|
||||
border-top: 1px solid #eaeaea;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.title a {
|
||||
color: #0070f3;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.title a:hover,
|
||||
.title a:focus,
|
||||
.title a:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
line-height: 1.15;
|
||||
font-size: 4rem;
|
||||
}
|
||||
|
||||
.title,
|
||||
.description {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin: 4rem 0;
|
||||
line-height: 1.5;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.code {
|
||||
background: #fafafa;
|
||||
border-radius: 5px;
|
||||
padding: 0.75rem;
|
||||
font-size: 1.1rem;
|
||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
||||
Bitstream Vera Sans Mono, Courier New, monospace;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
max-width: 800px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 1rem;
|
||||
padding: 1.5rem;
|
||||
text-align: left;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
border: 1px solid #eaeaea;
|
||||
border-radius: 10px;
|
||||
transition: color 0.15s ease, border-color 0.15s ease;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.card:hover,
|
||||
.card:focus,
|
||||
.card:active {
|
||||
color: #0070f3;
|
||||
border-color: #0070f3;
|
||||
}
|
||||
|
||||
.card h2 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.card p {
|
||||
margin: 0;
|
||||
font-size: 1.25rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 1em;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.grid {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.card,
|
||||
.footer {
|
||||
border-color: #222;
|
||||
}
|
||||
.code {
|
||||
background: #111;
|
||||
}
|
||||
.logo img {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
127
site/styles/globals.css
Normal file
127
site/styles/globals.css
Normal file
@@ -0,0 +1,127 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Roboto:wght@100;300;400;500;700&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
@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;
|
||||
}
|
||||
|
||||
.notification {
|
||||
box-shadow: none!important;
|
||||
}
|
||||
|
||||
.bg-bubbles {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
.bg-bubbles li {
|
||||
position: absolute;
|
||||
list-style: none;
|
||||
display: block;
|
||||
border: 1px solid rgba(255,255,255,0.1);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
bottom: -160px;
|
||||
-webkit-animation: square 25s infinite;
|
||||
animation: square 25s infinite;
|
||||
transition-timing-function: linear;
|
||||
}
|
||||
.bg-bubbles li:nth-child(1) {
|
||||
left: 10%;
|
||||
bottom: -60px;
|
||||
}
|
||||
.bg-bubbles li:nth-child(2) {
|
||||
left: 20%;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
-webkit-animation-delay: 2s;
|
||||
animation-delay: 2s;
|
||||
-webkit-animation-duration: 17s;
|
||||
animation-duration: 17s;
|
||||
}
|
||||
.bg-bubbles li:nth-child(3) {
|
||||
left: 25%;
|
||||
-webkit-animation-delay: 4s;
|
||||
animation-delay: 4s;
|
||||
}
|
||||
.bg-bubbles li:nth-child(4) {
|
||||
left: 40%;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
-webkit-animation-duration: 22s;
|
||||
animation-duration: 22s;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255,255,255,0.2);
|
||||
}
|
||||
.bg-bubbles li:nth-child(5) {
|
||||
left: 70%;
|
||||
bottom: -20px;
|
||||
}
|
||||
.bg-bubbles li:nth-child(6) {
|
||||
left: 80%;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
-webkit-animation-delay: 3s;
|
||||
animation-delay: 3s;
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255,255,255,0.15);
|
||||
}
|
||||
.bg-bubbles li:nth-child(7) {
|
||||
left: 32%;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
-webkit-animation-delay: 7s;
|
||||
animation-delay: 7s;
|
||||
}
|
||||
.bg-bubbles li:nth-child(8) {
|
||||
left: 55%;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
-webkit-animation-delay: 15s;
|
||||
animation-delay: 15s;
|
||||
-webkit-animation-duration: 40s;
|
||||
animation-duration: 40s;
|
||||
}
|
||||
.bg-bubbles li:nth-child(9) {
|
||||
left: 25%;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
-webkit-animation-delay: 2s;
|
||||
animation-delay: 2s;
|
||||
-webkit-animation-duration: 40s;
|
||||
animation-duration: 40s;
|
||||
background-color: rgba(255, 255, 255, 0.17);
|
||||
border: 1px solid rgba(255,255,255,0.22);
|
||||
}
|
||||
.bg-bubbles li:nth-child(10) {
|
||||
left: 90%;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
-webkit-animation-delay: 11s;
|
||||
animation-delay: 11s;
|
||||
}
|
||||
@-webkit-keyframes square {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-400px) rotate(600deg);
|
||||
}
|
||||
}
|
||||
@keyframes square {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-400px) rotate(600deg);
|
||||
}
|
||||
}
|
||||
19
site/tailwind.config.js
Normal file
19
site/tailwind.config.js
Normal file
@@ -0,0 +1,19 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./pages/**/*.{js,ts,jsx,tsx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
app: {
|
||||
900: '#5f4c7c',
|
||||
800: '#9178bd',
|
||||
700: '#b199da'
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
2540
site/yarn.lock
Normal file
2540
site/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user