2022-12-14 15:03:50 +00:00
|
|
|
import Head from 'next/head'
|
2023-01-10 09:41:42 +00:00
|
|
|
import { useState, useEffect, useRef } from 'react'
|
2022-12-14 15:03:50 +00:00
|
|
|
import { saveAs } from 'file-saver';
|
|
|
|
import CreatableSelect from 'react-select/creatable';
|
2023-01-10 09:41:42 +00:00
|
|
|
import Select from 'react-select';
|
|
|
|
import { useRouter } from 'next/router'
|
2023-01-19 11:18:02 +00:00
|
|
|
import allworkspaces from '../../../public/list.json'
|
2022-12-14 15:03:50 +00:00
|
|
|
|
2023-01-10 11:16:36 +00:00
|
|
|
|
|
|
|
export async function getStaticPaths() {
|
2023-01-19 11:18:02 +00:00
|
|
|
let paths = allworkspaces.workspaces.map(workspace => ({
|
2023-01-10 11:16:36 +00:00
|
|
|
params: {
|
2023-01-19 11:18:02 +00:00
|
|
|
workspace: [btoa(workspace.name)]
|
2023-01-10 11:16:36 +00:00
|
|
|
}
|
|
|
|
}))
|
|
|
|
paths.push({
|
2023-01-19 11:18:02 +00:00
|
|
|
params: { workspace: null }
|
2023-01-10 11:16:36 +00:00
|
|
|
})
|
|
|
|
return {
|
|
|
|
paths,
|
|
|
|
fallback: false, // can also be true or 'blocking'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// `getStaticPaths` requires using `getStaticProps`
|
|
|
|
export async function getStaticProps({ params }) {
|
2023-01-19 11:18:02 +00:00
|
|
|
const workspace = params.workspace
|
2023-01-10 11:16:36 +00:00
|
|
|
return {
|
|
|
|
// Passed to the page component as props
|
2023-01-19 11:18:02 +00:00
|
|
|
props: { workspace: workspace ?? null },
|
2023-01-10 11:16:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-19 11:18:02 +00:00
|
|
|
export default function New({ workspace }) {
|
2022-12-14 15:03:50 +00:00
|
|
|
|
2023-01-10 09:41:42 +00:00
|
|
|
const name = useRef(null);
|
|
|
|
const friendly_name = useRef(null);
|
|
|
|
const description = useRef(null);
|
|
|
|
|
|
|
|
const [categories, setCategories] = useState(null)
|
|
|
|
const [architecture, setArchitecture] = useState(null)
|
2022-12-14 15:03:50 +00:00
|
|
|
const [icon, setIcon] = useState(null)
|
|
|
|
const [ext, setExt] = useState('png')
|
2023-01-10 09:41:42 +00:00
|
|
|
const [inlineImage, setInlineImage] = useState(null)
|
2022-12-14 15:03:50 +00:00
|
|
|
|
2023-01-10 11:16:36 +00:00
|
|
|
const defaultState = {
|
2022-12-14 15:03:50 +00:00
|
|
|
friendly_name: null,
|
|
|
|
image_src: null,
|
|
|
|
description: null,
|
|
|
|
name: null,
|
2023-01-10 09:41:42 +00:00
|
|
|
cores: 1,
|
|
|
|
memory: 1024,
|
2022-12-14 15:03:50 +00:00
|
|
|
gpu_count: 0,
|
|
|
|
cpu_allocation_method: "Inherit",
|
|
|
|
docker_registry: "https://index.docker.io/v1/",
|
|
|
|
categories: [],
|
|
|
|
require_gpu: false,
|
|
|
|
enabled: true,
|
|
|
|
image_type: 'Container',
|
2023-01-10 11:16:36 +00:00
|
|
|
}
|
|
|
|
|
2023-01-19 11:18:02 +00:00
|
|
|
const [workspace, setWorkspace] = useState(defaultState)
|
2022-12-14 15:03:50 +00:00
|
|
|
|
2023-01-10 09:41:42 +00:00
|
|
|
const router = useRouter()
|
2023-01-19 11:18:02 +00:00
|
|
|
// const { workspace } = router.query
|
2023-01-10 09:41:42 +00:00
|
|
|
|
|
|
|
useEffect(() => {
|
2023-01-19 11:18:02 +00:00
|
|
|
console.log(workspace)
|
|
|
|
if(workspace === null) {
|
2023-01-10 11:16:36 +00:00
|
|
|
description.current.value = ''
|
|
|
|
name.current.value = ''
|
|
|
|
friendly_name.current.value = ''
|
|
|
|
setCategories(null)
|
|
|
|
setArchitecture(null)
|
2023-01-10 12:00:16 +00:00
|
|
|
setIcon(null)
|
2023-01-19 11:18:02 +00:00
|
|
|
setWorkspace(defaultState)
|
2023-01-10 11:16:36 +00:00
|
|
|
}
|
2023-01-19 11:18:02 +00:00
|
|
|
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) {
|
2023-01-10 11:16:36 +00:00
|
|
|
let catMap = []
|
2023-01-19 11:18:02 +00:00
|
|
|
workspaceDetails.categories.map((e) => catMap.push({
|
2023-01-10 11:16:36 +00:00
|
|
|
label: e,
|
|
|
|
value: e,
|
|
|
|
}))
|
|
|
|
setCategories(catMap)
|
|
|
|
}
|
2023-01-19 11:18:02 +00:00
|
|
|
if (workspaceDetails.architecture) {
|
2023-01-10 11:16:36 +00:00
|
|
|
let archMap = []
|
2023-01-19 11:18:02 +00:00
|
|
|
workspaceDetails.architecture.map((e) => archMap.push({
|
2023-01-10 11:16:36 +00:00
|
|
|
label: e,
|
|
|
|
value: e,
|
|
|
|
}))
|
|
|
|
setArchitecture(archMap)
|
|
|
|
}
|
2023-01-10 09:41:42 +00:00
|
|
|
|
2023-01-19 11:18:02 +00:00
|
|
|
setInlineImage('../../icons/' + workspaceDetails.image_src)
|
2023-01-10 09:41:42 +00:00
|
|
|
|
2023-01-19 11:18:02 +00:00
|
|
|
setWorkspace({
|
|
|
|
...workspace,
|
|
|
|
...workspaceDetails
|
2023-01-10 09:41:42 +00:00
|
|
|
})
|
2023-01-10 11:16:36 +00:00
|
|
|
}
|
2023-01-19 11:18:02 +00:00
|
|
|
}, [workspace])
|
2023-01-10 09:41:42 +00:00
|
|
|
|
2023-01-19 11:18:02 +00:00
|
|
|
const displayWorkspace = () => {
|
2022-12-14 15:03:50 +00:00
|
|
|
return {
|
2023-01-19 11:18:02 +00:00
|
|
|
...workspace,
|
|
|
|
// categories: JSON.stringify(workspace.categories)
|
2022-12-14 15:03:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const customStyles = {
|
|
|
|
control: (base, state) => ({
|
|
|
|
...base,
|
|
|
|
background: "#f1f5f9",
|
|
|
|
borderRadius: '0.5rem',
|
|
|
|
borderColor: "#94a3b8"
|
|
|
|
}),
|
|
|
|
multiValue: (styles, { data }) => {
|
|
|
|
return {
|
|
|
|
...styles,
|
|
|
|
backgroundColor: '#dde6f1',
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
useEffect(() => {
|
2023-01-19 11:18:02 +00:00
|
|
|
if (workspace && workspace.friendly_name) {
|
|
|
|
const updateWorkspace = {
|
|
|
|
...workspace
|
2022-12-14 15:03:50 +00:00
|
|
|
}
|
2023-01-19 11:18:02 +00:00
|
|
|
updateWorkspace.image_src = friendlyUrl(updateWorkspace.friendly_name) + '.' + ext
|
|
|
|
setWorkspace(updateWorkspace)
|
2022-12-14 15:03:50 +00:00
|
|
|
}
|
|
|
|
}, [ext])
|
|
|
|
|
|
|
|
const updateCategories = (items) => {
|
2023-01-19 11:18:02 +00:00
|
|
|
const updateWorkspace = {
|
|
|
|
...workspace
|
2022-12-14 15:03:50 +00:00
|
|
|
}
|
2023-01-19 11:18:02 +00:00
|
|
|
updateWorkspace.categories = items.map(cat => cat.value)
|
|
|
|
setWorkspace(updateWorkspace)
|
2023-01-10 09:41:42 +00:00
|
|
|
let catMap = []
|
2023-01-19 11:18:02 +00:00
|
|
|
updateWorkspace.categories.map((e) => catMap.push({
|
2023-01-10 09:41:42 +00:00
|
|
|
label: e,
|
|
|
|
value: e,
|
|
|
|
}))
|
|
|
|
setCategories(catMap)
|
|
|
|
}
|
|
|
|
|
|
|
|
const updateArchitecture = (items) => {
|
2023-01-19 11:18:02 +00:00
|
|
|
const updateWorkspace = {
|
|
|
|
...workspace
|
2023-01-10 09:41:42 +00:00
|
|
|
}
|
2023-01-19 11:18:02 +00:00
|
|
|
updateWorkspace.architecture = items.map(arch => arch.value)
|
|
|
|
setWorkspace(updateWorkspace)
|
2023-01-10 09:41:42 +00:00
|
|
|
let archMap = []
|
2023-01-19 11:18:02 +00:00
|
|
|
updateWorkspace.architecture.map((e) => archMap.push({
|
2023-01-10 09:41:42 +00:00
|
|
|
label: e,
|
|
|
|
value: e,
|
|
|
|
}))
|
|
|
|
setArchitecture(archMap)
|
2022-12-14 15:03:50 +00:00
|
|
|
}
|
|
|
|
|
2023-01-19 11:18:02 +00:00
|
|
|
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');
|
|
|
|
});
|
|
|
|
}
|
2022-12-14 15:03:50 +00:00
|
|
|
|
|
|
|
const handleChange = (event) => {
|
2023-01-19 11:18:02 +00:00
|
|
|
const updateWorkspace = {
|
|
|
|
...workspace
|
2022-12-14 15:03:50 +00:00
|
|
|
}
|
2023-01-19 11:18:02 +00:00
|
|
|
updateWorkspace[event.target.name] = event.target.value
|
2022-12-14 15:03:50 +00:00
|
|
|
if (event.target.name === 'icon') {
|
2023-01-19 11:18:02 +00:00
|
|
|
delete updateWorkspace.icon
|
2022-12-14 15:03:50 +00:00
|
|
|
setIcon({
|
|
|
|
value: event.target.value,
|
|
|
|
file: event.target.files[0]
|
|
|
|
})
|
|
|
|
setExt(event.target.value.substr(event.target.value.lastIndexOf('.') + 1))
|
2023-01-10 12:00:16 +00:00
|
|
|
setInlineImage(null)
|
2022-12-14 15:03:50 +00:00
|
|
|
// return
|
|
|
|
}
|
|
|
|
|
2023-01-19 11:18:02 +00:00
|
|
|
if (updateWorkspace.friendly_name) {
|
|
|
|
updateWorkspace.image_src = friendlyUrl(updateWorkspace.friendly_name) + '.' + ext
|
2022-12-14 15:03:50 +00:00
|
|
|
}
|
|
|
|
|
2023-01-19 11:18:02 +00:00
|
|
|
setWorkspace(updateWorkspace)
|
2022-12-14 15:03:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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>
|
2023-01-19 11:18:02 +00:00
|
|
|
<title>Kasm Workspaces</title>
|
|
|
|
<meta name="description" content="List of workspaces for Kasm Webspaces" />
|
2022-12-14 15:03:50 +00:00
|
|
|
<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'>
|
2023-01-19 11:18:02 +00:00
|
|
|
<h1 className='text-2xl font-medium mb-2'>Add Workspace</h1>
|
2022-12-14 15:03:50 +00:00
|
|
|
<div className='flex flex-col'>
|
2023-01-19 11:18:02 +00:00
|
|
|
<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>
|
2022-12-14 15:03:50 +00:00
|
|
|
|
|
|
|
<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>
|
2023-01-10 09:41:42 +00:00
|
|
|
<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' />
|
2022-12-14 15:03:50 +00:00
|
|
|
<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
|
2023-01-10 12:00:16 +00:00
|
|
|
instanceId="1"
|
2022-12-14 15:03:50 +00:00
|
|
|
name="categories"
|
|
|
|
isMulti
|
|
|
|
options={options}
|
|
|
|
onChange={updateCategories}
|
|
|
|
styles={customStyles}
|
2023-01-10 09:41:42 +00:00
|
|
|
value={categories}
|
2022-12-14 15:03:50 +00:00
|
|
|
/>
|
|
|
|
<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>
|
2023-01-10 09:41:42 +00:00
|
|
|
<input ref={description} name="description" onChange={handleChange} className='mb-2 p-2 rounded-lg bg-slate-100 border border-solid border-slate-400' />
|
2023-01-19 11:18:02 +00:00
|
|
|
<p className='mb-6 opacity-70'>A short description about the workspace</p>
|
2022-12-14 15:03:50 +00:00
|
|
|
|
|
|
|
<label className='mb-2 font-medium'>Docker Image</label>
|
2023-01-10 09:41:42 +00:00
|
|
|
<input ref={name} name="name" onChange={handleChange} className='mb-2 p-2 rounded-lg bg-slate-100 border border-solid border-slate-400' />
|
2022-12-14 15:03:50 +00:00
|
|
|
<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>
|
|
|
|
|
2023-01-10 09:41:42 +00:00
|
|
|
<label className='mb-2 font-medium'>Architecture</label>
|
|
|
|
<Select
|
2023-01-10 12:00:16 +00:00
|
|
|
instanceId="2"
|
2023-01-10 09:41:42 +00:00
|
|
|
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>
|
|
|
|
|
2022-12-14 15:03:50 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div className='w-full lg:w-1/2 p-16 bg-slate-100'>
|
2023-01-19 11:18:02 +00:00
|
|
|
<Workspace workspace={workspace} icon={icon} inlineImage={inlineImage} />
|
|
|
|
<pre className='my-8 overflow-y-auto text-xs'>{JSON.stringify(displayWorkspace(), null, 2)}</pre>
|
2023-01-10 09:41:42 +00:00
|
|
|
<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>
|
2022-12-14 15:03:50 +00:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2023-01-19 11:18:02 +00:00
|
|
|
function Workspace({ workspace, icon, inlineImage }) {
|
2022-12-14 15:03:50 +00:00
|
|
|
|
|
|
|
const [showDescription, setShowDescription] = useState(false);
|
|
|
|
|
|
|
|
let srcBlob = null
|
|
|
|
|
|
|
|
if (icon) {
|
|
|
|
const blob = new Blob([icon.file])
|
|
|
|
srcBlob = URL.createObjectURL(blob);
|
2023-01-19 11:18:02 +00:00
|
|
|
workspace.image_src = srcBlob
|
2022-12-14 15:03:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-01-19 11:18:02 +00:00
|
|
|
const workspaceExists = false
|
2022-12-14 15:03:50 +00:00
|
|
|
|
|
|
|
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"}>
|
2023-01-19 11:18:02 +00:00
|
|
|
<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} />
|
2022-12-14 15:03:50 +00:00
|
|
|
<div className="flex-col pl-28">
|
2023-01-19 11:18:02 +00:00
|
|
|
<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>
|
2022-12-14 15:03:50 +00:00
|
|
|
<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">
|
2023-01-19 11:18:02 +00:00
|
|
|
{workspace.architecture && workspace.architecture.map((arch, index) => (
|
2023-01-10 09:41:42 +00:00
|
|
|
<span key={'arch' + index} className="p-2 py-0 m-[1px] inline-block rounded bg-slate-400/70">{arch}</span>
|
|
|
|
))}
|
|
|
|
|
2023-01-19 11:18:02 +00:00
|
|
|
{workspace.categories.map((cat, index) => (
|
2023-01-10 09:41:42 +00:00
|
|
|
<span key={'cat' + index} className="p-2 py-0 m-[1px] inline-block rounded bg-slate-300/90">{cat}</span>
|
2022-12-14 15:03:50 +00:00
|
|
|
))}
|
|
|
|
</div>
|
2023-01-19 11:18:02 +00:00
|
|
|
{workspaceExists && workspaceExists.enabled === true && workspaceExists.available === false && (
|
2022-12-14 15:03:50 +00:00
|
|
|
<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)}>
|
2023-01-10 09:41:42 +00:00
|
|
|
<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>
|
2022-12-14 15:03:50 +00:00
|
|
|
</button>
|
2023-01-19 11:18:02 +00:00
|
|
|
<div className="flex flex-col flex-grow"><div className="font-bold">{workspace.friendly_name}</div> {workspace.description}</div>
|
2022-12-14 15:03:50 +00:00
|
|
|
<div className="flex flex-col justify-end gap-1">
|
|
|
|
{editButton()}
|
|
|
|
{installButton()}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
|
|
|
|
}
|
|
|
|
|