Change Applications to Workspaces

This commit is contained in:
Chris Hunt 2023-01-19 11:18:02 +00:00
parent 3b61336e30
commit 8a4c513331
11 changed files with 186 additions and 190 deletions

View File

@ -1,12 +1,12 @@
# 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.
# Kasm Apps
# Kasm Workspaces
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
This is a repository of the workspaces supported by Kasm. The workspaces list is automatically generated and can be used when creating new workspaces or using the 1 click installer
## Create your own app store
## Create your own workspace registry
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:
We have tried to make it as simple as possible for people to create their own registries that work with Kasm, the easiest way to do that is to follow these steps:
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
@ -18,11 +18,11 @@ We have tried to make it as simple as possible for people to create their own ap
* listUrl - The link to the root of your site. For example https://username.github.io/repositoryname/ it should always include a trailing slash.
* contactUrl - A link users can use to contact you on.
If you are using a domain or a subdomain, your basePath will just be the current version number `basePath: '/1.0',`, otherwise change the value to include what you chose for the repository name in step 2 `basePath: '/repositoryname/1.0',`.
1. Upload your apps to the /apps folder
1. Upload your workspaces to the /workspaces 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
1. You should now have a working site which includes any workspaces you added
[![](https://cdn.loom.com/sessions/thumbnails/256fac3d2bbb422b8e779ac1c8244d33-00001.gif)](https://www.loom.com/share/256fac3d2bbb422b8e779ac1c8244d33 "")
@ -35,6 +35,6 @@ If a new schema version comes out, this is what you will need to do.
## Discovery
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.
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.
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

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 = {
@ -32,7 +32,7 @@ glob("**/app.json", async function (err, files) {
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);
@ -40,7 +40,7 @@ glob("**/app.json", async function (err, files) {
let parsed = JSON.parse(filedata);
parsed.sha = hash.hash;
console.log(parsed.name + ' added')
apps.push(parsed);
workspaces.push(parsed);
if (fs.existsSync(folder + "/" + parsed.image_src)) {
let imagedata = fs.readFileSync(folder + "/" + parsed.image_src);
@ -53,13 +53,13 @@ glob("**/app.json", async function (err, files) {
let json = {
name: nextConfig.env.name || 'Unknown store',
appcount: apptotal,
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,
};
let data = JSON.stringify(json);

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.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 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/' + 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

@ -23,38 +23,36 @@ export default function Header({ searchText, changeSearch }) {
<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>
<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}
@ -64,7 +62,7 @@ export default function Header({ searchText, changeSearch }) {
</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>
<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

@ -3,14 +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/',
contactUrl: 'https://kasmweb.com/support',
},
reactStrictMode: true,
swcMinify: true,
basePath: '/kasm-apps/1.0',
basePath: '/kasm-registry/1.0',
trailingSlash: true,
images: {
unoptimized: true,

View File

@ -1,24 +1,24 @@
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)
useEffect(() => {
fetch('list.json')
.then((res) => res.json())
.then((apps) => {
setApps(apps)
.then((workspaces) => {
setWorkspaces(workspaces)
})
}, [])
let filteredapps = apps && apps.apps && apps.apps.length > 0 ? [...apps.apps] : [];
let filteredworkspaces = workspaces && workspaces.workspaces && workspaces.workspaces.length > 0 ? [...workspaces.workspaces] : [];
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 +33,20 @@ 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>
<h1 className='flex text-2xl justify-center mb-10'>Workspaces: <span className=''>{workspaces && workspaces.workspacecount}</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.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);
@ -92,58 +59,58 @@ export default function AddApp({ app }) {
image_type: 'Container',
}
const [application, setApplication] = useState(defaultState)
const [workspace, setWorkspace] = useState(defaultState)
const router = useRouter()
// const { app } = router.query
// const { workspace } = router.query
useEffect(() => {
console.log(app)
if(app === null) {
console.log(workspace)
if(workspace === null) {
description.current.value = ''
name.current.value = ''
friendly_name.current.value = ''
setCategories(null)
setArchitecture(null)
setIcon(null)
setApplication(defaultState)
setWorkspace(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.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
setWorkspace({
...workspace,
...workspaceDetails
})
}
}, [app])
}, [workspace])
const displayApplication = () => {
const displayWorkspace = () => {
return {
...application,
// categories: JSON.stringify(application.categories)
...workspace,
// categories: JSON.stringify(workspace.categories)
}
}
@ -163,23 +130,23 @@ export default function AddApp({ app }) {
}
useEffect(() => {
if (application && application.friendly_name) {
const updateapp = {
...application
if (workspace && workspace.friendly_name) {
const updateWorkspace = {
...workspace
}
updateapp.image_src = friendlyUrl(updateapp.friendly_name) + '.' + ext
setApplication(updateapp)
updateWorkspace.image_src = friendlyUrl(updateWorkspace.friendly_name) + '.' + ext
setWorkspace(updateWorkspace)
}
}, [ext])
const updateCategories = (items) => {
const updateapp = {
...application
const updateWorkspace = {
...workspace
}
updateapp.categories = items.map(cat => cat.value)
setApplication(updateapp)
updateWorkspace.categories = items.map(cat => cat.value)
setWorkspace(updateWorkspace)
let catMap = []
updateapp.categories.map((e) => catMap.push({
updateWorkspace.categories.map((e) => catMap.push({
label: e,
value: e,
}))
@ -187,27 +154,59 @@ export default function AddApp({ app }) {
}
const updateArchitecture = (items) => {
const updateapp = {
...application
const updateWorkspace = {
...workspace
}
updateapp.architecture = items.map(arch => arch.value)
setApplication(updateapp)
updateWorkspace.architecture = items.map(arch => arch.value)
setWorkspace(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(workspace.friendly_name)
folder.file('workspace.json', JSON.stringify(workspace, null, 2))
if (icon) {
folder.file(workspace.image_src, icon.file)
}
else if (inlineImage) {
const promise = fetch(inlineImage).then(response => response.blob())
folder.file(workspace.image_src, promise)
}
zip.generateAsync({ type: "blob" })
.then(function (content) {
// Force down of the Zip file
saveAs(content, friendlyUrl(workspace.friendly_name) + '.zip');
});
}
const handleChange = (event) => {
const updateapp = {
...application
const updateWorkspace = {
...workspace
}
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 +216,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)
setWorkspace(updateWorkspace)
}
const options = [
@ -240,15 +239,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 +271,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,8 +295,8 @@ 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>
<Workspace workspace={workspace} 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-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>
</div>
</div>
@ -307,7 +306,7 @@ export default function AddApp({ app }) {
}
function App({ app, icon, inlineImage }) {
function Workspace({ workspace, icon, inlineImage }) {
const [showDescription, setShowDescription] = useState(false);
@ -316,7 +315,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 +328,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 +359,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()}

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