Quasi finito
This commit is contained in:
@@ -7,6 +7,8 @@ import { AdminPanel } from '../pages/Admin.jsx'
|
||||
import { ApiKeys } from '../pages/ApiKeys.jsx'
|
||||
import { Login } from '../pages/Login.jsx'
|
||||
import { Dashboard } from '../pages/Dashboard.jsx'
|
||||
import { Buckets } from '../pages/Buckets.jsx'
|
||||
import { Bucket } from '../pages/Bucket.jsx'
|
||||
|
||||
export const MainContent = () => {
|
||||
const { setSidebarVisible, route, user } = useStore()
|
||||
@@ -17,6 +19,8 @@ export const MainContent = () => {
|
||||
login: Login,
|
||||
admin: AdminPanel,
|
||||
apiKeys: ApiKeys,
|
||||
buckets: Buckets,
|
||||
bucket: Bucket,
|
||||
}[route.id] || (() => <>Still nothing here</>)
|
||||
|
||||
return (
|
||||
|
||||
@@ -39,6 +39,10 @@ const App = () => {
|
||||
},
|
||||
})
|
||||
|
||||
if (id === 'unknown') {
|
||||
location.href = '/'
|
||||
}
|
||||
|
||||
const user = useUser(user => {
|
||||
if (id !== 'login' && user !== 'admin') {
|
||||
location.href = '/login'
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { useEffect, useRef, useState } from 'preact/hooks'
|
||||
import { Icon } from '../components/Icon.jsx'
|
||||
import { useStore } from '../context.jsx'
|
||||
import { useRemoteState } from '../util.jsx'
|
||||
|
||||
export const Bucket = () => {
|
||||
const {
|
||||
route: {
|
||||
params: { bucket },
|
||||
},
|
||||
showToast,
|
||||
} = useStore()
|
||||
|
||||
const [bucketObjects, updateBucketObjects] = useRemoteState(`/api/buckets/${bucket}`, [])
|
||||
|
||||
const fileInputRef = useRef(null)
|
||||
|
||||
const uploadFile = async () => {
|
||||
const formData = new FormData()
|
||||
|
||||
formData.append('file', fileInputRef.current.files[0])
|
||||
|
||||
showToast(<>Sto caricando il file...</>)
|
||||
|
||||
const res = await fetch(`/api/buckets/${bucket}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
const { id } = await res.json()
|
||||
|
||||
showToast(
|
||||
<>
|
||||
File caricato con id <code>{id}</code>
|
||||
</>
|
||||
)
|
||||
|
||||
await updateBucketObjects()
|
||||
}
|
||||
}
|
||||
|
||||
const deleteObject = async id => {
|
||||
const res = await fetch(`/api/buckets/${bucket}/${id}`, { method: 'DELETE' })
|
||||
|
||||
if (res.ok) {
|
||||
showToast('Oggetto rimosso')
|
||||
} else {
|
||||
showToast(`Errore: ${await res.text()}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="panel">
|
||||
<div class="row">
|
||||
<div class="row-group">
|
||||
<div class="title">
|
||||
Bucket / <code>{bucket}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row-group">
|
||||
<button class="icon" onClick={() => fileInputRef.current.click()}>
|
||||
<Icon name="file_upload" />
|
||||
</button>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={() => uploadFile()}
|
||||
style="display: none;"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table">
|
||||
<div class="cells">
|
||||
{/* Header */}
|
||||
<div class="header">Objects</div>
|
||||
<div class="header">Actions</div>
|
||||
{/* Rows */}
|
||||
{bucketObjects.map(key => (
|
||||
<>
|
||||
<div class="name">
|
||||
<code>{key}</code>
|
||||
</div>
|
||||
<div class="options">
|
||||
<a
|
||||
class="button icon flat"
|
||||
href={`/api/buckets/${bucket}/${key}`}
|
||||
>
|
||||
<Icon name="file_download" />
|
||||
</a>
|
||||
<button
|
||||
class="icon delete flat"
|
||||
onClick={() => deleteObject(key)}
|
||||
>
|
||||
<Icon name="delete" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { Icon } from '../components/Icon.jsx'
|
||||
import { showToast } from '../components/Toasts.jsx'
|
||||
import { useRemoteState } from '../util.jsx'
|
||||
|
||||
export const Buckets = () => {
|
||||
const [buckets, updateBuckets] = useRemoteState('/api/buckets', [])
|
||||
const [allBucketSettings, setAllBucketSettings] = useState({})
|
||||
|
||||
const updateBucketSettings = async bucket => {
|
||||
const res = await fetch(`/api/buckets/${bucket}/settings`)
|
||||
const value = await res.json()
|
||||
|
||||
setAllBucketSettings(s => ({ ...s, [bucket]: value }))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
for (const bucket of buckets) {
|
||||
updateBucketSettings(bucket)
|
||||
}
|
||||
}, [buckets])
|
||||
|
||||
const [newBucketName, setNewBucketName] = useState('')
|
||||
const [newBucketSettings, setNewBucketSettings] = useState('')
|
||||
|
||||
const createBucket = async () => {
|
||||
const { path } = JSON.parse(newBucketSettings || '{"path":""}')
|
||||
|
||||
const res = await fetch(`/api/buckets/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
bucket: newBucketName,
|
||||
path,
|
||||
}),
|
||||
})
|
||||
|
||||
if (res.ok) {
|
||||
await updateBuckets()
|
||||
} else {
|
||||
showToast(`Errore: ${await res.text()}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="panel">
|
||||
<div class="row">
|
||||
<div class="row-group">
|
||||
<div class="title">Buckets</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table">
|
||||
<div class="cells">
|
||||
{/* Header */}
|
||||
<div class="header">Bucket</div>
|
||||
<div class="header">Settings</div>
|
||||
<div class="header">Options</div>
|
||||
{/* Rows */}
|
||||
{buckets.map(bucket => (
|
||||
<>
|
||||
<div class="name">
|
||||
<a href={`/buckets/${bucket}`}>
|
||||
<code>{bucket}</code>
|
||||
</a>
|
||||
</div>
|
||||
<div class="settings">
|
||||
<input
|
||||
readonly
|
||||
class="mono"
|
||||
type="text"
|
||||
value={JSON.stringify(allBucketSettings[bucket])}
|
||||
/>
|
||||
</div>
|
||||
<div class="options">
|
||||
<button class="icon delete flat">
|
||||
<Icon name="delete" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form action="/api/buckets" method="POST">
|
||||
<div class="fill title">Nuovo Bucket</div>
|
||||
|
||||
<label for="new-bucket-name">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
name="bucket"
|
||||
id="new-bucket-name"
|
||||
placeholder="new-bucket"
|
||||
value={newBucketName}
|
||||
onInput={e => setNewBucketName(e.target.value)}
|
||||
/>
|
||||
<label for="new-bucket-settings">Settings</label>
|
||||
<input
|
||||
type="text"
|
||||
name="settings"
|
||||
id="new-bucket-settings"
|
||||
placeholder={`{"path":"${newBucketName || 'new-bucket'}/"}`}
|
||||
value={newBucketSettings}
|
||||
onInput={e => setNewBucketSettings(e.target.value)}
|
||||
/>
|
||||
|
||||
<div class="fill center">
|
||||
<button type="button" onClick={() => createBucket()}>
|
||||
Crea Bucket
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
)
|
||||
}
|
||||
+28
-1
@@ -33,8 +33,14 @@ export function useUser(onLoginStateChanged) {
|
||||
return user
|
||||
}
|
||||
|
||||
//
|
||||
// Hooks
|
||||
//
|
||||
|
||||
// Router
|
||||
|
||||
function matchPattern(pattern, url) {
|
||||
const r = `^${pattern.replace(/:([a-zA-Z0-9\_\-]+)/g, '(?<$1>.+?)')}$`
|
||||
const r = `^${pattern.replace(/:([a-zA-Z0-9\_\-]+)/g, '(?<$1>[^\\/\\?]+?)')}$`
|
||||
return new RegExp(r).exec(url)
|
||||
}
|
||||
|
||||
@@ -49,6 +55,27 @@ export const useRouter = routes => {
|
||||
return ['unknown', { id: 'unknown' }, {}]
|
||||
}
|
||||
|
||||
// useRemoteState
|
||||
|
||||
export function useRemoteState(url, initialValue = null) {
|
||||
const [value, setValue] = useState(initialValue)
|
||||
|
||||
const updateValue = async () => {
|
||||
const res = await fetch(url)
|
||||
if (!res.ok) {
|
||||
console.error(res.body)
|
||||
}
|
||||
|
||||
setValue(await res.json())
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
updateValue()
|
||||
}, [])
|
||||
|
||||
return [value, updateValue]
|
||||
}
|
||||
|
||||
//
|
||||
// Change Case Utility
|
||||
//
|
||||
|
||||
+82
-36
@@ -13,6 +13,7 @@
|
||||
--bg-dark-400: #55555f;
|
||||
--bg-dark-500: #38383d;
|
||||
|
||||
--fg-light-300: #5d5d5d;
|
||||
--fg-light-400: #3d3d3d;
|
||||
--fg-light-500: #333;
|
||||
|
||||
@@ -215,6 +216,8 @@ main {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
gap: 1rem;
|
||||
|
||||
flex-grow: 1;
|
||||
|
||||
padding: 1rem 1rem 0 1rem;
|
||||
@@ -265,6 +268,43 @@ main {
|
||||
// Components
|
||||
//
|
||||
|
||||
.toasts {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
bottom: 1rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
gap: 0.25rem;
|
||||
|
||||
transition: all 150ms linear;
|
||||
|
||||
font-size: 15px;
|
||||
|
||||
.toast {
|
||||
background: #38383d;
|
||||
color: var(--fg-dark-500);
|
||||
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
opacity: 0.95;
|
||||
|
||||
animation: toast-fade-in 250ms ease-out;
|
||||
transition: opacity 250ms ease-in;
|
||||
|
||||
&.removed {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--bg-light-100);
|
||||
border: 1px solid var(--bg-light-600);
|
||||
@@ -277,6 +317,10 @@ main {
|
||||
|
||||
& .title {
|
||||
font-weight: var(--ft-sans-wt-bold);
|
||||
|
||||
code {
|
||||
font-weight: var(--ft-sans-wt-normal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,6 +345,9 @@ main {
|
||||
|
||||
& > div {
|
||||
min-width: 4rem;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -385,6 +432,15 @@ button,
|
||||
background: var(--bg-light-700);
|
||||
}
|
||||
}
|
||||
|
||||
&.delete,
|
||||
&.error {
|
||||
color: #e62929;
|
||||
|
||||
&:hover {
|
||||
background: #e629291d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input[type='text'],
|
||||
@@ -408,6 +464,11 @@ textarea {
|
||||
font-size: 90%;
|
||||
font-family: var(--ft-mono);
|
||||
}
|
||||
|
||||
&:read-only {
|
||||
color: var(--fg-light-300);
|
||||
background: var(--bg-light-500);
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
@@ -540,51 +601,36 @@ $heading-scale: 1.33;
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delete {
|
||||
color: #e62929;
|
||||
.route-buckets {
|
||||
.table {
|
||||
min-width: 30rem;
|
||||
|
||||
&:hover {
|
||||
background: #e629291d;
|
||||
.cells {
|
||||
grid-template-columns: 1fr auto auto;
|
||||
gap: 0.25rem 1rem;
|
||||
|
||||
.settings {
|
||||
min-width: 20rem;
|
||||
|
||||
input[type='text'] {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toasts {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
.route-bucket {
|
||||
.table {
|
||||
min-width: 30rem;
|
||||
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
bottom: 1rem;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
gap: 0.25rem;
|
||||
|
||||
transition: all 150ms linear;
|
||||
|
||||
font-size: 15px;
|
||||
|
||||
.toast {
|
||||
background: #38383d;
|
||||
color: var(--fg-dark-500);
|
||||
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
opacity: 0.95;
|
||||
|
||||
animation: toast-fade-in 250ms ease-out;
|
||||
transition: opacity 250ms ease-in;
|
||||
|
||||
&.removed {
|
||||
opacity: 0;
|
||||
.cells {
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 0.25rem 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user