feat(statsPage): rework / cleaned / separated components

parent 46d24a9a
import React from "react";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/styles";
import Divider from "@material-ui/core/Divider";
import { withPaddedPaper } from "./shared";
import SqlInterface from "../stats/RequestSQLHandler";
import DatasetSelector from "../stats/DatasetSelector";
......@@ -8,8 +9,8 @@ import DatasetDescriptor from "../stats/DatasetDescriptor";
import {
availableDatasets,
changeDataset,
getDatasetColumns,
currentDatasetName,
getDatasetColumns,
} from "../stats/utils";
const useStyles = makeStyles((theme) => ({
......@@ -18,6 +19,10 @@ const useStyles = makeStyles((theme) => ({
marginBottom: theme.spacing(1),
},
requestInterface: {},
spacer: {
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
},
}));
/**
......@@ -25,6 +30,9 @@ const useStyles = makeStyles((theme) => ({
*/
function PageStats() {
const classes = useStyles();
const datasetInfo = availableDatasets.filter(
(el) => el.name === currentDatasetName
)[0].info;
return (
<>
......@@ -38,8 +46,12 @@ function PageStats() {
value={currentDatasetName}
/>
</div>
<DatasetDescriptor columns={getDatasetColumns(currentDatasetName)} />
<Divider className={classes.spacer} />
<DatasetDescriptor
columns={getDatasetColumns(currentDatasetName)}
info={datasetInfo || ""}
/>
<Divider className={classes.spacer} />
<div className={classes.requestInterface}>
<SqlInterface />
</div>
......
......@@ -5,13 +5,13 @@ import Typography from "@material-ui/core/Typography";
/**
* Component to display information about the dataset
*/
function DatasetDescriptor({ columns }) {
function DatasetDescriptor({ columns, info }) {
return (
<>
<Typography variant="body1">
{info && <Typography variant="body1">{info}</Typography>}
<Typography variant="h6">
Description des colonnes du jeu de données
</Typography>
{columns.map(({ name, description }) => (
<Typography variant="body2" key={name}>
<b>{name}</b>{description}
......@@ -28,6 +28,11 @@ DatasetDescriptor.propTypes = {
description: PropTypes.string.isRequired,
})
).isRequired,
info: PropTypes.string,
};
DatasetDescriptor.defaultProps = {
info: "",
};
export default DatasetDescriptor;
......@@ -3,13 +3,24 @@ import PropTypes from "prop-types";
import AppBar from "@material-ui/core/AppBar";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import { makeStyles } from "@material-ui/styles";
const useStyles = makeStyles({
appBar: { zIndex: 1000 },
});
/**
* Component to select the dataset
*/
function DatasetSelector({ datasets, value, onChange }) {
const classes = useStyles();
return (
<AppBar position="sticky" color="default">
<AppBar
color="default"
position="relative"
classes={{ root: classes.appBar }}
>
<Tabs
value={value}
onChange={(e, newDatasetName) => onChange(newDatasetName)}
......
import React from "react";
import PropTypes from "prop-types";
import Typography from "@material-ui/core/Typography";
import Button from "@material-ui/core/Button";
import { makeStyles } from "@material-ui/styles";
const useStyles = makeStyles((theme) => ({
container: { display: "flex", alignItems: "center" },
button: {
marginRight: theme.spacing(1),
marginTop: theme.spacing(0.5),
marginBottom: theme.spacing(0.5),
},
}));
/**
* Component to display information about the dataset
*/
function ExampleRequests({ requests, performRequest }) {
const classes = useStyles();
return (
<>
{requests.map(({ label, request }) => (
<div key={label} className={classes.container}>
<Button
variant="outlined"
onClick={() => performRequest(request)}
size="small"
className={classes.button}
>
Exécuter
</Button>
<Typography>{label}</Typography>
</div>
))}
</>
);
}
ExampleRequests.propTypes = {
requests: PropTypes.arrayOf(
PropTypes.shape({
label: PropTypes.string.isRequired,
request: PropTypes.string.isRequired,
})
).isRequired,
performRequest: PropTypes.func.isRequired,
};
export default React.memo(ExampleRequests);
import React from "react";
import PropTypes from "prop-types";
import alasql from "alasql";
import Button from "@material-ui/core/Button";
import DownloadIcon from "@material-ui/icons/CloudDownload";
/**
* Component to have a clickable button to export to CSV
*/
function ExportToCsvButton({ data, label }) {
return (
<Button
variant="outlined"
color="secondary"
onClick={() => {
alasql(
'SELECT * INTO CSV("export_rex_dri.csv", {headers:true}) FROM ?',
[data]
);
}}
>
<DownloadIcon />
&nbsp;{label}
</Button>
);
}
ExportToCsvButton.propTypes = {
data: PropTypes.array.isRequired,
label: PropTypes.string.isRequired,
};
export default ExportToCsvButton;
import React from "react";
import { Line } from "react-chartjs-2";
import PropTypes from "prop-types";
import Typography from "@material-ui/core/Typography";
const colors = [
"#1f77b4",
......@@ -15,25 +16,45 @@ const colors = [
"#17becf",
];
function PlotResult({ result }) {
if (
!Object.keys(result[0]).includes("x") ||
!Object.keys(result[0]).includes("y")
) {
const plotOptions = {
tooltips: {
intersect: false,
mode: "index",
},
scales: {
xAxes: [
{
type: "time",
},
],
},
};
function PlotResults({ results, title }) {
if (results.length === 0) {
return <></>;
}
const firstEl = results[0];
const cols = new Set(Object.keys(firstEl));
const canPlot = cols.has("x") && cols.has("y");
if (!canPlot) {
return (
<div>
<br />
Pour afficher les résultats sous forme graphique, vous devez préciser
'x' et 'y' dans le SELECT (pensez à utiliser un ORDER BY sur X). Vous
pouvez également préciser des catégories avec 'cat'.
<br />
</div>
<Typography variant="body1">
<i>
Pour afficher les résultats sous forme de graphique, vous devez
préciser les alias «&nbsp;x&nbsp;» et «&nbsp;y&nbsp;» dans le SELECT.
Vous pouvez également préciser des catégories avec l'alias
«&nbsp;cat&nbsp;».
</i>
</Typography>
);
}
if (!Object.keys(result[0]).includes("cat")) {
if (!cols.has("cat")) {
const mapping = new Map();
result.forEach(({ x, y }) => {
results.forEach(({ x, y }) => {
mapping.set(x, y);
});
......@@ -41,32 +62,34 @@ function PlotResult({ result }) {
X.sort();
return (
<div>
<Line
data={{
labels: X,
datasets: [
{
label: "y = f(x)",
backgroundColor: "rgb(255, 99, 132)",
borderColor: "rgb(255, 99, 132)",
data: X.map((x) => mapping.get(x)),
},
],
}}
/>
</div>
<Line
redraw
height="110vh"
data={{
labels: X,
datasets: [
{
label: title,
backgroundColor: "rgb(255, 99, 132)",
borderColor: "rgb(255, 99, 132)",
data: X.map((x) => mapping.get(x)),
},
],
}}
options={plotOptions}
/>
);
}
const categories = [...new Set(result.map((el) => el.cat))];
// Otherwise, we have categories
// We need to reconstruct all the curves
const categories = [...new Set(results.map((el) => el.cat))];
const mapping = new Map();
result.forEach(({ x, y, cat }) => {
results.forEach(({ x, y, cat }) => {
if (!mapping.has(x)) {
mapping.set(x, new Map());
}
mapping.get(x).set(cat, y);
});
......@@ -84,19 +107,25 @@ function PlotResult({ result }) {
}));
return (
<div>
<Line
data={{
labels: X,
datasets,
}}
/>
</div>
<Line
redraw
height="110vh"
data={{
labels: X,
datasets,
}}
options={plotOptions}
/>
);
}
PlotResult.propTypes = {
result: PropTypes.array.isRequired,
PlotResults.propTypes = {
results: PropTypes.array.isRequired,
title: PropTypes.string,
};
PlotResults.defaultProps = {
title: "y = f(x)",
};
export default PlotResult;
export default PlotResults;
import React from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/styles";
import Typography from "@material-ui/core/Typography";
import TableFromData from "./Table";
import PlotResults from "./PlotResults";
import ExportToCsvButton from "./ExportToCsvButton";
import ShareRequestButton from "./ShareRequestButton";
import { currentDatasetName, getDatasetExampleRequests } from "./utils";
const useStyles = makeStyles((theme) => ({
exportBtn: {
marginLeft: theme.spacing(1),
marginRight: theme.spacing(1),
},
exportBtnContainer: {
display: "flex",
marginTop: theme.spacing(2),
marginBottom: theme.spacing(2),
justifyContent: "center",
},
spacer: {
marginTop: theme.spacing(2),
},
}));
/**
* Component to display the result of a request
*/
function RequestResults({ results, rawDataset, request }) {
const classes = useStyles();
const exampleRequests = getDatasetExampleRequests(currentDatasetName);
const exampleMatch = exampleRequests.filter((el) => el.request === request);
// Recover label for title if it's an example request
const title =
exampleMatch.length === 0 ? "y = f(x)" : exampleMatch[0].graphTitle;
return (
<>
<ShareRequestButton />
<div className={classes.spacer} />
<Typography variant="h6">Résultats de la requête</Typography>
<div className={classes.spacer} />
<TableFromData data={results} />
<div className={classes.exportBtnContainer}>
<div className={classes.exportBtn}>
<ExportToCsvButton
label="Le résultat de la requête (CSV)"
data={results}
/>
</div>
<div className={classes.exportBtn}>
<ExportToCsvButton
label="Les données brutes (CSV)"
data={rawDataset}
/>
</div>
</div>
<div className={classes.spacer} />
<Typography variant="h6">Graphique correspondant</Typography>
<PlotResults results={results} title={title} />
</>
);
}
RequestResults.propTypes = {
results: PropTypes.array.isRequired,
rawDataset: PropTypes.array.isRequired,
request: PropTypes.string.isRequired,
};
export default RequestResults;
import React from "react";
import Button from "@material-ui/core/Button";
import ShareIcon from "@material-ui/icons/Share";
import NotificationService from "../../services/NotificationService";
function copyUrlToClipboard() {
const url = window.location.href;
const dummy = document.createElement("input");
document.body.appendChild(dummy);
dummy.value = url;
dummy.select();
document.execCommand("copy");
document.body.removeChild(dummy);
}
/**
* Component to have a clickable button to export to CSV
*/
function ShareRequestButton() {
return (
<Button
variant="contained"
color="secondary"
size="large"
fullWidth
onClick={() => {
copyUrlToClipboard();
NotificationService.info("L'URL de la page a été copiée 🎉");
}}
>
Partager cette page&nbsp;
<ShareIcon />
</Button>
);
}
ShareRequestButton.propTypes = {};
export default ShareRequestButton;
import React from "react";
import PropTypes from "prop-types";
import TextField from "@material-ui/core/TextField";
import Button from "@material-ui/core/Button";
/**
* Component to display information about the dataset
*/
function SqlEditor({ request, setRequest, performRequest }) {
return (
<>
<TextField
id="standard-multiline-static"
label="Requête d'exploration"
multiline
fullWidth
margin="normal"
placeholder="Entrez la requête SQL"
value={request}
onChange={(event) => setRequest(event.target.value)}
/>
<Button
variant="contained"
color="primary"
onClick={() => performRequest(request)}
>
Exécuter
</Button>
</>
);
}
SqlEditor.propTypes = {
request: PropTypes.string.isRequired,
setRequest: PropTypes.func.isRequired,
performRequest: PropTypes.func.isRequired,
};
export default React.memo(SqlEditor);
......@@ -22,7 +22,7 @@ const useStyles = makeStyles({
function TableFromData({ data }) {
const classes = useStyles();
const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(10);
const [rowsPerPage, setRowsPerPage] = React.useState(5);
if (data.length === 0) {
return <></>;
......@@ -77,6 +77,7 @@ function TableFromData({ data }) {
component="div"
count={data.length}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={[5, 10, 25, 50, 100]}
page={page}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment