diff --git a/.env b/.env
index 3ccb8fff3ef022e5adf5166117b885f96292f080..6f57ceae5a604a3032d31492dc684f08985b7bd0 100644
--- a/.env
+++ b/.env
@@ -1 +1,2 @@
-EXTEND_ESLINT=true
\ No newline at end of file
+EXTEND_ESLINT=true
+FAST_REFRESH=true
\ No newline at end of file
diff --git a/.eslintrc.js b/.eslintrc.js
index a07f51e6770e756c50518e784fefe34c8eacd9b7..f6bb6fa908c06aa7a4a3ac2dad1c091396b869c0 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -34,6 +34,8 @@ module.exports = {
     'max-len': 'off',
     'implicit-arrow-linebreak': 'off',
     'function-paren-newline': 'off',
+    'react/jsx-curly-newline': 'off',
+    'react/jsx-wrap-multilines': 'off',
   },
   parserOptions: {
     ecmaVersion: 2020,
diff --git a/.vscode/settings.json b/.vscode/settings.json
index aa04e679c831027c621517cf6afff9fb16269880..780df2f968e5562ad9c74e2c6afd5c1a5f20280a 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -18,6 +18,7 @@
     "app/dist": true,
     "app/main.prod.js": true,
     "app/main.prod.js.map": true,
+    "app/node_modules": true,
     "bower_components": true,
     "dll": true,
     "release": true,
@@ -25,7 +26,30 @@
     "npm-debug.log.*": true,
     "test/**/__snapshots__": true,
     "yarn.lock": true,
-    "*.{css,sass,scss}.d.ts": true
+    "*.{css,sass,scss}.d.ts": true,
+    "**/node_modules/**": true,
+    "**/.hg/store/**": true,
+    "app/package-lock.json": true
   },
+  "files.watcherExclude": {
+    ".git": true,
+    ".eslintcache": true,
+    "app/dist": true,
+    "app/main.prod.js": true,
+    "app/main.prod.js.map": true,
+    "app/node_modules": true,
+    "bower_components": true,
+    "dll": true,
+    "release": true,
+    "node_modules": true,
+    "npm-debug.log.*": true,
+    "test/**/__snapshots__": true,
+    "yarn.lock": true,
+    "*.{css,sass,scss}.d.ts": true,
+    "**/node_modules/**": true,
+    "**/.hg/store/**": true,
+    "app/package-lock.json": true
+  },
+
   "editor.tabSize": 2
 }
diff --git a/app/App.tsx b/app/App.tsx
index b4350105949cdb9c5e08472f708c6de431c57847..bdb35ee292871071511a8f32d0a8313923fc3721 100644
--- a/app/App.tsx
+++ b/app/App.tsx
@@ -1,7 +1,6 @@
 import React from 'react';
 import { createMuiTheme, ThemeProvider } from '@material-ui/core';
 import { HashRouter as Router, Switch, Route } from 'react-router-dom';
-import { LoadingProvider } from './context/LoadingContext';
 import routes from './routes';
 import LoginScreen from './containers/LoginScreen';
 import Dashboard from './containers/Dashboard';
