parent
a7256f3078
commit
ccf0e19e28
8 changed files with 16 additions and 400 deletions
@ -1,21 +0,0 @@ |
||||
export default function ClusterRow({ i, cluster, setCluster, deleteCluster, csrf, user }) { |
||||
|
||||
const splitCluster = cluster.split(','); |
||||
|
||||
return ( |
||||
<tr className='align-middle'> |
||||
<td className='text-left' style={{width:0}}> |
||||
<a className='btn btn-sm btn-danger' onClick={() => deleteCluster(csrf, key)}> |
||||
<i className='bi-trash-fill pe-none' width='16' height='16' /> |
||||
</a> |
||||
</td> |
||||
<td className='col-1 text-center'> |
||||
<input onSubmit={() => setCluster(csrf, i)} className='btn btn-sm btn-primary' type='button' value='Select' disabled={(i === user.activeCluster ? 'disabled' : null)} /> |
||||
</td> |
||||
<td> |
||||
({splitCluster.length}): {splitCluster.map(c => new URL(c).hostname).join(', ')} |
||||
</td> |
||||
</tr> |
||||
); |
||||
|
||||
}; |
@ -1,96 +0,0 @@ |
||||
import * as db from '../db.js'; |
||||
import { validClustersString, makeArrayIfSingle, extractMap, dynamicResponse } from '../util.js'; |
||||
|
||||
export async function clustersPage(app, req, res, next) { |
||||
res.locals.data = { |
||||
user: res.locals.user, |
||||
csrf: req.csrfToken(), |
||||
}; |
||||
return app.render(req, res, '/clusters'); |
||||
}; |
||||
|
||||
export async function clustersJson(req, res, next) { |
||||
return res.json({ |
||||
csrf: req.csrfToken(), |
||||
user: res.locals.user, |
||||
}); |
||||
}; |
||||
|
||||
/** |
||||
* POST /cluster |
||||
* set active cluster |
||||
*/ |
||||
export async function setCluster(req, res, next) { |
||||
if (res.locals.user.username !== 'admin') { |
||||
return dynamicResponse(req, res, 403, { error: 'Changing cluster is only supported on enterprise plans' }); |
||||
} |
||||
if (req.body == null || req.body.cluster == null) { |
||||
return dynamicResponse(req, res, 404, { error: 'Invalid cluster' }); |
||||
} |
||||
req.body.cluster = parseInt(req.body.cluster, 10) || 0; |
||||
if (!Number.isSafeInteger(req.body.cluster) |
||||
|| req.body.cluster > res.locals.user.clusters.length-1) { |
||||
return dynamicResponse(req, res, 404, { error: 'Invalid cluster' }); |
||||
} |
||||
try { |
||||
await db.db().collection('accounts') |
||||
.updateOne({_id: res.locals.user.username}, {$set: {activeCluster: req.body.cluster }}); |
||||
} catch (e) { |
||||
return next(e); |
||||
} |
||||
return dynamicResponse(req, res, 302, { redirect: '/account' }); |
||||
}; |
||||
|
||||
/** |
||||
* POST /cluster/add |
||||
* add cluster |
||||
*/ |
||||
export async function addCluster(req, res, next) { |
||||
if (res.locals.user.username !== 'admin') { |
||||
return dynamicResponse(req, res, 403, { error: 'Adding clusters is only supported on enterprise plans' }); |
||||
} |
||||
if (!req.body || !req.body.cluster |
||||
|| typeof req.body.cluster !== 'string' |
||||
|| !validClustersString(req.body.cluster)) { |
||||
return dynamicResponse(req, res, 400, { error: 'Invalid cluster' }); |
||||
} |
||||
try { |
||||
await db.db().collection('accounts') |
||||
.updateOne({_id: res.locals.user.username}, {$addToSet: {clusters: req.body.cluster }}); |
||||
} catch (e) { |
||||
return next(e); |
||||
} |
||||
return dynamicResponse(req, res, 302, { redirect: '/clusters' }); |
||||
}; |
||||
|
||||
/** |
||||
* POST /cluster/delete |
||||
* delete cluster |
||||
*/ |
||||
export async function deleteClusters(req, res, next) { |
||||
if (res.locals.user.username !== 'admin') { |
||||
return dynamicResponse(req, res, 403, { error: 'Removing clusters is only supported on enterprise plans' }); |
||||
} |
||||
//TODO: warning modal and extra "confirm" param before deleting cluster
|
||||
const existingClusters = new Set(res.locals.user.clusters); |
||||
req.body.cluster = makeArrayIfSingle(req.body.cluster); |
||||
if (!req.body || !req.body.cluster |
||||
|| !req.body.cluster.some(c => existingClusters.has(c))) { |
||||
return dynamicResponse(req, res, 400, { error: 'Invalid cluster' }); |
||||
} |
||||
const filteredClusters = res.locals.clusters.filter(c => !req.body.cluster.includes(c)); |
||||
// if (filteredClusters.length === 0) {
|
||||
// return dynamicResponse(req, res, 400, { error: 'Cannot delete last cluster' });
|
||||
// }
|
||||
let newActiveCluster = res.locals.user.activeCluster; |
||||
if (res.locals.user.activeCluster > filteredClusters.length-1) { |
||||
newActiveCluster = 0; |
||||
} |
||||
try { |
||||
await db.db().collection('accounts') |
||||
.updateOne({_id: res.locals.user.username}, {$set: {clusters: filteredClusters, activeCluster: newActiveCluster }}); |
||||
} catch (e) { |
||||
return next(e); |
||||
} |
||||
return dynamicResponse(req, res, 302, { redirect: '/clusters' }); |
||||
}; |
@ -1,121 +0,0 @@ |
||||
import React, { useState, useEffect } from 'react'; |
||||
import Head from 'next/head'; |
||||
import BackButton from '../components/BackButton.js'; |
||||
import ErrorAlert from '../components/ErrorAlert.js'; |
||||
import ClusterRow from '../components/ClusterRow.js'; |
||||
import * as API from '../api.js'; |
||||
import { useRouter } from 'next/router'; |
||||
|
||||
export default function Clusters(props) { |
||||
|
||||
const router = useRouter(); |
||||
const [state, dispatch] = useState(props); |
||||
const [error, setError] = useState(); |
||||
|
||||
useEffect(() => { |
||||
if (!state.user) { |
||||
API.getClusters(dispatch, setError, router); |
||||
} |
||||
}, [state.user, router]); |
||||
|
||||
if (!state.user) { |
||||
return ( |
||||
<div className='d-flex flex-column'> |
||||
{error && <ErrorAlert error={error} />} |
||||
<div className='text-center mb-4'> |
||||
<div className='spinner-border mt-5' role='status'> |
||||
<span className='visually-hidden'>Loading...</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
); |
||||
} |
||||
|
||||
const { user, csrf } = state; |
||||
|
||||
if (user && !user.onboarding) { |
||||
router.push('/onboarding'); |
||||
} |
||||
|
||||
async function addCluster(e) { |
||||
e.preventDefault(); |
||||
setError(null); |
||||
await API.addCluster({ _csrf: csrf, cluster: e.target.cluster.value }, dispatch, setError, router); |
||||
await API.getClusters(dispatch, setError, router); |
||||
e.target.reset(); |
||||
} |
||||
|
||||
async function deleteCluster(csrf, cluster) { |
||||
setError(null); |
||||
await API.deleteCluster({ _csrf: csrf, cluster }, dispatch, setError, router); |
||||
await API.getClusters(dispatch, setError, router); |
||||
} |
||||
|
||||
async function setCluster(csrf, cluster) { |
||||
setError(null); |
||||
await API.changeCluster({ _csrf: csrf, cluster }, dispatch, setError, router); |
||||
await API.getClusters(dispatch, setError, router); |
||||
} |
||||
|
||||
const clusterList = user.clusters.map((cluster, i) => (<ClusterRow |
||||
i={i} |
||||
key={cluster} |
||||
cluster={cluster} |
||||
csrf={csrf} |
||||
user={user} |
||||
setCluster={setCluster} |
||||
deleteCluster={deleteCluster} |
||||
/>)); |
||||
|
||||
return ( |
||||
<> |
||||
<Head> |
||||
<title>Clusters</title> |
||||
</Head> |
||||
|
||||
<h5 className='fw-bold'> |
||||
Clusters ({user.clusters.length}): |
||||
</h5> |
||||
|
||||
{/* Clusters table */} |
||||
<div className='table-responsive round-shadow'> |
||||
<form className='d-flex' onSubmit={addCluster} action='/forms/cluster/add' method='post'> |
||||
<input type='hidden' name='_csrf' value={csrf} /> |
||||
<table className='table text-nowrap'> |
||||
<tbody> |
||||
|
||||
{clusterList} |
||||
|
||||
{/* Add new cluster form */} |
||||
<tr className='align-middle'> |
||||
<td> |
||||
<button className='btn btn-sm btn-success' type='submit'> |
||||
<i className='bi-plus-lg pe-none' width='16' height='16' /> |
||||
</button> |
||||
</td> |
||||
<td colSpan='2'> |
||||
|
||||
<input className='form-control' type='text' name='cluster' placeholder='http://username:password@host:port, comma separated for multiple' required /> |
||||
</td> |
||||
</tr> |
||||
|
||||
</tbody> |
||||
</table> |
||||
</form> |
||||
</div> |
||||
|
||||
{error && <span className='mx-1'> |
||||
<ErrorAlert error={error} /> |
||||
</span>} |
||||
|
||||
{/* back to account */} |
||||
<BackButton to='/account' /> |
||||
|
||||
</> |
||||
); |
||||
|
||||
} |
||||
|
||||
export async function getServerSideProps({ req, res, query, resolvedUrl, locale, locales, defaultLocale}) { |
||||
return { props: res.locals.data }; |
||||
} |
Loading…
Reference in new issue