Skip to content

Commit

Permalink
Merge pull request #2090 from Inist-CNRS/feats/root-panel-improvement
Browse files Browse the repository at this point in the history
Root panel improvement
  • Loading branch information
parmentf authored Jul 18, 2024
2 parents 8797bee + 050df18 commit 67ca4ad
Show file tree
Hide file tree
Showing 15 changed files with 686 additions and 263 deletions.
8 changes: 2 additions & 6 deletions cypress/e2e/phase_4/rootAdmin.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,12 @@ describe('RootAdmin', () => {

// Create a new instance
// Find button ajouter
cy.get('button')
.contains('Ajouter')
.click();
cy.get('button').contains('Ajouter').click();
// Fill the form
cy.get('input[id="tenant-name-field"]').type('tenant-1');
cy.get('input[id="tenant-description-field"]').type('Description');
cy.get('input[id="tenant-author-field"]').type('Author');
cy.get('button')
.contains('Créer')
.click();
cy.get('button').contains('Créer').click();

// Should display the new instance with login form
cy.visit('http://localhost:3000/instance/tenant-1/');
Expand Down
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
"@bull-board/koa": "^5.9.1",
"@dnd-kit/core": "6.0.6",
"@dnd-kit/sortable": "7.0.1",
"@draconides/format": "1.0.3",
"@emotion/react": "11.7.1",
"@emotion/styled": "11.6.0",
"@ezs/analytics": "2.3.2",
Expand Down
46 changes: 44 additions & 2 deletions src/api/controller/rootAdmin.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import { auth } from 'config';
import { ObjectId } from 'mongodb';
import { createWorkerQueue, deleteWorkerQueue } from '../workers';
import {
ROOT_ROLE,
checkForbiddenNames,
checkNameTooLong,
ROOT_ROLE,
} from '../../common/tools/tenantTools';
import bullBoard from '../bullBoard';
import { insertConfigTenant } from '../services/configTenant';
import mongoClient from '../services/mongoClient';
import os from 'os';

const app = new Koa();
app.use(
Expand Down Expand Up @@ -51,7 +52,7 @@ const getTenants = async (ctx, filter) => {
for (const tenant of tenants) {
const db = await mongoClient(tenant.name);

tenant.totalSize = (await db.stats({ scale: 1024 })).totalSize;
tenant.totalSize = (await db.stats()).totalSize;

try {
tenant.dataset = await db.collection('dataset').find().count();
Expand Down Expand Up @@ -136,11 +137,52 @@ const deleteTenant = async (ctx) => {
}
};

// https://stackoverflow.com/questions/36816181/get-view-memory-cpu-usage-via-nodejs
// Initial value; wait at little amount of time before making a measurement.
let timesBefore = os.cpus().map((c) => c.times);

// Call this function periodically, e.g. using setInterval,
function getAverageUsage() {
let timesAfter = os.cpus().map((c) => c.times);
let timeDeltas = timesAfter.map((t, i) => ({
user: t.user - timesBefore[i].user,
sys: t.sys - timesBefore[i].sys,
idle: t.idle - timesBefore[i].idle,
}));

timesBefore = timesAfter;

return (
timeDeltas
.map(
(times) =>
1 - times.idle / (times.user + times.sys + times.idle),
)
.reduce((l1, l2) => l1 + l2) / timeDeltas.length
);
}

const systemInfo = async (ctx) => {
const dbStats = await ctx.rootAdminDb.stats();
ctx.body = {
cpu: os.cpus().length,
load: getAverageUsage(),
database: {
total: dbStats.fsTotalSize,
use: dbStats.fsUsedSize,
},
totalmem: os.totalmem(),
freemem: os.freemem(),
};
};

app.use(route.get('/tenant', getTenant));
app.use(route.post('/tenant', postTenant));
app.use(route.put('/tenant/:id', putTenant));
app.use(route.delete('/tenant', deleteTenant));

app.use(route.get('/system', systemInfo));

app.use(async (ctx) => {
ctx.status = 404;
});
Expand Down
192 changes: 126 additions & 66 deletions src/app/js/root-admin/CreateTenantDialog.js
Original file line number Diff line number Diff line change
@@ -1,94 +1,154 @@
import React, { useState } from 'react';
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
Dialog,
DialogContent,
DialogActions,
DialogTitle,
Button,
InputLabel,
FormControl,
FormHelperText,
TextField,
Box,
} from '@mui/material';

import NameField from './NameField';
import { deburr } from 'lodash';

import {
checkForbiddenNames,
forbiddenNamesMessage,
getTenantMaxSize,
MAX_DB_NAME_SIZE,
} from '../../../common/tools/tenantTools';

const cleanUpName = (name) => {
// We replace any accented and special char with the base letter or a dash
// https://stackoverflow.com/questions/36557202/replacing-special-characters-with-dashes

return deburr(name)
.replace(/[\s_\W]+/g, '-')
.replace(/^-/, '')
.substring(0, getTenantMaxSize(window.__DBNAME__))
.toLowerCase();
};

const CreateTenantDialog = ({ isOpen, handleClose, createAction }) => {
const [name, setName] = useState('');
const [description, setDescription] = useState('');
const [author, setAuthor] = useState('');

useEffect(() => {
if (isOpen) {
setName('');
setDescription('');
setAuthor('');
}
}, [isOpen]);

const handleName = (event) => {
setName(cleanUpName(event.target.value));
};

const handleDescription = (event) => {
setDescription(event.target.value);
};

const handleAuthor = (event) => {
setAuthor(event.target.value);
};

const handleSubmit = (event) => {
event.preventDefault();
createAction({ name: cleanUpName(name), description, author });
};

return (
<Dialog open={isOpen} onClose={handleClose} scroll="body" maxWidth="md">
<Dialog
open={isOpen}
onClose={handleClose}
scroll="body"
maxWidth="md"
fullWidth
>
<DialogTitle>Créer une nouvelle instance</DialogTitle>
<DialogContent>
<FormControl sx={{ marginTop: '1em', width: '100%' }}>
<InputLabel htmlFor="tenant-name-field">Nom</InputLabel>
<NameField
id="tenant-name-field"
fullWidth
placeholder={`Entrer le nom technique de l'instance`}
onChange={(event) => setName(event.target.value)}
error={checkForbiddenNames(name)}
value={name}
/>
<FormHelperText
id="component-helper-text"
sx={{ margin: 0 }}
>
Une instance ne peut pas être nommée{' '}
{forbiddenNamesMessage}. Pour composer le nom, seules
les lettres en minuscules, les chiffres et le tiret "-"
sont autorisés. Une limitation en nombre de caractères
est automatiquement appliquée en fonction du nom du
container de l’instance (
{getTenantMaxSize(window.__DBNAME__)} caractères).
</FormHelperText>
</FormControl>

<TextField
id="tenant-description-field"
fullWidth
label="Description"
placeholder="Entrer une description"
onChange={(event) => setDescription(event.target.value)}
value={description}
sx={{ marginTop: '1em' }}
/>

<TextField
id="tenant-author-field"
fullWidth
label="Auteur"
placeholder="Entrer le nom de l'auteur"
onChange={(event) => setAuthor(event.target.value)}
value={author}
sx={{ marginTop: '1em' }}
/>
<DialogContent
sx={{
overflow: 'visible',
}}
>
<form onSubmit={handleSubmit}>
<FormControl mt={1} fullWidth>
<TextField
id="tenant-name-field"
label="Nom"
fullWidth
placeholder="Entrer le nom technique de l'instance"
onChange={handleName}
error={checkForbiddenNames(name)}
value={name}
required
/>
<FormHelperText
id="component-helper-text"
sx={{ margin: 0 }}
>
Une instance ne peut pas être nommée{' '}
{forbiddenNamesMessage}. Pour composer le nom,
seules les lettres en minuscules, les chiffres et le
tiret &quot;-&quot; sont autorisés. Une limitation
en nombre de caractères est automatiquement
appliquée en fonction du nom du container de
l’instance ({getTenantMaxSize(window.__DBNAME__)}{' '}
caractères).
</FormHelperText>
</FormControl>

<Box mt={1}>
<TextField
id="tenant-description-field"
fullWidth
label="Description"
placeholder="Entrer une description"
onChange={handleDescription}
value={description}
/>
</Box>

<Box mt={1}>
<TextField
id="tenant-author-field"
fullWidth
label="Auteur"
placeholder="Entrer le nom de l'auteur"
onChange={handleAuthor}
value={author}
/>
</Box>

<Box mt={1}>
<Button
sx={{
width: 'calc(50% - 4px)',
marginRight: '4px',
}}
variant="contained"
color="warning"
onClick={handleClose}
>
Annuler
</Button>
<Button
sx={{
width: 'calc(50% - 4px)',
marginLeft: '4px',
}}
disabled={checkForbiddenNames(name)}
variant="contained"
type="submit"
>
Créer
</Button>
</Box>
</form>
</DialogContent>
<DialogActions>
<Button
variant="contained"
color="primary"
onClick={() => {
createAction({ name, description, author });
setName('');
setDescription('');
setAuthor('');
}}
disabled={checkForbiddenNames(name)}
sx={{ height: '100%' }}
>
Créer
</Button>
</DialogActions>
</Dialog>
);
};
Expand Down
Loading

0 comments on commit 67ca4ad

Please sign in to comment.