@@ -19,9 +18,9 @@ const theme = createMuiTheme({
       contrastText: '#FFFFFF',
     },
     text: {
-      primary: 'rgba(255, 255, 255, 0,9)',
-      secondary: 'rgba(255, 255, 255, 0,8)',
-      disabled: 'rgba(255, 255, 255, 0,6)',
+      primary: 'rgba(255, 255, 255, 0.9)',
+      secondary: 'rgba(255, 255, 255, 0.7)',
+      disabled: 'rgba(255, 255, 255, 0.6)',
     },
     background: {
       default: '#404040', // Dark 2
@@ -33,6 +32,9 @@ const theme = createMuiTheme({
     button: {
       textTransform: 'capitalize',
     },
+    subtitle2: {
+      color: 'rgba(255, 255, 255, 0.7)',
+    },
   },
   spacing: 8,
   props: {
@@ -43,26 +45,32 @@ const theme = createMuiTheme({
       margin: 'dense',
     },
   },
+  overrides: {
+    MuiSnackbarContent: {
+      root: {
+        backgroundColor: '#4B4B4B', // Dark 3
+        color: 'rgba(255, 255, 255, 0.9)',
+      },
+    },
+  },
 });
 
 export default function App() {
   return (
     <div className="content">
       <ThemeProvider theme={theme}>
-        <LoadingProvider>
-          <LoginDetailsProvider>
-            <Router>
-              <Switch>
-                <Route exact path={routes.login}>
-                  <LoginScreen />
-                </Route>
-                <Route path={routes.dashboard}>
-                  <Dashboard />
-                </Route>
-              </Switch>
-            </Router>
-          </LoginDetailsProvider>
-        </LoadingProvider>
+        <LoginDetailsProvider>
+          <Router>
+            <Switch>
+              <Route exact path={routes.login}>
+                <LoginScreen />
+              </Route>
+              <Route path={routes.dashboard}>
+                <Dashboard />
+              </Route>
+            </Switch>
+          </Router>
+        </LoginDetailsProvider>
       </ThemeProvider>
     </div>
   );
diff --git a/app/QueryOutput.ts b/app/QueryOutput.ts
index 1a7a89fc75f67c31b5bfe5a5508683ae23a44924..feaec6bacdec06713e081dcac2210de9440a22b6 100644
--- a/app/QueryOutput.ts
+++ b/app/QueryOutput.ts
@@ -1,6 +1,6 @@
 import { Result } from '../backend/utils/mysql';
 
 export interface QueryOutput {
-  results: Result | undefined;
-  error: undefined;
+  results?: Result;
+  error?: undefined;
 }
diff --git a/app/app.global.css b/app/app.global.css
index eed1a3f2d664da147cb1875058b76488799089b5..55a420d6b0ba5e7e880e01f2ca765cf33ff7adb4 100644
--- a/app/app.global.css
+++ b/app/app.global.css
@@ -20,3 +20,33 @@ body #root {
   min-height: 100%;
   min-width: 100%;
 }
+
+::-webkit-resizer {
+  border: 9px solid rgba(255, 255, 255, 0.1);
+  border-bottom-color: rgba(255, 255, 255, 0.5);
+  border-right-color: rgba(255, 255, 255, 0.5);
+  outline: 1px solid rgba(255, 255, 255, 0.1);
+  background-color: #636363;
+}
+
+/* width */
+::-webkit-scrollbar {
+  width: 12px;
+}
+
+/* Track */
+::-webkit-scrollbar-track {
+  background: #404040;
+  border-radius: 8px;
+}
+
+/* Handle */
+::-webkit-scrollbar-thumb {
+  background: #636363;
+  border-radius: 8px;
+}
+
+/* Handle on hover */
+::-webkit-scrollbar-thumb:hover {
+  background: #595959;
+}
diff --git a/app/app.html b/app/app.html
index 29649a36b3c9af33ca7058fec9543efeebd2c21e..05642a16b61f0c0934a49b5fb0eebafb4ca55439 100644
--- a/app/app.html
+++ b/app/app.html
@@ -16,6 +16,10 @@
       href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@400;500;600&display=swap"
       rel="stylesheet"
     />
+    <link
+      rel="stylesheet"
+      href="https://fonts.googleapis.com/icon?family=Material+Icons"
+    />
     <script>
       (() => {
         if (
diff --git a/app/components/ColorPicker.tsx b/app/components/ColorPicker.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..455bd0c255cc8cb46719046e461639d327057ae4
--- /dev/null
+++ b/app/components/ColorPicker.tsx
@@ -0,0 +1,75 @@
+import React, { useState } from 'react';
+import { ChromePicker, ColorResult } from 'react-color';
+import { isNaN } from 'lodash';
+import { IconButton, Icon, Popover, TextField } from '@material-ui/core';
+
+export default function ColorPicker(props: {
+  currentColor: string;
+  setColor: (value: string) => void;
+}) {
+  const [colorPickerAnchor, setColorPickerAnchor] = useState<Element | null>(
+    null
+  );
+  const { setColor, currentColor } = props;
+
+  const [colorError, setColorError] = useState('');
+
+  function isHexColor(hex: string) {
+    return hex.length === 6 && !isNaN(Number(`0x${hex}`));
+  }
+
+  function handleChangedColor(color?: ColorResult, colorString?: string) {
+    if (color) {
+      setColor(color.hex);
+      setColorError('');
+    } else if (colorString) {
+      if (isHexColor(colorString.replace('#', ''))) {
+        const convertedColorString = colorString.includes('#')
+          ? colorString
+          : `#${colorString}`;
+        setColor(convertedColorString);
+        setColorError('');
+      } else {
+        setColorError('Must be valid hex color');
+      }
+    }
+  }
+
+  return (
+    <div>
+      <TextField
+        variant="filled"
+        label="Color"
+        value={currentColor}
+        error={colorError.length > 0}
+        helperText={colorError}
+        onFocus={() => setColorError('')}
+        onChange={(event) => handleChangedColor(undefined, event.target.value)}
+      />
+      <IconButton
+        onClick={(event) => setColorPickerAnchor(event.currentTarget)}
+      >
+        <Icon>palette</Icon>
+      </IconButton>
+      <Popover
+        open={Boolean(colorPickerAnchor)}
+        anchorEl={colorPickerAnchor}
+        onClose={() => setColorPickerAnchor(null)}
+        anchorOrigin={{
+          vertical: 'bottom',
+          horizontal: 'center',
+        }}
+        transformOrigin={{
+          vertical: 'top',
+          horizontal: 'center',
+        }}
+      >
+        <ChromePicker
+          color={currentColor}
+          onChange={(color) => handleChangedColor(color)}
+          onChangeComplete={(color) => handleChangedColor(color)}
+        />
+      </Popover>
+    </div>
+  );
+}
diff --git a/app/components/ErrorView.tsx b/app/components/ErrorView.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2a4e789a77abcedbf08a50bfe71cd81818a1ff2b
--- /dev/null
+++ b/app/components/ErrorView.tsx
@@ -0,0 +1,38 @@
+import { Paper, Typography } from '@material-ui/core';
+import { makeStyles } from '@material-ui/core/styles';
+import React, { useContext } from 'react';
+import { RecordingContext } from '../context/RecordingContext';
+
+export default function ErrorView() {
+  const recordingContext = useContext(RecordingContext);
+  const error = recordingContext?.activeRecording?.error;
+
+  const useStyles = makeStyles((theme) => ({
+    content: {
+      display: 'flex',
+      flexFlow: 'column nowrap',
+      padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`,
+    },
+  }));
+
+  const classes = useStyles();
+
+  return (
+    <div>
+      {error && (
+        <Paper className={classes.content} elevation={1}>
+          <Typography variant="h5">Errors</Typography>
+          <Typography
+            variant="subtitle1"
+            style={{
+              whiteSpace: 'pre-line',
+            }}
+            color="error"
+          >
+            {error}
+          </Typography>
+        </Paper>
+      )}
+    </div>
+  );
+}
diff --git a/app/components/ExplainAnalyzeTable.tsx b/app/components/ExplainAnalyzeTable.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..07cc3a257e0e9a69ba55173acfc9054424a7d0b0
--- /dev/null
+++ b/app/components/ExplainAnalyzeTable.tsx
@@ -0,0 +1,59 @@
+import React from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+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 TableRow from '@material-ui/core/TableRow';
+import Paper from '@material-ui/core/Paper';
+
+const useStyles = makeStyles({
+  table: {
+    // minWidth: 650,
+  },
+});
+
+function createData(description: string, data: string) {
+  return { description, data };
+}
+
+export default function ExplainAnalyzeTable(data: { node: any }) {
+  const classes = useStyles();
+  const { node } = data;
+
+  if (node.name !== 'root') {
+    const rows = [
+      createData('Command', node.name),
+      createData('Actual time', node.time),
+      createData('Rows', node.rows.toString()),
+      createData('Loops', node.loops.toString()),
+    ];
+
+    // TODO: Fix this:
+    // eslint-disable-next-line no-restricted-globals
+    if (!isNaN(node.cost_est) && !isNaN(node.rows_est)) {
+      rows.splice(
+        1,
+        0,
+        createData('Est. cost', node.cost_est?.toString()),
+        createData('Est. rows', node.rows_est?.toString())
+      );
+    }
+
+    return (
+      <TableContainer component={Paper}>
+        <Table className={classes.table} aria-label="simple table">
+          <TableBody>
+            {rows.map((row) => (
+              <TableRow key={row.description}>
+                <TableCell>{row.description}</TableCell>
+                <TableCell align="left">{row.data}</TableCell>
+              </TableRow>
+            ))}
+          </TableBody>
+        </Table>
+      </TableContainer>
+    );
+  }
+  return null;
+}
diff --git a/app/components/IDInfo.tsx b/app/components/IDInfo.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1fe032c8c0dce173baf80046c5bd97c5a4e537b9
--- /dev/null
+++ b/app/components/IDInfo.tsx
@@ -0,0 +1,29 @@
+import React from 'react';
+import { makeStyles, Paper, Typography } from '@material-ui/core';
+import SqlManagerSingleton from '../../backend/recorder/SqlManagerSingleton';
+
+const useStyles = makeStyles((theme) => ({
+  wrapper: {
+    padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`,
+  },
+}));
+
+export default function IDInfo() {
+  const manager = SqlManagerSingleton.getInstance();
+
+  const classes = useStyles();
+
+  return (
+    <Paper elevation={1} className={classes.wrapper}>
+      <Typography variant="subtitle2">
+        {`Runner ConnectionID: ${manager.runner?.connectionID} Runner ThreadID: ${manager.runner?.threadID}`}
+      </Typography>
+      <Typography variant="subtitle2">
+        {`Monitor ConnectionID: ${manager.monitor?.connectionID} Monitor ThreadID: ${manager.monitor?.threadID}`}
+      </Typography>
+      <Typography variant="subtitle2">
+        {`Agent ConnectionID: ${manager.agent?.connectionID} Agent ThreadID: ${manager.agent?.threadID}`}
+      </Typography>
+    </Paper>
+  );
+}
diff --git a/app/components/Login.tsx b/app/components/Login.tsx
index d8aa34fc491a7052f03d672196f730580560fd56..dbf33ab080f4ff5d6445a694cd9f9353843d40ab 100644
--- a/app/components/Login.tsx
+++ b/app/components/Login.tsx
@@ -6,11 +6,12 @@ import {
   Typography,
   makeStyles,
   LinearProgress,
+  FormControlLabel,
+  Checkbox,
 } from '@material-ui/core';
 import { useHistory, useLocation } from 'react-router';
 import clsx from 'clsx';
 import SqlManagerSingleton from '../../backend/recorder/SqlManagerSingleton';
-import { LoadingContext } from '../context/LoadingContext';
 import routes from '../routes';
 import { LoginDetailsContext } from '../context/LoginDetailsContext';
 
@@ -59,7 +60,6 @@ const DefaultProps = {
 };
 
 export default function Login({ onConnect = undefined }: LoginProps) {
-  const loadingContext = useContext(LoadingContext);
   const loginDetailsContext = useContext(LoginDetailsContext);
 
   const history = useHistory();
@@ -82,20 +82,27 @@ export default function Login({ onConnect = undefined }: LoginProps) {
   );
   const [connecting, setConnecting] = useState(false);
   const [errorMessage, setErrorMessage] = useState('');
+  const [savePassword, setSavePassword] = useState(
+    loginDetailsContext?.loginDetails?.savePassword || false
+  );
 
   async function handleConnectByLogin() {
     setConnecting(true);
     const manager = SqlManagerSingleton.getInstance();
     try {
-      loadingContext?.setConnected(false);
-      loadingContext?.setConnecting(true);
-      await manager.connect({ host, user, password, port }, database);
+      await manager.connect(
+        { host, user, password, port },
+        database,
+        savePassword
+      );
+      loginDetailsContext?.setLoginDetails({
+        ...{ host, user, password: savePassword ? password : '', port },
+        database,
+        savePassword,
+      });
       setErrorMessage('');
-      loadingContext?.setConnecting(false);
-      loadingContext?.setConnected(true);
 
       if (location.pathname === routes.login) {
-        console.log('Routing to dashboard');
         history.push(routes.dashboard);
       } else if (onConnect) {
         onConnect();
@@ -126,8 +133,6 @@ export default function Login({ onConnect = undefined }: LoginProps) {
       } else {
         setErrorMessage('Failed to connect');
       }
-      loadingContext?.setConnecting(false);
-      loadingContext?.setConnected(false);
 
       setConnecting(false);
     }
@@ -193,6 +198,16 @@ export default function Login({ onConnect = undefined }: LoginProps) {
             onKeyUp={handleKeyUp}
           />
         </div>
+        <FormControlLabel
+          label="Save password"
+          control={
+            <Checkbox
+              color="primary"
+              checked={savePassword}
+              onChange={(event) => setSavePassword(event.target.checked)}
+            />
+          }
+        />
         {!!errorMessage && (
           <Typography
             color="error"
diff --git a/app/components/MemoryChart.tsx b/app/components/MemoryChart.tsx
index 0d19382f39abeff45394b9e3ebbf4a3314b8fa1e..5ae42c553cda33d08b7d7651c4168c9b22145f36 100644
--- a/app/components/MemoryChart.tsx
+++ b/app/components/MemoryChart.tsx
@@ -1,7 +1,20 @@
+import {
+  Backdrop,
+  Button,
+  Checkbox,
+  Fade,
+  ListItem,
+  ListItemIcon,
+  ListItemText,
+  makeStyles,
+  Modal,
+  TextField,
+  Typography,
+} from '@material-ui/core';
 import _ from 'lodash';
-import React, { useState, useContext } from 'react';
+import React, { useState, useContext, CSSProperties } from 'react';
+import { FixedSizeList } from 'react-window';
 import {
-  Legend,
   LineChart,
   Line,
   CartesianGrid,
@@ -9,54 +22,65 @@ import {
   YAxis,
   Tooltip,
   ResponsiveContainer,
-  Surface,
-  Symbols,
   ReferenceLine,
   Label,
 } from 'recharts';
 import { RecordingContext } from '../context/RecordingContext';
 
-interface Payload {
-  value: string;
-  id: number;
-  type: string;
-  color: string;
-}
+const useStyles = makeStyles((theme) => ({
+  lineChart: {
+    zIndex: 999,
+  },
+  buttonWrapper: {
+    display: 'flex',
+    flexFlow: 'row nowrap',
+    padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`,
+  },
+  element: {
+    marginBottom: theme.spacing(1),
+  },
+  paper: {
+    position: 'absolute',
+    width: 500,
+    backgroundColor: theme.palette.background.paper,
+    padding: theme.spacing(2, 4, 3),
+    outline: 'none',
+    borderRadius: 4,
+  },
+  listItemRoot: {
+    padding: 0,
+  },
+}));
 
-interface ChartColors {
-  [key: string]: string;
+function getModalStyle() {
+  return {
+    top: '50%',
+    left: '50%',
+    transform: 'translate(-50%,-50%)',
+  };
 }
 
 export default function MemoryChart() {
+  const classes = useStyles();
   const [disabled, setDisabled] = useState<Array<string>>([]);
-  const [chartColors, setChartColors] = useState({ total: '#82ca9d' });
-  const validStages = [
-    'stage/sql/preparing',
-    'stage/sql/optimizing',
-    'stage/sql/executing',
-    'stage/sql/end',
-  ];
+  const [validStages, setValidStages] = useState<Array<string>>([]);
+  // const validStages = [
+  //   'stage/sql/preparing',
+  //   'stage/sql/optimizing',
+  //   'stage/sql/executing',
+  //   'stage/sql/end',
+  // ];
+  const [modalStyle] = React.useState(getModalStyle);
+  const [openEvent, setOpenEvent] = React.useState(false);
+  const [openStage, setOpenStage] = React.useState(false);
+  const [checked, setChecked] = useState<Array<string>>([]);
+  const [filteredMemory, setFilteredMemory] = useState<Array<string>>([]);
+  const [filteredStages, setFilteredStages] = useState<Array<string>>([]);
 
   const recordingContext = useContext(RecordingContext);
-  const stageTimes = recordingContext?.stageTimes;
-  const chartData = recordingContext?.chartData;
-
-  function getColor() {
-    return `#${(0x1000000 + Math.random() * 0xffffff)
-      .toString(16)
-      .substr(1, 6)}`;
-  }
-
-  function assignColor(key: string) {
-    const tempObject: ChartColors = {};
-    tempObject[key] = getColor();
-    return tempObject;
-  }
-
-  if (chartData && _.keys(chartColors).length === 1) {
-    const dataKeys = _.keys(chartData[0]).slice(1);
-    _.forEach(dataKeys, (key) => _.assign(chartColors, assignColor(key)));
-  }
+  const stageTimes = recordingContext?.activeRecording?.stageTimes;
+  const chartData = recordingContext?.activeRecording?.chartData;
+  const testColors = recordingContext?.activeRecording?.chartColors;
 
   function getMaxTime() {
     // eslint-disable-next-line prefer-spread
@@ -64,7 +88,7 @@ export default function MemoryChart() {
   }
 
   // When a legend is clicked the datakey is added to the disabled list, which will remove it form the line chart
-  function handleClick(dataKey: string) {
+  function eventToggle(dataKey: string) {
     if (_.includes(disabled, dataKey)) {
       setDisabled(disabled.filter((obj) => obj !== dataKey));
     } else {
@@ -72,38 +96,12 @@ export default function MemoryChart() {
     }
   }
 
-  function renderCustomizedLegend(props: { payload: Array<Payload> }) {
-    const { payload } = props;
-    return (
-      <div className="customized-legend">
-        {payload.map((entry: { value: string; color: string }) => {
-          const { value, color } = entry;
-          const active = _.includes(disabled, value);
-          const style = {
-            marginRight: 10,
-            color: active ? '#AAA' : '#000',
-          };
-
-          return (
-            <span
-              role="presentation"
-              className="legend-item"
-              onClick={() => handleClick(value)}
-              style={style}
-              key={value}
-            >
-              <Surface width={10} height={10}>
-                <Symbols cx={5} cy={5} type="circle" size={50} fill={color} />
-                {active && (
-                  <Symbols cx={5} cy={5} type="circle" size={25} fill="#FFF" />
-                )}
-              </Surface>
-              <span style={{ color: '#FFF' }}>{value}</span>
-            </span>
-          );
-        })}
-      </div>
-    );
+  function validStageToggle(stage: string) {
+    if (_.includes(validStages, stage)) {
+      setValidStages(validStages.filter((obj) => obj !== stage));
+    } else {
+      setValidStages(validStages.concat(stage));
+    }
   }
 
   function formatBytes(byte: number) {
@@ -116,79 +114,298 @@ export default function MemoryChart() {
     return `${Math.floor(byte)} B`;
   }
 
-  return (
-    <div className="highlight-bar-charts">
-      <ResponsiveContainer height={400}>
-        <LineChart
-          width={800}
-          height={500}
-          data={chartData}
-          margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
-        >
-          <CartesianGrid strokeDasharray="3 10" />
-          {/* TODO: Create our own Axis tick component like this: http://recharts.org/en-US/examples/CustomizedLabelLineChart */}
-          <XAxis
-            dataKey="relative_time"
-            type="number"
-            tick={{ stroke: '#FFFFFF', strokeWidth: '0.5px' }}
-            domain={[0, getMaxTime()]}
-            tickCount={20}
-          />
-          <YAxis
-            type="number"
-            tickFormatter={formatBytes}
-            tick={{ stroke: '#FFFFFF', strokeWidth: '0.5px' }}
-          />
-          <Tooltip
-            formatter={(value) => formatBytes(Number(value))}
-            offset={100}
+  function handleToggle(value: string, memory: boolean) {
+    const curIndex = checked.indexOf(value);
+    const newChecked = [...checked];
+
+    if (curIndex === -1) {
+      if (memory) {
+        eventToggle(value);
+      } else {
+        validStageToggle(value);
+      }
+      newChecked.push(value);
+    } else {
+      if (memory) {
+        eventToggle(value);
+      } else {
+        validStageToggle(value);
+      }
+      newChecked.splice(curIndex, 1);
+    }
+    setChecked(newChecked);
+  }
+
+  // Opens the modal to select event
+  function handleOpenEvent() {
+    if (chartData) {
+      setFilteredMemory(_.keys(chartData[0]).slice(1));
+    }
+    setOpenEvent(true);
+  }
+
+  function handleCloseEvent() {
+    setOpenEvent(false);
+  }
+
+  // Opens the modal to select event
+  function handleOpenStage() {
+    if (stageTimes) {
+      setFilteredStages(_.map(_.uniqBy(stageTimes, 'stage'), 'stage'));
+    }
+    setOpenStage(true);
+  }
+
+  function handleCloseStage() {
+    setOpenStage(false);
+  }
+
+  // Sets filteredMemory with a list of the datakeys that matches the text in the search bar
+  function searchBarFilter(search: string, memory: boolean) {
+    let filteredList = [''];
+    if (memory) {
+      if (chartData) {
+        filteredList = _.filter(_.keys(chartData[0]).slice(1), (o) =>
+          _.includes(o.toLowerCase(), search.toLowerCase())
+        );
+      }
+      setFilteredMemory(filteredList);
+    } else {
+      if (stageTimes) {
+        filteredList = _.filter(
+          _.map(_.uniqBy(stageTimes, 'stage'), 'stage'),
+          (o) => _.includes(o.toLowerCase(), search.toLowerCase())
+        );
+      }
+      setFilteredStages(filteredList);
+    }
+  }
+
+  function renderEventRow(props: { index: number; style: CSSProperties }) {
+    const { index, style } = props;
+
+    const memoryType = filteredMemory[index];
+
+    const labelId = `checkbox-list-label-${memoryType}`;
+    return (
+      <ListItem
+        classes={{ root: classes.listItemRoot }}
+        style={style}
+        key={memoryType}
+        role={undefined}
+        dense
+        button
+        onClick={() => handleToggle(memoryType, true)}
+      >
+        <ListItemIcon>
+          <Checkbox
+            edge="start"
+            checked={checked.indexOf(memoryType) === -1}
+            tabIndex={-1}
+            disableRipple
+            inputProps={{ 'aria-labelledby': labelId }}
+            color="primary"
           />
-          <Legend
-            verticalAlign="bottom"
-            height={36}
-            align="center"
-            payload={_.toPairs(chartColors).map((pair, i) => ({
-              value: pair[0],
-              id: i,
-              type: 'line',
-              color: pair[1],
-            }))}
-            content={renderCustomizedLegend}
+        </ListItemIcon>
+        <ListItemText id={labelId} primary={`${memoryType}`} />
+      </ListItem>
+    );
+  }
+
+  function renderStageRow(props: { index: number; style: CSSProperties }) {
+    const { index, style } = props;
+
+    const stage = filteredStages[index];
+
+    const labelId = `checkbox-list-label-${stage}`;
+    return (
+      <ListItem
+        classes={{ root: classes.listItemRoot }}
+        style={style}
+        key={stage}
+        role={undefined}
+        dense
+        button
+        onClick={() => handleToggle(stage, false)}
+      >
+        <ListItemIcon>
+          <Checkbox
+            edge="start"
+            checked={checked.indexOf(stage) !== -1}
+            tabIndex={-1}
+            disableRipple
+            inputProps={{ 'aria-labelledby': labelId }}
+            color="primary"
           />
+        </ListItemIcon>
+        <ListItemText id={labelId} primary={`${stage}`} />
+      </ListItem>
+    );
+  }
 
-          {/* Creates an array from the key:value pairs in chartColors.
-           The keys that are not disabled becomes a line */}
-          {_.toPairs(chartColors)
-            .filter((pair) => !_.includes(disabled, pair[0]))
-            .map((pair) => (
-              <Line
-                type="monotone"
-                dataKey={pair[0]}
-                key={pair[0]}
-                stroke={pair[1]}
-                dot={false}
-              />
-            ))}
-          {/* Filters stageTimes to only include stages that are in validStages and only the first instance of them.
-          Then creates reference lines from the data that remains */}
-
-          {_.forEach(_.uniqBy(stageTimes, 'stage'))
-            ?.filter((stage) => _.includes(validStages, stage.stage))
-            ?.map((item) => (
-              <ReferenceLine
-                x={item.startTime}
-                stroke="#769CFF"
-                key={_.uniqueId('ref_')}
+  return (
+    <div className={classes.lineChart}>
+      <div className={classes.buttonWrapper}>
+        <div style={{ flexGrow: 1 }}>
+          <Typography variant="h6">Memory performance</Typography>
+        </div>
+        <Button
+          variant="outlined"
+          color="primary"
+          onClick={handleOpenEvent}
+          style={{ marginRight: 16 }}
+        >
+          Pick Events
+        </Button>
+        <Modal
+          open={openEvent}
+          onClose={handleCloseEvent}
+          aria-labelledby="simple-modal-title"
+          closeAfterTransition
+          BackdropComponent={Backdrop}
+          BackdropProps={{
+            timeout: 500,
+          }}
+        >
+          <Fade in={openEvent}>
+            <div style={modalStyle} className={classes.paper}>
+              <div className={classes.element}>
+                <Typography variant="h5">Pick events to show</Typography>
+              </div>
+              <form>
+                <TextField
+                  id="filled-basic"
+                  label="Search event..."
+                  variant="filled"
+                  onChange={(e) => searchBarFilter(e.target.value, true)}
+                />
+              </form>
+              <FixedSizeList
+                height={400}
+                width="100%"
+                itemSize={48}
+                itemCount={filteredMemory.length}
               >
-                <Label
-                  value={item.stage}
-                  position="top"
-                  style={{ color: '#FFFFFF' }}
+                {renderEventRow}
+              </FixedSizeList>
+            </div>
+          </Fade>
+        </Modal>
+        {/* Stage modal */}
+        <Button variant="outlined" color="primary" onClick={handleOpenStage}>
+          Pick Stages
+        </Button>
+        <Modal
+          open={openStage}
+          onClose={handleCloseStage}
+          aria-labelledby="simple-modal-title"
+          closeAfterTransition
+          BackdropComponent={Backdrop}
+          BackdropProps={{
+            timeout: 500,
+          }}
+        >
+          <Fade in={openStage}>
+            <div style={modalStyle} className={classes.paper}>
+              <div className={classes.element}>
+                <Typography variant="h5">Pick stages to show</Typography>
+              </div>
+              <form>
+                <TextField
+                  id="filled-basic"
+                  label="Search stage"
+                  variant="filled"
+                  onChange={(e) => searchBarFilter(e.target.value, false)}
                 />
-              </ReferenceLine>
-            ))}
-        </LineChart>
-      </ResponsiveContainer>
+              </form>
+              <FixedSizeList
+                height={400}
+                width="100%"
+                itemSize={48}
+                itemCount={filteredStages.length}
+              >
+                {renderStageRow}
+              </FixedSizeList>
+            </div>
+          </Fade>
+        </Modal>
+      </div>
+      {chartData && chartData.length > 2 ? (
+        <div>
+          <ResponsiveContainer height={400}>
+            <LineChart
+              width={800}
+              height={500}
+              data={chartData}
+              margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
+              style={{ fontFamily: 'Roboto, sans-serif' }}
+            >
+              <CartesianGrid strokeDasharray="3 10" />
+              {/* TODO: Create our own Axis tick component like this: http://recharts.org/en-US/examples/CustomizedLabelLineChart */}
+              <XAxis
+                dataKey="relative_time"
+                type="number"
+                domain={[0, getMaxTime()]}
+                tickCount={20}
+                stroke="#FFFFFF"
+              />
+              <YAxis
+                type="number"
+                tickFormatter={formatBytes}
+                stroke="#FFFFFF"
+              />
+              <Tooltip
+                formatter={(value) => formatBytes(Number(value))}
+                offset={150}
+              />
+
+              {/* Creates an array from the key:value pairs in chartColors.
+             The keys that are not disabled becomes a line */}
+              {_.toPairs(testColors)
+                .filter((pair) => !_.includes(disabled, pair[0]))
+                .map((pair) => (
+                  <Line
+                    type="monotone"
+                    dataKey={pair[0]}
+                    key={pair[0]}
+                    stroke={pair[1]}
+                    dot={false}
+                  />
+                ))}
+              {/* Filters stageTimes to only include stages that are in validStages and only the first instance of them.
+                  en creates reference lines from the data that remains */}
+
+              {_.forEach(_.uniqBy(stageTimes, 'stage'))
+                ?.filter((stage) => _.includes(validStages, stage.stage))
+                ?.map((item) => (
+                  <ReferenceLine
+                    x={item.startTime}
+                    stroke="#769CFF"
+                    key={_.uniqueId('ref_')}
+                  >
+                    <Label
+                      value={item.stage}
+                      position="top"
+                      style={{ color: '#FFFFFF' }}
+                    />
+                  </ReferenceLine>
+                ))}
+            </LineChart>
+          </ResponsiveContainer>
+        </div>
+      ) : (
+        <div>
+          {chartData ? (
+            <Typography variant="subtitle1" style={{ padding: '8px 16px' }}>
+              There are not enough data points to show a meaningful graph
+            </Typography>
+          ) : (
+            <Typography variant="subtitle1" style={{ padding: '8px 16px' }}>
+              No data
+            </Typography>
+          )}
+        </div>
+      )}
     </div>
   );
 }
diff --git a/app/components/OptimizerTrace.tsx b/app/components/OptimizerTrace.tsx
index cb2f869e48bb67e5f11a4768dc369b8ab2919b4c..529d9c92e69700564767120bbaa740f4cdb5f672 100644
--- a/app/components/OptimizerTrace.tsx
+++ b/app/components/OptimizerTrace.tsx
@@ -1,21 +1,86 @@
-import React, { useContext } from 'react';
+import { Button, Typography } from '@material-ui/core';
+import React, { useState, useContext } from 'react';
 import ReactJson from 'react-json-view';
+import { makeStyles } from '@material-ui/core/styles';
 import { RecordingContext } from '../context/RecordingContext';
 
 export default function OptimizerTrace() {
+  const [expandAll, setExpandAll] = useState<boolean | undefined>(undefined);
+
+  function getHeight() {
+    if (typeof expandAll === 'undefined' || expandAll) return 211;
+    return 'auto';
+  }
+
+  const useStyles = makeStyles((theme) => ({
+    container: {
+      resize: 'vertical',
+      overflowY: 'scroll',
+      minHeight: 70,
+    },
+    header: {
+      display: 'flex',
+      flexDirection: 'row',
+      paddingBottom: theme.spacing(1),
+      paddingLeft: theme.spacing(1),
+      paddingRight: theme.spacing(1),
+      justifyContent: 'space-between',
+      position: 'sticky',
+      top: 0,
+      zIndex: 1,
+      backgroundColor: '#4B4B4B',
+    },
+    trace: { zIndex: 0 },
+  }));
+
   const recordingContext = useContext(RecordingContext);
-  const optimizerTrace = recordingContext?.optimizerTrace;
+  const optimizerTrace = recordingContext?.activeRecording?.optimizerTrace;
+
+  function handleButtonClick() {
+    if (typeof expandAll === 'undefined') {
+      setExpandAll(true);
+      return;
+    }
+    setExpandAll(!expandAll);
+  }
+
+  function getCollapseLevel() {
+    if (typeof expandAll === 'undefined') return 5;
+    return !expandAll;
+  }
+
+  const classes = useStyles();
+
   return (
-    <div>
-      {optimizerTrace && (
-        <ReactJson
-          src={optimizerTrace}
-          theme="twilight"
-          name="OptimizerTrace"
-          collapsed={5}
-          indentWidth={2}
-        />
-      )}
+    <div
+      style={{
+        height: getHeight(),
+      }}
+      className={classes.container}
+    >
+      <div className={classes.header}>
+        <Typography variant="h6">Optimizer trace</Typography>
+        <div>
+          <Button
+            variant="contained"
+            color="primary"
+            onClick={() => handleButtonClick()}
+          >
+            {expandAll ? 'Collapse all' : 'Expand all'}
+          </Button>
+        </div>
+      </div>
+      <div className={classes.trace}>
+        {optimizerTrace && (
+          <ReactJson
+            src={optimizerTrace}
+            theme="twilight"
+            name="OptimizerTrace"
+            collapsed={getCollapseLevel()}
+            indentWidth={2}
+          />
+        )}
+      </div>
     </div>
   );
 }
diff --git a/app/components/QueryRecorder.tsx b/app/components/QueryRecorder.tsx
index 1889d6e910f17e8e3eceef27ad52cd5b7b8457a2..4b21aad916a989e76800be4dcf6bdc6a5eceb4e3 100644
--- a/app/components/QueryRecorder.tsx
+++ b/app/components/QueryRecorder.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/jsx-wrap-multilines */
 import React, { useState, useContext } from 'react';
 import { makeStyles } from '@material-ui/core/styles';
 import {
@@ -9,23 +8,28 @@ import {
   FormControlLabel,
   Checkbox,
   CircularProgress,
+  Slider,
+  IconButton,
+  Icon,
+  Snackbar,
 } from '@material-ui/core';
+import Switch from '@material-ui/core/Switch';
+import Collapse from '@material-ui/core/Collapse';
 import clsx from 'clsx';
+import { v4 as uuid } from 'uuid';
 import { RecordingContext } from '../context/RecordingContext';
-import ResultJson from './ResultJson';
 import SqlManagerSingleton from '../../backend/recorder/SqlManagerSingleton';
+import { RawRecording } from '../../backend/recorder/SqlManager';
+import ColorPicker from './ColorPicker';
 
 const useStyles = makeStyles((theme) => ({
   container: {
-    maxWidth: 600 + theme.spacing(4),
+    // maxWidth: 600 + theme.spacing(4),
   },
   content: {
     display: 'flex',
     flexFlow: 'column nowrap',
-    paddingTop: theme.spacing(1),
-    paddingBottom: theme.spacing(1),
-    paddingLeft: theme.spacing(2),
-    paddingRight: theme.spacing(2),
+    padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`,
   },
   element: {
     marginBottom: theme.spacing(1),
@@ -46,41 +50,126 @@ const useStyles = makeStyles((theme) => ({
   code: {
     fontFamily: 'Roboto Mono !important',
   },
+  slider: {
+    marginRight: theme.spacing(2),
+    marginLeft: theme.spacing(2),
+  },
 }));
 
+const defaultTimestep = 200; // ms
+
 export default function QueryRecorder() {
-  const [changedQuery, setChangedQuery] = useState('');
-  const [output, setOutput] = useState('');
+  const [query, setQuery] = useState('');
   const [explainAnalyze, setExplainAnalyze] = useState(false);
+  const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
+  const [timeStep, setTimeStepValue] = useState(
+    (defaultTimestep as number) / 1000
+  );
+  const [recordingLabel, setRecordingLabel] = useState('');
+  const [recordingColor, setRecordingColor] = useState('');
   const [waitRecording, setWaitRecording] = useState(false);
+  const [userQueryIsRunning, setUserQueryIsRunning] = useState(false);
+  const [managerState, setManagerState] = useState('');
+  const [donePopupOpen, setDonePopupOpen] = useState(false);
+  const [recordingTime, setRecordingTime] = useState(0);
 
   const manager = SqlManagerSingleton.getInstance();
   const recordingContext = useContext(RecordingContext);
 
+  function updateContext(
+    result: RawRecording,
+    elapsed: number,
+    label: string,
+    color: string,
+    error?: string
+  ) {
+    const uniqueID = uuid();
+    recordingContext?.recordingListItems?.forEach((listItem) => {
+      listItem.viewing = false;
+    });
+    const existingListItems = recordingContext?.recordingListItems || [];
+    const existingRecordings = recordingContext?.recordings || [];
+    const newRecording = {
+      queryOutput: result.result,
+      error,
+      stageTimes: !error ? result.stageTimes : undefined,
+      chartData: !error ? result.memoryPerformance : undefined,
+      optimizerTrace: !error ? result.optimizerTrace : undefined,
+      chartColors: !error ? result.chartColors : undefined,
+      explainAnalyze:
+        explainAnalyze || query.toLowerCase().includes('explain analyze'),
+      explainAnalyzeTree: !error ? result.explainAnalyze : undefined,
+      label,
+      uuid: uniqueID,
+    };
+    const newListItem = {
+      query,
+      elapsed,
+      label,
+      color,
+      uuid: uniqueID,
+      viewing: true,
+    };
+    recordingContext?.setActiveRecording(newRecording);
+    recordingContext?.setRecordingListItems([
+      newListItem,
+      ...existingListItems,
+    ]);
+    recordingContext?.setRecordings([newRecording, ...existingRecordings]);
+    setDonePopupOpen(true);
+  }
+
   async function record() {
     setWaitRecording(true);
-    if (!manager) {
-      console.log('not connected');
+    if (!manager.client) {
+      console.error('QueryRecorder: Not connected.');
+      setWaitRecording(false);
       return;
     }
-
     try {
-      const result = await manager.record(changedQuery, explainAnalyze);
-      if (result?.error) {
-        console.log(`QueryRecorder: Result: ${result}`);
-        recordingContext?.setQueryOutput(result.result.error);
-        const errorcode = JSON.stringify(result.error.info.code, undefined, 2);
-        const errormsg = JSON.stringify(result.error.info.msg, undefined, 2);
-        setOutput(`Errorcode: ${errorcode}\n${errormsg}`);
+      // TODO: Get time elapsed from result
+      const t0 = performance.now();
+      const result = await manager.record(
+        query,
+        setUserQueryIsRunning,
+        timeStep,
+        explainAnalyze,
+        setManagerState
+      );
+      const t1 = performance.now();
+      setRecordingTime(t1 - t0);
+      if (result.error === 'cancelled') {
+        updateContext(
+          result,
+          t1 - t0,
+          recordingLabel,
+          recordingColor,
+          'Query was cancelled'
+        );
+      } else if (result?.error) {
+        let error;
+        if (result.error?.info) {
+          const errorcode = JSON.stringify(
+            result?.error?.info?.code,
+            undefined,
+            2
+          );
+          const errormsg = JSON.stringify(
+            result?.error?.info?.msg,
+            undefined,
+            2
+          );
+          error = `Errorcode: ${errorcode}\n${errormsg}`;
+        } else {
+          error = String(result.error);
+        }
+        updateContext(result, t1 - t0, recordingLabel, recordingColor, error);
       } else {
-        recordingContext?.setQueryOutput(result.result);
-        setOutput(JSON.stringify(result?.result, undefined, 2));
-        recordingContext?.setOptimizerTrace(result.optimizerTrace);
-        recordingContext?.setChartData(result.memoryPerformance);
-        recordingContext?.setStageTimes(result.stageTimes);
+        updateContext(result, t1 - t0, recordingLabel, recordingColor);
       }
+      setRecordingLabel('');
     } catch (error) {
-      console.log(`QueryRecorder: Error: ${error}`);
+      console.error(`QueryRecorder: Error: ${error}`);
     } finally {
       setWaitRecording(false);
     }
@@ -88,12 +177,35 @@ export default function QueryRecorder() {
 
   const classes = useStyles();
 
-  function handleKeyUp(e: { key: string; ctrlKey: boolean }) {
+  function checkShortcut(e: { key: string; ctrlKey: boolean }) {
     if (e.key === 'Enter' && e.ctrlKey) {
       record();
     }
   }
 
+  function handleSliderChange(_: unknown, newTimeStep: number | number[]) {
+    setTimeStepValue((newTimeStep as number) / 1000);
+  }
+
+  function getSliderValueText(value: number) {
+    return `${value} ms`;
+  }
+
+  const marks = [
+    {
+      value: 100,
+      label: '100 ms',
+    },
+    {
+      value: 1000,
+      label: '1000 ms',
+    },
+  ];
+
+  function toggleAdvancedOptions() {
+    setShowAdvancedOptions((prev) => !prev);
+  }
+
   return (
     <div className={classes.container}>
       <Paper className={classes.content} elevation={1}>
@@ -113,9 +225,9 @@ export default function QueryRecorder() {
             label="Query"
             rows={2}
             rowsMax={10}
-            value={changedQuery}
-            onChange={(event) => setChangedQuery(event.target.value)}
-            onKeyUp={handleKeyUp}
+            value={query}
+            onChange={(event) => setQuery(event.target.value)}
+            onKeyUp={checkShortcut}
             helperText="Ctrl + Enter: Run"
           />
         </div>
@@ -131,6 +243,60 @@ export default function QueryRecorder() {
             label="Explain analyze"
           />
         </div>
+        <div className={classes.element}>
+          <FormControlLabel
+            control={
+              <Switch
+                checked={showAdvancedOptions}
+                onChange={toggleAdvancedOptions}
+                onKeyUp={(event) =>
+                  event.key === 'Enter' && toggleAdvancedOptions()
+                }
+                color="primary"
+                inputProps={{ 'aria-label': 'primary checkbox' }}
+              />
+            }
+            label="Advanced options"
+          />
+        </div>
+        <div className={classes.element}>
+          <Collapse in={showAdvancedOptions}>
+            <div className={classes.element}>
+              <div>
+                <Typography variant="subtitle1">
+                  Recording timestep (ms)
+                </Typography>
+              </div>
+              <div className={classes.slider}>
+                <Slider
+                  defaultValue={defaultTimestep}
+                  aria-labelledby="discrete-slider-small-steps"
+                  getAriaValueText={getSliderValueText}
+                  onChange={handleSliderChange}
+                  step={50}
+                  marks={marks}
+                  min={100}
+                  max={1000}
+                  valueLabelDisplay="auto"
+                />
+              </div>
+            </div>
+            <div className={classes.element}>
+              <TextField
+                variant="filled"
+                label="Recording label"
+                value={recordingLabel}
+                onChange={(event) => setRecordingLabel(event.target.value)}
+                onKeyUp={checkShortcut}
+              />
+            </div>
+            <ColorPicker
+              currentColor={recordingColor}
+              setColor={setRecordingColor}
+            />
+          </Collapse>
+        </div>
+
         <div className={clsx(classes.element, classes.recordButtonWrapper)}>
           <Button
             disabled={waitRecording}
@@ -140,6 +306,16 @@ export default function QueryRecorder() {
           >
             Record
           </Button>
+          <Button
+            style={{ margin: '8px' }}
+            disabled={!userQueryIsRunning}
+            variant="outlined"
+            color="primary"
+            onClick={() => manager.cancelQuery()}
+          >
+            Cancel
+          </Button>
+
           {waitRecording && (
             <CircularProgress
               style={{ marginLeft: '16px' }}
@@ -148,9 +324,30 @@ export default function QueryRecorder() {
             />
           )}
         </div>
-        <div className={clsx(classes.element, classes.input)}>
-          <ResultJson result={output} />
-        </div>
+        {managerState.length > 0 && (
+          <div className={classes.element}>
+            <Typography variant="subtitle2">{managerState}</Typography>
+          </div>
+        )}
+        <Snackbar
+          anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
+          autoHideDuration={6000}
+          open={donePopupOpen}
+          onClose={() => setDonePopupOpen(false)}
+          message={`Recording finished in ${recordingTime.toFixed(1)} ms`}
+          action={
+            <>
+              <IconButton
+                size="small"
+                aria-label="close"
+                color="inherit"
+                onClick={() => setDonePopupOpen(false)}
+              >
+                <Icon>close</Icon>
+              </IconButton>
+            </>
+          }
+        />
       </Paper>
     </div>
   );
diff --git a/app/components/RecordingOptions/ChangeColor.tsx b/app/components/RecordingOptions/ChangeColor.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a666fe53a8c619d5da1d29cac47f1c16c65c29f1
--- /dev/null
+++ b/app/components/RecordingOptions/ChangeColor.tsx
@@ -0,0 +1,58 @@
+import React, { useState, useContext } from 'react';
+import { Dialog, DialogTitle, Button, MenuItem } from '@material-ui/core';
+import { RecordingContext } from '../../context/RecordingContext';
+import ColorPicker from '../ColorPicker';
+
+export default function ChangeColor(props: {
+  recordinguuid: string;
+  handleIconMenuClose: () => void;
+}) {
+  const { recordinguuid, handleIconMenuClose } = props;
+
+  const [changedColor, setChangedColor] = useState('');
+  const [changeColorDialogOpen, setChangeColorDialogOpen] = useState(false);
+
+  const recordingContext = useContext(RecordingContext);
+  const recordingListItems = recordingContext?.recordingListItems || [];
+
+  function handleChangeColorDialogClose() {
+    setChangeColorDialogOpen(false);
+  }
+
+  function handleChangeColorOptionClick() {
+    handleIconMenuClose();
+    setChangeColorDialogOpen(true);
+  }
+
+  function handleChangeColorButtonClick() {
+    const newRecordingListItems = [...recordingListItems];
+    newRecordingListItems.forEach((r) => {
+      if (r.uuid === recordinguuid) {
+        r.color = changedColor;
+      }
+    });
+    recordingContext?.setRecordingListItems(newRecordingListItems);
+
+    handleChangeColorDialogClose();
+  }
+
+  return (
+    <div>
+      <MenuItem onClick={handleChangeColorOptionClick}>Change color</MenuItem>
+      <Dialog
+        onClose={handleChangeColorDialogClose}
+        open={changeColorDialogOpen}
+      >
+        <DialogTitle>Change color</DialogTitle>
+        <ColorPicker currentColor={changedColor} setColor={setChangedColor} />
+        <Button
+          variant="contained"
+          color="primary"
+          onClick={() => handleChangeColorButtonClick()}
+        >
+          Change
+        </Button>
+      </Dialog>
+    </div>
+  );
+}
diff --git a/app/components/RecordingOptions/ChangeLabel.tsx b/app/components/RecordingOptions/ChangeLabel.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..612e07c209b549b507667a520530e615f7412957
--- /dev/null
+++ b/app/components/RecordingOptions/ChangeLabel.tsx
@@ -0,0 +1,77 @@
+import React, { useState, useContext } from 'react';
+import {
+  Dialog,
+  DialogTitle,
+  TextField,
+  Button,
+  MenuItem,
+} from '@material-ui/core';
+import { RecordingContext } from '../../context/RecordingContext';
+
+export default function ChangeLabel(props: {
+  recordinguuid: string;
+  handleIconMenuClose: () => void;
+}) {
+  const { recordinguuid, handleIconMenuClose } = props;
+  const [changedLabel, setChangedLabel] = useState('');
+  const [changeLabelDialogOpen, setChangeLabelDialogOpen] = useState(false);
+
+  const recordingContext = useContext(RecordingContext);
+  const recordingListItems = recordingContext?.recordingListItems || [];
+  const recordings = recordingContext?.recordings || [];
+
+  function handleChangeLabelDialogClose() {
+    setChangeLabelDialogOpen(false);
+  }
+
+  function handleChangeLabelOptionClick() {
+    handleIconMenuClose();
+    setChangeLabelDialogOpen(true);
+  }
+
+  function handleChangeLabelButtonClick() {
+    const newRecordingListItems = [...recordingListItems];
+    newRecordingListItems.forEach((r) => {
+      if (r.uuid === recordinguuid) {
+        r.label = changedLabel;
+      }
+    });
+    recordingContext?.setRecordingListItems(newRecordingListItems);
+
+    const newRecordings = [...recordings];
+    newRecordings.forEach((r) => {
+      if (r.uuid === recordinguuid) {
+        r.label = changedLabel;
+      }
+    });
+    recordingContext?.setRecordings(newRecordings);
+
+    handleChangeLabelDialogClose();
+  }
+
+  return (
+    <div>
+      <MenuItem onClick={handleChangeLabelOptionClick}>Change label</MenuItem>
+      <Dialog
+        onClose={handleChangeLabelDialogClose}
+        open={changeLabelDialogOpen}
+      >
+        <DialogTitle>Change label</DialogTitle>
+        <TextField
+          autoFocus
+          variant="filled"
+          label="New label"
+          value={changedLabel}
+          onChange={(event) => setChangedLabel(event.target.value)}
+        />
+        <Button
+          variant="contained"
+          color="primary"
+          onClick={() => handleChangeLabelButtonClick()}
+        >
+          Change
+        </Button>
+      </Dialog>
+    </div>
+  );
+}
diff --git a/app/components/RecordingOptions/RecordingOptions.tsx b/app/components/RecordingOptions/RecordingOptions.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..785c3e351ff144fb18401fc65465074eaa580889
--- /dev/null
+++ b/app/components/RecordingOptions/RecordingOptions.tsx
@@ -0,0 +1,93 @@
+import React, { useState, useContext } from 'react';
+import {
+  Icon,
+  IconButton,
+  Menu,
+  MenuItem,
+  ListItemIcon,
+} from '@material-ui/core';
+import ChangeLabel from './ChangeLabel';
+import ChangeColor from './ChangeColor';
+import { RecordingContext } from '../../context/RecordingContext';
+
+export default function RecordingOptions(props: { recordinguuid: string }) {
+  const [iconAnchorEl, setIconAnchorEl] = useState<null | HTMLElement>(null);
+
+  const { recordinguuid } = props;
+
+  const recordingContext = useContext(RecordingContext);
+  const recordingListItems = recordingContext?.recordingListItems || [];
+  const recordings = recordingContext?.recordings || [];
+
+  function handleIconButtonClick(event: React.MouseEvent<HTMLButtonElement>) {
+    setIconAnchorEl(event.currentTarget);
+  }
+
+  function handleIconMenuClose() {
+    setIconAnchorEl(null);
+  }
+
+  function handleCopyQuery() {
+    const recordingListItem = recordingListItems.find(
+      (r) => r.uuid === recordinguuid
+    );
+    navigator.clipboard.writeText(recordingListItem?.query || '');
+    handleIconMenuClose();
+  }
+
+  function handleRemoveRecording() {
+    const activeRecordinguuid = recordingContext?.activeRecording?.uuid || '';
+    recordingContext?.setRecordingListItems(
+      recordingListItems.filter((r) => r.uuid !== recordinguuid)
+    );
+    recordingContext?.setRecordings(
+      recordings.filter((r) => r.uuid !== recordinguuid)
+    );
+    if (activeRecordinguuid === recordinguuid) {
+      const newActiveRecording = recordings.find(
+        (r) => r.uuid !== recordinguuid
+      );
+      recordingContext?.setActiveRecording(newActiveRecording);
+      if (newActiveRecording) {
+        const newActiveRecordingListItem = recordingListItems.find(
+          (r) => r.uuid === newActiveRecording.uuid
+        );
+        if (newActiveRecordingListItem) {
+          newActiveRecordingListItem.viewing = true;
+        }
+      }
+    }
+    handleIconMenuClose();
+  }
+
+  return (
+    <ListItemIcon>
+      <IconButton
+        edge="end"
+        aria-controls="icon-menu"
+        aria-haspopup="true"
+        onClick={(event) => handleIconButtonClick(event)}
+      >
+        <Icon>more_vert</Icon>
+      </IconButton>
+      <Menu
+        id="icon-menu"
+        anchorEl={iconAnchorEl}
+        keepMounted
+        open={Boolean(iconAnchorEl)}
+        onClose={handleIconMenuClose}
+      >
+        <ChangeLabel
+          recordinguuid={recordinguuid}
+          handleIconMenuClose={handleIconMenuClose}
+        />
+        <ChangeColor
+          recordinguuid={recordinguuid}
+          handleIconMenuClose={handleIconMenuClose}
+        />
+        <MenuItem onClick={handleCopyQuery}>Copy query</MenuItem>
+        <MenuItem onClick={handleRemoveRecording}>Remove</MenuItem>
+      </Menu>
+    </ListItemIcon>
+  );
+}
diff --git a/app/components/Recordings.tsx b/app/components/Recordings.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..7efb8e002c06b1d1ee2620ef5488b26bd97aebb5
--- /dev/null
+++ b/app/components/Recordings.tsx
@@ -0,0 +1,308 @@
+import {
+  Button,
+  ListItem,
+  ListItemText,
+  makeStyles,
+  Paper,
+  TextField,
+  Tooltip,
+  Typography,
+} from '@material-ui/core';
+import InputAdornment from '@material-ui/core/InputAdornment';
+import SearchIcon from '@material-ui/icons/Search';
+import React, { CSSProperties, useContext, useState } from 'react';
+import { FixedSizeList } from 'react-window';
+import useDeepCompareEffect from 'use-deep-compare-effect';
+import { RecordingContext } from '../context/RecordingContext';
+import RecordingOptions from './RecordingOptions/RecordingOptions';
+
+const useStyles = makeStyles((theme) => ({
+  wrapper: {
+    // maxWidth: 600 + theme.spacing(4),
+  },
+  content: {
+    display: 'flex',
+    flexFlow: 'column nowrap',
+    padding: `${theme.spacing(1)}px ${theme.spacing(2)}px`,
+  },
+  listItemTextPrimary: {
+    textOverflow: 'ellipsis',
+    whiteSpace: 'nowrap',
+    overflow: 'hidden',
+    // textAlign: 'left',
+  },
+  listItemTextRoot: {
+    margin: 0,
+    padding: '12px 0px',
+  },
+  listItemRoot: {
+    padding: 0,
+  },
+}));
+
+export default function Recordings() {
+  const recordingContext = useContext(RecordingContext);
+  const recordings = recordingContext?.recordingListItems || [];
+
+  const [search, setSearch] = useState('');
+  const [displayedRecordings, setDisplayedRecordings] = useState(recordings);
+
+  const [activeRecording, setActiveRecording] = useState<string | undefined>(
+    recordings.find((recording) => recording.viewing)?.uuid
+  );
+
+  useDeepCompareEffect(() => {
+    // Using useDeepCompareEffect because we want this to happen when 'recordings'
+    // changes, even if it is an array (useEffect only supports primitive values)
+    const newActiveRecording = recordings.find((recording) => recording.viewing)
+      ?.uuid;
+    setActiveRecording(newActiveRecording);
+    setDisplayedRecordings(recordings);
+  }, [recordings]);
+
+  const classes = useStyles();
+
+  function handleRecordingClick(index: number) {
+    if (index < displayedRecordings.length) {
+      const { uuid } = displayedRecordings[index];
+      const originalIndex = recordings.findIndex(
+        (value) => value.uuid === uuid
+      );
+      const newRecordingListItems = recordings;
+      if (activeRecording) {
+        const activeItemIndex = newRecordingListItems.findIndex(
+          (value) => value.uuid === activeRecording
+        );
+        newRecordingListItems[activeItemIndex].viewing = false;
+      }
+      newRecordingListItems[originalIndex].viewing = true;
+      recordingContext?.setRecordingListItems(newRecordingListItems);
+      setActiveRecording(uuid);
+      if (recordingContext?.recordings) {
+        recordingContext?.setActiveRecording(
+          recordingContext.recordings[originalIndex]
+        );
+      }
+    }
+  }
+
+  function handleSaveClick(index: number) {
+    // TODO: Implement
+    console.log(`Clicked ${index} button`);
+  }
+
+  function formatElapsed(elapsed: number) {
+    // Assuming a query will not run for longer than a few hours
+    if (elapsed > 1000 * 60 * 60) {
+      return `${(elapsed / (1000 * 60 * 60)).toFixed(1)} h`;
+    }
+    if (elapsed > 1000 * 60) {
+      return `${(elapsed / (1000 * 60)).toFixed(1)} min`;
+    }
+    if (elapsed > 1000) {
+      return `${(elapsed / 1000).toFixed(1)} s`;
+    }
+    return `${elapsed.toFixed(1)} ms`;
+  }
+
+  function filterRecordings() {
+    const filtered = recordings.filter((recording) =>
+      recording.label.toLowerCase().includes(search.toLowerCase())
+    );
+    setDisplayedRecordings(filtered);
+  }
+
+  function checkShortcut(e: { key: string }) {
+    if (e.key === 'Enter') {
+      filterRecordings();
+    }
+  }
+
+  function renderRow(props: { index: number; style: CSSProperties }) {
+    const { index, style } = props;
+    const recording = displayedRecordings[index];
+
+    return (
+      <ListItem
+        classes={{ root: classes.listItemRoot }}
+        style={style}
+        key={index}
+        button
+        selected={recording.uuid === activeRecording}
+      >
+        <Tooltip title={index + 1}>
+          <ListItemText
+            primary={index + 1}
+            classes={{
+              primary: classes.listItemTextPrimary,
+              root: classes.listItemTextRoot,
+            }}
+            style={{
+              flexGrow: 1,
+              flexShrink: 1,
+              flexBasis: 30,
+              height: 30,
+              padding: 0,
+              marginRight: 4,
+              backgroundColor: recording.color,
+              borderRadius: 100,
+              textAlign: 'center',
+              display: 'flex',
+              flexFlow: 'column nowrap',
+              justifyContent: 'center',
+            }}
+            onClick={() => handleRecordingClick(index)}
+          />
+        </Tooltip>
+        <Tooltip title={recording.label}>
+          <ListItemText
+            primary={recording.label}
+            classes={{
+              primary: classes.listItemTextPrimary,
+              root: classes.listItemTextRoot,
+            }}
+            style={{
+              flexGrow: 4,
+              flexShrink: 1,
+              flexBasis: 226,
+              paddingRight: 4,
+            }}
+            onClick={() => handleRecordingClick(index)}
+          />
+        </Tooltip>
+        <Tooltip title={formatElapsed(recording.elapsed)}>
+          <ListItemText
+            primary={formatElapsed(recording.elapsed)}
+            classes={{
+              primary: classes.listItemTextPrimary,
+              root: classes.listItemTextRoot,
+            }}
+            style={{
+              flexGrow: 2,
+              flexShrink: 1,
+              flexBasis: 70,
+              paddingRight: 4,
+            }}
+            onClick={() => handleRecordingClick(index)}
+          />
+        </Tooltip>
+        <Tooltip title={recording.query}>
+          <ListItemText
+            primary={recording.query}
+            classes={{
+              primary: classes.listItemTextPrimary,
+              root: classes.listItemTextRoot,
+            }}
+            style={{ flexGrow: 1, flexShrink: 1, flexBasis: 150 }}
+            onClick={() => handleRecordingClick(index)}
+          />
+        </Tooltip>
+        <Button
+          variant="outlined"
+          color="primary"
+          onClick={() => handleSaveClick(index)}
+        >
+          Save
+        </Button>
+        <RecordingOptions recordinguuid={recording.uuid} />
+      </ListItem>
+    );
+  }
+
+  return (
+    <div className={classes.wrapper}>
+      <Paper className={classes.content} elevation={1}>
+        <Typography variant="h5">Recordings</Typography>
+        <TextField
+          id="outlined-search"
+          label="Search label..."
+          type="search"
+          variant="outlined"
+          value={search}
+          onChange={(event) => setSearch(event.target.value)}
+          onKeyUp={checkShortcut}
+          InputProps={{
+            endAdornment: (
+              <InputAdornment position="start">
+                <SearchIcon />
+              </InputAdornment>
+            ),
+          }}
+          style={{
+            marginTop: 8,
+            width: 170,
+          }}
+        />
+        <ListItem classes={{ root: classes.listItemRoot }}>
+          <ListItemText
+            primary="#"
+            classes={{
+              primary: classes.listItemTextPrimary,
+              root: classes.listItemTextRoot,
+            }}
+            style={{
+              flexGrow: 1,
+              flexShrink: 1,
+              flexBasis: 30,
+              height: 30,
+              padding: 0,
+              marginRight: 4,
+              borderRadius: 100,
+              textAlign: 'center',
+              display: 'flex',
+              flexFlow: 'column nowrap',
+              justifyContent: 'center',
+            }}
+          />
+          <ListItemText
+            primary="Label"
+            classes={{
+              primary: classes.listItemTextPrimary,
+              root: classes.listItemTextRoot,
+            }}
+            style={{
+              flexGrow: 4,
+              flexShrink: 1,
+              flexBasis: 226,
+              paddingRight: 4,
+            }}
+          />
+          <ListItemText
+            primary="Elapsed"
+            classes={{
+              primary: classes.listItemTextPrimary,
+              root: classes.listItemTextRoot,
+            }}
+            style={{
+              flexGrow: 2,
+              flexShrink: 1,
+              flexBasis: 70,
+              paddingRight: 4,
+            }}
+          />
+          <ListItemText
+            primary="Query"
+            classes={{
+              primary: classes.listItemTextPrimary,
+              root: classes.listItemTextRoot,
+            }}
+            style={{
+              flexGrow: 1,
+              flexShrink: 1,
+              flexBasis: 150,
+            }}
+          />
+          <div style={{ minWidth: 120, maxWidth: 120, height: 48 }} />
+        </ListItem>
+        <FixedSizeList
+          height={400}
+          width="100%"
+          itemSize={48}
+          itemCount={displayedRecordings.length}
+        >
+          {renderRow}
+        </FixedSizeList>
+      </Paper>
+    </div>
+  );
+}
diff --git a/app/components/ResultJson.tsx b/app/components/ResultJson.tsx
index 9cdb8ebb1f3c72ba951a1f46bc2a3d59bb621418..0a7bf7b10de339f66580516b9cbdf554501f33d0 100644
--- a/app/components/ResultJson.tsx
+++ b/app/components/ResultJson.tsx
@@ -1,41 +1,44 @@
 import React from 'react';
 import { makeStyles } from '@material-ui/core/styles';
 import { TextField } from '@material-ui/core';
+
 import clsx from 'clsx';
 
 interface TResultProps {
   result: string;
+  textColor: string;
 }
 
-const useStyles = makeStyles((theme) => ({
-  container: {
-    maxWidth: 600 + theme.spacing(4),
-  },
-  content: {
-    display: 'flex',
-    flexFlow: 'column nowrap',
-    paddingTop: theme.spacing(1),
-    paddingBottom: theme.spacing(1),
-    paddingLeft: theme.spacing(2),
-    paddingRight: theme.spacing(2),
-  },
-  element: {
-    marginBottom: theme.spacing(1),
-  },
-  input: {
-    display: 'flex',
-    flexFlow: 'column nowrap',
-    flexGrow: 1,
-  },
-  result: {
-    userSelect: 'none',
-  },
-  code: {
-    fontFamily: 'Roboto Mono !important',
-  },
-}));
-
 export default function ResultJson(props: TResultProps) {
+  const useStyles = makeStyles((theme) => ({
+    container: {
+      maxWidth: 600 + theme.spacing(4),
+    },
+    content: {
+      display: 'flex',
+      flexFlow: 'column nowrap',
+      paddingTop: theme.spacing(1),
+      paddingBottom: theme.spacing(1),
+      paddingLeft: theme.spacing(2),
+      paddingRight: theme.spacing(2),
+    },
+    element: {
+      marginBottom: theme.spacing(1),
+    },
+    input: {
+      display: 'flex',
+      flexFlow: 'column nowrap',
+      flexGrow: 1,
+    },
+    result: {
+      userSelect: 'none',
+    },
+    code: {
+      fontFamily: 'Roboto Mono !important',
+      color: props.textColor,
+    },
+  }));
+
   const classes = useStyles();
   const { result } = props;
   return (
diff --git a/app/components/ResultTable.tsx b/app/components/ResultTable.tsx
index 90a9675ca10cd8cbdb7418eca66bfe2ef01af666..57638a6dbb93cc492d4434fc3c35859efe745ee1 100644
--- a/app/components/ResultTable.tsx
+++ b/app/components/ResultTable.tsx
@@ -1,39 +1,60 @@
-/* eslint-disable prefer-template */
 import React, { useContext } from 'react';
-import {
-  makeStyles,
-  withStyles,
-  createStyles,
-  Theme,
-} from '@material-ui/core/styles';
-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 TableRow from '@material-ui/core/TableRow';
-import Paper from '@material-ui/core/Paper';
-import { ResultValue } from '@mysql/xdevapi';
+import { makeStyles, withStyles, createStyles } from '@material-ui/core/styles';
+import { TableCell, Typography } from '@material-ui/core';
+import { AutoSizer, Column, Table } from 'react-virtualized';
+import { uniqueId } from 'lodash';
+import clsx from 'clsx';
 import { RecordingContext } from '../context/RecordingContext';
 
-// interface TResultProps {
-//   result: TResult | undefined;
-// }
-
-const useStyles = makeStyles({
-  table: {
-    minWidth: 650,
+const useStyles = makeStyles((theme) => ({
+  tableWrapper: {
+    minHeight: 200,
+    height: '100%',
+    width: '100%',
+    resize: 'vertical',
+    overflowY: 'scroll',
+    scrollbarWidth: 'none',
+    paddingBottom: theme.spacing(2),
+  },
+  flexContainer: {
+    display: 'flex',
+    alignItems: 'center',
+    boxSizing: 'border-box',
+  },
+  tableHeader: {
+    flex: '1 1 !important',
+    backgroundColor: '#404040',
+  },
+  tableRow: {
+    display: 'flex',
+    flex: '1 1 !important',
+    textOverflow: 'ellipsis',
+    whiteSpace: 'nowrap',
+    overflow: 'hidden',
+    '& .ReactVirtualized__Table__rowColumn': {
+      flex: '1 1 !important',
+      textOverflow: 'ellipsis',
+      whiteSpace: 'nowrap',
+      overflow: 'hidden',
+    },
+  },
+  tableRowHover: {
+    '&:hover': {
+      backgroundColor: '#404040',
+    },
   },
-  container: {
-    maxHeight: 211,
+  tableCell: {
+    flex: '1 1 !important',
+    textOverflow: 'ellipsis',
+    whiteSpace: 'nowrap',
+    overflow: 'hidden',
   },
-});
+}));
 
-const StyledTableCell = withStyles((theme: Theme) =>
+const StyledTableCell = withStyles(() =>
   createStyles({
     head: {
       backgroundColor: '#404040',
-      color: theme.palette.common.white,
     },
     body: {
       fontSize: 14,
@@ -46,66 +67,116 @@ export default function ResultTable() {
 
   const classes = useStyles();
 
-  const result = recordingContext?.queryOutput;
+  const result = recordingContext?.activeRecording?.queryOutput;
+
+  if (!result || !result.results) {
+    return (
+      <Typography style={{ padding: '8px 16px' }} variant="subtitle1">
+        No data to show
+      </Typography>
+    );
+  }
 
-  const tableHeader: string[] | undefined = result?.results?.labels;
-  const tableBody: ResultValue[][] | undefined = result?.results?.values;
-  const types: number[] | undefined = result?.results?.types;
+  const { labels, types, values } = result.results;
 
   function formatDate(d: Date) {
-    let month = '' + (d.getMonth() + 1);
-    let day = '' + d.getDate();
+    let month = `${d.getMonth() + 1}`;
+    let day = `${d.getDate()}`;
     const year = d.getFullYear();
 
-    if (month.length < 2) month = '0' + month;
-    if (day.length < 2) day = '0' + day;
+    if (month.length < 2) month = `0${month}`;
+    if (day.length < 2) day = `0${day}`;
 
     return [year, month, day].join('-');
   }
 
-  function format(index: number, entry: any): string {
+  function format(index: number, entry: unknown): string {
     let datatype = 0;
     if (types) {
       datatype = types[index];
     }
     if (datatype === 12) {
       if (typeof entry === 'number') {
-        const date = new Date(entry);
+        const date = new Date(Number(entry));
         return formatDate(date);
       }
-      const date = new Date(entry);
+      const date = new Date(String(entry));
       return formatDate(date);
     }
-    return entry;
+    return String(entry);
+  }
+
+  function headerRenderer(label: string, columnIndex: number) {
+    return (
+      <StyledTableCell
+        key={columnIndex}
+        component="div"
+        variant="head"
+        className={clsx(classes.tableCell, classes.flexContainer)}
+        style={{ height: 48 }}
+      >
+        {label}
+      </StyledTableCell>
+    );
+  }
+
+  function cellRenderer(rowIndex: number, columnIndex: number) {
+    return (
+      <TableCell
+        key={`${columnIndex}-${rowIndex}`}
+        component="div"
+        variant="body"
+        className={clsx(classes.tableCell, classes.flexContainer)}
+      >
+        {format(columnIndex, values[rowIndex][columnIndex])}
+      </TableCell>
+    );
   }
 
   return (
-    <TableContainer className={classes.container} component={Paper}>
-      <Table className={classes.table} aria-label="simple table" stickyHeader>
-        <TableHead>
-          <TableRow>
-            {tableHeader?.map((column) => (
-              <StyledTableCell key={column}>{column}</StyledTableCell>
-            ))}
-          </TableRow>
-        </TableHead>
-        <TableBody>
-          {tableBody?.map((row) => (
-            <TableRow key={row.join('-')}>
-              {row.map((cell) => (
-                <TableCell
-                  key={tableBody
-                    .indexOf(row)
-                    .toString()
-                    .concat(row.indexOf(cell).toString())}
-                >
-                  {format(row.indexOf(cell), cell)}
-                </TableCell>
+    <div className={classes.tableWrapper}>
+      <div
+        style={{
+          minHeight: 201,
+          height: '100%',
+          width: '100%',
+          paddingBottom: 1,
+        }}
+      >
+        <AutoSizer>
+          {({ height, width }) => (
+            <Table
+              height={height}
+              width={width}
+              rowHeight={48}
+              headerHeight={48}
+              gridStyle={{
+                direction: 'inherit',
+              }}
+              rowCount={values.length}
+              estimatedColumnSize={100}
+              rowGetter={({ index }) => values[Number(index)]}
+              rowClassName={({ index }) =>
+                clsx(classes.tableRow, classes.flexContainer, {
+                  [classes.tableRowHover]: index !== -1,
+                })
+              }
+            >
+              {labels?.map((label, index) => (
+                <Column
+                  key={`${label}+${uniqueId()}`}
+                  headerRenderer={() => headerRenderer(label, index)}
+                  headerClassName={classes.tableHeader}
+                  className={classes.flexContainer}
+                  cellRenderer={({ rowIndex }) => cellRenderer(rowIndex, index)}
+                  dataKey={label}
+                  width={100}
+                />
               ))}
-            </TableRow>
-          ))}
-        </TableBody>
-      </Table>
-    </TableContainer>
+            </Table>
+          )}
+        </AutoSizer>
+      </div>
+    </div>
   );
 }
diff --git a/app/components/TreeViewExplainAnalyze.tsx b/app/components/TreeViewExplainAnalyze.tsx
index 55261710a27b8893050683a83ee0b01ff94de916..2d80da70e94b33a2379ac5adad8c9e5dab9a589e 100644
--- a/app/components/TreeViewExplainAnalyze.tsx
+++ b/app/components/TreeViewExplainAnalyze.tsx
@@ -1,32 +1,11 @@
-import React, { useContext, useState } from 'react';
+import React, { useContext } from 'react';
 import { makeStyles } from '@material-ui/core/styles';
 import TreeView from '@material-ui/lab/TreeView';
 import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
 import ChevronRightIcon from '@material-ui/icons/ChevronRight';
 import TreeItem from '@material-ui/lab/TreeItem';
 import { RecordingContext } from '../context/RecordingContext';
-
-interface Node {
-  id: string;
-  offset: number;
-  command: string;
-  cost_est?: number;
-  rows_est?: number;
-  time: string;
-  rows: number;
-  loops: number;
-  children: Node[];
-}
-
-const rootNode: Node = {
-  id: '0',
-  offset: -1,
-  command: '',
-  time: '',
-  rows: -1,
-  loops: -1,
-  children: [],
-};
+import { ExplainAnalyzeNode } from '../../backend/data-processor/types/ExplainAnalyzeNode';
 
 const useStyles = makeStyles({
   root: {
@@ -34,88 +13,24 @@ const useStyles = makeStyles({
   },
 });
 
-/*
-  Helper function
-  INPUT: EXPLAIN ANALYZE data (string)
-  RETURN: List of Node objects
- */
-function extractData(data: string) {
-  rootNode.children = [];
-  const regexp = /([ ]*)?-> (.+?)( \(cost=([\d.]+) rows=(\d+)\))? \(actual time=([\d.]+) rows=(\d+) loops=(\d+)\)/gm;
-  const matches = [...data.matchAll(regexp)];
-  const nodes: Node[] = [rootNode];
-  // Create object for each line. Root has ID 0
-  let idIterator = 1;
-  matches.forEach((element) => {
-    const node: Node = {
-      id: idIterator.toString(),
-      offset: element[1] === undefined ? 0 : element[1].length,
-      command: element[2],
-      cost_est: Number(element[4]),
-      rows_est: Number(element[5]),
-      time: element[6],
-      rows: Number(element[7]),
-      loops: Number(element[8]),
-      children: [],
-    };
-    nodes.push(node);
-    idIterator += 1;
-  });
-  return nodes;
-}
-
-/*
-  INPUT: EXPLAIN ANALYZE data
-  ACTION: Assigns children of root node, and to all child nodes, based on offset and order
-  RETURN: List of Node objects
-*/
-function buildTree(explainAnalyzeData: string) {
-  // Extract relevant data
-  const nodes: Node[] = extractData(explainAnalyzeData);
-  // Assign children of each object
-  // Iterate through all objects
-  for (let i = 1; i < nodes.length; i += 1) {
-    // Set current node as child if its offset is smaller
-    if (nodes[i - 1].offset < nodes[i].offset) {
-      nodes[i - 1].children.push(nodes[i]);
-    } else {
-      // If current node's offset is smaller then it cannot be child of the previous node
-      // Iterate backwards from current node until a node with smaller offset has been reached
-      for (let j = i - 1; i > 0; j -= 1) {
-        if (nodes[j].offset < nodes[i].offset) {
-          nodes[j].children.push(nodes[i]);
-          break;
-        }
-      }
-    }
-  }
-}
-
 export default function RecursiveTreeView() {
   const recordingContext = useContext(RecordingContext);
+  const data = recordingContext?.activeRecording?.explainAnalyzeTree;
 
   const classes = useStyles();
-  const [previousData, setPreviousData] = useState('');
-  const data = JSON.stringify(recordingContext?.queryOutput, undefined, 2);
-  if (!data) return null;
-  // TODO: Only run when EXPLAIN ANALYZE detected
-  if (data !== previousData) {
-    buildTree(data);
-    setPreviousData(data);
-  }
 
-  const RenderTree = (nodes: Node) => (
-    <TreeItem key={nodes.id} nodeId={nodes.id} label={nodes.command}>
+  const RenderTree = (nodes: ExplainAnalyzeNode) => (
+    <TreeItem key={nodes.id} nodeId={nodes.id} label={nodes.name}>
       {Array.isArray(nodes.children)
         ? nodes.children.map((node) => RenderTree(node))
         : null}
     </TreeItem>
   );
 
-  const RenderTreeWithoutRoot = (root: Node) => {
+  const RenderTreeWithoutRoot = (root?: ExplainAnalyzeNode) => {
     // Renders only the children of root
     const elements: JSX.Element[] = [];
-    root.children.forEach((child) => {
+    root?.children.forEach((child) => {
       elements.push(RenderTree(child));
     });
     return elements;
@@ -129,7 +44,7 @@ export default function RecursiveTreeView() {
       defaultExpanded={['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10']}
       defaultExpandIcon={<ChevronRightIcon />}
     >
-      {[RenderTreeWithoutRoot(rootNode)]}
+      {[RenderTreeWithoutRoot(data)]}
     </TreeView>
   );
 }
diff --git a/app/containers/Dashboard.tsx b/app/containers/Dashboard.tsx
index 9142121b71a68a3ae4be50c081f08d8dcca83271..c719061ba04b177d61ccdf1430d987d0fcf23bf8 100644
--- a/app/containers/Dashboard.tsx
+++ b/app/containers/Dashboard.tsx
@@ -1,14 +1,16 @@
-import React, { useContext, useState } from 'react';
+import React, { useContext, useEffect, useState } from 'react';
 import { Button, Dialog, makeStyles } from '@material-ui/core';
 import { useLocation, useHistory } from 'react-router';
 import PanelGroup from 'react-panelgroup';
 import QueryRecorder from '../components/QueryRecorder';
-import { LoadingContext } from '../context/LoadingContext';
 import routes from '../routes';
 import Login from '../components/Login';
 import { LoginDetailsContext } from '../context/LoginDetailsContext';
 import { RecordingProvider } from '../context/RecordingContext';
 import ResultView from './ResultView';
+import Recordings from '../components/Recordings';
+import ErrorView from '../components/ErrorView';
+import IDInfo from '../components/IDInfo';
 
 const useStyles = makeStyles((theme) => ({
   wrapper: {
@@ -25,28 +27,30 @@ const useStyles = makeStyles((theme) => ({
     width: '100%',
     overflowY: 'auto',
   },
+  item: {
+    margin: `${theme.spacing(1)}px ${theme.spacing(0)}`,
+  },
 }));
 
 export default function Dashboard() {
   const [changeConnectionOpen, setChangeConnectionOpen] = useState(false);
 
-  const loadingContext = useContext(LoadingContext);
   const loginDetailsContext = useContext(LoginDetailsContext);
 
   const location = useLocation();
   const history = useHistory();
 
-  if (
-    location.pathname === routes.dashboard &&
-    !loadingContext?.connected &&
-    !loadingContext?.connecting &&
-    !changeConnectionOpen
-  ) {
-    history.push(routes.login);
-  }
+  useEffect(() => {
+    if (
+      location.pathname === routes.dashboard &&
+      !changeConnectionOpen &&
+      !loginDetailsContext?.loginDetails
+    ) {
+      history.push(routes.login);
+    }
+  }, []);
 
   function openConnectionDialog() {
-    loginDetailsContext?.setLoginDetails(undefined);
     setChangeConnectionOpen(true);
   }
 
@@ -64,7 +68,7 @@ export default function Dashboard() {
           ]}
         >
           <div className={classes.column}>
-            <div>
+            <div className={classes.item}>
               <Button
                 variant="outlined"
                 color="primary"
@@ -79,7 +83,18 @@ export default function Dashboard() {
                 <Login onConnect={() => setChangeConnectionOpen(false)} />
               </Dialog>
             </div>
-            <QueryRecorder />
+            <div className={classes.item}>
+              <IDInfo />
+            </div>
+            <div className={classes.item}>
+              <QueryRecorder />
+            </div>
+            <div className={classes.item}>
+              <ErrorView />
+            </div>
+            <div className={classes.item}>
+              <Recordings />
+            </div>
           </div>
           <div className={classes.column}>
             <ResultView />
diff --git a/app/containers/ResultView.tsx b/app/containers/ResultView.tsx
index 27a909d41af83256334fd2ceec7e67d6d61b8ac2..d82ef6212ff009785bec41fe9637ab70ffe88fbe 100644
--- a/app/containers/ResultView.tsx
+++ b/app/containers/ResultView.tsx
@@ -1,33 +1,58 @@
-/* eslint-disable react/jsx-wrap-multilines */
 import {
   makeStyles,
   Checkbox,
   FormControlLabel,
   Paper,
+  Typography,
+  Tooltip,
 } from '@material-ui/core';
 import React, { useContext, useState } from 'react';
+import { FlameGraph } from 'react-flame-graph';
 import MemoryChart from '../components/MemoryChart';
 import OptimizerTrace from '../components/OptimizerTrace';
 import ResultTable from '../components/ResultTable';
 import { RecordingContext } from '../context/RecordingContext';
 import TreeViewExplainAnalyze from '../components/TreeViewExplainAnalyze';
+import ExplainAnalyzeTable from '../components/ExplainAnalyzeTable';
+import { ExplainAnalyzeNode } from '../../backend/data-processor/types/ExplainAnalyzeNode';
 
 const useStyles = makeStyles((theme) => ({
   wrapper: {
     display: 'flex',
     flexFlow: 'column nowrap',
     alignContent: 'center',
+    paddingBottom: 300,
   },
   item: {
     margin: `${theme.spacing(1)}px ${theme.spacing(0)}`,
   },
+  title: {
+    textOverflow: 'ellipsis',
+    whiteSpace: 'nowrap',
+    overflow: 'hidden',
+  },
 }));
 
+const emptyNode: ExplainAnalyzeNode = {
+  id: '0',
+  offset: -1,
+  name: 'root',
+  time: '',
+  rows: -1,
+  value: 0,
+  timeFirstRow: 0,
+  timeAllRows: 0,
+  loops: -1,
+  children: [],
+};
+
 export default function ResultView() {
   const [showMemoryChart, setShowMemoryChart] = useState(true);
   const [showOptimizerTrace, setShowOptimizerTrace] = useState(false);
   const [showResultTable, setShowResultTable] = useState(false);
   const [showExplainAnalyze, setShowExplainAnalyze] = useState(false);
+  const [selectedNode, setSelectedNode] = useState(emptyNode);
+  const [hoveredNode, setHoveredNode] = useState(emptyNode);
 
   const recordingContext = useContext(RecordingContext);
 
@@ -35,6 +60,15 @@ export default function ResultView() {
 
   return (
     <div className={classes.wrapper} style={{ minWidth: '200px' }}>
+      <div className={classes.item}>
+        <Tooltip title={recordingContext?.activeRecording?.label || ''}>
+          <Typography variant="h4" className={classes.title}>
+            {`Results for recording ${
+              recordingContext?.activeRecording?.label || ''
+            }`}
+          </Typography>
+        </Tooltip>
+      </div>
       <div>
         <FormControlLabel
           label="Show result table"
@@ -47,7 +81,7 @@ export default function ResultView() {
           }
         />
         <FormControlLabel
-          label="Show memory chart"
+          label="Show memory performance"
           control={
             <Checkbox
               color="primary"
@@ -66,16 +100,20 @@ export default function ResultView() {
             />
           }
         />
-        <FormControlLabel
-          label="Show explain analyze"
-          control={
-            <Checkbox
-              color="primary"
-              checked={showExplainAnalyze}
-              onChange={(event) => setShowExplainAnalyze(event.target.checked)}
-            />
-          }
-        />
+        {recordingContext?.activeRecording?.explainAnalyze && (
+          <FormControlLabel
+            label="Show explain analyze"
+            control={
+              <Checkbox
+                color="primary"
+                checked={showExplainAnalyze}
+                onChange={(event) =>
+                  setShowExplainAnalyze(event.target.checked)
+                }
+              />
+            }
+          />
+        )}
       </div>
       {showResultTable && (
         <Paper className={classes.item}>
@@ -85,16 +123,42 @@ export default function ResultView() {
       {showMemoryChart && (
         <Paper
           className={classes.item}
-          style={{ paddingBottom: 80, paddingTop: 8 }}
+          style={{ paddingBottom: 8, paddingTop: 8 }}
         >
           <MemoryChart />
         </Paper>
       )}
-      {showOptimizerTrace && recordingContext?.optimizerTrace && (
+      {showOptimizerTrace && recordingContext?.activeRecording?.optimizerTrace && (
         <Paper className={classes.item} style={{ padding: 8 }}>
           <OptimizerTrace />
         </Paper>
       )}
+      {showExplainAnalyze &&
+        recordingContext?.activeRecording?.explainAnalyzeTree && (
+          <Paper className={classes.item}>
+            <FlameGraph
+              data={recordingContext?.activeRecording?.explainAnalyzeTree}
+              height={200}
+              width={800}
+              disableDefaultTooltips
+              onChange={(node: {
+                source: React.SetStateAction<ExplainAnalyzeNode>;
+              }) => {
+                setSelectedNode(node.source);
+              }}
+              onMouseOver={(
+                event: any,
+                itemData: React.SetStateAction<ExplainAnalyzeNode>
+              ) => {
+                setHoveredNode(itemData);
+              }}
+              onMouseOut={(event: any, itemData: any) => {
+                setHoveredNode(selectedNode);
+              }}
+            />
+            <ExplainAnalyzeTable node={hoveredNode} />
+          </Paper>
+        )}
       {showExplainAnalyze && (
         <Paper className={classes.item}>
           <TreeViewExplainAnalyze />
diff --git a/app/context/LoadingContext.tsx b/app/context/LoadingContext.tsx
deleted file mode 100644
index 6b08e087f83b7ad0e92c071eae4a821082e14e3d..0000000000000000000000000000000000000000
--- a/app/context/LoadingContext.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import React, { useState, createContext } from 'react';
-
-type ChildrenType = {
-  children: React.ReactNode;
-};
-
-type ContextType = {
-  connecting: boolean;
-  setConnecting: (value: boolean) => void;
-  connected: boolean;
-  setConnected: (value: boolean) => void;
-  querying: boolean;
-  setQuerying: (value: boolean) => void;
-};
-
-export const LoadingContext = createContext<ContextType | undefined>(undefined);
-
-export const LoadingProvider = ({ children }: ChildrenType) => {
-  const [connecting, setConnecting] = useState(false);
-  const [connected, setConnected] = useState(false);
-  const [querying, setQuerying] = useState(false);
-
-  return (
-    <LoadingContext.Provider
-      value={{
-        connecting,
-        setConnecting,
-        connected,
-        setConnected,
-        querying,
-        setQuerying,
-      }}
-    >
-      {children}
-    </LoadingContext.Provider>
-  );
-};
diff --git a/app/context/RecordingContext.tsx b/app/context/RecordingContext.tsx
index b64e3d9f270ed8054b8cff117755305a77f30b5a..6eae3d1d8ae259d0009a81d3e6ca384f6aecd8a9 100644
--- a/app/context/RecordingContext.tsx
+++ b/app/context/RecordingContext.tsx
@@ -1,24 +1,18 @@
-import { ResultValue } from '@mysql/xdevapi';
 import React, { useState, createContext } from 'react';
-import {
-  ChartData,
-  StageTimes,
-} from '../../backend/data-processor/DataProcessor';
-import { QueryOutput } from '../QueryOutput';
+import { Recording } from '../types/Recording';
+import { RecordingListItem } from '../types/RecordingListItem';
 
 type ChildrenType = {
   children: React.ReactNode;
 };
 
 type ContextType = {
-  chartData: ChartData | undefined;
-  setChartData: (value: ChartData | undefined) => void;
-  stageTimes: StageTimes | undefined;
-  setStageTimes: (value: StageTimes | undefined) => void;
-  optimizerTrace: ResultValue[][] | undefined;
-  setOptimizerTrace: (value: ResultValue[][] | undefined) => void;
-  queryOutput: QueryOutput | undefined;
-  setQueryOutput: (value: QueryOutput | undefined) => void;
+  activeRecording?: Recording;
+  setActiveRecording: (value?: Recording) => void;
+  recordings?: Recording[];
+  setRecordings: (value?: Recording[]) => void;
+  recordingListItems?: RecordingListItem[];
+  setRecordingListItems: (value?: RecordingListItem[]) => void;
 };
 
 export const RecordingContext = createContext<ContextType | undefined>(
@@ -26,27 +20,23 @@ export const RecordingContext = createContext<ContextType | undefined>(
 );
 
 export const RecordingProvider = ({ children }: ChildrenType) => {
-  const [chartData, setChartData] = useState<ChartData | undefined>(undefined);
-  const [stageTimes, setStageTimes] = useState<StageTimes | undefined>(
-    undefined
-  );
-  const [optimizerTrace, setOptimizerTrace] = useState<
-    ResultValue[][] | undefined
-  >(undefined);
-  const [queryOutput, setQueryOutput] = useState<QueryOutput | undefined>(
+  const [activeRecording, setActiveRecording] = useState<Recording | undefined>(
     undefined
   );
+  const [recordings, setRecordings] = useState<Recording[] | undefined>([]);
+  const [recordingListItems, setRecordingListItems] = useState<
+    RecordingListItem[] | undefined
+  >([]);
+
   return (
     <RecordingContext.Provider
       value={{
-        chartData,
-        setChartData,
-        stageTimes,
-        setStageTimes,
-        optimizerTrace,
-        setOptimizerTrace,
-        queryOutput,
-        setQueryOutput,
+        activeRecording,
+        setActiveRecording,
+        recordings,
+        setRecordings,
+        recordingListItems,
+        setRecordingListItems,
       }}
     >
       {children}
diff --git a/app/package.json b/app/package.json
index 44aac567f561e3768c9b6a6891f61250672b2248..1241e56ab6d00bdc5cd12cab7348fa55d2833ae2 100644
--- a/app/package.json
+++ b/app/package.json
@@ -1,7 +1,7 @@
 {
   "name": "mysql-query-profiler",
   "productName": "MySQL Query Profiler",
-  "version": "0.2.0",
+  "version": "0.3.0",
   "description": "A profiler for MySQL queries using Electron and React",
   "main": "./main.prod.js",
   "author": {
diff --git a/app/types/Recording.ts b/app/types/Recording.ts
new file mode 100644
index 0000000000000000000000000000000000000000..7e9aed6268bd87345fd161235d8d9494d7e60bb7
--- /dev/null
+++ b/app/types/Recording.ts
@@ -0,0 +1,19 @@
+import { ResultValue } from '@mysql/xdevapi';
+import { ChartColors } from '../../backend/data-processor/types/ChartColors';
+import { ChartData } from '../../backend/data-processor/types/ChartData';
+import { ExplainAnalyzeNode } from '../../backend/data-processor/types/ExplainAnalyzeNode';
+import { StageTimes } from '../../backend/data-processor/types/StageTimes';
+import { QueryOutput } from '../QueryOutput';
+
+export interface Recording {
+  chartData?: ChartData;
+  stageTimes?: StageTimes;
+  chartColors?: ChartColors;
+  optimizerTrace?: ResultValue[][];
+  queryOutput?: QueryOutput;
+  explainAnalyze: boolean;
+  explainAnalyzeTree: ExplainAnalyzeNode;
+  error?: string;
+  label: string; // On both in order to avoid having to create an 'activeRecordingListItem'
+  uuid: string;
+}
diff --git a/app/types/RecordingListItem.ts b/app/types/RecordingListItem.ts
new file mode 100644
index 0000000000000000000000000000000000000000..fc61436c2d49e3a2665e798e3f41d3fb30c318eb
--- /dev/null
+++ b/app/types/RecordingListItem.ts
@@ -0,0 +1,8 @@
+export interface RecordingListItem {
+  uuid: string;
+  query: string;
+  elapsed: number;
+  label: string;
+  color: string;
+  viewing: boolean;
+}
diff --git a/babel.config.js b/babel.config.js
index efad5dcd9e4a0e2bdd617f3a0e9f91fce62ae0da..c4cdae93b8881e927475047612c055d14d67ca12 100644
--- a/babel.config.js
+++ b/babel.config.js
@@ -2,8 +2,6 @@
 
 const developmentEnvironments = ['development', 'test'];
 
-const developmentPlugins = [require('react-hot-loader/babel')];
-
 const productionPlugins = [
   require('babel-plugin-dev-expression'),
 
@@ -56,7 +54,7 @@ module.exports = (api) => {
       [require('@babel/plugin-proposal-class-properties'), { loose: true }],
       require('@babel/plugin-proposal-json-strings'),
 
-      ...(development ? developmentPlugins : productionPlugins),
+      ...(development ? [] : productionPlugins),
     ],
   };
 };
diff --git a/backend/data-processor/DataProcessor.ts b/backend/data-processor/DataProcessor.ts
index 2cb653aa1b5b75ac61bc4955e008564befbe5e6d..c3b0b6be4509db3c0499611fd87d0ffc7fbe4e84 100644
--- a/backend/data-processor/DataProcessor.ts
+++ b/backend/data-processor/DataProcessor.ts
@@ -1,33 +1,15 @@
 /* eslint-disable class-methods-use-this */
 import { ResultValue } from '@mysql/xdevapi';
+import lodash from 'lodash';
 import { Result } from '../utils/mysql';
+import { ChartColors } from './types/ChartColors';
+import { ChartData } from './types/ChartData';
+import { ExplainAnalyzeNode } from './types/ExplainAnalyzeNode';
+import { MemoryPerformance } from './types/MemoryPerformance';
+import { MemoryPerformanceDataPoint } from './types/MemoryPerformanceDataPoint';
+import { StageTimes } from './types/StageTimes';
 
-export interface MemoryPerformanceTimeline {
-  eventName: string;
-  isBytesUsedZero: boolean;
-  times: Array<number>;
-  bytesUsed: Array<number>;
-}
-
-export interface MemoryPerformanceDataPoint {
-  relative_time: number;
-  [eventName: string]: number;
-}
-
-export interface StageTimePoint {
-  stage: string;
-  source: string;
-  startTime: number;
-  endTime: number;
-}
-
-export type MemoryPerformance = Array<MemoryPerformanceTimeline>;
-
-export type ChartData = Array<MemoryPerformanceDataPoint>;
-
-export type StageTimes = Array<StageTimePoint>;
-
-export class DataProcessor {
+export default class DataProcessor {
   static processMemoryPerformance(performanceData?: Result) {
     // TODO: Add more restrictive tests
     if (performanceData && performanceData.labels && performanceData.values) {
@@ -77,7 +59,6 @@ export class DataProcessor {
         timeLine.isBytesUsedZero =
           timeLine.bytesUsed.reduce((a, b) => a + b, 0) === 0;
       });
-      console.log(result);
       return DataProcessor.getChartData(result);
     }
     throw new Error('DataProcessor: performanceData had wrong format');
@@ -99,8 +80,7 @@ export class DataProcessor {
       });
       result.push(memoryPerformanceDataPoint);
     });
-
-    console.log(result);
+    DataProcessor.log(`Memory data points: ${result.length}`);
     return result;
   }
 
@@ -116,7 +96,6 @@ export class DataProcessor {
           endTime: Number(resultValue[3]),
         });
       });
-      console.log(result);
       return result;
     }
     throw new Error('DataProcessor: stageTimesRaw had wrong format');
@@ -126,11 +105,6 @@ export class DataProcessor {
     let traceResult;
     if (optimizerTrace && optimizerTrace.values) {
       traceResult = optimizerTrace;
-      console.log(
-        `Dataprocessor.processOptimizerTrace: Traceresult: ${JSON.stringify(
-          traceResult
-        )}`
-      );
       if (traceResult.values.length > 0 && traceResult.values[0].length > 1) {
         traceResult.values[0][1] = JSON.parse(String(traceResult.values[0][1]));
       }
@@ -138,4 +112,127 @@ export class DataProcessor {
     }
     throw new Error('DataProcessor: optimizerTrace had wrong format');
   }
+
+  static assignColor(key: string) {
+    const tempObject: ChartColors = {};
+    tempObject[key] = `#${(0x1000000 + Math.random() * 0xffffff)
+      .toString(16)
+      .substr(1, 6)}`;
+    return tempObject;
+  }
+
+  static processChartColors(chartData: ChartData) {
+    const chartColors: ChartColors = {};
+    const dataKeys = lodash.keys(chartData[0]).slice(1);
+    lodash.forEach(dataKeys, (key) =>
+      lodash.assign(chartColors, DataProcessor.assignColor(key))
+    );
+    return chartColors;
+  }
+
+  // -> Limit: 10 row(s)  (actual time=0.098..0.103 rows=10 loops=1)
+  //   -> Table scan on employees  (cost=30192.25 rows=299600) (actual time=0.095..0.099 rows=10 loops=1)
+
+  // " Table scan on ints  (actual time=0.006..0.029 rows=5 loops=1)"
+  // " Materialize  (actual time=2.662..2.697 rows=5 loops=1)       "
+  // " Rows fetched before execution  (actual time=0.00
+  static processExplainAnalyze(explainAnalyze?: Result) {
+    if (
+      !explainAnalyze ||
+      explainAnalyze?.values.length === 0 ||
+      explainAnalyze.values[0].length === 0
+    ) {
+      throw Error('DataProcessor: Explain analyze had no values');
+    }
+    const explainText = String(explainAnalyze?.values[0][0]);
+    const rootNode: ExplainAnalyzeNode = {
+      id: '0',
+      offset: -1,
+      name: 'root',
+      time: '',
+      rows: -1,
+      value: 0,
+      timeFirstRow: 0,
+      timeAllRows: 0,
+      loops: -1,
+      children: [],
+    };
+    const regexp = /([ ]*)?-> (.+?)( \(cost=([\d.]+) rows=(\d+)\))? \(actual time=([\d.]+) rows=(\d+) loops=(\d+)\)/gm;
+    const regexpTime = /(\d*\.?\d*)\.\.(\d*\.?\d*)/;
+    const matches = [...explainText.matchAll(regexp)];
+    const nodes: ExplainAnalyzeNode[] = [rootNode];
+    // Create object for each line. Root has ID 0
+    let idIterator = 1;
+    matches.forEach((element) => {
+      const timeResult = element[6].match(regexpTime);
+      if (timeResult != null) {
+        const timeFirst = Number(timeResult[1]) * Number(element[8]);
+        const timeAll = Number(timeResult[2]) * Number(element[8]);
+
+        const node: ExplainAnalyzeNode = {
+          id: idIterator.toString(),
+          offset: element[1] === undefined ? 0 : element[1].length,
+          name: element[2],
+          cost_est: Number(element[4]),
+          rows_est: Number(element[5]),
+          time: element[6],
+          value: timeAll,
+          timeFirstRow: timeFirst,
+          timeAllRows: timeAll,
+          rows: Number(element[7]),
+          loops: Number(element[8]),
+          children: [],
+        };
+        nodes.push(node);
+        idIterator += 1;
+      }
+    });
+
+    // Assign children of each object
+    // Iterate through all objects
+    for (let i = 1; i < nodes.length; i += 1) {
+      // Set current node as child if its offset is smaller
+      if (nodes[i - 1].offset < nodes[i].offset) {
+        nodes[i - 1].children.push(nodes[i]);
+      } else {
+        // If current node's offset is smaller then it cannot be child of the previous node
+        // Iterate backwards from current node until a node with smaller offset has been reached
+        for (let j = i - 1; i > 0; j -= 1) {
+          if (nodes[j].offset < nodes[i].offset) {
+            nodes[j].children.push(nodes[i]);
+            break;
+          }
+        }
+      }
+    }
+
+    const node = DataProcessor.fixNodeTimes(nodes[0]);
+
+    DataProcessor.log(explainText);
+    return node;
+  }
+
+  /**
+   * Makes sure total time is represented accurately in the flame graph
+   * If a node has smaller total time than its children, the total time of its children are added
+   * @param node
+   */
+  static fixNodeTimes(node: ExplainAnalyzeNode) {
+    if (!node.children.length) {
+      return node;
+    }
+    let sumChildren = 0;
+    node.children.forEach((element) => {
+      const nextNode = DataProcessor.fixNodeTimes(element);
+      sumChildren += nextNode.value;
+    });
+    if (node.value < sumChildren) {
+      node.value += sumChildren;
+    }
+    return node;
+  }
+
+  static log(message: string) {
+    console.log(`DataProcessor: ${String(message)}`);
+  }
 }
diff --git a/backend/data-processor/types/ChartColors.ts b/backend/data-processor/types/ChartColors.ts
new file mode 100644
index 0000000000000000000000000000000000000000..94914f6bfec5ce87a0359cc03cb3a06f2215dc49
--- /dev/null
+++ b/backend/data-processor/types/ChartColors.ts
@@ -0,0 +1,3 @@
+export interface ChartColors {
+  [key: string]: string;
+}
diff --git a/backend/data-processor/types/ChartData.ts b/backend/data-processor/types/ChartData.ts
new file mode 100644
index 0000000000000000000000000000000000000000..1485bfbe52b6361d2c9d11d487a51ecd8d798431
--- /dev/null
+++ b/backend/data-processor/types/ChartData.ts
@@ -0,0 +1,3 @@
+import { MemoryPerformanceDataPoint } from './MemoryPerformanceDataPoint';
+
+export type ChartData = Array<MemoryPerformanceDataPoint>;
diff --git a/backend/data-processor/types/ExplainAnalyzeNode.ts b/backend/data-processor/types/ExplainAnalyzeNode.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2e5706165df24e59e6e4021758cb2d11b3b0d14c
--- /dev/null
+++ b/backend/data-processor/types/ExplainAnalyzeNode.ts
@@ -0,0 +1,15 @@
+export interface ExplainAnalyzeNode {
+  id: string;
+  offset: number;
+  // Endret fra command til name, siden koponenten krever det
+  name: string;
+  cost_est?: number;
+  rows_est?: number;
+  time: string;
+  value: number;
+  timeFirstRow: number;
+  timeAllRows: number;
+  rows: number;
+  loops: number;
+  children: ExplainAnalyzeNode[];
+}
diff --git a/backend/data-processor/types/MemoryPerformance.ts b/backend/data-processor/types/MemoryPerformance.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a8e93473ba2ecff420758169ead5c1c01b85d831
--- /dev/null
+++ b/backend/data-processor/types/MemoryPerformance.ts
@@ -0,0 +1,3 @@
+import { MemoryPerformanceTimeline } from './MemoryPerformanceTimeline';
+
+export type MemoryPerformance = Array<MemoryPerformanceTimeline>;
diff --git a/backend/data-processor/types/MemoryPerformanceDataPoint.ts b/backend/data-processor/types/MemoryPerformanceDataPoint.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f8c4832b60f0707c4c3a8f9238557b8f3d18fe38
--- /dev/null
+++ b/backend/data-processor/types/MemoryPerformanceDataPoint.ts
@@ -0,0 +1,4 @@
+export interface MemoryPerformanceDataPoint {
+  relative_time: number;
+  [eventName: string]: number;
+}
diff --git a/backend/data-processor/types/MemoryPerformanceTimeline.ts b/backend/data-processor/types/MemoryPerformanceTimeline.ts
new file mode 100644
index 0000000000000000000000000000000000000000..437bdbcb5e15780e2bc1abb7c43c7ab94a790e20
--- /dev/null
+++ b/backend/data-processor/types/MemoryPerformanceTimeline.ts
@@ -0,0 +1,6 @@
+export interface MemoryPerformanceTimeline {
+  eventName: string;
+  isBytesUsedZero: boolean;
+  times: Array<number>;
+  bytesUsed: Array<number>;
+}
diff --git a/backend/data-processor/types/StageTime.ts b/backend/data-processor/types/StageTime.ts
new file mode 100644
index 0000000000000000000000000000000000000000..8a6215914a5a186c795a4e58476e97f31924f380
--- /dev/null
+++ b/backend/data-processor/types/StageTime.ts
@@ -0,0 +1,6 @@
+export interface StageTime {
+  stage: string;
+  source: string;
+  startTime: number;
+  endTime: number;
+}
diff --git a/backend/data-processor/types/StageTimes.ts b/backend/data-processor/types/StageTimes.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0b9215c9def496ffba0c7859e33a95671090d187
--- /dev/null
+++ b/backend/data-processor/types/StageTimes.ts
@@ -0,0 +1,3 @@
+import { StageTime } from './StageTime';
+
+export type StageTimes = Array<StageTime>;
diff --git a/backend/recorder/SqlAgent.ts b/backend/recorder/SqlAgent.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5bc7eed2007d9833766d8a0ca536cdc3ad4b5290
--- /dev/null
+++ b/backend/recorder/SqlAgent.ts
@@ -0,0 +1,84 @@
+import { Session, Client } from '@mysql/xdevapi';
+import { createSession, runQuery } from '../utils/mysql';
+import SqlMonitor from './SqlMonitor';
+
+/**
+ * A class responsible for performing actions when Runner and Monitor are busy.
+ * Example actions:
+ * - Aborting a query
+ * - Checking whether a procedure is running
+ * - Checking the health of the other sessions
+ */
+export default class SqlAgent {
+  session?: Session;
+
+  connectionID?: string;
+
+  threadID?: string;
+
+  /**
+   * Connects the Agent to a session through a given client
+   * @param client
+   * @param database
+   */
+  async connect(client?: Client, database?: string) {
+    this.session = await createSession(client, database);
+    let resultSet = await this.executeQuery(`SELECT connection_id();`);
+    this.connectionID = String(resultSet?.results?.values[0]);
+    resultSet = await this.executeQuery(
+      `select thread_id from performance_schema.threads where processlist_id=${this.connectionID}`
+    );
+    this.threadID = String(resultSet?.results?.values[0]);
+    console.log(
+      `SqlAgent: ConnectionID: ${this.connectionID}, ThreadID: ${this.threadID}`
+    );
+  }
+
+  /**
+   * Waits for a session with the given connectionID to begin running the specified query
+   * @param connectionID
+   * @param query
+   */
+  async waitRunning(threadID: string) {
+    await this.executeQuery('DROP PROCEDURE IF EXISTS wait_running;');
+    await this.executeQuery(`
+    CREATE PROCEDURE wait_running(
+      IN thd_id BIGINT UNSIGNED
+    )
+    BEGIN
+      DECLARE state VARCHAR(16);
+      REPEAT
+        SET state = (SELECT PROCESSLIST_COMMAND FROM performance_schema.threads WHERE THREAD_ID=thd_id);
+      UNTIL state = 'Query' END REPEAT;
+    END
+    `);
+    await this.executeQuery(`CALL wait_running(${threadID});`);
+  }
+
+  async checkThreadState(threadID: string) {
+    return this.executeQuery(
+      `SELECT PROCESSLIST_COMMAND FROM performance_schema.threads WHERE THREAD_ID=${threadID}`
+    );
+  }
+
+  async forceSleepMonitor(
+    sqlMonitor: SqlMonitor,
+    client?: Client,
+    database?: string
+  ) {
+    await this.killQuery(sqlMonitor.connectionID || '');
+    await sqlMonitor.connect(client, database);
+  }
+
+  async killQuery(connectionID: string) {
+    return this.executeQuery(`KILL QUERY ${connectionID}`);
+  }
+
+  async killConnection(connectionID: string) {
+    return this.executeQuery(`KILL CONNECTION ${connectionID};`);
+  }
+
+  async executeQuery(query: string) {
+    return runQuery(query, this.session);
+  }
+}
diff --git a/backend/recorder/SqlManager.ts b/backend/recorder/SqlManager.ts
index ab5ca284fb3d55bd6eea8783e175b91063523e65..b3d37d1a560fc38754f75ac93976000a3abacfd6 100644
--- a/backend/recorder/SqlManager.ts
+++ b/backend/recorder/SqlManager.ts
@@ -1,9 +1,25 @@
-import { URI, Client } from '@mysql/xdevapi';
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import { URI, Client, ResultValue } from '@mysql/xdevapi';
 import FileIO from '../utils/FileIO';
 import SqlRunner from './SqlRunner';
 import SqlMonitor from './SqlMonitor';
-import { closeConnection } from '../utils/mysql';
-import { DataProcessor } from '../data-processor/DataProcessor';
+import { closeConnection, Result } from '../utils/mysql';
+import { ChartData } from '../data-processor/types/ChartData';
+import { StageTimes } from '../data-processor/types/StageTimes';
+import DataProcessor from '../data-processor/DataProcessor';
+import SqlAgent from './SqlAgent';
+import { ChartColors } from '../data-processor/types/ChartColors';
+import { ExplainAnalyzeNode } from '../data-processor/types/ExplainAnalyzeNode';
+
+export interface RawRecording {
+  result?: { results?: Result; error?: any };
+  memoryPerformance?: ChartData;
+  chartColors?: ChartColors;
+  error?: any;
+  optimizerTrace?: ResultValue[][];
+  stageTimes?: StageTimes;
+  explainAnalyze?: ExplainAnalyzeNode;
+}
 
 export default class SqlManager {
   connection?: URI;
@@ -16,38 +32,107 @@ export default class SqlManager {
 
   monitor?: SqlMonitor;
 
-  async connect(connection?: URI, database?: string) {
+  agent?: SqlAgent;
+
+  userQueryIsRunning = false;
+
+  cancel = false;
+
+  async connect(connection?: URI, database?: string, savePassword?: boolean) {
     this.connection = connection;
     this.database = database;
     this.runner = new SqlRunner();
     this.client = await this.runner.connect(connection, database);
     this.monitor = new SqlMonitor();
     await this.monitor.connect(this.client, database);
-    await FileIO.saveLoginDetails({ ...connection, database });
+    this.agent = new SqlAgent();
+    await this.agent.connect(this.client, database);
+    await FileIO.saveLoginDetails({ ...connection, database, savePassword });
   }
 
-  async record(query: string, explainAnalyze?: boolean) {
-    if (!this.runner || !this.monitor || !this.runner.connectionID) {
+  async record(
+    query: string,
+    setUserQueryIsRunning: (running: boolean) => void,
+    monitorTimeStep: number,
+    explainAnalyze?: boolean,
+    stateCallback?: (state: string) => void
+  ): Promise<RawRecording> {
+    if (
+      !this.runner ||
+      !this.monitor ||
+      !this.runner.connectionID ||
+      !this.runner.threadID ||
+      !this.monitor.connectionID ||
+      !this.monitor.threadID ||
+      !this.agent
+    ) {
       throw new Error(
-        'SqlManager.record: Runner, monitor or connectionID was missing. Maybe you forgot to call connect()?'
+        'SqlManager.record: Runner, monitor, agent or connectionID was missing. Maybe you forgot to call connect()?'
       );
     }
-    await this.monitor.prepareMonitoring(this.runner.connectionID);
-    this.monitor.monitorConnection(this.runner.connectionID);
+    const stageIgnoreBeforeResult = await this.monitor.getStageIgnoreBeforeResult(
+      this.runner.threadID
+    );
+    const monitoring = this.monitor.monitorConnection(
+      this.runner.connectionID,
+      stageIgnoreBeforeResult,
+      monitorTimeStep
+    );
 
-    // Wait to make sure monitor is ready for runner to execute query
-    // TODO: Find a more optimal solution than waiting 1 second
-    // eslint-disable-next-line compat/compat
-    await new Promise((resolve) => setTimeout(resolve, 1000));
+    stateCallback?.call(undefined, 'Waiting for monitor to be ready');
+    await this.agent.waitRunning(this.monitor.threadID);
 
+    stateCallback?.call(undefined, 'Running query');
+    setUserQueryIsRunning(true);
+    const t0 = performance.now();
     const result = await this.runner.executeQuery(query, explainAnalyze);
+    const t1 = performance.now();
+    setUserQueryIsRunning(false);
+
+    stateCallback?.call(undefined, 'Query done');
+    const queryTime = t1 - t0;
+    SqlManager.log(
+      `Query took ${queryTime} ms, required at least ${
+        monitorTimeStep * 1000 * 2
+      } ms`
+    );
+
+    if (this.cancel) {
+      stateCallback?.call(undefined, 'Cancel detected, reconnecting');
+      await this.connect(this.connection, this.database);
+      this.cancel = false;
+      return { error: 'cancelled' };
+    }
+
+    // Handling error, aborting
     if (result?.error) {
+      stateCallback?.call(undefined, 'Error detected, aborting monitoring');
       await this.reset();
-      return { result, memoryPerformance: undefined, error: result.error };
+      stateCallback?.call(undefined, 'Getting optimizer trace');
+      const optimizerTrace = await this.runner.getOptimizerTrace();
+      stateCallback?.call(undefined, '');
+      return {
+        result,
+        optimizerTrace: DataProcessor.processOptimizerTrace(
+          optimizerTrace.results
+        ),
+        error: result.error,
+        explainAnalyze: DataProcessor.processExplainAnalyze(result.results),
+      };
     }
+    stateCallback?.call(undefined, 'Getting optimizer trace');
     const optimizerTrace = await this.runner.getOptimizerTrace();
+
+    stateCallback?.call(
+      undefined,
+      'Waiting for monitoring to finish (may take up to 5 minutes)'
+    );
+    await monitoring;
+    stateCallback?.call(undefined, 'Getting memory performance');
     const memoryPerformance = await this.monitor.dumpData();
+    stateCallback?.call(undefined, 'Getting stage times');
     const stageTimes = await this.monitor.getStageTimes();
+    stateCallback?.call(undefined, '');
     return {
       result,
       optimizerTrace: DataProcessor.processOptimizerTrace(
@@ -57,20 +142,56 @@ export default class SqlManager {
         memoryPerformance.results
       ),
       stageTimes: DataProcessor.processStageTimes(stageTimes.results),
+      chartColors: DataProcessor.processChartColors(
+        DataProcessor.processMemoryPerformance(memoryPerformance.results)
+      ),
       error: undefined,
+      explainAnalyze: DataProcessor.processExplainAnalyze(result.results),
     };
   }
 
   async reset() {
-    if (!this.runner || !this.monitor || !this.monitor.connectionID) {
-      throw new Error(
-        `SqlManager.reset: Runner, monitor or connectionID was missing. Invalid attempt at reset?`
+    await this.disconnect();
+    await this.connect(this.connection, this.database);
+  }
+
+  async disconnect() {
+    if (
+      !this.runner ||
+      !this.runner.connectionID ||
+      !this.monitor ||
+      !this.monitor.connectionID ||
+      !this.agent
+    ) {
+      console.error(
+        `SqlManager.disconnect: Runner ${Boolean(
+          this.runner
+        )}, Monitor ${Boolean(this.monitor)}, Agent ${Boolean(this.agent)}`
       );
+      return;
     }
-    // Kill monitor manually from runner as client.close won't interfere if monitor has called its procedure
-    await this.runner.executeQuery(`kill ${this.monitor.connectionID};`);
+    // Kill monitor manually from agent as client.close won't interfere if monitor has called its procedure
+    await this.agent?.killConnection(this.monitor.connectionID);
+    await this.agent?.killConnection(this.runner.connectionID);
     await closeConnection(this.client);
-    await this.connect(this.connection, this.database);
+    SqlManager.log('disconnect(): Disconnected successfully');
+  }
+
+  async cancelQuery() {
+    this.cancel = true;
+    await this.disconnect();
+  }
+
+  async checkThreadStates() {
+    const runnerState = await this.agent?.checkThreadState(
+      this.runner?.threadID || ''
+    );
+    const monitorState = await this.agent?.checkThreadState(
+      this.monitor?.threadID || ''
+    );
+    SqlManager.log(
+      `Runner: ${runnerState?.results?.values}\nMonitor: ${monitorState?.results?.values}`
+    );
   }
 
   async executeQuery(query: string) {
@@ -78,4 +199,8 @@ export default class SqlManager {
     const result = await this.runner.executeQuery(query);
     return result;
   }
+
+  static log(message: any) {
+    console.log(`SqlManager: ${JSON.stringify(message)}`);
+  }
 }
diff --git a/backend/recorder/SqlMonitor.ts b/backend/recorder/SqlMonitor.ts
index 6c9af3285bc41aacbf7335c430bbf01fe4096788..9065b624516cbca444a5b4be7713ea934287c548 100644
--- a/backend/recorder/SqlMonitor.ts
+++ b/backend/recorder/SqlMonitor.ts
@@ -1,60 +1,55 @@
 import { Session, Client } from '@mysql/xdevapi';
 import { createSession, runQuery } from '../utils/mysql';
 
+/**
+ * A class responsible for monitoring the resource usage of another session
+ */
 export default class SqlMonitor {
   session?: Session;
 
   connectionID?: string;
 
+  threadID?: string;
+
   async connect(client?: Client, database?: string) {
+    SqlMonitor.log('Connecting');
     this.session = await createSession(client, database);
-    const resultSet = await runQuery(`SELECT connection_id();`, this.session);
+    let resultSet = await this.executeQuery(`SELECT connection_id();`);
     this.connectionID = String(resultSet?.results?.values[0]);
-    console.log(`SqlMonitor: ConnectionID: ${this.connectionID}`);
-  }
-
-  async prepareMonitoring(connectionID: string) {
-    await this.enableStageLogging();
-    const stageIgnoreBeforeResult = await runQuery(
-      `SELECT MAX(TIMER_END) FROM performance_schema.events_stages_history_long WHERE THREAD_ID=(SELECT THREAD_ID FROM performance_schema.threads WHERE PROCESSLIST_ID=${connectionID});`,
-      this.session
-    );
-    await this.createMonitorProcedure(
-      Number(stageIgnoreBeforeResult.results?.values[0])
+    resultSet = await this.executeQuery(
+      `SELECT thread_id FROM performance_schema.threads WHERE processlist_id=${this.connectionID}`
     );
+    this.threadID = String(resultSet?.results?.values[0]);
+    await this.enableStageLogging();
+    await this.createMonitorProcedure();
   }
 
   async enableStageLogging() {
     // Allow logging of stages
-    await runQuery(
-      `update performance_schema.setup_instruments set enabled="YES", timed="yes" where name like "stage/%";`,
-      this.session
+    await this.executeQuery(
+      `UPDATE performance_schema.setup_instruments SET enabled="YES", timed="yes" WHERE name LIKE "stage/%";`
     );
-    await runQuery(
-      `update performance_schema.setup_consumers set enabled="YES" where name like "events_stages%";`,
-      this.session
+    await this.executeQuery(
+      `UPDATE performance_schema.setup_consumers SET enabled="YES" WHERE name LIKE "events_stages%";`
     );
   }
 
-  async createMonitorProcedure(stageIgnoreBefore: number) {
-    await runQuery(
-      'DROP PROCEDURE IF EXISTS monitor_connection;',
-      this.session
-    );
+  async createMonitorProcedure() {
+    await this.executeQuery('DROP PROCEDURE IF EXISTS monitor_connection;');
     // Create monitoring procedure
-    await runQuery(
+    await this.executeQuery(
       `CREATE PROCEDURE monitor_connection(
-        IN conn_id BIGINT UNSIGNED
+        IN conn_id BIGINT UNSIGNED,
+        IN stage_ignore_before BIGINT UNSIGNED,
+        IN timestep_size FLOAT
       )
       BEGIN
-        DECLARE thd_id BIGINT UNSIGNED;
+        DECLARE thd_ID BIGINT UNSIGNED;
         DECLARE state VARCHAR(16);
-        DECLARE stage_ignore_before BIGINT UNSIGNED;
         DECLARE stage_min_ts BIGINT UNSIGNED;
 
-        SET thd_id = (SELECT THREAD_ID FROM performance_schema.threads WHERE PROCESSLIST_ID=conn_id);
+        SET thd_id = (SELECT thread_id FROM performance_schema.threads WHERE processlist_id=conn_id);
 
-        SET stage_ignore_before = ${stageIgnoreBefore};
         DROP TABLE IF EXISTS monitoring_data;
         CREATE TABLE monitoring_data (
             TS DATETIME(6),
@@ -95,7 +90,7 @@ export default class SqlMonitor {
               HIGH_NUMBER_OF_BYTES_USED
             FROM performance_schema.memory_summary_by_thread_by_event_name
             WHERE THREAD_ID = thd_id;
-          DO SLEEP(0.1);
+          DO SLEEP(timestep_size);
         UNTIL state = 'Sleep' END REPEAT;
 
         SET stage_min_ts = (SELECT MIN(timer_start) FROM performance_schema.events_stages_history_long WHERE THREAD_ID=thd_id AND timer_start > stage_ignore_before);
@@ -111,33 +106,55 @@ export default class SqlMonitor {
             (timer_end - stage_min_ts) / 1000000000000 AS end
           FROM performance_schema.events_stages_history_long
           WHERE THREAD_ID = thd_id AND timer_start > stage_ignore_before ORDER BY timer_start;
-      END`,
-      this.session
+      END`
+    );
+  }
+
+  async getStageIgnoreBeforeResult(threadID: string) {
+    const stageIgnoreBeforeResult = await this.executeQuery(
+      `SELECT MAX(TIMER_END) FROM performance_schema.events_stages_history_long WHERE THREAD_ID=${threadID};`
     );
+    return Number(stageIgnoreBeforeResult.results?.values[0]);
   }
 
-  async monitorConnection(connectionID: string) {
-    await runQuery(`call monitor_connection(${connectionID});`, this.session);
+  async monitorConnection(
+    connectionID: string,
+    stageIgnoreBeforeResult: number,
+    timestep: number
+  ) {
+    SqlMonitor.log(
+      `Monitoring with timestep of ${timestep} seconds, connectionID ${connectionID}`
+    );
+    await this.executeQuery(
+      `call monitor_connection(${connectionID}, ${stageIgnoreBeforeResult}, ${timestep});`
+    );
   }
 
   async dumpData() {
-    await runQuery(
-      'SET @min_ts = (SELECT UNIX_TIMESTAMP(MIN(TS)) FROM monitoring_data);',
-      this.session
+    await this.executeQuery(
+      'SET @min_ts = (SELECT UNIX_TIMESTAMP(MIN(TS)) FROM monitoring_data);'
     );
 
-    const data = await runQuery(
-      `SELECT UNIX_TIMESTAMP(TS) - @min_ts, EVENT_NAME, CURRENT_NUMBER_OF_BYTES_USED FROM monitoring_data ORDER BY 1;`,
-      this.session
+    const data = await this.executeQuery(
+      `SELECT UNIX_TIMESTAMP(TS) - @min_ts, EVENT_NAME, CURRENT_NUMBER_OF_BYTES_USED FROM monitoring_data ORDER BY 1;`
     );
+    await this.executeQuery(`DROP TABLE IF EXISTS monitoring_data;`);
     return data;
   }
 
   async getStageTimes() {
-    const stageTimesRaw = await runQuery(
-      `select event_name, source, start, end from monitoring_stages;`,
-      this.session
+    const stageTimesRaw = await this.executeQuery(
+      `select event_name, source, start, end from monitoring_stages;`
     );
+    await this.executeQuery(`DROP TABLE IF EXISTS monitoring_stages;`);
     return stageTimesRaw;
   }
+
+  async executeQuery(query: string) {
+    return runQuery(query, this.session);
+  }
+
+  static log(message: unknown) {
+    console.log(`SqlMonitor: ${String(message)}`);
+  }
 }
diff --git a/backend/recorder/SqlRunner.ts b/backend/recorder/SqlRunner.ts
index 1f94564e0a95c48102b4125a28518df4a2cb6a15..ccea83e7616c2d46115bc9fb35edc859f6c179ca 100644
--- a/backend/recorder/SqlRunner.ts
+++ b/backend/recorder/SqlRunner.ts
@@ -1,19 +1,31 @@
 import { URI, Session } from '@mysql/xdevapi';
 import { createConnection, createSession, runQuery } from '../utils/mysql';
 
+/**
+ * A class responsible for running queries which will be recorded
+ */
 export default class SqlRunner {
   session?: Session;
 
   connectionID?: string;
 
+  threadID?: string;
+
   async connect(connection?: URI, database?: string) {
     const client = await createConnection(connection, {
-      pooling: { enabled: true, maxSize: 2 },
+      pooling: { enabled: true },
     });
     this.session = await createSession(client, database);
-    const resultSet = await runQuery(`SELECT connection_id();`, this.session);
+    let resultSet = await runQuery(`SELECT connection_id();`, this.session);
     this.connectionID = String(resultSet?.results?.values[0]);
-    console.log(`SqlRunner: ConnectionID: ${this.connectionID}`);
+    resultSet = await runQuery(
+      `select thread_id from performance_schema.threads where processlist_id=${this.connectionID}`,
+      this.session
+    );
+    this.threadID = String(resultSet?.results?.values[0]);
+    console.log(
+      `SqlRunner: ConnectionID: ${this.connectionID}, ThreadID: ${this.threadID}`
+    );
     return client;
   }
 
diff --git a/backend/utils/FileIO.ts b/backend/utils/FileIO.ts
index 7dfc66e3a28c05342c9bed78897c2e293fa34e9f..8cc94ed2d50f7f910f384867133f130e2fff20cd 100644
--- a/backend/utils/FileIO.ts
+++ b/backend/utils/FileIO.ts
@@ -6,9 +6,13 @@ import { LoginDetails } from './LoginDetails';
 export default class FileIO {
   static async saveLoginDetails(loginDetails: LoginDetails) {
     const loginDetailsLocation = 'loginDetails.json';
+    const fixedLoginDetails = loginDetails;
+    if (!loginDetails.savePassword) {
+      fixedLoginDetails.password = '';
+    }
     await fs.promises.writeFile(
       loginDetailsLocation,
-      JSON.stringify(loginDetails, undefined, 2)
+      JSON.stringify(fixedLoginDetails, undefined, 2)
     );
   }
 
@@ -22,5 +26,3 @@ export default class FileIO {
     return undefined;
   }
 }
-
-// export default { saveLoginDetails, loadLoginDetails };
diff --git a/backend/utils/LoginDetails.ts b/backend/utils/LoginDetails.ts
index 9b33d6393321662ee9ca08fc1e4013657dcff80e..2016fd683e310bfb7ef034d45af3bc5149a267f1 100644
--- a/backend/utils/LoginDetails.ts
+++ b/backend/utils/LoginDetails.ts
@@ -2,4 +2,5 @@ import { URI } from '@mysql/xdevapi';
 
 export interface LoginDetails extends URI {
   database?: string;
+  savePassword?: boolean;
 }
diff --git a/backend/utils/mysql.ts b/backend/utils/mysql.ts
index 79a45106b8040805e5efe117a5a77246a8b6033b..5a1e03ef3d471d80681092ecc615a51a1128cfcb 100644
--- a/backend/utils/mysql.ts
+++ b/backend/utils/mysql.ts
@@ -138,7 +138,6 @@ export async function createSession(client?: Client, database?: string) {
     const result = await runQuery(`USE ${database};`, session);
     if (result.error) throw result.error;
   }
-  console.log(session.inspect());
   return session;
 }
 
diff --git a/backend/utils/mysqlx.d.ts b/backend/utils/mysqlx.d.ts
index 8f16fecc90f25ab72c4d918af4a2a98e003362cb..1c016529fd9ec8ee4a9eabd20ea642dff4a740da 100644
--- a/backend/utils/mysqlx.d.ts
+++ b/backend/utils/mysqlx.d.ts
@@ -44,7 +44,7 @@ declare module '@mysql/xdevapi' {
    * Might be wrong, docs were unclear here
    */
   interface PoolingOptions {
-    pooling: { enabled: boolean; maxSize: number };
+    pooling: { enabled: boolean; maxSize?: number };
     enforceJSON?: boolean;
     allowUndefined?: boolean;
   }
diff --git a/configs/webpack.config.base.js b/configs/webpack.config.base.js
index c4c004a735464b3243ff50b0a493a106de416da9..b59295684e305d5cf32f9117d48b8edff1d7d9e9 100644
--- a/configs/webpack.config.base.js
+++ b/configs/webpack.config.base.js
@@ -1,14 +1,15 @@
 /**
  * Base webpack config used across other specific configs
  */
-
 import path from 'path';
 import webpack from 'webpack';
 import { dependencies as externals } from '../app/package.json';
 
+const isDevelopment = process.env.NODE_ENV !== 'production';
+
 export default {
   externals: [...Object.keys(externals || {})],
-
+  mode: isDevelopment ? 'development' : 'production',
   module: {
     rules: [
       {
@@ -18,6 +19,9 @@ export default {
           loader: 'babel-loader',
           options: {
             cacheDirectory: true,
+            plugins: [
+              isDevelopment && require.resolve('react-refresh/babel'),
+            ].filter(Boolean),
           },
         },
       },
@@ -38,11 +42,13 @@ export default {
     modules: [path.join(__dirname, '..', 'app'), 'node_modules'],
   },
 
+  optimization: {
+    namedModules: true,
+  },
+
   plugins: [
     new webpack.EnvironmentPlugin({
       NODE_ENV: 'production',
     }),
-
-    new webpack.NamedModulesPlugin(),
-  ],
+  ].filter(Boolean),
 };
diff --git a/configs/webpack.config.renderer.dev.babel.js b/configs/webpack.config.renderer.dev.babel.js
index 093ba545ce711309d077a9bb953e462d6f342b06..37cec9b9539889cc0652964e3f5c0b706c0825eb 100644
--- a/configs/webpack.config.renderer.dev.babel.js
+++ b/configs/webpack.config.renderer.dev.babel.js
@@ -11,6 +11,7 @@ import webpack from 'webpack';
 import chalk from 'chalk';
 import { merge } from 'webpack-merge';
 import { spawn, execSync } from 'child_process';
+import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
 import baseConfig from './webpack.config.base';
 import CheckNodeEnv from '../internals/scripts/CheckNodeEnv';
 
@@ -50,7 +51,6 @@ export default merge(baseConfig, {
   entry: [
     'core-js',
     'regenerator-runtime/runtime',
-    ...(process.env.PLAIN_HMR ? [] : ['react-hot-loader/patch']),
     `webpack-dev-server/client?http://localhost:${port}/`,
     'webpack/hot/only-dev-server',
     require.resolve('../app/index.tsx'),
@@ -191,10 +191,8 @@ export default merge(baseConfig, {
       },
     ],
   },
-  resolve: {
-    alias: {
-      'react-dom': '@hot-loader/react-dom',
-    },
+  optimization: {
+    noEmitOnErrors: true,
   },
   plugins: [
     requiredByDLLConfig
@@ -208,8 +206,7 @@ export default merge(baseConfig, {
     new webpack.HotModuleReplacementPlugin({
       multiStep: true,
     }),
-
-    new webpack.NoEmitOnErrorsPlugin(),
+    new ReactRefreshWebpackPlugin(),
 
     /**
      * Create global constants which can be configured at compile time.
diff --git a/package.json b/package.json
index d7cb1fce85f735472d4c1c2162de312c09442c8c..48174a9ebbb95491a876af70944f3d963c9d6107 100644
--- a/package.json
+++ b/package.json
@@ -142,6 +142,7 @@
     "@babel/preset-react": "^7.10.4",
     "@babel/preset-typescript": "^7.10.4",
     "@babel/register": "^7.10.5",
+    "@pmmmwh/react-refresh-webpack-plugin": "^0.4.2",
     "@types/electron-devtools-installer": "^2.2.0",
     "@types/enzyme": "^3.10.5",
     "@types/enzyme-adapter-react-16": "^1.0.6",
@@ -150,13 +151,17 @@
     "@types/mysql": "^2.15.15",
     "@types/node": "12",
     "@types/react": "^16.9.49",
+    "@types/react-color": "^3.0.4",
     "@types/react-dom": "^16.9.8",
     "@types/react-panelgroup": "^1.0.1",
     "@types/react-router": "^5.1.8",
     "@types/react-router-dom": "^5.1.5",
     "@types/react-test-renderer": "^16.9.2",
+    "@types/react-virtualized": "^9.21.10",
+    "@types/react-window": "^1.8.2",
     "@types/recharts": "^1.8.15",
     "@types/regenerator-runtime": "^0.13.0",
+    "@types/uuid": "^8.3.0",
     "@types/webpack": "^4.41.21",
     "@types/webpack-env": "^1.15.2",
     "@typescript-eslint/eslint-plugin": "^4.0.1",
@@ -202,6 +207,7 @@
     "opencollective-postinstall": "^2.0.3",
     "optimize-css-assets-webpack-plugin": "^5.0.3",
     "prettier": "^2.1.1",
+    "react-refresh": "^0.8.3",
     "react-test-renderer": "^16.12.0",
     "rimraf": "^3.0.0",
     "sass-loader": "^9.0.2",
@@ -214,21 +220,22 @@
     "testcafe-browser-provider-electron": "^0.0.15",
     "testcafe-react-selectors": "^4.0.0",
     "ts-node": "^9.0.0",
+    "type-fest": "^0.17.0",
     "typescript": "^3.9.7",
     "typings-for-css-modules-loader": "^1.7.0",
     "url-loader": "^4.1.0",
-    "webpack": "^4.43.0",
-    "webpack-bundle-analyzer": "^3.8.0",
+    "webpack": "^4.44.2",
+    "webpack-bundle-analyzer": "^3.9.0",
     "webpack-cli": "^3.3.12",
     "webpack-dev-server": "^3.11.0",
-    "webpack-merge": "^5.0.9",
-    "yarn": "^1.22.5"
+    "webpack-merge": "^5.1.4",
+    "yarn": "^1.22.10"
   },
   "dependencies": {
     "@hot-loader/react-dom": "^16.13.0",
     "@material-ui/core": "^4.11.0",
-    "@material-ui/lab": "^4.0.0-alpha.56",
     "@material-ui/icons": "^4.9.1",
+    "@material-ui/lab": "^4.0.0-alpha.56",
     "@mysql/xdevapi": "8.0.21",
     "concurrently": "^5.3.0",
     "electron-log": "^4.2.4",
@@ -238,14 +245,19 @@
     "lodash": "^4.17.20",
     "react": "^16.13.1",
     "react-dom": "^16.12.0",
+    "react-flame-graph": "^1.4.0",
     "react-hot-loader": "^4.12.21",
+    "react-color": "^2.18.1",
     "react-json-view": "^1.19.1",
     "react-panelgroup": "^1.0.12",
     "react-router-dom": "^5.2.0",
+    "react-virtualized": "^9.22.2",
+    "react-window": "^1.8.5",
     "recharts": "^1.8.5",
     "regenerator-runtime": "^0.13.7",
     "source-map-support": "^0.5.19",
-    "use-deep-compare-effect": "^1.4.0"
+    "use-deep-compare-effect": "^1.4.0",
+    "uuid": "^8.3.0"
   },
   "devEngines": {
     "node": ">=7.x",
diff --git a/yarn.lock b/yarn.lock
index d79c754ed012a760f7173780032ef7e955876933..8d6bf039776667ca71bdb72e180a7a7e851633a8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1105,7 +1105,7 @@
     core-js-pure "^3.0.0"
     regenerator-runtime "^0.13.4"
 
-"@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
+"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
   version "7.11.2"
   resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.11.2.tgz#f549c13c754cc40b87644b9fa9f09a6a95fe0736"
   integrity sha512-TeWkU52so0mPtDcaCTxNBI/IHiz0pZgr8VEFqXFtZWpYD08ZB6FaSwVAS8MKRQAP3bYKiVjwysOJgMFY28o6Tw==
@@ -1237,6 +1237,11 @@
     prop-types "^15.6.2"
     scheduler "^0.19.0"
 
+"@icons/material@^0.2.4":
+  version "0.2.4"
+  resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8"
+  integrity sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==
+
 "@istanbuljs/load-nyc-config@^1.0.0":
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced"
@@ -1565,6 +1570,18 @@
   dependencies:
     mkdirp "^1.0.4"
 
+"@pmmmwh/react-refresh-webpack-plugin@^0.4.2":
+  version "0.4.2"
+  resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.4.2.tgz#1f9741e0bde9790a0e13272082ed7272a083620d"
+  integrity sha512-Loc4UDGutcZ+Bd56hBInkm6JyjyCwWy4t2wcDXzN8EDPANgVRj0VP8Nxn0Zq2pc+WKauZwEivQgbDGg4xZO20A==
+  dependencies:
+    ansi-html "^0.0.7"
+    error-stack-parser "^2.0.6"
+    html-entities "^1.2.1"
+    native-url "^0.2.6"
+    schema-utils "^2.6.5"
+    source-map "^0.7.3"
+
 "@sindresorhus/is@^0.14.0":
   version "0.14.0"
   resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea"
@@ -1860,6 +1877,14 @@
   resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24"
   integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==
 
+"@types/react-color@^3.0.4":
+  version "3.0.4"
+  resolved "https://registry.yarnpkg.com/@types/react-color/-/react-color-3.0.4.tgz#c63daf012ad067ac0127bdd86725f079d02082bd"
+  integrity sha512-EswbYJDF1kkrx93/YU+BbBtb46CCtDMvTiGmcOa/c5PETnwTiSWoseJ1oSWeRl/4rUXkhME9bVURvvPg0W5YQw==
+  dependencies:
+    "@types/react" "*"
+    "@types/reactcss" "*"
+
 "@types/react-dom@^16.9.8":
   version "16.9.8"
   resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423"
@@ -1905,6 +1930,21 @@
   dependencies:
     "@types/react" "*"
 
+"@types/react-virtualized@^9.21.10":
+  version "9.21.10"
+  resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.10.tgz#cd072dc9c889291ace2c4c9de8e8c050da8738b7"
+  integrity sha512-f5Ti3A7gGdLkPPFNHTrvKblpsPNBiQoSorOEOD+JPx72g/Ng2lOt4MYfhvQFQNgyIrAro+Z643jbcKafsMW2ag==
+  dependencies:
+    "@types/prop-types" "*"
+    "@types/react" "*"
+
+"@types/react-window@^1.8.2":
+  version "1.8.2"
+  resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.2.tgz#a5a6b2762ce73ffaab7911ee1397cf645f2459fe"
+  integrity sha512-gP1xam68Wc4ZTAee++zx6pTdDAH08rAkQrWm4B4F/y6hhmlT9Mgx2q8lTCXnrPHXsr15XjRN9+K2DLKcz44qEQ==
+  dependencies:
+    "@types/react" "*"
+
 "@types/react@*", "@types/react@^16.9.49":
   version "16.9.49"
   resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.49.tgz#09db021cf8089aba0cdb12a49f8021a69cce4872"
@@ -1913,6 +1953,13 @@
     "@types/prop-types" "*"
     csstype "^3.0.2"
 
+"@types/reactcss@*":
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/@types/reactcss/-/reactcss-1.2.3.tgz#af28ae11bbb277978b99d04d1eedfd068ca71834"
+  integrity sha512-d2gQQ0IL6hXLnoRfVYZukQNWHuVsE75DzFTLPUuyyEhJS8G2VvlE+qfQQ91SJjaMqlURRCNIsX7Jcsw6cEuJlA==
+  dependencies:
+    "@types/react" "*"
+
 "@types/recharts@^1.8.15":
   version "1.8.15"
   resolved "https://registry.yarnpkg.com/@types/recharts/-/recharts-1.8.15.tgz#02bc06085c9a31a58c00194d15377b45cf506bbf"
@@ -1965,6 +2012,11 @@
   dependencies:
     "@types/react" "*"
 
+"@types/uuid@^8.3.0":
+  version "8.3.0"
+  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f"
+  integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==
+
 "@types/webpack-env@^1.15.2":
   version "1.15.3"
   resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.15.3.tgz#fb602cd4c2f0b7c0fb857e922075fdf677d25d84"
@@ -2454,7 +2506,7 @@ ansi-escapes@^4.2.1, ansi-escapes@^4.3.0:
   dependencies:
     type-fest "^0.11.0"
 
-ansi-html@0.0.7:
+ansi-html@0.0.7, ansi-html@^0.0.7:
   version "0.0.7"
   resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e"
   integrity sha1-gTWEAhliqenm/QOflA0S9WynhZ4=
@@ -5612,7 +5664,7 @@ dom-helpers@^3.4.0:
   dependencies:
     "@babel/runtime" "^7.1.2"
 
-dom-helpers@^5.0.1:
+dom-helpers@^5.0.1, dom-helpers@^5.1.3:
   version "5.2.0"
   resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.0.tgz#57fd054c5f8f34c52a3eeffdb7e7e93cd357d95b"
   integrity sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==
@@ -6060,6 +6112,13 @@ error-stack-parser@^1.3.3, error-stack-parser@^1.3.6:
   dependencies:
     stackframe "^0.3.1"
 
+error-stack-parser@^2.0.6:
+  version "2.0.6"
+  resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.6.tgz#5a99a707bd7a4c58a797902d48d82803ede6aad8"
+  integrity sha512-d51brTeqC+BHlwF0BhPtcYgF5nlzf9ZZ0ZIUQNZpc9ZB9qw5IJ2diTrBY9jlCJkTLITYPjmiX6OWCwH+fuyNgQ==
+  dependencies:
+    stackframe "^1.1.1"
+
 es-abstract@^1.17.0, es-abstract@^1.17.0-next.1, es-abstract@^1.17.2, es-abstract@^1.17.4, es-abstract@^1.17.5:
   version "1.17.6"
   resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.17.6.tgz#9142071707857b2cacc7b89ecb670316c3e2d52a"
@@ -6919,6 +6978,11 @@ flatted@^2.0.0:
   resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.2.tgz#4575b21e2bcee7434aa9be662f4b7b5f9c2b5138"
   integrity sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==
 
+flow-bin@^0.118.0:
+  version "0.118.0"
+  resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.118.0.tgz#fb706364a58c682d67a2ca7df39396467dc397d1"
+  integrity sha512-jlbUu0XkbpXeXhan5xyTqVK1jmEKNxE8hpzznI3TThHTr76GiFwK0iRzhDo4KNy+S9h/KxHaqVhTP86vA6wHCg==
+
 flush-write-stream@^1.0.0:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.1.1.tgz#8dd7d873a1babc207d94ead0c2e0e44276ebf2e8"
@@ -7622,7 +7686,7 @@ html-encoding-sniffer@^2.0.1:
   dependencies:
     whatwg-encoding "^1.0.5"
 
-html-entities@^1.3.1:
+html-entities@^1.2.1, html-entities@^1.3.1:
   version "1.3.1"
   resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44"
   integrity sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA==
@@ -9462,7 +9526,7 @@ lodash.uniq@^4.5.0:
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
   integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
 
-"lodash@4.6.1 || ^4.16.1", lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.4:
+"lodash@4.6.1 || ^4.16.1", lodash@^4.0.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.4:
   version "4.17.20"
   resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
   integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
@@ -9627,6 +9691,11 @@ matcher@^3.0.0:
   dependencies:
     escape-string-regexp "^4.0.0"
 
+material-colors@^1.2.1:
+  version "1.2.6"
+  resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46"
+  integrity sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg==
+
 math-expression-evaluator@^1.2.14:
   version "1.2.22"
   resolved "https://registry.yarnpkg.com/math-expression-evaluator/-/math-expression-evaluator-1.2.22.tgz#c14dcb3d8b4d150e5dcea9c68c8dad80309b0d5e"
@@ -9675,6 +9744,16 @@ media-typer@0.3.0:
   resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
   integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
 
+"memoize-one@>=3.1.1 <6":
+  version "5.1.1"
+  resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.1.1.tgz#047b6e3199b508eaec03504de71229b8eb1d75c0"
+  integrity sha512-HKeeBpWvqiVJD57ZUAsJNm71eHTykffzcLZVYWiVfQeI1rJtuEaS7hQiEpWfVVk18donPwJEcFKIkCmPJNOhHA==
+
+memoize-one@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-3.1.1.tgz#ef609811e3bc28970eac2884eece64d167830d17"
+  integrity sha512-YqVh744GsMlZu6xkhGslPSqSurOv6P+kLN2J3ysBZfagLcL5FdRK/0UpgLoL8hwjjEvvAVkjJZyFP+1T6p1vgA==
+
 memory-fs@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.2.0.tgz#f2bb25368bc121e391c2520de92969caee0a0290"
@@ -10067,6 +10146,13 @@ nanomatch@^1.2.9:
     snapdragon "^0.8.1"
     to-regex "^3.0.1"
 
+native-url@^0.2.6:
+  version "0.2.6"
+  resolved "https://registry.yarnpkg.com/native-url/-/native-url-0.2.6.tgz#ca1258f5ace169c716ff44eccbddb674e10399ae"
+  integrity sha512-k4bDC87WtgrdD362gZz6zoiXQrl40kYlBmpfmSjwRO1VU0V5ccwJTlxuE72F6m3V0vc1xOf6n3UCP9QyerRqmA==
+  dependencies:
+    querystring "^0.2.0"
+
 natural-compare@^1.4.0:
   version "1.4.0"
   resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7"
@@ -11465,7 +11551,7 @@ prop-types-exact@^1.2.0:
     object.assign "^4.1.0"
     reflect.ownkeys "^0.2.0"
 
-prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
+prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2:
   version "15.7.2"
   resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
   integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
@@ -11603,7 +11689,7 @@ querystring-es3@^0.2.0:
   resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
   integrity sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=
 
-querystring@0.2.0:
+querystring@0.2.0, querystring@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/querystring/-/querystring-0.2.0.tgz#b209849203bb25df820da756e747005878521620"
   integrity sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=
@@ -11688,6 +11774,18 @@ react-base16-styling@^0.6.0:
     lodash.flow "^3.3.0"
     pure-color "^1.2.0"
 
+react-color@^2.18.1:
+  version "2.18.1"
+  resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.18.1.tgz#2cda8cc8e06a9e2c52ad391a30ddad31972472f4"
+  integrity sha512-X5XpyJS6ncplZs74ak0JJoqPi+33Nzpv5RYWWxn17bslih+X7OlgmfpmGC1fNvdkK7/SGWYf1JJdn7D2n5gSuQ==
+  dependencies:
+    "@icons/material" "^0.2.4"
+    lodash "^4.17.11"
+    material-colors "^1.2.1"
+    prop-types "^15.5.10"
+    reactcss "^1.2.0"
+    tinycolor2 "^1.4.1"
+
 react-dom@^16.12.0:
   version "16.13.1"
   resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
@@ -11698,6 +11796,15 @@ react-dom@^16.12.0:
     prop-types "^15.6.2"
     scheduler "^0.19.1"
 
+react-flame-graph@^1.4.0:
+  version "1.4.0"
+  resolved "https://registry.yarnpkg.com/react-flame-graph/-/react-flame-graph-1.4.0.tgz#52d118cc94348f630a812fc0ec530a5b73c30cdb"
+  integrity sha512-DaCK9ZX+xK0mNca72kUE5cu6T8hGe/KLsefQWf+eT9sVt+0WP1dVxZCGD8Svfn2KrZB9Mv011Intg/yG2YWSxA==
+  dependencies:
+    flow-bin "^0.118.0"
+    memoize-one "^3.1.1"
+    react-window "^1"
+
 react-hot-loader@^4.12.21:
   version "4.12.21"
   resolved "https://registry.yarnpkg.com/react-hot-loader/-/react-hot-loader-4.12.21.tgz#332e830801fb33024b5a147d6b13417f491eb975"
@@ -11739,6 +11846,11 @@ react-panelgroup@^1.0.12:
   dependencies:
     prop-types "^15.6.1"
 
+react-refresh@^0.8.3:
+  version "0.8.3"
+  resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
+  integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
+
 react-resize-detector@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/react-resize-detector/-/react-resize-detector-2.3.0.tgz#57bad1ae26a28a62a2ddb678ba6ffdf8fa2b599c"
@@ -11825,6 +11937,26 @@ react-transition-group@^4.4.0:
     loose-envify "^1.4.0"
     prop-types "^15.6.2"
 
+react-virtualized@^9.22.2:
+  version "9.22.2"
+  resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.2.tgz#217a870bad91e5438f46f01a009e1d8ce1060a5a"
+  integrity sha512-5j4h4FhxTdOpBKtePSs1yk6LDNT4oGtUwjT7Nkh61Z8vv3fTG/XeOf8J4li1AYaexOwTXnw0HFVxsV0GBUqwRw==
+  dependencies:
+    "@babel/runtime" "^7.7.2"
+    clsx "^1.0.4"
+    dom-helpers "^5.1.3"
+    loose-envify "^1.4.0"
+    prop-types "^15.7.2"
+    react-lifecycles-compat "^3.0.4"
+
+react-window@^1, react-window@^1.8.5:
+  version "1.8.5"
+  resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.5.tgz#a56b39307e79979721021f5d06a67742ecca52d1"
+  integrity sha512-HeTwlNa37AFa8MDZFZOKcNEkuF2YflA0hpGPiTT9vR7OawEt+GZbfM6wqkBahD3D3pUjIabQYzsnY/BSJbgq6Q==
+  dependencies:
+    "@babel/runtime" "^7.0.0"
+    memoize-one ">=3.1.1 <6"
+
 react@^16.13.1:
   version "16.13.1"
   resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
@@ -11834,6 +11966,13 @@ react@^16.13.1:
     object-assign "^4.1.1"
     prop-types "^15.6.2"
 
+reactcss@^1.2.0:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/reactcss/-/reactcss-1.2.3.tgz#c00013875e557b1cf0dfd9a368a1c3dab3b548dd"
+  integrity sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==
+  dependencies:
+    lodash "^4.0.1"
+
 read-config-file@6.0.0:
   version "6.0.0"
   resolved "https://registry.yarnpkg.com/read-config-file/-/read-config-file-6.0.0.tgz#224b5dca6a5bdc1fb19e63f89f342680efdb9299"
@@ -13067,6 +13206,11 @@ stackframe@^0.3.1:
   resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-0.3.1.tgz#33aa84f1177a5548c8935533cbfeb3420975f5a4"
   integrity sha1-M6qE8Rd6VUjIk1Uzy/6zQgl19aQ=
 
+stackframe@^1.1.1:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.2.0.tgz#52429492d63c62eb989804c11552e3d22e779303"
+  integrity sha512-GrdeshiRmS1YLMYgzF16olf2jJ/IzxXY9lhKOskuVziubpTYcYqyOwYeJKzQkwy7uN0fYSsbsC4RQaXf9LCrYA==
+
 stat-mode@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-1.0.0.tgz#68b55cb61ea639ff57136f36b216a291800d1465"
@@ -13880,6 +14024,11 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.2, tiny-warning@^1.0.3:
   resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
   integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
 
+tinycolor2@^1.4.1:
+  version "1.4.2"
+  resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.4.2.tgz#3f6a4d1071ad07676d7fa472e1fac40a719d8803"
+  integrity sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==
+
 tmp-promise@^1.0.5:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/tmp-promise/-/tmp-promise-1.1.0.tgz#bb924d239029157b9bc1d506a6aa341f8b13e64c"
@@ -14121,6 +14270,11 @@ type-fest@^0.13.1:
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934"
   integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==
 
+type-fest@^0.17.0:
+  version "0.17.0"
+  resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.17.0.tgz#268bb55d38701ce3915f60a4367a1e9f28672deb"
+  integrity sha512-EFi9HE4hHj85XnVV80uAUMgICQmhxYgiEvtmfpcD6jqn6zYr36HxAU6k+i/DSY28TK7/lYL0s4v/kWmiKdqaoA==
+
 type-fest@^0.6.0:
   version "0.6.0"
   resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"
@@ -14613,7 +14767,7 @@ webidl-conversions@^6.1.0:
   resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-6.1.0.tgz#9111b4d7ea80acd40f5270d666621afa78b69514"
   integrity sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==
 
-webpack-bundle-analyzer@^3.8.0:
+webpack-bundle-analyzer@^3.9.0:
   version "3.9.0"
   resolved "https://registry.yarnpkg.com/webpack-bundle-analyzer/-/webpack-bundle-analyzer-3.9.0.tgz#f6f94db108fb574e415ad313de41a2707d33ef3c"
   integrity sha512-Ob8amZfCm3rMB1ScjQVlbYYUEJyEjdEtQ92jqiFUYt5VkEeO2v5UMbv49P/gnmCZm3A6yaFQzCBvpZqN4MUsdA==
@@ -14707,7 +14861,7 @@ webpack-log@^2.0.0:
     ansi-colors "^3.0.0"
     uuid "^3.3.2"
 
-webpack-merge@^5.0.9:
+webpack-merge@^5.1.4:
   version "5.1.4"
   resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.1.4.tgz#a2c3a0c38ac2c02055c47bb1d42de1f072f1aea4"
   integrity sha512-LSmRD59mxREGkCBm9PCW3AaV4doDqxykGlx1NvioEE0FgkT2GQI54Wyvg39ptkiq2T11eRVoV39udNPsQvK+QQ==
@@ -14723,7 +14877,7 @@ webpack-sources@^1.1.0, webpack-sources@^1.4.0, webpack-sources@^1.4.1, webpack-
     source-list-map "^2.0.0"
     source-map "~0.6.1"
 
-webpack@^4.43.0:
+webpack@^4.44.2:
   version "4.44.2"
   resolved "https://registry.yarnpkg.com/webpack/-/webpack-4.44.2.tgz#6bfe2b0af055c8b2d1e90ed2cd9363f841266b72"
   integrity sha512-6KJVGlCxYdISyurpQ0IPTklv+DULv05rs2hseIXer6D7KrUicRDLFb4IUM1S6LUAKypPM/nSiVSuv8jHu1m3/Q==
@@ -15051,10 +15205,10 @@ yargs@^15.3.1, yargs@^15.4.1:
     y18n "^4.0.0"
     yargs-parser "^18.1.2"
 
-yarn@^1.22.5:
-  version "1.22.5"
-  resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.5.tgz#1933b7635429ca00847222dd9d38f05646e2df23"
-  integrity sha512-5uzKXwdMc++mYktXqkfpNYT9tY8ViWegU58Hgbo+KXzrzzhEyP1Ip+BTtXloLrXNcNlxFJbLiFKGaS9vK9ym6Q==
+yarn@^1.22.10:
+  version "1.22.10"
+  resolved "https://registry.yarnpkg.com/yarn/-/yarn-1.22.10.tgz#c99daa06257c80f8fa2c3f1490724e394c26b18c"
+  integrity sha512-IanQGI9RRPAN87VGTF7zs2uxkSyQSrSPsju0COgbsKQOOXr5LtcVPeyXWgwVa0ywG3d8dg6kSYKGBuYK021qeA==
 
 yauzl@^2.10.0:
   version "2.10.0"