diff --git a/app/.env b/app/.env index 8e4014a..c2213d4 100644 --- a/app/.env +++ b/app/.env @@ -1 +1 @@ -REACT_APP_ENODO_HUB_URI="http://localhost:8001" \ No newline at end of file +REACT_APP_ENODO_HUB_URI="http://localhost" \ No newline at end of file diff --git a/app/package.json b/app/package.json index 8177f9f..2402141 100644 --- a/app/package.json +++ b/app/package.json @@ -1,6 +1,6 @@ { "name": "app", - "version": "0.1.0", + "version": "0.2.0", "private": true, "dependencies": { "@material-ui/core": "^4.11.0", @@ -9,6 +9,7 @@ "@testing-library/jest-dom": "^5.11.6", "@testing-library/react": "^11.2.2", "@testing-library/user-event": "^12.2.2", + "chroma-js": "^2.1.2", "create-react-app": "^4.0.3", "moment": "2.29.1", "react": "^16.14.0", diff --git a/app/src/App.js b/app/src/App.js index 9039cca..02fb9b2 100644 --- a/app/src/App.js +++ b/app/src/App.js @@ -3,6 +3,7 @@ import DashboardIcon from '@material-ui/icons/Dashboard'; import Divider from '@material-ui/core/Divider'; import DnsIcon from '@material-ui/icons/Dns'; import Drawer from '@material-ui/core/Drawer'; +import LabelIcon from '@material-ui/icons/Label'; import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; import ListItemIcon from '@material-ui/core/ListItemIcon'; @@ -19,6 +20,7 @@ import './App.css'; import * as ROUTES from './constants/routes'; import DashboardPage from './pages/Dashboard'; import FailedJobsPage from "./pages/FailedJobs"; +import LabelsPage from './pages/Labels'; import NetworkPage from "./pages/Network"; import OutputStreamsPage from "./pages/OutputStreams"; import SettingsPage from "./pages/Settings"; @@ -90,6 +92,12 @@ const App = (props) => { + + + + + + @@ -139,6 +147,7 @@ const App = (props) => { + diff --git a/app/src/components/BasicPageLayout/index.js b/app/src/components/BasicPageLayout/index.js index 191f432..3af822b 100644 --- a/app/src/components/BasicPageLayout/index.js +++ b/app/src/components/BasicPageLayout/index.js @@ -14,9 +14,6 @@ import { makeStyles } from '@material-ui/core/styles'; import { socket } from '../../store'; const useStyles = makeStyles(theme => ({ - leftmenubtn: { - marginLeft: theme.spacing(2) - }, toolbar: theme.mixins.toolbar, sidebar: { width: 350, @@ -32,7 +29,7 @@ const useStyles = makeStyles(theme => ({ } })); -const BasicPageLayout = ({ title, buttonAction, buttonText, hideJobDrawer, children }) => { +const BasicPageLayout = ({ title, buttonAction, buttonText, hideJobDrawer, titleButton, children }) => { const classes = useStyles(); const [stats, setStats] = useState(null); @@ -62,18 +59,25 @@ const BasicPageLayout = ({ title, buttonAction, buttonText, hideJobDrawer, child - - - {title} - - + + + + + {title} + + {titleButton ? + + {titleButton} + : null} + + + {buttonAction && buttonText && {buttonText} } - - - + + {children} @@ -115,7 +119,7 @@ const BasicPageLayout = ({ title, buttonAction, buttonText, hideJobDrawer, child {`Currently there are ${stats ? stats.no_active_jobs : 0} jobs being processed by a worker.`} - + {'Failed'} diff --git a/app/src/components/FailedJobs/ErrorDialog.js b/app/src/components/FailedJobs/ErrorDialog.js new file mode 100644 index 0000000..d810d34 --- /dev/null +++ b/app/src/components/FailedJobs/ErrorDialog.js @@ -0,0 +1,29 @@ +import React from 'react'; +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogTitle from '@material-ui/core/DialogTitle'; + +export default function ErrorDialog({open, handleClose, error}) { + + return ( + + {"Error"} + + + {error} + + + + + {'Close'} + + + + ); +} \ No newline at end of file diff --git a/app/src/components/Labels/AddLabelDialog.js b/app/src/components/Labels/AddLabelDialog.js new file mode 100644 index 0000000..ffeaacb --- /dev/null +++ b/app/src/components/Labels/AddLabelDialog.js @@ -0,0 +1,22 @@ +import React from "react"; + +import SerieConfigurator from '../Serie/SerieConfigurator'; +import { socket } from '../../store'; + +export default function AddLabelDialog({ handleClose }) { + + const onSubmit = (config) => { + socket.emit('/api/enodo/labels/create', config, () => { + handleClose(); + }); + }; + + return ( + + ); +} diff --git a/app/src/components/Labels/ConfigDialog.js b/app/src/components/Labels/ConfigDialog.js new file mode 100644 index 0000000..6f19908 --- /dev/null +++ b/app/src/components/Labels/ConfigDialog.js @@ -0,0 +1,15 @@ +import React from "react"; + +import SerieConfigurator from '../Serie/SerieConfigurator'; + +export default function ConfigDialog({ handleClose, label }) { + + return ( + + ); +} diff --git a/app/src/components/Labels/DeleteLabelDialog.js b/app/src/components/Labels/DeleteLabelDialog.js new file mode 100644 index 0000000..9af1dc8 --- /dev/null +++ b/app/src/components/Labels/DeleteLabelDialog.js @@ -0,0 +1,41 @@ +import React from 'react'; +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import DialogTitle from '@material-ui/core/DialogTitle'; + +import { socket } from '../../store'; + +export default function DeleteLabelDialog({ open, handleClose, selectedLabel }) { + + const onDelete = () => { + const data = { selector: selectedLabel.name }; + socket.emit('/api/enodo/labels/delete', data, () => { + handleClose(); + }); + }; + + return ( + + {'Delete label?'} + + + {'Are you sure you want to delete this label? This results in the removal of all related series from Enodo.'} + + + + + {'Cancel'} + + + {'Confirm'} + + + + ); +} diff --git a/app/src/components/Serie/Dialog.js b/app/src/components/Serie/Dialog.js index b336f28..a14884c 100644 --- a/app/src/components/Serie/Dialog.js +++ b/app/src/components/Serie/Dialog.js @@ -15,9 +15,8 @@ const SerieDetails = (props) => { maxWidth='lg' open={true} onClose={closeCb} - aria-labelledby="max-width-dialog-title" > - Serie Data + {'Series data'} {props.children} diff --git a/app/src/components/Serie/Edit.js b/app/src/components/Serie/Edit.js index 5a2a997..87d9104 100644 --- a/app/src/components/Serie/Edit.js +++ b/app/src/components/Serie/Edit.js @@ -23,7 +23,7 @@ function EditSerie({ close, currentSerie }) { }} onClose={close} dialog='edit' - currentSerie={currentSerie} + currentConfig={currentSerie} socketError={socketError} /> ) diff --git a/app/src/components/Serie/Info.js b/app/src/components/Serie/Info.js index e429c04..2758179 100644 --- a/app/src/components/Serie/Info.js +++ b/app/src/components/Serie/Info.js @@ -1,17 +1,43 @@ +import Box from '@material-ui/core/Box'; +import Button from '@material-ui/core/Button'; +import CircularProgress from '@material-ui/core/CircularProgress'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import Grid from '@material-ui/core/Grid'; import React from 'react'; import Table from '@material-ui/core/Table'; import TableBody from '@material-ui/core/TableBody'; import TableCell from '@material-ui/core/TableCell'; import TableHead from '@material-ui/core/TableHead'; import TableRow from '@material-ui/core/TableRow'; +import Typography from '@material-ui/core/Typography'; import { useGlobal } from '../../store'; +import { healthToColor } from '../../util/GlobalMethods'; -const styles = () => ({ - table: { - minWidth: 650 - } -}); +function CircularProgressWithLabel(props) { + return ( + + + + + {`${Math.round(props.value,)}%`} + + + + ); +} const Info = (props) => { @@ -29,45 +55,80 @@ const Info = (props) => { return ; } + const closeCb = props.close; + return ( - - - - - - - - - Name - - {serie.name} - - - - Initially analyzed - - {serie.analysed ? "Yes" : "No"} - - - - Datapoints - - {serie.datapoint_count} - - - - Ignore - - {serie.ignore ? 'Yes' : 'No'} - - - - Trend - - {serie.series_characteristics && serie.series_characteristics.trend ? serie.series_characteristics.trend : '?'} - - - + + + + + {'Series info'} + + + + + + {'Health: '} + + + + + + + + + + + + + + + + + + + Name + + {serie.name} + + + + Initially analyzed + + {serie.analysed ? "Yes" : "No"} + + + + Datapoints + + {serie.datapoint_count} + + + + Ignore + + {serie.ignore ? 'Yes' : 'No'} + + + + Trend + + {serie.series_characteristics && serie.series_characteristics.trend ? serie.series_characteristics.trend : '?'} + + + + + + + {'Close'} + + + ); } diff --git a/app/src/components/Serie/SerieConfigurator.js b/app/src/components/Serie/SerieConfigurator.js index d8e5361..ca959ab 100644 --- a/app/src/components/Serie/SerieConfigurator.js +++ b/app/src/components/Serie/SerieConfigurator.js @@ -24,7 +24,7 @@ import { makeStyles, useTheme } from '@material-ui/core/styles'; import { JobTypes } from '../../constants/enums'; import { useGlobal } from '../../store'; -function JobConfigurator({ title, jobType, config, setConfig, toggleCheckbox, changeModel, changeSchedule, changeActivitated, checkedJobs }) { +function JobConfigurator({ title, jobType, config, setConfig, toggleCheckbox, changeModel, changeSchedule, changeActivitated, checkedJobs, disabled }) { const [models] = useGlobal( state => state.enodo_model @@ -45,6 +45,7 @@ function JobConfigurator({ title, jobType, config, setConfig, toggleCheckbox, ch checked={checkedJobs[jobType]} onChange={toggleCheckbox} color="primary" + disabled={disabled} /> @@ -59,15 +60,18 @@ function JobConfigurator({ title, jobType, config, setConfig, toggleCheckbox, ch onChange={changeModel} label={'Job Model'} name={jobType} + disabled={disabled} > {'None'} - {models.map((model) => ( - - {model.model_name} - - ))} + {models.map((model) => { + if (model.supported_jobs.includes(jobType)) { + return + {model.name} + + } + })} @@ -82,24 +86,26 @@ function JobConfigurator({ title, jobType, config, setConfig, toggleCheckbox, ch name={jobType} required error={!config.job_schedule} + disabled={disabled} /> {config.model && - {Object.entries(models.find(m => m.model_name === config.model).model_arguments).map(([key, value]) => ( - + {models.find(m => m.name === config.model).model_arguments.map((argument) => ( + { - setConfig({ ...config, model_params: { ...config.model_params, [key]: Number(e.target.value) } }); + setConfig({ ...config, model_params: { ...config.model_params, [argument.name]: Number(e.target.value) } }); }} - required={value} - error={value && !config.model_params[key]} + required={argument.required} + error={argument.required && !config.model_params[argument.name]} type='number' + disabled={disabled} /> ))} @@ -112,6 +118,7 @@ function JobConfigurator({ title, jobType, config, setConfig, toggleCheckbox, ch onChange={changeActivitated} name={jobType} color="primary" + disabled={disabled} /> } label="Activated" @@ -129,32 +136,47 @@ const useStyles = makeStyles(() => ({ } })); -function SerieConfigurator({ title, dialog, onSubmit, onClose, currentSerie, socketError }) { +const DialogTypes = { + ADD: 'add', + EDIT: 'edit', + ADD_LABEL: 'addLabel', + INFO_LABEL: 'infoLabel' +}; + +function SerieConfigurator({ title, dialog, onSubmit, onClose, currentConfig, socketError }) { const classes = useStyles(); const theme = useTheme(); const [activeStep, setActiveStep] = useState(0); + const existingConfig = dialog === DialogTypes.EDIT || dialog === DialogTypes.INFO_LABEL; + const [checkedJobs, setCheckedJobs] = useState({ - [JobTypes.JOB_BASE_ANALYSIS]: dialog === 'edit' && currentSerie.config.job_config[JobTypes.JOB_BASE_ANALYSIS] ? true : false, - [JobTypes.JOB_FORECAST]: dialog === 'edit' && currentSerie.config.job_config[JobTypes.JOB_FORECAST] ? true : false, - [JobTypes.JOB_ANOMALY_DETECT]: dialog === 'edit' && currentSerie.config.job_config[JobTypes.JOB_ANOMALY_DETECT] ? true : false, - [JobTypes.JOB_STATIC_RULES]: dialog === 'edit' && currentSerie.config.job_config[JobTypes.JOB_STATIC_RULES] ? true : false + [JobTypes.JOB_BASE_ANALYSIS]: existingConfig && currentConfig.config.job_config[JobTypes.JOB_BASE_ANALYSIS] ? true : false, + [JobTypes.JOB_FORECAST]: existingConfig && currentConfig.config.job_config[JobTypes.JOB_FORECAST] ? true : false, + [JobTypes.JOB_ANOMALY_DETECT]: existingConfig && currentConfig.config.job_config[JobTypes.JOB_ANOMALY_DETECT] ? true : false, + [JobTypes.JOB_STATIC_RULES]: existingConfig && currentConfig.config.job_config[JobTypes.JOB_STATIC_RULES] ? true : false }) // Config // Name - const [name, setName] = useState(dialog === 'edit' ? currentSerie.name : ''); + const [name, setName] = useState(existingConfig ? currentConfig.name : ''); + + // Label description. Only used for configuring labels. + const [labelDescription, setLabelDescription] = useState(dialog === DialogTypes.INFO_LABEL ? currentConfig.description : ''); // Min no. data points - const [minDataPoints, setMinDataPoints] = useState(dialog === 'edit' ? currentSerie.config.min_data_points : 2); + const [minDataPoints, setMinDataPoints] = useState(existingConfig ? currentConfig.config.min_data_points : 2); + + // Realtime analysis + const [realtime, setRealtime] = useState(existingConfig ? currentConfig.config.realtime : false); // Job specific config - const [baseAnalysisJobConfig, setBaseAnalysisJobConfig] = useState(dialog === 'edit' && currentSerie.config.job_config[JobTypes.JOB_BASE_ANALYSIS] ? currentSerie.config.job_config[JobTypes.JOB_BASE_ANALYSIS] : null); - const [forecastJobConfig, setForcastJobConfig] = useState(dialog === 'edit' && currentSerie.config.job_config[JobTypes.JOB_FORECAST] ? currentSerie.config.job_config[JobTypes.JOB_FORECAST] : null); - const [anomalyDetectionJobConfig, setAnomalyDetectionJobConfig] = useState(dialog === 'edit' && currentSerie.config.job_config[JobTypes.JOB_ANOMALY_DETECT] ? currentSerie.config.job_config[JobTypes.JOB_ANOMALY_DETECT] : null); - const [staticRulesJobConfig, setStaticRuleJobConfig] = useState(dialog === 'edit' && currentSerie.config.job_config[JobTypes.JOB_STATIC_RULES] ? currentSerie.config.job_config[JobTypes.JOB_STATIC_RULES] : null); + const [baseAnalysisJobConfig, setBaseAnalysisJobConfig] = useState(existingConfig && currentConfig.config.job_config[JobTypes.JOB_BASE_ANALYSIS] ? currentConfig.config.job_config[JobTypes.JOB_BASE_ANALYSIS] : null); + const [forecastJobConfig, setForcastJobConfig] = useState(existingConfig && currentConfig.config.job_config[JobTypes.JOB_FORECAST] ? currentConfig.config.job_config[JobTypes.JOB_FORECAST] : null); + const [anomalyDetectionJobConfig, setAnomalyDetectionJobConfig] = useState(existingConfig && currentConfig.config.job_config[JobTypes.JOB_ANOMALY_DETECT] ? currentConfig.config.job_config[JobTypes.JOB_ANOMALY_DETECT] : null); + const [staticRulesJobConfig, setStaticRuleJobConfig] = useState(existingConfig && currentConfig.config.job_config[JobTypes.JOB_STATIC_RULES] ? currentConfig.config.job_config[JobTypes.JOB_STATIC_RULES] : null); const toggleCheckbox = (event) => { setCheckedJobs({ ...checkedJobs, [event.target.name]: event.target.checked }); @@ -250,6 +272,12 @@ function SerieConfigurator({ title, dialog, onSubmit, onClose, currentSerie, soc setActiveStep((prevActiveStep) => prevActiveStep - 1); }; + const addVariant = dialog === DialogTypes.ADD || dialog === DialogTypes.ADD_LABEL; + const infoVariant = dialog === DialogTypes.INFO_LABEL; + + const aboutSeries = dialog === DialogTypes.ADD || dialog === DialogTypes.EDIT; + const aboutLabels = dialog === DialogTypes.ADD_LABEL || dialog === DialogTypes.INFO_LABEL; + return ( { @@ -280,10 +308,25 @@ function SerieConfigurator({ title, dialog, onSubmit, onClose, currentSerie, soc }} required error={!name} - disabled={dialog === 'edit' ? true : false} + disabled={existingConfig} /> + {aboutLabels ? + + + { + setLabelDescription(e.target.value); + }} + disabled={infoVariant} + /> + + + : null} + + { + setRealtime(e.target.checked) + }} + color="primary" + disabled={infoVariant} + /> + } + label="Real-time analysis" + /> + + {realtime && + {'Enabling real-time analysis can have a negative impact on the performance of the system.'} + } : @@ -318,6 +380,7 @@ function SerieConfigurator({ title, dialog, onSubmit, onClose, currentSerie, soc changeSchedule={changeSchedule} changeActivitated={changeActivitated} checkedJobs={checkedJobs} + disabled={infoVariant} /> @@ -332,6 +395,7 @@ function SerieConfigurator({ title, dialog, onSubmit, onClose, currentSerie, soc changeSchedule={changeSchedule} changeActivitated={changeActivitated} checkedJobs={checkedJobs} + disabled={infoVariant} /> @@ -346,6 +410,7 @@ function SerieConfigurator({ title, dialog, onSubmit, onClose, currentSerie, soc changeSchedule={changeSchedule} changeActivitated={changeActivitated} checkedJobs={checkedJobs} + disabled={infoVariant} /> @@ -360,7 +425,11 @@ function SerieConfigurator({ title, dialog, onSubmit, onClose, currentSerie, soc changeSchedule={changeSchedule} changeActivitated={changeActivitated} checkedJobs={checkedJobs} + disabled={infoVariant} /> + {infoVariant && + {'When series are added by making use of labels, it is not possible to adjust the configuration of these series afterwards.'} + } } { let config = { min_data_points: minDataPoints, + realtime: realtime, job_config: {} }; if (baseAnalysisJobConfig) { @@ -398,25 +469,36 @@ function SerieConfigurator({ title, dialog, onSubmit, onClose, currentSerie, soc } onSubmit( - dialog === 'add' ? + dialog === DialogTypes.ADD ? { name: name, config: config } : - { - name: name, - data: { - config: config + dialog === DialogTypes.EDIT ? + { + name: name, + data: { + config: config + } + } : + { + name: name, + description: labelDescription, + series_config: config } - } ); }} > - {dialog === 'add' ? 'Add' : 'Edit'} + {addVariant ? 'Add' : 'Edit'} } backButton={ - + {theme.direction === 'rtl' ? : } {'Back'} @@ -431,7 +513,7 @@ function SerieConfigurator({ title, dialog, onSubmit, onClose, currentSerie, soc {'Close'} - + ) }; diff --git a/app/src/constants/routes.js b/app/src/constants/routes.js index 37c4e97..57a3c15 100644 --- a/app/src/constants/routes.js +++ b/app/src/constants/routes.js @@ -1,5 +1,6 @@ export const LANDING = '/'; export const TIME_SERIES = '/time-series'; +export const LABELS = '/labels'; export const NETWORK = '/network'; export const OUTPUT_STREAMS = '/output-streams'; export const SETTINGS = '/settings'; diff --git a/app/src/pages/FailedJobs/index.js b/app/src/pages/FailedJobs/index.js index d22bcc7..5dd03e2 100644 --- a/app/src/pages/FailedJobs/index.js +++ b/app/src/pages/FailedJobs/index.js @@ -1,4 +1,6 @@ import Button from '@material-ui/core/Button'; +import ErrorIcon from '@material-ui/icons/Error'; +import IconButton from '@material-ui/core/IconButton'; import InputBase from '@material-ui/core/InputBase'; import Moment from 'moment'; import Paper from '@material-ui/core/Paper'; @@ -17,6 +19,7 @@ import { makeStyles, fade } from '@material-ui/core/styles'; import { useHistory } from "react-router-dom"; import BasicPageLayout from '../../components/BasicPageLayout'; +import ErrorDialog from "../../components/FailedJobs/ErrorDialog"; import ResolveDialog from "../../components/FailedJobs/ResolveDialog"; import { getComparator, stableSort, historyGetQueryParam } from '../../util/GlobalMethods'; import { socket } from '../../store'; @@ -85,6 +88,8 @@ const FailedJobsPage = () => { const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(5); const [search, setSearch] = useState(''); + const [openErrorDialog, setOpenErrorDialog] = useState(false); + const [error, setError] = useState(''); const [failedJobs, setFailedJobs] = useState([]); @@ -130,6 +135,16 @@ const FailedJobsPage = () => { setPage(0); }; + const handleOpenErrorDialog = (error) => { + setOpenErrorDialog(true); + setError(error); + }; + + const handleCloseErrorDialog = () => { + setOpenErrorDialog(false); + setError(''); + }; + const emptyRows = rowsPerPage - Math.min(rowsPerPage, failedJobs.length - page * rowsPerPage); return ( @@ -166,7 +181,7 @@ const FailedJobsPage = () => { @@ -225,46 +240,50 @@ const FailedJobsPage = () => { {'Worker id'} + - {failedJobs ? - - {stableSort( - failedJobs.filter(j => search ? j.series_name.toLowerCase().includes(search.toLowerCase()) : true), - getComparator(order, orderBy) - ).slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - .map((job, index) => { - return ( - - - {job.rid} - - - {job.job_type} - - - {job.series_name} - - - {Moment.unix(job.send_at).format('YYYY-MM-DD HH:mm')} - - - {job.worker_id} - - - ); - })} - {emptyRows > 0 && ( - - - - )} - : - No failed jobs found} + + {stableSort( + failedJobs.filter(j => search ? j.series_name.toLowerCase().includes(search.toLowerCase()) : true), + getComparator(order, orderBy) + ).slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((job, index) => { + return ( + + + {job.rid} + + + {job.job_type} + + + {job.series_name} + + + {Moment.unix(job.send_at).format('YYYY-MM-DD HH:mm')} + + + {job.worker_id} + + + handleOpenErrorDialog(job.error)}> + + + + + ); + })} + {emptyRows > 0 && ( + + + + )} + { }} failedJobs={failedJobs} /> + ); } diff --git a/app/src/pages/Labels/index.js b/app/src/pages/Labels/index.js new file mode 100644 index 0000000..9d916ad --- /dev/null +++ b/app/src/pages/Labels/index.js @@ -0,0 +1,353 @@ +import ClickAwayListener from '@material-ui/core/ClickAwayListener'; +import Grid from '@material-ui/core/Grid'; +import IconButton from "@material-ui/core/IconButton/IconButton"; +import InputBase from '@material-ui/core/InputBase'; +import MenuItem from '@material-ui/core/MenuItem'; +import MenuList from '@material-ui/core/MenuList'; +import MoreIcon from '@material-ui/icons/MoreVert'; +import Paper from '@material-ui/core/Paper'; +import Popper from '@material-ui/core/Popper'; +import React, { useState, useEffect } from 'react'; +import RefreshIcon from '@material-ui/icons/Refresh'; +import SearchIcon from '@material-ui/icons/Search'; +import Table from '@material-ui/core/Table'; +import TableBody from '@material-ui/core/TableBody'; +import TableCell from '@material-ui/core/TableCell'; +import TableContainer from '@material-ui/core/TableContainer'; +import TableHead from '@material-ui/core/TableHead'; +import TablePagination from '@material-ui/core/TablePagination'; +import TableRow from '@material-ui/core/TableRow'; +import TableSortLabel from '@material-ui/core/TableSortLabel'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; +import moment from 'moment'; +import { makeStyles, fade } from '@material-ui/core/styles'; + +import AddLabelDialog from "../../components/Labels/AddLabelDialog"; +import BasicPageLayout from '../../components/BasicPageLayout'; +import DeleteLabelDialog from "../../components/Labels/DeleteLabelDialog"; +import ConfigDialog from "../../components/Labels/ConfigDialog"; +import { getComparator, stableSort } from '../../util/GlobalMethods'; +import { socket } from '../../store'; + +const useStyles = makeStyles((theme) => ({ + root: { + width: '100%', + }, + paper: { + width: '100%', + marginBottom: theme.spacing(2), + }, + table: { + minWidth: 750, + }, + popper: { + zIndex: 1500, + }, + grow: { + flexGrow: 1, + }, + search: { + position: 'relative', + borderRadius: theme.shape.borderRadius, + backgroundColor: fade(theme.palette.common.white, 0.15), + '&:hover': { + backgroundColor: fade(theme.palette.common.white, 0.25), + }, + marginRight: theme.spacing(2), + marginLeft: 0, + width: '100%', + [theme.breakpoints.up('sm')]: { + marginLeft: theme.spacing(3), + width: 'auto', + }, + }, + searchIcon: { + padding: theme.spacing(0, 2), + height: '100%', + position: 'absolute', + pointerEvents: 'none', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, + inputRoot: { + color: 'inherit', + }, + inputInput: { + padding: theme.spacing(1, 1, 1, 0), + // vertical padding + font size from searchIcon + paddingLeft: `calc(1em + ${theme.spacing(4)}px)`, + transition: theme.transitions.create('width'), + width: '100%', + [theme.breakpoints.up('md')]: { + width: '20ch', + }, + } +})); + +const LabelsPage = () => { + const classes = useStyles(); + + const [addLabelModalState, setAddLabelModalState] = useState(false); + const [infoLabelModalState, setInfoLabelModalState] = useState(false); + const [deleteLabelModalState, setDeleteLabelModalState] = useState(false); + + const [order, setOrder] = useState('asc'); + const [orderBy, setOrderBy] = useState('name'); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + const [search, setSearch] = useState(''); + + const [referenceObject, setReferenceObject] = useState(null); + const [selectedLabel, setSelectedLabel] = useState(null); + + const [lastUpdate, setLastUpdate] = useState(null); + const [labels, setLabels] = useState([]); + + const retrieveLabels = () => { + socket.emit('/api/enodo/labels', {}, (data) => { + setLastUpdate(data.data.last_update); + setLabels(data.data.labels); + }); + }; + + useEffect(() => { + retrieveLabels(); + }, []); + + useEffect(() => { + const interval = setInterval(() => { + retrieveLabels(); + }, 5000); + + return () => { + clearInterval(interval); + }; + }, []); + + const handleRequestSort = (event, property) => { + const isAsc = orderBy === property && order === 'asc'; + setOrder(isAsc ? 'desc' : 'asc'); + setOrderBy(property); + }; + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const openMenu = (event, label) => { + label.config = label.series_config; + setSelectedLabel(label); + setReferenceObject(event.currentTarget); + }; + + const closeMenu = () => { + setReferenceObject(null); + }; + + const closeAddDialog = () => { + setAddLabelModalState(false); + setSelectedLabel(null); + retrieveLabels(); + }; + + const closeInfoDialog = () => { + setInfoLabelModalState(false); + setSelectedLabel(null); + }; + + const closeDeleteDialog = () => { + setDeleteLabelModalState(false); + setSelectedLabel(null); + retrieveLabels(); + }; + + const emptyRows = rowsPerPage - Math.min(rowsPerPage, labels.length - page * rowsPerPage); + + return ( + setAddLabelModalState(true)} + buttonText='Add' + titleButton={ + + + + } + > + + + + + + {'Last update: ' + (lastUpdate ? moment.unix(lastUpdate).format('YYYY-MM-DD HH:mm') : 'unknown')} + + + + + + + setSearch(e.target.value)} + /> + + + + + + + + + handleRequestSort(e, 'name')} + > + {'Name'} + + + + handleRequestSort(e, 'description')} + > + {'Description'} + + + + handleRequestSort(e, 'selector')} + > + {'Selector'} + + + + + + + {stableSort( + labels.filter(s => search ? s.name.toLowerCase().includes(search.toLowerCase()) : true), + getComparator(order, orderBy) + ).slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((label, index) => { + return ( + + + {label.name} + + + {label.description} + + + {label.selector} + + + openMenu(e, label)} + > + + + + + ); + })} + {emptyRows > 0 && ( + + + + )} + + + + + + + + + { + setInfoLabelModalState(true); + closeMenu(); + }} + > + + {'View config'} + + + { + setDeleteLabelModalState(true); + closeMenu(); + }} + > + + {'Delete label'} + + + + + + + + {addLabelModalState && + } + {infoLabelModalState && + } + + + ); +} + +export default LabelsPage; diff --git a/app/src/pages/TimeSeries/index.js b/app/src/pages/TimeSeries/index.js index 73fda7c..d6f4a9b 100644 --- a/app/src/pages/TimeSeries/index.js +++ b/app/src/pages/TimeSeries/index.js @@ -2,6 +2,8 @@ import Badge from '@material-ui/core/Badge'; import CancelIcon from '@material-ui/icons/Cancel'; import CheckCircleIcon from '@material-ui/icons/CheckCircle'; import ClickAwayListener from '@material-ui/core/ClickAwayListener'; +import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'; +import Grid from '@material-ui/core/Grid'; import IconButton from "@material-ui/core/IconButton/IconButton"; import InputBase from '@material-ui/core/InputBase'; import MenuItem from '@material-ui/core/MenuItem'; @@ -20,8 +22,11 @@ import TablePagination from '@material-ui/core/TablePagination'; import TableRow from '@material-ui/core/TableRow'; import TableSortLabel from '@material-ui/core/TableSortLabel'; import Toolbar from '@material-ui/core/Toolbar'; +import Tooltip from '@material-ui/core/Tooltip'; import Typography from '@material-ui/core/Typography'; +import UpdateIcon from '@material-ui/icons/Update'; import WorkOffIcon from '@material-ui/icons/WorkOff'; +import LabelIcon from '@material-ui/icons/Label'; import { Chart } from "react-google-charts"; import { makeStyles, fade } from '@material-ui/core/styles'; import { useHistory } from "react-router-dom"; @@ -30,9 +35,9 @@ import * as ROUTES from '../../constants/routes'; import AddSerie from "../../components/Serie/Add"; import BasicPageLayout from '../../components/BasicPageLayout'; import EditSerie from "../../components/Serie/Edit"; -import Info from "../../components/Serie/Info"; +import InfoDialog from "../../components/Serie/Info"; import SerieDetails from "../../components/Serie/Dialog"; -import { getComparator, stableSort } from '../../util/GlobalMethods'; +import { getComparator, stableSort, healthToColor, healthToText } from '../../util/GlobalMethods'; import { useGlobal, socket } from '../../store'; const useStyles = makeStyles((theme) => ({ @@ -89,6 +94,12 @@ const useStyles = makeStyles((theme) => ({ width: '20ch', }, }, + health: { + width: 90 + }, + name: { + marginBottom: theme.spacing(0.5) + } })); const TimeSeriesPage = () => { @@ -250,10 +261,11 @@ const TimeSeriesPage = () => { + @@ -283,64 +295,90 @@ const TimeSeriesPage = () => { - {series ? - - {stableSort( - series.filter(s => search ? s.name.toLowerCase().includes(search.toLowerCase()) : true), - getComparator(order, orderBy) - ).slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - .map((series, index) => { - return ( - - + + {stableSort( + series.filter(s => search ? s.name.toLowerCase().includes(search.toLowerCase()) : true), + getComparator(order, orderBy) + ).slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((series, index) => { + return ( + + + + + + + + + {series.config.realtime ? + + + + + + : null} + {series.label_name ? + + + + + + : null} + + + + {series.name} - - - {series.config.job_config.job_base_analysis ? - : } - - - {series.config.job_config.job_forecast ? - : } - - - {series.config.job_config.job_anomaly_detect ? - : } - - - {series.config.job_config.job_static_rules ? - : } - - - {getNoFailedJobs(series.name) > 0 ? - navigateToFailedJobs(series.name)}> - - - - : null} - - - openMenu(e, series)} - > - - - - - ); - })} - {emptyRows > 0 && ( - - - - )} - : - No series found} + + + + {series.config.job_config.job_base_analysis ? + : } + + + {series.config.job_config.job_forecast ? + : } + + + {series.config.job_config.job_anomaly_detect ? + : } + + + {series.config.job_config.job_static_rules ? + : } + + + {getNoFailedJobs(series.name) > 0 ? + navigateToFailedJobs(series.name)}> + + + + : null} + + + openMenu(e, series)} + > + + + + + ); + })} + {emptyRows > 0 && ( + + + + )} + { closeMenu(); }} > - + {'Delete series'} @@ -434,12 +472,13 @@ const TimeSeriesPage = () => { } {viewType === "info" && - { - setViewType(''); - setSelectedSeriesName(null); - }}> - - + { + setViewType(''); + setSelectedSeriesName(null); + }} + serie={selectedSeriesName} + /> } {addSerieModalState && { setAddSerieModalState(false) }} /> diff --git a/app/src/util/GlobalMethods.js b/app/src/util/GlobalMethods.js index 7b8e445..66498b2 100644 --- a/app/src/util/GlobalMethods.js +++ b/app/src/util/GlobalMethods.js @@ -1,3 +1,8 @@ +import * as chroma from 'chroma-js'; +import green from '@material-ui/core/colors/green'; +import orange from '@material-ui/core/colors/orange'; +import red from '@material-ui/core/colors/red'; + function descendingComparator(a, b, orderBy) { if (b[orderBy] < a[orderBy]) { return -1; @@ -28,4 +33,14 @@ export function historyGetQueryParam(history, name) { const params = new URLSearchParams(history.location.search); const result = params.get(name); return result; -} \ No newline at end of file +} + +export function healthToText(severity) { + const sevs = ["low", "medium", "high"]; + const x = Math.floor((1 / (3 + 1)) * (((3 - 1) * severity) + 1) * sevs.length); + return sevs[x]; +}; + +export function healthToColor(domain, severity) { + return chroma.scale([red[500], orange[500], green[500]]).domain(domain)(severity); +};