Merge pull request #95 from pi-hole/ci/formatter

Use a Formatter for Consistent Code Style
This commit is contained in:
Mark Drobnak
2018-10-21 15:24:00 -04:00
committed by GitHub
65 changed files with 1631 additions and 1106 deletions
+4
View File
@@ -29,6 +29,10 @@ jobs:
- v1-build-{{ checksum "package-lock.json" }}
- v1-build-
- *install_packages
- run:
name: "Check Formatting"
command: |
npm run check-format
- *test
- run:
name: "Build"
+3 -7
View File
@@ -6523,14 +6523,12 @@
"balanced-match": {
"version": "1.0.0",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"brace-expansion": {
"version": "1.1.11",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -6550,8 +6548,7 @@
"concat-map": {
"version": "0.0.1",
"bundled": true,
"dev": true,
"optional": true
"dev": true
},
"console-control-strings": {
"version": "1.1.0",
@@ -6699,7 +6696,6 @@
"version": "3.0.4",
"bundled": true,
"dev": true,
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -11724,7 +11720,7 @@
"dependencies": {
"semver": {
"version": "5.3.0",
"resolved": "http://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
"integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8=",
"dev": true
}
+4 -1
View File
@@ -15,6 +15,7 @@
"node-fetch": "^2.1.2",
"node-sass-chokidar": "^1.3.0",
"npm-run-all": "^4.1.3",
"prettier": "^1.14.3",
"react-scripts": "^2.0.5",
"react-test-renderer": "^16.4.1"
},
@@ -53,7 +54,9 @@
"watch-css": "node-sass-chokidar --include-path ./node_modules ./src/scss -o ./src/scss --watch --recursive",
"update-translations": "node update-translations.js",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
"eject": "react-scripts eject",
"format": "prettier --write \"src/**/*.js\"",
"check-format": "prettier --list-different \"src/**/*.js\""
},
"browserslist": [
">0.2%",
+6 -4
View File
@@ -8,12 +8,14 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from 'react';
import PropTypes from 'prop-types';
import React from "react";
import PropTypes from "prop-types";
const Alert = (props) => {
const Alert = props => {
return (
<div className={"alert alert-" + props.type + " alert-dismissible fade show"}>
<div
className={"alert alert-" + props.type + " alert-dismissible fade show"}
>
<button type="button" className="close" onClick={props.onClick}>
&times;
</button>
+11 -7
View File
@@ -8,23 +8,27 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from 'react';
import { shallow } from 'enzyme';
import Alert from './Alert';
import React from "react";
import { shallow } from "enzyme";
import Alert from "./Alert";
it('renders an alert correctly', () => {
it("renders an alert correctly", () => {
const message = "test message";
const onClick = jest.fn();
const wrapper = shallow(<Alert type="info" message={message} onClick={onClick}/>);
const wrapper = shallow(
<Alert type="info" message={message} onClick={onClick} />
);
expect(wrapper).toHaveClassName("alert-info");
expect(wrapper).toIncludeText(message);
});
it('calls onClick when clicked', () => {
it("calls onClick when clicked", () => {
const message = "test message";
const onClick = jest.fn();
const wrapper = shallow(<Alert type="info" message={message} onClick={onClick}/>);
const wrapper = shallow(
<Alert type="info" message={message} onClick={onClick} />
);
wrapper.find("button").simulate("click");
expect(onClick).toBeCalled();
+11 -11
View File
@@ -8,24 +8,24 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from 'react';
import React from "react";
const boxedLayoutToggle = () => {
document.body.classList.toggle('boxcontainer');
document.body.classList.toggle('background-image');
document.body.classList.toggle("boxcontainer");
document.body.classList.toggle("background-image");
};
export default () => {
return (
<div className="container-toggler">
<button className="container-toggler-button"
title="BOXED_LAYOUT_TEST_BUTTON" type="button"
onClick={boxedLayoutToggle}>
<button
className="container-toggler-button"
title="BOXED_LAYOUT_TEST_BUTTON"
type="button"
onClick={boxedLayoutToggle}
>
<i className="fa fa-exchange" />
</button>
</div>
)
}
);
};
@@ -8,16 +8,16 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from 'react';
import { shallow } from 'enzyme';
import BoxedLayoutButton from './BoxedLayoutButton';
import React from "react";
import { shallow } from "enzyme";
import BoxedLayoutButton from "./BoxedLayoutButton";
it("renders without crashing", () => {
shallow(<BoxedLayoutButton/>);
shallow(<BoxedLayoutButton />);
});
it("toggles boxed layout", () => {
const wrapper = shallow(<BoxedLayoutButton/>);
const wrapper = shallow(<BoxedLayoutButton />);
expect(document.body.classList).not.toContain("boxcontainer");
expect(document.body.classList).not.toContain("background-image");
+6 -4
View File
@@ -8,7 +8,7 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from 'react';
import React from "react";
import { translate } from "react-i18next";
const Footer = props => {
@@ -17,12 +17,14 @@ const Footer = props => {
return (
<footer className="app-footer">
<div>
<i className="fa fa-paypal"/>
<i className="fa fa-paypal" />
<strong>
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&amp;hosted_button_id=3J2L3Z4DHW9UY">
&nbsp;{t("Donate")}
&nbsp;
{t("Donate")}
</a>
</strong> {t("if you found this useful")}
</strong>{" "}
{t("if you found this useful")}
</div>
<div className="ml-auto">
<b className="hidden-xs-down">{t("Core Version")}: </b>
+4 -4
View File
@@ -8,10 +8,10 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from 'react';
import { shallow } from 'enzyme';
import React from "react";
import { shallow } from "enzyme";
import Footer from "./Footer";
it('renders without crashing', () => {
shallow(<Footer/>);
it("renders without crashing", () => {
shallow(<Footer />);
});
+29 -13
View File
@@ -8,7 +8,7 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from 'react';
import React from "react";
import { Link } from "react-router-dom";
import BoxedLayoutButton from "./BoxedLayoutButton";
@@ -19,16 +19,16 @@ import BoxedLayoutButton from "./BoxedLayoutButton";
const sidebarMinimize = e => {
e.preventDefault();
document.body.classList.toggle('sidebar-minimized');
document.body.classList.toggle('brand-minimized');
document.body.classList.toggle("sidebar-minimized");
document.body.classList.toggle("brand-minimized");
};
export const mobileSidebarToggle = () => {
document.body.classList.toggle('sidebar-show');
document.body.classList.toggle("sidebar-show");
};
export const mobileSidebarHide = () => {
document.body.classList.remove('sidebar-show');
document.body.classList.remove("sidebar-show");
};
//const asideToggle = e => {
@@ -38,20 +38,36 @@ export const mobileSidebarHide = () => {
export default () => (
<header className="app-header navbar">
<button className="navbar-toggler d-lg-none" style={{ marginLeft: "16px" }}
onClick={mobileSidebarToggle} type="button">
<button
className="navbar-toggler d-lg-none"
style={{ marginLeft: "16px" }}
onClick={mobileSidebarToggle}
type="button"
>
&#9776;
</button>
<Link to="/dashboard" className="navbar-brand" style={{ "textAlign": "center" }}>
<span style={{color: "white", lineHeight: "40px"}}>
<span className="navbar-brand-full">Pi-<b>hole</b></span>
<span className="navbar-brand-minimized">P<b>h</b></span>
<Link
to="/dashboard"
className="navbar-brand"
style={{ textAlign: "center" }}
>
<span style={{ color: "white", lineHeight: "40px" }}>
<span className="navbar-brand-full">
Pi-
<b>hole</b>
</span>
<span className="navbar-brand-minimized">
P<b>h</b>
</span>
</span>
</Link>
<ul className="nav navbar-nav d-md-down-none mr-auto">
<li className="nav-item">
<button className="nav-link navbar-toggler sidebar-toggler"
type="button" onClick={sidebarMinimize}>
<button
className="nav-link navbar-toggler sidebar-toggler"
type="button"
onClick={sidebarMinimize}
>
&#9776;
</button>
</li>
+39 -21
View File
@@ -17,18 +17,26 @@ import { mobileSidebarHide } from "./Header";
import { api } from "../../utils";
import StatusBadge from "./StatusBadge";
const handleClick = (e) => {
const handleClick = e => {
e.preventDefault();
e.target.parentElement.classList.toggle("open");
};
export const dropDownClassList = (routeName, props) =>
props.location.pathname.startsWith(routeName) ? "nav-item nav-dropdown open" : "nav-item nav-dropdown";
props.location.pathname.startsWith(routeName)
? "nav-item nav-dropdown open"
: "nav-item nav-dropdown";
export const navItem = (item, key, props) => (
<NavItem key={key}>
<NavLink to={item.url} onClick={mobileSidebarHide} className="nav-link" activeClassName="active">
<i className={"nav-icon " + item.icon}/>{props.t(item.name)}
<NavLink
to={item.url}
onClick={mobileSidebarHide}
className="nav-link"
activeClassName="active"
>
<i className={"nav-icon " + item.icon} />
{props.t(item.name)}
</NavLink>
</NavItem>
);
@@ -36,26 +44,25 @@ export const navItem = (item, key, props) => (
export const navDropdown = (item, key, props) => (
<li key={key} className={dropDownClassList(item.url, props)}>
<button className="nav-link nav-dropdown-toggle" onClick={handleClick}>
<i className={"nav-icon " + item.icon}/>{props.t(item.name)}
<i className={"nav-icon " + item.icon} />
{props.t(item.name)}
</button>
<ul className="nav-dropdown-items">
{navList(item.children, props)}
</ul>
<ul className="nav-dropdown-items">{navList(item.children, props)}</ul>
</li>
);
export const navList = (items, props) =>
items.map((item, index) => {
// Don't show an item if it requires auth and we're not logged in
if(item.auth && !api.loggedIn)
return null;
if (item.auth && !api.loggedIn) return null;
// Some items (login page) should only be shown when logged in or logged out, not both
if(item.authStrict && item.auth !== api.loggedIn)
return null;
if (item.authStrict && item.auth !== api.loggedIn) return null;
// At this point it's ok to show the item
return item.children ? navDropdown(item, index, props) : navItem(item, index, props);
return item.children
? navDropdown(item, index, props)
: navItem(item, index, props);
});
const Sidebar = ({ items, ...props }) => {
@@ -64,17 +71,28 @@ const Sidebar = ({ items, ...props }) => {
<nav className="sidebar-nav">
<Nav>
<li className="nav-title">
<img src={logo} className="img-responsive pull-left" style={{ height: "67px" }} alt=""/>
<p className="pull-left"
style={{
paddingLeft: "15px", textTransform: "initial", fontSize: "14px", marginBottom: "initial",
lineHeight: "14px", color: "white"
}}>
<img
src={logo}
className="img-responsive pull-left"
style={{ height: "67px" }}
alt=""
/>
<p
className="pull-left"
style={{
paddingLeft: "15px",
textTransform: "initial",
fontSize: "14px",
marginBottom: "initial",
lineHeight: "14px",
color: "white"
}}
>
{props.t("Status")}
</p>
<br/>
<br />
<span style={{ textTransform: "initial", paddingLeft: "15px" }}>
<StatusBadge/>
<StatusBadge />
</span>
</li>
{navList(items, props)}
+24 -8
View File
@@ -14,13 +14,17 @@ import { dropDownClassList, navDropdown, navItem, navList } from "./Sidebar";
import { api } from "../../utils";
it("expands active drop down items", () => {
const classList = dropDownClassList("/testRoute", { location: { pathname: "/testRoute/page" } });
const classList = dropDownClassList("/testRoute", {
location: { pathname: "/testRoute/page" }
});
expect(classList).toContain("open");
});
it("does not expand inactive drop down items", () => {
const classList = dropDownClassList("/testRoute", { location: { pathname: "/page" } });
const classList = dropDownClassList("/testRoute", {
location: { pathname: "/page" }
});
expect(classList).not.toContain("open");
});
@@ -28,7 +32,9 @@ it("does not expand inactive drop down items", () => {
it("creates nav items with correct data", () => {
const item = { url: "/testUrl", icon: "test-icon", name: "testName" };
const key = "testKey";
const wrapper = shallow(React.createElement(() => navItem(item, key, { t: key => key })));
const wrapper = shallow(
React.createElement(() => navItem(item, key, { t: key => key }))
);
expect(wrapper.key()).toEqual(key);
expect(wrapper.childAt(0)).toHaveProp("to", item.url);
@@ -51,7 +57,9 @@ it("creates a nav dropdown with correct data", () => {
pathname: "/blacklist/exact"
}
};
const wrapper = shallow(React.createElement(() => navDropdown(item, key, props)));
const wrapper = shallow(
React.createElement(() => navDropdown(item, key, props))
);
expect(wrapper.key()).toEqual(key);
expect(wrapper.childAt(0).childAt(0)).toHaveClassName(item.icon);
@@ -68,7 +76,9 @@ it("shows auth routes when logged in", () => {
icon: "fa fa-database",
auth: true
};
const wrapper = shallow(React.createElement(() => <ul>{navList([item], { t: key => key })}</ul>));
const wrapper = shallow(
React.createElement(() => <ul>{navList([item], { t: key => key })}</ul>)
);
expect(wrapper.children()).toHaveLength(1);
});
@@ -82,7 +92,9 @@ it("hides auth routes when not logged in", () => {
icon: "fa fa-database",
auth: true
};
const wrapper = shallow(React.createElement(() => <ul>{navList([item], { t: key => key })}</ul>));
const wrapper = shallow(
React.createElement(() => <ul>{navList([item], { t: key => key })}</ul>)
);
expect(wrapper.children()).toHaveLength(0);
});
@@ -97,7 +109,9 @@ it("hides strict non-auth routes when logged in", () => {
auth: false,
authStrict: true
};
const wrapper = shallow(React.createElement(() => <ul>{navList([item], { t: key => key })}</ul>));
const wrapper = shallow(
React.createElement(() => <ul>{navList([item], { t: key => key })}</ul>)
);
expect(wrapper.children()).toHaveLength(0);
});
@@ -112,7 +126,9 @@ it("hides strict auth routes when not logged in", () => {
auth: true,
authStrict: true
};
const wrapper = shallow(React.createElement(() => <ul>{navList([item], { t: key => key })}</ul>));
const wrapper = shallow(
React.createElement(() => <ul>{navList([item], { t: key => key })}</ul>)
);
expect(wrapper.children()).toHaveLength(0);
});
+20 -11
View File
@@ -1,6 +1,6 @@
import React, { Component } from 'react';
import { translate } from 'react-i18next';
import { api, makeCancelable, ignoreCancel } from '../../utils';
import React, { Component } from "react";
import { translate } from "react-i18next";
import { api, makeCancelable, ignoreCancel } from "../../utils";
class StatusBadge extends Component {
state = {
@@ -8,7 +8,10 @@ class StatusBadge extends Component {
};
updateStatus = () => {
this.updateHandler = makeCancelable(api.getStatus(), { repeat: this.updateStatus, interval: 5000 });
this.updateHandler = makeCancelable(api.getStatus(), {
repeat: this.updateStatus,
interval: 5000
});
this.updateHandler.promise
.then(res => {
this.setState({ status: res.status });
@@ -30,14 +33,20 @@ class StatusBadge extends Component {
render() {
const { t } = this.props;
return (
this.state.status === "enabled"
?
<span><i className="fa fa-circle text-success"/>&nbsp;{t("Enabled")}</span>
:
<span><i className="fa fa-circle text-danger"/>&nbsp;{t("Disabled")}</span>
return this.state.status === "enabled" ? (
<span>
<i className="fa fa-circle text-success" />
&nbsp;
{t("Enabled")}
</span>
) : (
<span>
<i className="fa fa-circle text-danger" />
&nbsp;
{t("Disabled")}
</span>
);
}
}
export default translate("common")(StatusBadge);
export default translate("common")(StatusBadge);
+3 -3
View File
@@ -18,7 +18,7 @@ const endpoint = "/admin/api/dns/status";
it("shows green enabled message if API returns enabled", async () => {
fetchMock.mock(endpoint, { body: { status: "enabled" } });
const wrapper = shallow(<StatusBadge/>);
const wrapper = shallow(<StatusBadge />);
await tick();
wrapper.update();
@@ -30,7 +30,7 @@ it("shows green enabled message if API returns enabled", async () => {
it("shows red disabled message if API doesn't return enabled", async () => {
fetchMock.mock(endpoint, { body: { status: "disabled" } });
const wrapper = shallow(<StatusBadge/>);
const wrapper = shallow(<StatusBadge />);
await tick();
wrapper.update();
@@ -42,7 +42,7 @@ it("shows red disabled message if API doesn't return enabled", async () => {
it("shows disabled message if API call failed", async () => {
fetchMock.mock(endpoint, { error: {} });
const wrapper = shallow(<StatusBadge/>);
const wrapper = shallow(<StatusBadge />);
await tick();
wrapper.update();
+31 -32
View File
@@ -8,8 +8,8 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import React, { Component } from "react";
import PropTypes from "prop-types";
class ChartTooltip extends Component {
state = {
@@ -24,20 +24,22 @@ class ChartTooltip extends Component {
let lastTime = Date.now();
this.props.handler.custom = tooltip => {
// Always disable the tooltip
if(tooltip.opacity === 0)
this.setState({ tooltip });
if (tooltip.opacity === 0) this.setState({ tooltip });
// Limit how quickly we set state (50ms)
const now = Date.now();
if(now - lastTime < 50)
return;
if (now - lastTime < 50) return;
lastTime = now;
this.setState({ tooltip });
};
// Don't render anything if there is nothing to render
if(tooltip === null || tooltip.opacity === 0 || this.props.chart.current === null)
if (
tooltip === null ||
tooltip.opacity === 0 ||
this.props.chart.current === null
)
return null;
// Get the graph's position data so we can offset the tooltip
@@ -45,12 +47,11 @@ class ChartTooltip extends Component {
let width = tooltip.caretX;
// Prevent compression of the tooltip at the right edge of the screen
if(document.offsetWidth - tooltip.caretX < 400)
if (document.offsetWidth - tooltip.caretX < 400)
width = document.offsetWidth - 400;
// Prevent tooltip disappearing behind the sidebar
if(tooltip.caretX < 100)
width = 100;
if (tooltip.caretX < 100) width = 100;
// Tooltip CSS styling
const style = {
@@ -65,44 +66,42 @@ class ChartTooltip extends Component {
// Transform and sort the tooltip data
let data = [];
if(tooltip.body) {
data = tooltip.body
.map(body => body.lines)
.map((item, i) => ({
data: item[0],
colors: tooltip.labelColors[i]
}));
if (tooltip.body) {
data = tooltip.body.map(body => body.lines).map((item, i) => ({
data: item[0],
colors: tooltip.labelColors[i]
}));
}
data.sort((a, b) => a.data.split(": ")[0].localeCompare(b.data.split(": ")[0]));
data.sort((a, b) =>
a.data.split(": ")[0].localeCompare(b.data.split(": ")[0])
);
return (
<div className="chartjs-tooltip" style={style}>
<table>
<thead>
{
tooltip.title.map((title, i) => (
{tooltip.title.map((title, i) => (
<tr key={i}>
<th>{title}</th>
</tr>
))
}
))}
</thead>
<tbody>
{
data.map((item, i) => (
{data.map((item, i) => (
<tr key={i}>
<td>
<span className="chartjs-tooltip-key"
style={{
background: item.colors.backgroundColor,
borderColor: item.colors.borderColor,
borderWidth: "2px"
}}/>
<span
className="chartjs-tooltip-key"
style={{
background: item.colors.backgroundColor,
borderColor: item.colors.borderColor,
borderWidth: "2px"
}}
/>
{item.data}
</td>
</tr>
))
}
))}
</tbody>
</table>
</div>
+108 -81
View File
@@ -8,11 +8,11 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import { Line } from 'react-chartjs-2';
import { translate } from 'react-i18next';
import { padNumber, api, makeCancelable, ignoreCancel } from '../../utils';
import React, { Component } from "react";
import ReactDOM from "react-dom";
import { Line } from "react-chartjs-2";
import { translate } from "react-i18next";
import { padNumber, api, makeCancelable, ignoreCancel } from "../../utils";
import ChartTooltip from "./ChartTooltip";
class ClientsGraph extends Component {
@@ -31,54 +31,65 @@ class ClientsGraph extends Component {
}
updateGraph() {
this.updateHandler = makeCancelable(api.getClientsGraph(), { repeat: this.updateGraph, interval: 10 * 60 * 1000 });
this.updateHandler.promise.then(res => {
// Remove last data point as it's not yet finished
res.over_time.splice(-1, 1);
this.updateHandler = makeCancelable(api.getClientsGraph(), {
repeat: this.updateGraph,
interval: 10 * 60 * 1000
});
this.updateHandler.promise
.then(res => {
// Remove last data point as it's not yet finished
res.over_time.splice(-1, 1);
const colors = [
"#20a8d8",
"#f86c6b",
"#4dbd74",
"#f8cb00",
"#263238",
"#63c2de",
"#b0bec5"
];
const labels = res.over_time.map(step => new Date(1000 * step.timestamp));
const datasets = [];
const colors = [
"#20a8d8",
"#f86c6b",
"#4dbd74",
"#f8cb00",
"#263238",
"#63c2de",
"#b0bec5"
];
const labels = res.over_time.map(
step => new Date(1000 * step.timestamp)
);
const datasets = [];
// Fill in dataset metadata
let i = 0;
for(let client of res.clients) {
datasets.push({
label: client.name.length !== 0 ? client.name : client.ip,
// If we ran out of colors, make a random one
backgroundColor: i < colors.length
? colors[i]
: '#' + parseInt("" + Math.random() * 0xffffff, 10).toString(16).padStart(6, "0"),
pointRadius: 0,
pointHitRadius: 5,
pointHoverRadius: 5,
cubicInterpolationMode: "monotone",
data: []
});
// Fill in dataset metadata
let i = 0;
for (let client of res.clients) {
datasets.push({
label: client.name.length !== 0 ? client.name : client.ip,
// If we ran out of colors, make a random one
backgroundColor:
i < colors.length
? colors[i]
: "#" +
parseInt("" + Math.random() * 0xffffff, 10)
.toString(16)
.padStart(6, "0"),
pointRadius: 0,
pointHitRadius: 5,
pointHoverRadius: 5,
cubicInterpolationMode: "monotone",
data: []
});
i++;
}
// Fill in data & labels
for(let step of res.over_time) {
for(let destination in datasets) {
if(datasets.hasOwnProperty(destination))
datasets[destination].data.push(step.data[destination]);
i++;
}
}
datasets.sort((a, b) => a.label.localeCompare(b.label));
// Fill in data & labels
for (let step of res.over_time) {
for (let destination in datasets) {
if (datasets.hasOwnProperty(destination))
datasets[destination].data.push(step.data[destination]);
}
}
this.setState({ data: { labels, datasets }, loading: false });
}).catch(ignoreCancel)
datasets.sort((a, b) => a.label.localeCompare(b.label));
this.setState({ data: { labels, datasets }, loading: false });
})
.catch(ignoreCancel);
}
componentDidMount() {
@@ -111,56 +122,72 @@ class ClientsGraph extends Component {
return t("Client activity from {{from}} to {{to}}", { from, to });
},
label: (tooltipItems, data) => {
return data.datasets[tooltipItems.datasetIndex].label + ": " + tooltipItems.yLabel;
return (
data.datasets[tooltipItems.datasetIndex].label +
": " +
tooltipItems.yLabel
);
}
}
},
legend: { display: false },
scales: {
xAxes: [{
type: "time",
time: {
unit: "hour",
displayFormats: { hour: "HH:mm" },
tooltipFormat: "HH:mm"
xAxes: [
{
type: "time",
time: {
unit: "hour",
displayFormats: { hour: "HH:mm" },
tooltipFormat: "HH:mm"
}
}
}],
yAxes: [{
ticks: { beginAtZero: true },
stacked: true
}]
],
yAxes: [
{
ticks: { beginAtZero: true },
stacked: true
}
]
},
maintainAspectRatio: false
};
return (
<div className="card">
<div className="card-header">
{t("Clients Over Last 24 Hours")}
</div>
<div className="card-header">{t("Clients Over Last 24 Hours")}</div>
<div className="card-body">
<Line width={970} height={170} data={this.state.data} options={options} ref={this.graphRef}/>
<Line
width={970}
height={170}
data={this.state.data}
options={options}
ref={this.graphRef}
/>
</div>
{
this.state.loading
?
<div className="card-img-overlay" style={{ background: "rgba(255,255,255,0.7)" }}>
<i className="fa fa-refresh fa-spin"
style={{ position: "absolute", top: "50%", left: "50%", fontSize: "30px" }}/>
</div>
:
null
}
{
// Now you're thinking with portals!
ReactDOM.createPortal(
<ChartTooltip chart={this.graphRef} handler={options.tooltips}/>,
document.body
)
}
{this.state.loading ? (
<div
className="card-img-overlay"
style={{ background: "rgba(255,255,255,0.7)" }}
>
<i
className="fa fa-refresh fa-spin"
style={{
position: "absolute",
top: "50%",
left: "50%",
fontSize: "30px"
}}
/>
</div>
) : null}
{// Now you're thinking with portals!
ReactDOM.createPortal(
<ChartTooltip chart={this.graphRef} handler={options.tooltips} />,
document.body
)}
</div>
);
}
}
export default translate("dashboard")(ClientsGraph);
export default translate("dashboard")(ClientsGraph);
+15 -7
View File
@@ -39,7 +39,7 @@ const fakeData = {
it("shows loading indicator before first load", () => {
fetchMock.mock(endpoint, ignoreAPI);
const wrapper = shallow(<ClientsGraph/>);
const wrapper = shallow(<ClientsGraph />);
expect(wrapper.state().loading).toBeTruthy();
expect(wrapper.children(".card-img-overlay")).toExist();
@@ -48,7 +48,7 @@ it("shows loading indicator before first load", () => {
it("hides loading indicator after first load", async () => {
fetchMock.mock(endpoint, { body: fakeData });
const wrapper = shallow(<ClientsGraph/>);
const wrapper = shallow(<ClientsGraph />);
await tick();
wrapper.update();
@@ -60,13 +60,21 @@ it("hides loading indicator after first load", async () => {
it("loads API data correctly", async () => {
fetchMock.mock(endpoint, { body: fakeData });
const wrapper = shallow(<ClientsGraph/>);
const wrapper = shallow(<ClientsGraph />);
await tick();
wrapper.update();
expect(wrapper.state().data.labels[0]).toEqual(new Date(1000 * fakeData.over_time[0].timestamp));
expect(wrapper.state().data.datasets[2].label).toEqual(fakeData.clients[2].name);
expect(wrapper.state().data.datasets[1].label).toEqual(fakeData.clients[1].ip);
expect(wrapper.state().data.datasets[0].data.length).toEqual(fakeData.over_time.length - 1);
expect(wrapper.state().data.labels[0]).toEqual(
new Date(1000 * fakeData.over_time[0].timestamp)
);
expect(wrapper.state().data.datasets[2].label).toEqual(
fakeData.clients[2].name
);
expect(wrapper.state().data.datasets[1].label).toEqual(
fakeData.clients[1].ip
);
expect(wrapper.state().data.datasets[0].data.length).toEqual(
fakeData.over_time.length - 1
);
});
@@ -16,7 +16,8 @@ import GenericDoughnutChart from "./GenericDoughnutChart";
const ForwardDestinationsChart = ({ t }) => (
<GenericDoughnutChart
title={t("Queries Answered By Destination")}
apiCall={api.getForwardDestinations}/>
apiCall={api.getForwardDestinations}
/>
);
export default translate("dashboard")(ForwardDestinationsChart);
+100 -77
View File
@@ -27,52 +27,61 @@ class GenericDoughnutChart extends Component {
}
updateChart = () => {
this.updateHandler = makeCancelable(
this.props.apiCall(),
{ repeat: this.updateChart, interval: 10 * 60 * 1000 }
);
this.updateHandler.promise.then(apiData => {
const colors = [
"#20a8d8",
"#f86c6b",
"#4dbd74",
"#f8cb00",
"#263238",
"#63c2de",
"#b0bec5"
];
const data = [];
const labels = [];
const usedColors = [];
this.updateHandler = makeCancelable(this.props.apiCall(), {
repeat: this.updateChart,
interval: 10 * 60 * 1000
});
this.updateHandler.promise
.then(apiData => {
const colors = [
"#20a8d8",
"#f86c6b",
"#4dbd74",
"#f8cb00",
"#263238",
"#63c2de",
"#b0bec5"
];
const data = [];
const labels = [];
const usedColors = [];
// Fill in dataset metadata
let i = 0;
for(let entry of apiData) {
data.push(entry.percent);
labels.push(entry.name.length !== 0 ? entry.name : entry.ip);
usedColors.push(
// If we ran out of colors, make a random one
i < colors.length
? colors[i]
: "#" + parseInt("" + Math.random() * 0xffffff, 10).toString(16).padStart(6, "0")
);
// Fill in dataset metadata
let i = 0;
for (let entry of apiData) {
data.push(entry.percent);
labels.push(entry.name.length !== 0 ? entry.name : entry.ip);
usedColors.push(
// If we ran out of colors, make a random one
i < colors.length
? colors[i]
: "#" +
parseInt("" + Math.random() * 0xffffff, 10)
.toString(16)
.padStart(6, "0")
);
i++;
}
i++;
}
this.setState({
loading: false,
data,
colors: usedColors,
labels
});
}).catch(ignoreCancel);
this.setState({
loading: false,
data,
colors: usedColors,
labels
});
})
.catch(ignoreCancel);
};
handleClick = (e, index) => {
// Hide the entry by clicking on the internal legend item
const chart = this.chartRef.current.chartInstance;
chart.legend.options.onClick.call(chart, e, chart.legend.legendItems[index]);
chart.legend.options.onClick.call(
chart,
e,
chart.legend.legendItems[index]
);
// Cause an update so the external legend gets updated
this.forceUpdate();
@@ -96,7 +105,9 @@ class GenericDoughnutChart extends Component {
label: (tooltipItems, data) => {
const dataset = data.datasets[tooltipItems.datasetIndex];
const label = data.labels[tooltipItems.index];
return label + ": " + dataset.data[tooltipItems.index].toFixed(1) + "%";
return (
label + ": " + dataset.data[tooltipItems.index].toFixed(1) + "%"
);
}
}
},
@@ -105,55 +116,67 @@ class GenericDoughnutChart extends Component {
// Get the metadata for the items, so we know if they are hidden or not.
// If the chart has not been created yet, make some fake metadata.
const meta = this.chartRef.current !== null
? this.chartRef.current.chartInstance.getDatasetMeta(0).data
: this.state.data.map(() => ({ hidden: false }));
const meta =
this.chartRef.current !== null
? this.chartRef.current.chartInstance.getDatasetMeta(0).data
: this.state.data.map(() => ({ hidden: false }));
return (
<div className="card">
<div className="card-header">
{this.props.title}
</div>
<div className="card-header">{this.props.title}</div>
<div className="card-body">
<div className="float-left" style={{ width: "67%" }}>
<Doughnut width={100} height={250} options={options} ref={this.chartRef}
data={{
datasets: [{
data: this.state.data,
backgroundColor: this.state.colors
}],
labels: this.state.labels
}}/>
<Doughnut
width={100}
height={250}
options={options}
ref={this.chartRef}
data={{
datasets: [
{
data: this.state.data,
backgroundColor: this.state.colors
}
],
labels: this.state.labels
}}
/>
</div>
<div className="float-right" style={{ width: "33%" }}>
<ul className="chart-legend">
{
this.state.labels
{this.state.labels
// Zip label and color together
.map((label, i) => [label, this.state.colors[i]])
// Create the list items
.map(([label, color], i) => (
<li key={i}
className={meta[i] && meta[i].hidden ? "strike" : ""}
onClick={e => this.handleClick(e, i)}>
<span style={{ backgroundColor: color }}/>
{label}
</li>
))
}
.map((label, i) => [label, this.state.colors[i]])
// Create the list items
.map(([label, color], i) => (
<li
key={i}
className={meta[i] && meta[i].hidden ? "strike" : ""}
onClick={e => this.handleClick(e, i)}
>
<span style={{ backgroundColor: color }} />
{label}
</li>
))}
</ul>
</div>
</div>
{
this.state.loading
?
<div className="card-img-overlay" style={{ background: "rgba(255,255,255,0.7)" }}>
<i className="fa fa-refresh fa-spin"
style={{ position: "absolute", top: "50%", left: "50%", fontSize: "30px" }}/>
</div>
:
null
}
{this.state.loading ? (
<div
className="card-img-overlay"
style={{ background: "rgba(255,255,255,0.7)" }}
>
<i
className="fa fa-refresh fa-spin"
style={{
position: "absolute",
top: "50%",
left: "50%",
fontSize: "30px"
}}
/>
</div>
) : null}
</div>
);
}
@@ -164,4 +187,4 @@ GenericDoughnutChart.propTypes = {
apiCall: PropTypes.func.isRequired
};
export default GenericDoughnutChart;
export default GenericDoughnutChart;
@@ -20,7 +20,7 @@ const fakeData = [
it("shows loading indicator before first load", () => {
const wrapper = shallow(
<GenericDoughnutChart title={""} apiCall={ignoreAPI}/>
<GenericDoughnutChart title={""} apiCall={ignoreAPI} />
);
expect(wrapper.state().loading).toBeTruthy();
@@ -29,7 +29,10 @@ it("shows loading indicator before first load", () => {
it("hides loading indicator after first load", async () => {
const wrapper = shallow(
<GenericDoughnutChart title={""} apiCall={() => Promise.resolve(fakeData)}/>
<GenericDoughnutChart
title={""}
apiCall={() => Promise.resolve(fakeData)}
/>
);
await tick();
@@ -41,7 +44,10 @@ it("hides loading indicator after first load", async () => {
it("loads API data correctly", async () => {
const wrapper = shallow(
<GenericDoughnutChart title={""} apiCall={() => Promise.resolve(fakeData)}/>
<GenericDoughnutChart
title={""}
apiCall={() => Promise.resolve(fakeData)}
/>
);
await tick();
@@ -56,7 +62,7 @@ it("loads API data correctly", async () => {
it("displays the title", () => {
const title = "title";
const wrapper = shallow(
<GenericDoughnutChart title={title} apiCall={ignoreAPI}/>
<GenericDoughnutChart title={title} apiCall={ignoreAPI} />
);
expect(wrapper.find(".card-header")).toHaveText(title);
@@ -64,7 +70,7 @@ it("displays the title", () => {
it("calls the API callback", () => {
const apiCall = jest.fn(ignoreAPI);
shallow(<GenericDoughnutChart title={""} apiCall={apiCall}/>);
shallow(<GenericDoughnutChart title={""} apiCall={apiCall} />);
expect(apiCall).toHaveBeenCalled();
});
+82 -53
View File
@@ -8,10 +8,10 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React, { Component } from 'react';
import { Line } from 'react-chartjs-2';
import { translate } from 'react-i18next';
import { padNumber, api, makeCancelable, ignoreCancel } from '../../utils';
import React, { Component } from "react";
import { Line } from "react-chartjs-2";
import { translate } from "react-i18next";
import { padNumber, api, makeCancelable, ignoreCancel } from "../../utils";
class QueriesGraph extends Component {
state = {
@@ -27,23 +27,30 @@ class QueriesGraph extends Component {
}
updateGraph() {
this.updateHandler = makeCancelable(api.getHistoryGraph(), { repeat: this.updateGraph, interval: 10 * 60 * 1000 });
this.updateHandler.promise.then(res => {
// Remove last data point as it's not yet finished
res.blocked_over_time.splice(-1, 1);
res.domains_over_time.splice(-1, 1);
this.updateHandler = makeCancelable(api.getHistoryGraph(), {
repeat: this.updateGraph,
interval: 10 * 60 * 1000
});
this.updateHandler.promise
.then(res => {
// Remove last data point as it's not yet finished
res.blocked_over_time.splice(-1, 1);
res.domains_over_time.splice(-1, 1);
const labels = res.domains_over_time.map(step => new Date(1000 * step.timestamp));
const domains_over_time = res.domains_over_time.map(step => step.count);
const blocked_over_time = res.blocked_over_time.map(step => step.count);
const labels = res.domains_over_time.map(
step => new Date(1000 * step.timestamp)
);
const domains_over_time = res.domains_over_time.map(step => step.count);
const blocked_over_time = res.blocked_over_time.map(step => step.count);
this.setState({
loading: false,
labels,
domains_over_time,
blocked_over_time
});
}).catch(ignoreCancel);
this.setState({
loading: false,
labels,
domains_over_time,
blocked_over_time
});
})
.catch(ignoreCancel);
}
componentDidMount() {
@@ -102,57 +109,79 @@ class QueriesGraph extends Component {
return t("Queries from {{from}} to {{to}}", { from, to });
},
label: (tooltipItems, data) => {
if(tooltipItems.datasetIndex === 1) {
if (tooltipItems.datasetIndex === 1) {
let percentage = 0.0;
const total = parseInt(data.datasets[0].data[tooltipItems.index], 10);
const blocked = parseInt(data.datasets[1].data[tooltipItems.index], 10);
const total = parseInt(
data.datasets[0].data[tooltipItems.index],
10
);
const blocked = parseInt(
data.datasets[1].data[tooltipItems.index],
10
);
if(total > 0)
percentage = 100.0 * blocked / total;
if (total > 0) percentage = (100.0 * blocked) / total;
return data.datasets[tooltipItems.datasetIndex].label + ": " + tooltipItems.yLabel
+ " (" + percentage.toFixed(1) + "%)";
}
else
return data.datasets[tooltipItems.datasetIndex].label + ": " + tooltipItems.yLabel;
return (
data.datasets[tooltipItems.datasetIndex].label +
": " +
tooltipItems.yLabel +
" (" +
percentage.toFixed(1) +
"%)"
);
} else
return (
data.datasets[tooltipItems.datasetIndex].label +
": " +
tooltipItems.yLabel
);
}
}
},
legend: { display: false },
scales: {
xAxes: [{
type: "time",
time: {
unit: "hour",
displayFormats: { hour: "HH:mm" },
tooltipFormat: "HH:mm"
xAxes: [
{
type: "time",
time: {
unit: "hour",
displayFormats: { hour: "HH:mm" },
tooltipFormat: "HH:mm"
}
}
}],
yAxes: [{
ticks: { beginAtZero: true }
}]
],
yAxes: [
{
ticks: { beginAtZero: true }
}
]
},
maintainAspectRatio: false
};
return (
<div className="card">
<div className="card-header">
{t("Queries Over Last 24 Hours")}
</div>
<div className="card-header">{t("Queries Over Last 24 Hours")}</div>
<div className="card-body">
<Line width={970} height={170} data={data} options={options}/>
<Line width={970} height={170} data={data} options={options} />
</div>
{
this.state.loading
?
<div className="card-img-overlay" style={{ background: "rgba(255,255,255,0.7)" }}>
<i className="fa fa-refresh fa-spin"
style={{ position: "absolute", top: "50%", left: "50%", fontSize: "30px" }}/>
</div>
:
null
}
{this.state.loading ? (
<div
className="card-img-overlay"
style={{ background: "rgba(255,255,255,0.7)" }}
>
<i
className="fa fa-refresh fa-spin"
style={{
position: "absolute",
top: "50%",
left: "50%",
fontSize: "30px"
}}
/>
</div>
) : null}
</div>
);
}
+3 -5
View File
@@ -8,15 +8,13 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from 'react';
import { translate } from 'react-i18next';
import React from "react";
import { translate } from "react-i18next";
import { api } from "../../utils";
import GenericDoughnutChart from "./GenericDoughnutChart";
const QueryTypesChart = ({ t }) => (
<GenericDoughnutChart
title={t("Query Types")}
apiCall={api.getQueryTypes}/>
<GenericDoughnutChart title={t("Query Types")} apiCall={api.getQueryTypes} />
);
export default translate("dashboard")(QueryTypesChart);
+25 -25
View File
@@ -8,8 +8,8 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React, { Component, Fragment } from 'react';
import { translate } from 'react-i18next';
import React, { Component, Fragment } from "react";
import { translate } from "react-i18next";
import { api, ignoreCancel, makeCancelable } from "../../utils";
class SummaryStats extends Component {
@@ -27,16 +27,20 @@ class SummaryStats extends Component {
}
updateStats() {
this.updateHandler = makeCancelable(api.getSummary(), { repeat: this.updateStats, interval: 5000 });
this.updateHandler.promise.then(res => {
this.setState({
blockedQueries: res.blocked_queries.toLocaleString(),
totalQueries: res.total_queries.toLocaleString(),
percentBlocked: res.percent_blocked.toFixed(2).toLocaleString() + "%",
gravityDomains: res.domains_blocked.toLocaleString(),
uniqueClients: res.unique_clients
});
})
this.updateHandler = makeCancelable(api.getSummary(), {
repeat: this.updateStats,
interval: 5000
});
this.updateHandler.promise
.then(res => {
this.setState({
blockedQueries: res.blocked_queries.toLocaleString(),
totalQueries: res.total_queries.toLocaleString(),
percentBlocked: res.percent_blocked.toFixed(2).toLocaleString() + "%",
gravityDomains: res.domains_blocked.toLocaleString(),
uniqueClients: res.unique_clients
});
})
.catch(ignoreCancel)
.catch(() => {
this.setState({
@@ -70,8 +74,10 @@ class SummaryStats extends Component {
</div>
<div className="card-img-overlay">
<h3>{this.state.totalQueries}</h3>
<p style={{marginBottom: "0px"}}>
{t("Total Queries ({{count}} clients)", { count: this.state.uniqueClients })}
<p style={{ marginBottom: "0px" }}>
{t("Total Queries ({{count}} clients)", {
count: this.state.uniqueClients
})}
</p>
</div>
</div>
@@ -85,9 +91,7 @@ class SummaryStats extends Component {
</div>
<div className="card-img-overlay">
<h3>{this.state.blockedQueries}</h3>
<p style={{marginBottom: "0px"}}>
{t("Queries Blocked")}
</p>
<p style={{ marginBottom: "0px" }}>{t("Queries Blocked")}</p>
</div>
</div>
</div>
@@ -100,9 +104,7 @@ class SummaryStats extends Component {
</div>
<div className="card-img-overlay">
<h3>{this.state.percentBlocked}</h3>
<p style={{marginBottom: "0px"}}>
{t("Percent Blocked")}
</p>
<p style={{ marginBottom: "0px" }}>{t("Percent Blocked")}</p>
</div>
</div>
</div>
@@ -110,14 +112,12 @@ class SummaryStats extends Component {
<div className="card border-0 bg-danger stat-height-lock">
<div className="card-body">
<div className="card-icon">
<i className="fa fa-list-alt fa-2x" />
<i className="fa fa-list-alt fa-2x" />
</div>
</div>
<div className="card-img-overlay">
<h3>{this.state.gravityDomains}</h3>
<p style={{marginBottom: "0px"}}>
{t("Domains On Blocklist")}
</p>
<p style={{ marginBottom: "0px" }}>{t("Domains On Blocklist")}</p>
</div>
</div>
</div>
@@ -126,4 +126,4 @@ class SummaryStats extends Component {
}
}
export default translate(['common', 'dashboard'])(SummaryStats);
export default translate(["common", "dashboard"])(SummaryStats);
+14 -6
View File
@@ -30,22 +30,30 @@ const fakeData = {
it("displays summary stats correctly", async () => {
fetchMock.mock(endpoint, fakeData);
const wrapper = shallow(<SummaryStats/>);
const wrapper = shallow(<SummaryStats />);
await tick();
wrapper.update();
expect(wrapper.childAt(0).find("h3")).toHaveText(fakeData.total_queries.toLocaleString());
expect(wrapper.childAt(0).find("h3")).toHaveText(
fakeData.total_queries.toLocaleString()
);
expect(wrapper.state().uniqueClients).toEqual(fakeData.unique_clients);
expect(wrapper.childAt(1).find("h3")).toHaveText(fakeData.blocked_queries.toLocaleString());
expect(wrapper.childAt(2).find("h3")).toHaveText(fakeData.percent_blocked.toFixed(2).toLocaleString() + "%");
expect(wrapper.childAt(3).find("h3")).toHaveText(fakeData.domains_blocked.toLocaleString());
expect(wrapper.childAt(1).find("h3")).toHaveText(
fakeData.blocked_queries.toLocaleString()
);
expect(wrapper.childAt(2).find("h3")).toHaveText(
fakeData.percent_blocked.toFixed(2).toLocaleString() + "%"
);
expect(wrapper.childAt(3).find("h3")).toHaveText(
fakeData.domains_blocked.toLocaleString()
);
});
it("displays an error message on error", async () => {
fetchMock.mock(endpoint, { error: {} });
const wrapper = shallow(<SummaryStats/>);
const wrapper = shallow(<SummaryStats />);
await tick();
wrapper.update();
+18 -22
View File
@@ -21,11 +21,7 @@ const TopBlocked = ({ t, ...props }) => (
total_blocked: 0,
top_blocked: []
}}
headers={[
t("Domain"),
t("Hits"),
t("Frequency")
]}
headers={[t("Domain"), t("Hits"), t("Frequency")]}
emptyMessage={t("No Domains Found")}
isEmpty={state => state.top_blocked.length === 0}
apiCall={api.getTopBlocked}
@@ -38,31 +34,31 @@ const TopBlocked = ({ t, ...props }) => (
}}
generateRows={state => {
return state.top_blocked.map(item => {
const percentage = item.count / state.total_blocked * 100;
const percentage = (item.count / state.total_blocked) * 100;
return (
<tr key={item.domain}>
<td>
{item.domain}
</td>
<td>
{item.count.toLocaleString()}
</td>
<td style={{ "verticalAlign": "middle" }}>
<div className="progress"
title={
t("{{percent}}% of {{total}}", {
percent: percentage.toFixed(1),
total: state.total_blocked.toLocaleString()
})
}>
<div className="progress-bar bg-warning" style={{ width: percentage + "%" }}/>
<td>{item.domain}</td>
<td>{item.count.toLocaleString()}</td>
<td style={{ verticalAlign: "middle" }}>
<div
className="progress"
title={t("{{percent}}% of {{total}}", {
percent: percentage.toFixed(1),
total: state.total_blocked.toLocaleString()
})}
>
<div
className="progress-bar bg-warning"
style={{ width: percentage + "%" }}
/>
</div>
</td>
</tr>
);
});
}}/>
}}
/>
);
export default translate(["common", "dashboard"])(TopBlocked);
+5 -3
View File
@@ -33,7 +33,7 @@ const fakeData = {
it("loads the API data into state correctly", async () => {
fetchMock.mock(endpoint, fakeData);
const wrapper = shallow(<TopBlocked/>).dive();
const wrapper = shallow(<TopBlocked />).dive();
await tick();
wrapper.update();
@@ -45,11 +45,13 @@ it("loads the API data into state correctly", async () => {
it("creates an appropriately sized table", async () => {
fetchMock.mock(endpoint, fakeData);
const wrapper = shallow(<TopBlocked/>).dive();
const wrapper = shallow(<TopBlocked />).dive();
await tick();
wrapper.update();
// Add one to the expected length to account for the table header
expect(wrapper.find("tbody").children("tr")).toHaveLength(fakeData.top_blocked.length + 1);
expect(wrapper.find("tbody").children("tr")).toHaveLength(
fakeData.top_blocked.length + 1
);
});
+18 -22
View File
@@ -21,11 +21,7 @@ const TopClients = ({ t, ...props }) => (
total_queries: 0,
top_clients: []
}}
headers={[
t("Client"),
t("Requests"),
t("Frequency")
]}
headers={[t("Client"), t("Requests"), t("Frequency")]}
emptyMessage={t("No Clients Found")}
isEmpty={state => state.top_clients.length === 0}
apiCall={api.getTopClients}
@@ -38,31 +34,31 @@ const TopClients = ({ t, ...props }) => (
}}
generateRows={state => {
return state.top_clients.map(item => {
const percentage = item.count / state.total_queries * 100;
const percentage = (item.count / state.total_queries) * 100;
return (
<tr key={item.name + "|" + item.ip}>
<td>
{item.name !== "" ? item.name : item.ip}
</td>
<td>
{item.count.toLocaleString()}
</td>
<td style={{ "verticalAlign": "middle" }}>
<div className="progress"
title={
t("{{percent}}% of {{total}}", {
percent: percentage.toFixed(1),
total: state.total_queries.toLocaleString()
})
}>
<div className="progress-bar bg-primary" style={{ width: percentage + "%" }}/>
<td>{item.name !== "" ? item.name : item.ip}</td>
<td>{item.count.toLocaleString()}</td>
<td style={{ verticalAlign: "middle" }}>
<div
className="progress"
title={t("{{percent}}% of {{total}}", {
percent: percentage.toFixed(1),
total: state.total_queries.toLocaleString()
})}
>
<div
className="progress-bar bg-primary"
style={{ width: percentage + "%" }}
/>
</div>
</td>
</tr>
);
});
}}/>
}}
/>
);
export default translate(["common", "dashboard"])(TopClients);
+5 -3
View File
@@ -33,7 +33,7 @@ const fakeData = {
it("loads the API data into state correctly", async () => {
fetchMock.mock(endpoint, fakeData);
const wrapper = shallow(<TopClients/>).dive();
const wrapper = shallow(<TopClients />).dive();
await tick();
wrapper.update();
@@ -45,11 +45,13 @@ it("loads the API data into state correctly", async () => {
it("creates an appropriately sized table", async () => {
fetchMock.mock(endpoint, fakeData);
const wrapper = shallow(<TopClients/>).dive();
const wrapper = shallow(<TopClients />).dive();
await tick();
wrapper.update();
// Add one to the expected length to account for the table header
expect(wrapper.find("tbody").children("tr")).toHaveLength(fakeData.top_clients.length + 1);
expect(wrapper.find("tbody").children("tr")).toHaveLength(
fakeData.top_clients.length + 1
);
});
+18 -22
View File
@@ -21,11 +21,7 @@ const TopDomains = ({ t, ...props }) => (
total_queries: 0,
top_domains: []
}}
headers={[
t("Domain"),
t("Hits"),
t("Frequency")
]}
headers={[t("Domain"), t("Hits"), t("Frequency")]}
emptyMessage={t("No Domains Found")}
isEmpty={state => state.top_domains.length === 0}
apiCall={api.getTopDomains}
@@ -38,31 +34,31 @@ const TopDomains = ({ t, ...props }) => (
}}
generateRows={state => {
return state.top_domains.map(item => {
const percentage = item.count / state.total_queries * 100;
const percentage = (item.count / state.total_queries) * 100;
return (
<tr key={item.domain}>
<td>
{item.domain}
</td>
<td>
{item.count.toLocaleString()}
</td>
<td style={{ "verticalAlign": "middle" }}>
<div className="progress"
title={
t("{{percent}}% of {{total}}", {
percent: percentage.toFixed(1),
total: state.total_queries.toLocaleString()
})
}>
<div className="progress-bar bg-success" style={{ width: percentage + "%" }}/>
<td>{item.domain}</td>
<td>{item.count.toLocaleString()}</td>
<td style={{ verticalAlign: "middle" }}>
<div
className="progress"
title={t("{{percent}}% of {{total}}", {
percent: percentage.toFixed(1),
total: state.total_queries.toLocaleString()
})}
>
<div
className="progress-bar bg-success"
style={{ width: percentage + "%" }}
/>
</div>
</td>
</tr>
);
});
}}/>
}}
/>
);
export default translate(["common", "dashboard"])(TopDomains);
+5 -3
View File
@@ -33,7 +33,7 @@ const fakeData = {
it("loads the API data into state correctly", async () => {
fetchMock.mock(endpoint, fakeData);
const wrapper = shallow(<TopDomains/>).dive();
const wrapper = shallow(<TopDomains />).dive();
await tick();
wrapper.update();
@@ -45,11 +45,13 @@ it("loads the API data into state correctly", async () => {
it("creates an appropriately sized table", async () => {
fetchMock.mock(endpoint, fakeData);
const wrapper = shallow(<TopDomains/>).dive();
const wrapper = shallow(<TopDomains />).dive();
await tick();
wrapper.update();
// Add one to the expected length to account for the table header
expect(wrapper.find("tbody").children("tr")).toHaveLength(fakeData.top_domains.length + 1);
expect(wrapper.find("tbody").children("tr")).toHaveLength(
fakeData.top_domains.length + 1
);
});
+29 -25
View File
@@ -19,27 +19,29 @@ class TopTable extends Component {
};
updateChart = () => {
this.updateHandler = makeCancelable(
this.props.apiCall(),
{ repeat: this.updateChart, interval: 10 * 60 * 1000 }
);
this.updateHandler = makeCancelable(this.props.apiCall(), {
repeat: this.updateChart,
interval: 10 * 60 * 1000
});
this.updateHandler.promise
.then(res => this.props.apiHandler(this, res))
.catch(ignoreCancel);
};
generateTable = () => {
if(this.props.isEmpty(this.state)) {
if (this.props.isEmpty(this.state)) {
return this.props.emptyMessage;
}
return (
<table className="table table-bordered">
<tbody>
<tr>
{this.props.headers.map((header, i) => <th key={i}>{header}</th>)}
</tr>
{this.props.generateRows(this.state)}
<tr>
{this.props.headers.map((header, i) => (
<th key={i}>{header}</th>
))}
</tr>
{this.props.generateRows(this.state)}
</tbody>
</table>
);
@@ -56,24 +58,26 @@ class TopTable extends Component {
render() {
return (
<div className="card">
<div className="card-header">
{this.props.title}
</div>
<div className="card-header">{this.props.title}</div>
<div className="card-body">
<div style={{ overflowX: "auto" }}>
{this.generateTable()}
</div>
<div style={{ overflowX: "auto" }}>{this.generateTable()}</div>
</div>
{
this.state.loading
?
<div className="card-img-overlay" style={{ background: "rgba(255,255,255,0.7)" }}>
<i className="fa fa-refresh fa-spin"
style={{ position: "absolute", top: "50%", left: "50%", fontSize: "30px" }}/>
</div>
:
null
}
{this.state.loading ? (
<div
className="card-img-overlay"
style={{ background: "rgba(255,255,255,0.7)" }}
>
<i
className="fa fa-refresh fa-spin"
style={{
position: "absolute",
top: "50%",
left: "50%",
fontSize: "30px"
}}
/>
</div>
) : null}
</div>
);
}
+19 -14
View File
@@ -21,7 +21,8 @@ it("shows loading indicator before first load", () => {
isEmpty={() => true}
apiCall={ignoreAPI}
apiHandler={jest.fn()}
generateRows={jest.fn()}/>
generateRows={jest.fn()}
/>
);
expect(wrapper.state().loading).toBeTruthy();
@@ -37,7 +38,8 @@ it("hides loading indicator after first load", async () => {
isEmpty={() => true}
apiCall={() => Promise.resolve()}
apiHandler={self => self.setState({ loading: false })}
generateRows={jest.fn()}/>
generateRows={jest.fn()}
/>
);
await tick();
@@ -59,7 +61,8 @@ it("sets the initial state", () => {
isEmpty={() => true}
apiCall={ignoreAPI}
apiHandler={jest.fn()}
generateRows={jest.fn()}/>
generateRows={jest.fn()}
/>
);
expect(wrapper.state().testInitialState).toEqual(true);
@@ -75,18 +78,15 @@ it("sets the title", () => {
isEmpty={() => true}
apiCall={ignoreAPI}
apiHandler={jest.fn()}
generateRows={jest.fn()}/>
generateRows={jest.fn()}
/>
);
expect(wrapper.find(".card-header")).toHaveText(title);
});
it("adds the headers", () => {
const headers = [
"header1",
"header2",
"header3"
];
const headers = ["header1", "header2", "header3"];
const wrapper = shallow(
<TopTable
title={""}
@@ -96,7 +96,8 @@ it("adds the headers", () => {
isEmpty={() => false}
apiCall={ignoreAPI}
apiHandler={jest.fn()}
generateRows={jest.fn()}/>
generateRows={jest.fn()}
/>
);
expect(
@@ -119,7 +120,8 @@ it("shows the empty message when empty", () => {
isEmpty={() => true}
apiCall={ignoreAPI}
apiHandler={jest.fn()}
generateRows={jest.fn()}/>
generateRows={jest.fn()}
/>
);
expect(wrapper.find(".card-body").childAt(0)).toHaveText(emptyMessage);
@@ -137,7 +139,8 @@ it("calls apiCall and apiHandler to retrieve data", async () => {
isEmpty={() => true}
apiCall={apiCall}
apiHandler={apiHandler}
generateRows={jest.fn()}/>
generateRows={jest.fn()}
/>
);
await tick();
@@ -157,7 +160,8 @@ it("does not call generateRows when empty", () => {
isEmpty={() => true}
apiCall={ignoreAPI}
apiHandler={jest.fn()}
generateRows={generateRows}/>
generateRows={generateRows}
/>
);
expect(generateRows).not.toHaveBeenCalled();
@@ -175,7 +179,8 @@ it("calls generateRows with state when not empty", () => {
isEmpty={() => false}
apiCall={ignoreAPI}
apiHandler={jest.fn()}
generateRows={generateRows}/>
generateRows={generateRows}
/>
);
expect(generateRows).toHaveBeenCalledWith(state);
+17 -16
View File
@@ -8,9 +8,9 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import React, { Component } from "react";
import PropTypes from "prop-types";
import { translate } from "react-i18next";
import { api } from "../../utils";
class DomainInput extends Component {
@@ -19,10 +19,9 @@ class DomainInput extends Component {
isValid: true
};
handleChange = (e) =>
this.setState({ domain: e.target.value });
handleChange = e => this.setState({ domain: e.target.value });
handleSubmit = (e) => {
handleSubmit = e => {
e.preventDefault();
const domain = this.state.domain;
@@ -51,15 +50,17 @@ class DomainInput extends Component {
disabled={!api.loggedIn}
/>
<span className="btn-group input-group-append">
{
api.loggedIn ?
<button className="btn border-secondary" type="submit" >
{t("Add")}
</button>
: null
}
<button onClick={onRefresh} className="btn border-secondary" type="button">
<i className="fa fa-refresh"/>
{api.loggedIn ? (
<button className="btn border-secondary" type="submit">
{t("Add")}
</button>
) : null}
<button
onClick={onRefresh}
className="btn border-secondary"
type="button"
>
<i className="fa fa-refresh" />
</button>
</span>
</form>
@@ -76,7 +77,7 @@ DomainInput.propTypes = {
};
DomainInput.defaultProps = {
placeholder: ''
placeholder: ""
};
export default translate(["common", "lists"])(DomainInput);
+164 -48
View File
@@ -8,107 +8,199 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from 'react';
import { shallow } from 'enzyme';
import DomainInput from './DomainInput';
import React from "react";
import { shallow } from "enzyme";
import DomainInput from "./DomainInput";
import { api } from "../../utils";
import { isValidDomain } from "../../validate";
it('has a placeholder', () => {
const placeholder = 'placeholder';
const wrapper = shallow(<DomainInput placeholder={placeholder} onEnter={jest.fn()} onRefresh={jest.fn()} isValid={isValidDomain} onValidationError={jest.fn()}/>);
it("has a placeholder", () => {
const placeholder = "placeholder";
const wrapper = shallow(
<DomainInput
placeholder={placeholder}
onEnter={jest.fn()}
onRefresh={jest.fn()}
isValid={isValidDomain}
onValidationError={jest.fn()}
/>
);
expect(wrapper.find('input')).toHaveProp('placeholder', placeholder);
expect(wrapper.find("input")).toHaveProp("placeholder", placeholder);
});
it('sets state to input', () => {
it("sets state to input", () => {
const domain = "domain";
const wrapper = shallow(<DomainInput onEnter={jest.fn()} onRefresh={jest.fn()} isValid={isValidDomain} onValidationError={jest.fn()}/>);
const wrapper = shallow(
<DomainInput
onEnter={jest.fn()}
onRefresh={jest.fn()}
isValid={isValidDomain}
onValidationError={jest.fn()}
/>
);
wrapper.find('input').simulate('change', { target: { value: domain } });
wrapper.find("input").simulate("change", { target: { value: domain } });
expect(wrapper.state().domain).toEqual(domain);
});
it('only has one button when not logged in', () => {
const wrapper = shallow(<DomainInput onEnter={jest.fn()} onRefresh={jest.fn()} isValid={isValidDomain} onValidationError={jest.fn()}/>);
it("only has one button when not logged in", () => {
const wrapper = shallow(
<DomainInput
onEnter={jest.fn()}
onRefresh={jest.fn()}
isValid={isValidDomain}
onValidationError={jest.fn()}
/>
);
expect(wrapper.find('button')).toHaveLength(1);
expect(wrapper.find("button")).toHaveLength(1);
});
it('disables input when not logged in', () => {
const wrapper = shallow(<DomainInput onEnter={jest.fn()} onRefresh={jest.fn()} isValid={isValidDomain} onValidationError={jest.fn()}/>);
it("disables input when not logged in", () => {
const wrapper = shallow(
<DomainInput
onEnter={jest.fn()}
onRefresh={jest.fn()}
isValid={isValidDomain}
onValidationError={jest.fn()}
/>
);
expect(wrapper.find('input')).toBeDisabled();
expect(wrapper.find("input")).toBeDisabled();
});
it('enables input when logged in', () => {
it("enables input when logged in", () => {
api.loggedIn = true;
const wrapper = shallow(<DomainInput onEnter={jest.fn()} onRefresh={jest.fn()} isValid={isValidDomain} onValidationError={jest.fn()}/>);
const wrapper = shallow(
<DomainInput
onEnter={jest.fn()}
onRefresh={jest.fn()}
isValid={isValidDomain}
onValidationError={jest.fn()}
/>
);
expect(wrapper.find('input')).not.toBeDisabled();
expect(wrapper.find("input")).not.toBeDisabled();
});
it('calls onRefresh when the refresh button is clicked', () => {
it("calls onRefresh when the refresh button is clicked", () => {
const onRefresh = jest.fn();
const wrapper = shallow(<DomainInput onEnter={jest.fn()} onRefresh={onRefresh} isValid={isValidDomain} onValidationError={jest.fn()}/>);
const wrapper = shallow(
<DomainInput
onEnter={jest.fn()}
onRefresh={onRefresh}
isValid={isValidDomain}
onValidationError={jest.fn()}
/>
);
wrapper.find('button').last().simulate('click');
wrapper
.find("button")
.last()
.simulate("click");
expect(onRefresh).toHaveBeenCalled();
});
it('has two buttons when logged in', () => {
it("has two buttons when logged in", () => {
api.loggedIn = true;
const wrapper = shallow(<DomainInput onEnter={jest.fn()} onRefresh={jest.fn()} isValid={isValidDomain} onValidationError={jest.fn()}/>);
const wrapper = shallow(
<DomainInput
onEnter={jest.fn()}
onRefresh={jest.fn()}
isValid={isValidDomain}
onValidationError={jest.fn()}
/>
);
expect(wrapper.find('button')).toHaveLength(2);
expect(wrapper.find("button")).toHaveLength(2);
});
it('does not call onEnter when input is empty', () => {
it("does not call onEnter when input is empty", () => {
api.loggedIn = true;
const onEnter = jest.fn();
const wrapper = shallow(<DomainInput onEnter={onEnter} onRefresh={jest.fn()} isValid={isValidDomain} onValidationError={jest.fn()}/>);
const wrapper = shallow(
<DomainInput
onEnter={onEnter}
onRefresh={jest.fn()}
isValid={isValidDomain}
onValidationError={jest.fn()}
/>
);
wrapper.find('button').first().simulate('click');
wrapper
.find("button")
.first()
.simulate("click");
expect(onEnter).not.toHaveBeenCalled();
});
it('calls onEnter when input is not empty', () => {
it("calls onEnter when input is not empty", () => {
api.loggedIn = true;
const domain = 'domain.com';
const domain = "domain.com";
const onEnter = jest.fn();
const wrapper = shallow(<DomainInput onEnter={onEnter} onRefresh={jest.fn()} isValid={isValidDomain} onValidationError={jest.fn()}/>);
const wrapper = shallow(
<DomainInput
onEnter={onEnter}
onRefresh={jest.fn()}
isValid={isValidDomain}
onValidationError={jest.fn()}
/>
);
wrapper.find('input').simulate('change', { target: { value: domain } });
wrapper.find('form').first().simulate('submit', { preventDefault: jest.fn() });
wrapper.find("input").simulate("change", { target: { value: domain } });
wrapper
.find("form")
.first()
.simulate("submit", { preventDefault: jest.fn() });
expect(onEnter).toHaveBeenCalledWith(domain);
});
it('clears input after clicking add button', () => {
it("clears input after clicking add button", () => {
api.loggedIn = true;
const wrapper = shallow(<DomainInput onEnter={jest.fn()} onRefresh={jest.fn()} isValid={isValidDomain} onValidationError={jest.fn()}/>);
const wrapper = shallow(
<DomainInput
onEnter={jest.fn()}
onRefresh={jest.fn()}
isValid={isValidDomain}
onValidationError={jest.fn()}
/>
);
wrapper.find('input').simulate('change', { target: { value: 'domain.com' } });
wrapper.find('form').first().simulate('submit', { preventDefault: jest.fn() });
wrapper.find("input").simulate("change", { target: { value: "domain.com" } });
wrapper
.find("form")
.first()
.simulate("submit", { preventDefault: jest.fn() });
expect(wrapper.state().domain).toEqual('');
expect(wrapper.state().domain).toEqual("");
});
// Tests to check the validation functionality
it("sets state.isValid to true when domain is properly formatted", () => {
api.loggedIn = true;
const wrapper = shallow(<DomainInput onEnter={jest.fn()} onRefresh={jest.fn()} isValid={isValidDomain} onValidationError={jest.fn()}/>);
const wrapper = shallow(
<DomainInput
onEnter={jest.fn()}
onRefresh={jest.fn()}
isValid={isValidDomain}
onValidationError={jest.fn()}
/>
);
wrapper.find('input').simulate('change', { target: { value: "valid.domain" }});
wrapper
.find("input")
.simulate("change", { target: { value: "valid.domain" } });
expect(wrapper.state().domain).toEqual("valid.domain");
expect(wrapper.state().isValid).toEqual(true);
});
@@ -116,21 +208,45 @@ it("sets state.isValid to true when domain is properly formatted", () => {
it("sets state.isValid to false when domain is not properly formatted", () => {
api.loggedIn = true;
const wrapper = shallow(<DomainInput onEnter={jest.fn()} onRefresh={jest.fn()} isValid={isValidDomain} onValidationError={jest.fn()}/>);
const wrapper = shallow(
<DomainInput
onEnter={jest.fn()}
onRefresh={jest.fn()}
isValid={isValidDomain}
onValidationError={jest.fn()}
/>
);
wrapper.find('input').simulate('change', { target: { value: "invalid.domain." }});
wrapper
.find("input")
.simulate("change", { target: { value: "invalid.domain." } });
expect(wrapper.state().domain).toEqual("invalid.domain.");
wrapper.find('form').first().simulate('submit', { preventDefault: jest.fn() });
wrapper
.find("form")
.first()
.simulate("submit", { preventDefault: jest.fn() });
expect(wrapper.state().isValid).toEqual(false);
});
it("sets is-invalid class to the input when domain is not properly formatted", () => {
api.loggedIn = true;
const wrapper = shallow(<DomainInput onEnter={jest.fn()} onRefresh={jest.fn()} isValid={isValidDomain} onValidationError={jest.fn()}/>);
const wrapper = shallow(
<DomainInput
onEnter={jest.fn()}
onRefresh={jest.fn()}
isValid={isValidDomain}
onValidationError={jest.fn()}
/>
);
wrapper.find('input').simulate('change', { target: { value: "invalid.domain." }});
wrapper.find('form').first().simulate('submit', { preventDefault: jest.fn() });
expect(wrapper.find('input').hasClass('is-valid')).toBe(false);
expect(wrapper.find('input').hasClass('is-invalid')).toBe(true);
wrapper
.find("input")
.simulate("change", { target: { value: "invalid.domain." } });
wrapper
.find("form")
.first()
.simulate("submit", { preventDefault: jest.fn() });
expect(wrapper.find("input").hasClass("is-valid")).toBe(false);
expect(wrapper.find("input").hasClass("is-invalid")).toBe(true);
});
+34 -28
View File
@@ -8,14 +8,14 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import React, { Component } from "react";
import PropTypes from "prop-types";
import { translate } from "react-i18next";
import { api, ignoreCancel, makeCancelable } from "../../utils";
class DomainList extends Component {
onRemove(domain) {
if(this.props.domains.includes(domain)) {
if (this.props.domains.includes(domain)) {
const prevDomains = this.props.domains.slice();
this.removeHandler = makeCancelable(this.props.apiCall(domain));
@@ -32,29 +32,35 @@ class DomainList extends Component {
return (
<ul className="list-group">
{
this.props.domains.length > 0
?
this.props.domains.map(item => (
<li key={item} className="list-group-item">
{
api.loggedIn ?
<button className="btn btn-danger btn-sm pull-right" type="button"
style={{ marginTop: "2px" }} onClick={() => this.onRemove(item)}>
<span className="fa fa-trash-o"/>
</button>
: null
}
<span style={{ display: "table-cell", verticalAlign: "middle", height: "32px" }}>
{item}
</span>
</li>
))
:
<div className="alert alert-info" role="alert">
{t("There are no domains in this list")}
</div>
}
{this.props.domains.length > 0 ? (
this.props.domains.map(item => (
<li key={item} className="list-group-item">
{api.loggedIn ? (
<button
className="btn btn-danger btn-sm pull-right"
type="button"
style={{ marginTop: "2px" }}
onClick={() => this.onRemove(item)}
>
<span className="fa fa-trash-o" />
</button>
) : null}
<span
style={{
display: "table-cell",
verticalAlign: "middle",
height: "32px"
}}
>
{item}
</span>
</li>
))
) : (
<div className="alert alert-info" role="alert">
{t("There are no domains in this list")}
</div>
)}
</ul>
);
}
@@ -71,4 +77,4 @@ DomainList.defaultProps = {
domains: []
};
export default translate(["common", "lists"])(DomainList);
export default translate(["common", "lists"])(DomainList);
+72 -28
View File
@@ -8,81 +8,125 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from 'react';
import { shallow } from 'enzyme';
import DomainList from './DomainList';
import React from "react";
import { shallow } from "enzyme";
import DomainList from "./DomainList";
import { api } from "../../utils";
const domains = [
'domain1.com',
'domain2.com',
'domain3.com'
];
const domains = ["domain1.com", "domain2.com", "domain3.com"];
it('shows a list of domains', () => {
it("shows a list of domains", () => {
const wrapper = shallow(
<DomainList domains={domains} onRemoved={jest.fn()} onFailed={jest.fn()} apiCall={jest.fn()}/>
<DomainList
domains={domains}
onRemoved={jest.fn()}
onFailed={jest.fn()}
apiCall={jest.fn()}
/>
);
expect(wrapper.find('li')).toHaveLength(domains.length);
expect(wrapper.find("li")).toHaveLength(domains.length);
});
it('shows an alert if there are no domains', () => {
it("shows an alert if there are no domains", () => {
const wrapper = shallow(
<DomainList domains={[]} onRemoved={jest.fn()} onFailed={jest.fn()} apiCall={jest.fn()}/>
<DomainList
domains={[]}
onRemoved={jest.fn()}
onFailed={jest.fn()}
apiCall={jest.fn()}
/>
);
expect(wrapper.find('li')).toHaveLength(0);
expect(wrapper.find('ul').childAt(0)).toHaveClassName('alert-info');
expect(wrapper).toIncludeText('There are no domains in this list');
expect(wrapper.find("li")).toHaveLength(0);
expect(wrapper.find("ul").childAt(0)).toHaveClassName("alert-info");
expect(wrapper).toIncludeText("There are no domains in this list");
});
it('does not have a delete button when not logged in', () => {
it("does not have a delete button when not logged in", () => {
const wrapper = shallow(
<DomainList domains={domains} onRemoved={jest.fn()} onFailed={jest.fn()} apiCall={jest.fn()}/>
<DomainList
domains={domains}
onRemoved={jest.fn()}
onFailed={jest.fn()}
apiCall={jest.fn()}
/>
);
expect(wrapper.find("ul").childAt(0).find("button")).not.toExist();
expect(
wrapper
.find("ul")
.childAt(0)
.find("button")
).not.toExist();
});
it('has a delete button when logged in', () => {
it("has a delete button when logged in", () => {
api.loggedIn = true;
const wrapper = shallow(
<DomainList domains={domains} onRemoved={jest.fn()} onFailed={jest.fn()} apiCall={jest.fn()}/>
<DomainList
domains={domains}
onRemoved={jest.fn()}
onFailed={jest.fn()}
apiCall={jest.fn()}
/>
);
expect(wrapper.find("ul").childAt(0).find("button")).toExist();
expect(
wrapper
.find("ul")
.childAt(0)
.find("button")
).toExist();
});
it('calls onRemoved and API callback when a domain is removed', () => {
it("calls onRemoved and API callback when a domain is removed", () => {
api.loggedIn = true;
const onRemoved = jest.fn();
const onFailed = jest.fn();
const apiCall = jest.fn();
const wrapper = shallow(
<DomainList domains={domains} onRemoved={onRemoved} onFailed={onFailed} apiCall={apiCall}/>
<DomainList
domains={domains}
onRemoved={onRemoved}
onFailed={onFailed}
apiCall={apiCall}
/>
);
wrapper.find("ul").childAt(0).find("button").simulate("click");
wrapper
.find("ul")
.childAt(0)
.find("button")
.simulate("click");
expect(onRemoved).toHaveBeenCalled();
expect(apiCall).toHaveBeenCalled();
expect(onFailed).not.toHaveBeenCalled();
});
it('calls all callbacks when a domain is removed and the API call fails', async () => {
it("calls all callbacks when a domain is removed and the API call fails", async () => {
api.loggedIn = true;
const onRemoved = jest.fn();
const onFailed = jest.fn();
const apiCall = jest.fn(() => Promise.reject(new Error()));
const wrapper = shallow(
<DomainList domains={domains} onRemoved={onRemoved} onFailed={onFailed} apiCall={apiCall}/>
<DomainList
domains={domains}
onRemoved={onRemoved}
onFailed={onFailed}
apiCall={apiCall}
/>
);
wrapper.find("ul").childAt(0).find("button").simulate("click");
wrapper
.find("ul")
.childAt(0)
.find("button")
.simulate("click");
await tick();
+42 -35
View File
@@ -8,9 +8,9 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import React, { Component } from "react";
import PropTypes from "prop-types";
import { translate } from "react-i18next";
import DomainInput from "./DomainInput";
import Alert from "../common/Alert";
import DomainList from "./DomainList";
@@ -26,7 +26,7 @@ class ListPage extends Component {
onEnter = domain => {
// Check if the domain was already added
if(this.state.domains.includes(domain)) {
if (this.state.domains.includes(domain)) {
this.onAlreadyAdded(domain);
} else {
// Store the domains before adding the new domain, for a possible rollback
@@ -34,9 +34,10 @@ class ListPage extends Component {
// Try to add the domain
this.addHandler = makeCancelable(this.props.add(domain));
this.addHandler.promise.then(() => {
this.onAdded(domain);
})
this.addHandler.promise
.then(() => {
this.onAdded(domain);
})
.catch(ignoreCancel)
.catch(() => {
this.onAddFailed(domain, prevDomains);
@@ -92,9 +93,10 @@ class ListPage extends Component {
onRefresh = () => {
this.refreshHandler = makeCancelable(this.props.refresh());
this.refreshHandler.promise.then(data => {
this.setState({ domains: data });
})
this.refreshHandler.promise
.then(data => {
this.setState({ domains: data });
})
.catch(ignoreCancel);
};
@@ -109,48 +111,53 @@ class ListPage extends Component {
}
componentWillUnmount() {
if(this.addHandler)
this.addHandler.cancel();
if (this.addHandler) this.addHandler.cancel();
if(this.removeHandler)
this.removeHandler.cancel();
if (this.removeHandler) this.removeHandler.cancel();
if(this.refreshHandler)
this.refreshHandler.cancel();
if (this.refreshHandler) this.refreshHandler.cancel();
}
render() {
return (
<div style={{ marginBottom: "24px" }}>
<h2 className="text-center">{this.props.title}</h2>
<br/>
<br />
<DomainInput
placeholder={this.props.placeholder}
onEnter={this.onEnter}
onRefresh={this.onRefresh}
isValid={this.props.isValid}
onValidationError={this.handleValidationError}/>
{ this.props.note }
{
this.state.infoMsg
? <Alert message={this.state.infoMsg} type="info" onClick={() => this.setState({ infoMsg: "" })}/>
: null
}
{
this.state.successMsg
? <Alert message={this.state.successMsg} type="success" onClick={() => this.setState({ successMsg: "" })}/>
: null
}
{
this.state.errorMsg
? <Alert message={this.state.errorMsg} type="danger" onClick={() => this.setState({ errorMsg: "" })}/>
: null
}
onValidationError={this.handleValidationError}
/>
{this.props.note}
{this.state.infoMsg ? (
<Alert
message={this.state.infoMsg}
type="info"
onClick={() => this.setState({ infoMsg: "" })}
/>
) : null}
{this.state.successMsg ? (
<Alert
message={this.state.successMsg}
type="success"
onClick={() => this.setState({ successMsg: "" })}
/>
) : null}
{this.state.errorMsg ? (
<Alert
message={this.state.errorMsg}
type="danger"
onClick={() => this.setState({ errorMsg: "" })}
/>
) : null}
<DomainList
domains={this.state.domains}
apiCall={this.props.remove}
onRemoved={this.onRemoved}
onFailed={this.onRemoveFailed}/>
onFailed={this.onRemoveFailed}
/>
</div>
);
}
+28 -14
View File
@@ -23,7 +23,8 @@ it("shows the title", () => {
refresh={ignoreAPI}
remove={ignoreAPI}
isValid={jest.fn()}
validationErrorMsg=""/>
validationErrorMsg=""
/>
);
expect(wrapper.find("h2")).toHaveText(title);
@@ -40,7 +41,8 @@ it("shows the placeholder", () => {
refresh={ignoreAPI}
remove={ignoreAPI}
isValid={jest.fn()}
validationErrorMsg=""/>
validationErrorMsg=""
/>
);
expect(wrapper.find("DomainInput")).toHaveProp("placeholder", placeholder);
@@ -57,7 +59,8 @@ it("shows the note", () => {
refresh={ignoreAPI}
remove={ignoreAPI}
isValid={jest.fn()}
validationErrorMsg=""/>
validationErrorMsg=""
/>
);
expect(wrapper).toIncludeText(note);
@@ -73,7 +76,8 @@ it("starts with no alerts shown", () => {
refresh={ignoreAPI}
remove={ignoreAPI}
isValid={jest.fn()}
validationErrorMsg=""/>
validationErrorMsg=""
/>
);
expect(wrapper.find("Alert")).toHaveLength(0);
@@ -90,7 +94,8 @@ it("loads domains after mounting", async () => {
refresh={() => Promise.resolve(domains)}
remove={ignoreAPI}
isValid={jest.fn()}
validationErrorMsg=""/>
validationErrorMsg=""
/>
);
await tick();
@@ -111,7 +116,8 @@ it("checks if the domain was already added", async () => {
refresh={() => Promise.resolve(domains)}
remove={ignoreAPI}
isValid={jest.fn()}
validationErrorMsg=""/>
validationErrorMsg=""
/>
);
// Setup with domains (wait for promise to resolve) and mock function
@@ -137,7 +143,8 @@ it("calls the add prop when adding a domain", () => {
refresh={ignoreAPI}
remove={ignoreAPI}
isValid={jest.fn()}
validationErrorMsg=""/>
validationErrorMsg=""
/>
);
wrapper.instance().onEnter(domain);
@@ -157,7 +164,8 @@ it("calls onAdding when adding a domain", () => {
refresh={ignoreAPI}
remove={ignoreAPI}
isValid={jest.fn()}
validationErrorMsg=""/>
validationErrorMsg=""
/>
);
wrapper.instance().onAdding = onAdding;
@@ -179,7 +187,8 @@ it("calls onAdded after API request succeeds", async () => {
refresh={ignoreAPI}
remove={ignoreAPI}
isValid={jest.fn()}
validationErrorMsg=""/>
validationErrorMsg=""
/>
);
wrapper.instance().onAdded = onAdded;
@@ -202,7 +211,8 @@ it("calls onAddFailed after API request fails", async () => {
refresh={ignoreAPI}
remove={ignoreAPI}
isValid={jest.fn()}
validationErrorMsg=""/>
validationErrorMsg=""
/>
);
wrapper.instance().onAddFailed = onAddFailed;
@@ -224,7 +234,8 @@ it("adds the domain in onAdded", async () => {
refresh={ignoreAPI}
remove={ignoreAPI}
isValid={jest.fn()}
validationErrorMsg=""/>
validationErrorMsg=""
/>
);
wrapper.instance().onEnter(domain);
@@ -245,7 +256,8 @@ it("resets the domains when adding failed", async () => {
refresh={ignoreAPI}
remove={ignoreAPI}
isValid={jest.fn()}
validationErrorMsg=""/>
validationErrorMsg=""
/>
);
wrapper.instance().onEnter(domain);
@@ -267,7 +279,8 @@ it("removes the domain when onRemoved is called", async () => {
refresh={() => Promise.resolve(domains)}
remove={ignoreAPI}
isValid={jest.fn()}
validationErrorMsg=""/>
validationErrorMsg=""
/>
);
wrapper.instance().onRemoved(domain);
@@ -288,7 +301,8 @@ it("resets the domains when removal failed", () => {
refresh={() => Promise.resolve(domains)}
remove={ignoreAPI}
isValid={jest.fn()}
validationErrorMsg=""/>
validationErrorMsg=""
/>
);
wrapper.instance().onRemoveFailed(domain, domains);
wrapper.update();
+42 -30
View File
@@ -8,11 +8,11 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React, { Component } from 'react';
import ReactTable from 'react-table';
import { translate } from 'react-i18next';
import { api, ignoreCancel, makeCancelable, padNumber } from '../../utils';
import 'react-table/react-table.css';
import React, { Component } from "react";
import ReactTable from "react-table";
import { translate } from "react-i18next";
import { api, ignoreCancel, makeCancelable, padNumber } from "../../utils";
import "react-table/react-table.css";
class QueryLog extends Component {
updateHandler = null;
@@ -28,12 +28,14 @@ class QueryLog extends Component {
updateTable() {
this.updateHandler = makeCancelable(api.getHistory());
this.updateHandler.promise.then(data => {
this.setState({
history: data,
loading: false
});
}).catch(ignoreCancel);
this.updateHandler.promise
.then(data => {
this.setState({
history: data,
loading: false
});
})
.catch(ignoreCancel);
}
componentDidMount() {
@@ -57,19 +59,21 @@ class QueryLog extends Component {
data={this.state.history}
loading={this.state.loading}
getTrProps={(state, rowInfo) => {
if(rowInfo && rowInfo.row.status !== 0)
if (rowInfo && rowInfo.row.status !== 0)
return {
style: {
color: [1, 4, 5].includes(rowInfo.row.status) ? 'red' : 'green'
color: [1, 4, 5].includes(rowInfo.row.status) ? "red" : "green"
}
};
else
return {};
else return {};
}}
defaultSorted={[{
id: "time",
desc: true
}]} />
defaultSorted={[
{
id: "time",
desc: true
}
]}
/>
);
}
}
@@ -91,7 +95,9 @@ const columns = t => [
Cell: row => {
const date = new Date(row.value * 1000);
return `${padNumber(date.getHours())}-${padNumber(date.getMinutes())}-${padNumber(date.getSeconds())}`;
return `${padNumber(date.getHours())}-${padNumber(
date.getMinutes()
)}-${padNumber(date.getSeconds())}`;
}
},
{
@@ -120,27 +126,33 @@ const columns = t => [
accessor: r => r[4],
width: 140,
Cell: row => status(t)[row.value],
filterMethod: (filter, row) =>
status(t)[row[filter.id]]
.toLowerCase()
.includes(
filter.value.toLowerCase()
)
filterMethod: (filter, row) => {
const rowStatus = status(t)[row[filter.id]].toLowerCase();
return rowStatus.includes(filter.value.toLowerCase());
}
},
{
Header: t("Action"),
width: 100,
filterable: false,
Cell: data => {
if([1, 4, 5].includes(data.row.status))
if ([1, 4, 5].includes(data.row.status))
return (
<button type="button" className="btn btn-success full-width" onClick={() => api.addWhitelist(data.row.domain)}>
<button
type="button"
className="btn btn-success full-width"
onClick={() => api.addWhitelist(data.row.domain)}
>
{t("Whitelist")}
</button>
);
if([2, 3].includes(data.row.status))
if ([2, 3].includes(data.row.status))
return (
<button type="button" className="btn btn-danger full-width" onClick={() => api.addBlacklist(data.row.domain)}>
<button
type="button"
className="btn btn-danger full-width"
onClick={() => api.addBlacklist(data.row.domain)}
>
{t("Blacklist")}
</button>
);
+37 -13
View File
@@ -8,9 +8,9 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { translate } from 'react-i18next';
import React, { Component } from "react";
import PropTypes from "prop-types";
import { translate } from "react-i18next";
class ForgotPassword extends Component {
state = {
@@ -33,23 +33,47 @@ class ForgotPassword extends Component {
const { t } = this.props;
return (
<div style={{'width': '100%'}}>
<div className={'card ' + (this.props.error ? 'border-danger': 'border-primary')}>
<div className={'card-header ' + (this.props.error ? 'bg-danger' : 'bg-primary')}
style={{ paddingRight: "10px" }}>
<h3 className="card-title" style={{'fontSize': '18px', 'display': 'inline-block', 'margin': 0}}>
<div style={{ width: "100%" }}>
<div
className={
"card " + (this.props.error ? "border-danger" : "border-primary")
}
>
<div
className={
"card-header " + (this.props.error ? "bg-danger" : "bg-primary")
}
style={{ paddingRight: "10px" }}
>
<h3
className="card-title"
style={{ fontSize: "18px", display: "inline-block", margin: 0 }}
>
{t("Forgot Password")}
</h3>
<span className="pull-right">
<button type="button" className="btn btn-card-tool" onClick={this.onClick}>
<i className={'fa ' + (this.isExpanded() ? 'fa-minus' : 'fa-plus')}/>
<button
type="button"
className="btn btn-card-tool"
onClick={this.onClick}
>
<i
className={
"fa " + (this.isExpanded() ? "fa-minus" : "fa-plus")
}
/>
</button>
</span>
</div>
<div className={'card-body bg-light' + (this.isExpanded() ? '' : ' collapse')} style={{'padding': '10px'}}>
<div
className={
"card-body bg-light" + (this.isExpanded() ? "" : " collapse")
}
style={{ padding: "10px" }}
>
{t("forgot_password_description")}
<pre style={{'textAlign': 'center'}}>sudo pihole -a -p</pre>
<pre style={{ textAlign: "center" }}>sudo pihole -a -p</pre>
</div>
</div>
</div>
@@ -61,4 +85,4 @@ ForgotPassword.propTypes = {
error: PropTypes.bool.isRequired
};
export default translate("login")(ForgotPassword);
export default translate("login")(ForgotPassword);
+47 -22
View File
@@ -8,39 +8,64 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from 'react';
import { shallow } from 'enzyme';
import React from "react";
import { shallow } from "enzyme";
import ForgotPassword from "./ForgotPassword";
it('collapses and displays normal if there is no error', () => {
const wrapper = shallow(<ForgotPassword error={false}/>);
it("collapses and displays normal if there is no error", () => {
const wrapper = shallow(<ForgotPassword error={false} />);
expect(wrapper.childAt(0)).toHaveClassName('border-primary');
expect(wrapper.childAt(0).childAt(0)).toHaveClassName('bg-primary');
expect(wrapper.childAt(0).children().last()).toHaveClassName('collapse');
expect(wrapper.childAt(0)).toHaveClassName("border-primary");
expect(wrapper.childAt(0).childAt(0)).toHaveClassName("bg-primary");
expect(
wrapper
.childAt(0)
.children()
.last()
).toHaveClassName("collapse");
});
it('expands and displays red if there is an error', () => {
const wrapper = shallow(<ForgotPassword error={true}/>);
it("expands and displays red if there is an error", () => {
const wrapper = shallow(<ForgotPassword error={true} />);
expect(wrapper.childAt(0)).toHaveClassName('border-danger');
expect(wrapper.childAt(0).childAt(0)).toHaveClassName('bg-danger');
expect(wrapper.childAt(0).children().last()).not.toHaveClassName('collapse');
expect(wrapper.childAt(0)).toHaveClassName("border-danger");
expect(wrapper.childAt(0).childAt(0)).toHaveClassName("bg-danger");
expect(
wrapper
.childAt(0)
.children()
.last()
).not.toHaveClassName("collapse");
});
it('expands and collapses if clicked without error', () => {
const wrapper = shallow(<ForgotPassword error={false}/>);
it("expands and collapses if clicked without error", () => {
const wrapper = shallow(<ForgotPassword error={false} />);
wrapper.find('button').simulate('click');
expect(wrapper.childAt(0).children().last()).not.toHaveClassName('collapse');
wrapper.find("button").simulate("click");
expect(
wrapper
.childAt(0)
.children()
.last()
).not.toHaveClassName("collapse");
wrapper.find('button').simulate('click');
expect(wrapper.childAt(0).children().last()).toHaveClassName('collapse');
wrapper.find("button").simulate("click");
expect(
wrapper
.childAt(0)
.children()
.last()
).toHaveClassName("collapse");
});
it('does not collapse if clicked with error', () => {
const wrapper = shallow(<ForgotPassword error={true}/>);
it("does not collapse if clicked with error", () => {
const wrapper = shallow(<ForgotPassword error={true} />);
wrapper.find('button').simulate('click');
expect(wrapper.childAt(0).children().last()).not.toHaveClassName('collapse');
wrapper.find("button").simulate("click");
expect(
wrapper
.childAt(0)
.children()
.last()
).not.toHaveClassName("collapse");
});
+30 -20
View File
@@ -26,21 +26,25 @@ class DHCPInfo extends Component {
constructor(props) {
super(props);
this.updateDHCPInfo = this.updateDHCPInfo.bind(this);
};
}
updateDHCPInfo() {
this.updateHandler = makeCancelable(api.getDHCPInfo(), { repeat: this.updateDHCPInfo, interval: 600000 });
this.updateHandler.promise.then(res => {
this.setState({
active: res.active,
ip_start: res.ip_start,
ip_end: res.ip_end,
router_ip: res.router_ip,
lease_time: res.lease_time,
domain: res.domain,
ipv6_support: res.ipv6_support
});
})
this.updateHandler = makeCancelable(api.getDHCPInfo(), {
repeat: this.updateDHCPInfo,
interval: 600000
});
this.updateHandler.promise
.then(res => {
this.setState({
active: res.active,
ip_start: res.ip_start,
ip_end: res.ip_end,
router_ip: res.router_ip,
lease_time: res.lease_time,
domain: res.domain,
ipv6_support: res.ipv6_support
});
})
.catch(ignoreCancel)
.catch(() => {
this.setState({
@@ -68,13 +72,19 @@ class DHCPInfo extends Component {
return (
<pre>
{t("DHCP Active")}: {this.state.active.toString()}<br/>
{t("Start IP")}: {this.state.ip_start}<br/>
{t("End IP")}: {this.state.ip_end}<br/>
{t("Router IP")}: {this.state.router_ip}<br/>
{t("Lease Time")}: {this.state.lease_time} h<br/>
{t("Domain")}: {this.state.domain}<br/>
{t("IPv6 Support")}: {this.state.ipv6_support.toString()}<br/>
{t("DHCP Active")}: {this.state.active.toString()}
<br />
{t("Start IP")}: {this.state.ip_start}
<br />
{t("End IP")}: {this.state.ip_end}
<br />
{t("Router IP")}: {this.state.router_ip}
<br />
{t("Lease Time")}: {this.state.lease_time} h<br />
{t("Domain")}: {this.state.domain}
<br />
{t("IPv6 Support")}: {this.state.ipv6_support.toString()}
<br />
</pre>
);
}
+54 -36
View File
@@ -14,17 +14,17 @@ import { api, ignoreCancel, makeCancelable } from "../../utils";
class DNSInfo extends Component {
state = {
"upstream_dns": ["---"],
"conditional_forwarding": {
"enabled": "---",
"router_ip": "---",
"domain": "---"
upstream_dns: ["---"],
conditional_forwarding: {
enabled: "---",
router_ip: "---",
domain: "---"
},
"options": {
"fqdn_required": "---",
"bogus_priv": "---",
"dnssec": "---",
"listening_type": "---"
options: {
fqdn_required: "---",
bogus_priv: "---",
dnssec: "---",
listening_type: "---"
}
};
@@ -34,24 +34,28 @@ class DNSInfo extends Component {
}
updateDNSInfo() {
this.updateHandler = makeCancelable(api.getDNSInfo(), { repeat: this.updateDNSInfo, interval: 600000 });
this.updateHandler.promise.then(res => {
this.setState(res);
})
this.updateHandler = makeCancelable(api.getDNSInfo(), {
repeat: this.updateDNSInfo,
interval: 600000
});
this.updateHandler.promise
.then(res => {
this.setState(res);
})
.catch(ignoreCancel)
.catch(() => {
this.setState({
"upstream_dns": ["-!-"],
"options": {
"fqdn_required": "-!-",
"bogus_priv": "-!-",
"dnssec": "-!-",
"listening_type": "-!-"
upstream_dns: ["-!-"],
options: {
fqdn_required: "-!-",
bogus_priv: "-!-",
dnssec: "-!-",
listening_type: "-!-"
},
"conditional_forwarding": {
"enabled": "-!-",
"router_ip": "-!-",
"domain": "-!-"
conditional_forwarding: {
enabled: "-!-",
router_ip: "-!-",
domain: "-!-"
}
});
});
@@ -72,27 +76,41 @@ class DNSInfo extends Component {
<div className="row">
<div className="col-lg-4 col-sm-4 col-xs-4">
<pre>
<br/>
{t("Upstream DNS Servers")}:<br/>
{this.state.upstream_dns.map(item => item + "\n")}<br/>
<br />
{t("Upstream DNS Servers")}:<br />
{this.state.upstream_dns.map(item => item + "\n")}
<br />
</pre>
</div>
<div className="col-lg-4 col-sm-4 col-xs-4">
<pre>
<br/>
{t("Interface listening behavior")}: {this.state.options.listening_type}<br/>
{t("Forward FQDNs only")}: {this.state.options.fqdn_required.toString()}<br/>
{t("Only forward public reverse lookups")}:{this.state.options.bogus_priv.toString()}<br/>
<br />
{t("Interface listening behavior")}:{" "}
{this.state.options.listening_type}
<br />
{t("Forward FQDNs only")}:{" "}
{this.state.options.fqdn_required.toString()}
<br />
{t("Only forward public reverse lookups")}:
{this.state.options.bogus_priv.toString()}
<br />
{t("Use DNSSEC")}: {this.state.options.dnssec.toString()}
</pre>
</div>
<div className="col-lg-4 col-sm-4 col-xs-4">
<pre>
<br/>
{t("Conditional Forwarding")}<br/>
{t("Enabled")}: {this.state.conditional_forwarding.enabled.toString()}<br/>
{t("Router IP")}: {this.state.conditional_forwarding.router_ip.toString()}<br/>
{t("Local Domain Name")}: {this.state.conditional_forwarding.domain.toString()}<br/>
<br />
{t("Conditional Forwarding")}
<br />
{t("Enabled")}:{" "}
{this.state.conditional_forwarding.enabled.toString()}
<br />
{t("Router IP")}:{" "}
{this.state.conditional_forwarding.router_ip.toString()}
<br />
{t("Local Domain Name")}:{" "}
{this.state.conditional_forwarding.domain.toString()}
<br />
</pre>
</div>
</div>
+23 -18
View File
@@ -25,23 +25,26 @@ class FTLInfo extends Component {
}
updateFTLInfo() {
this.updateHandler = makeCancelable(api.getFTLdb(), { repeat: this.updateFTLInfo, interval: 600000 });
this.updateHandler.promise.then(res => {
this.setState({
queries: res.queries.toLocaleString(),
filesize: res.filesize.toLocaleString(),
sqlite_version: res.sqlite_version
});
})
this.updateHandler = makeCancelable(api.getFTLdb(), {
repeat: this.updateFTLInfo,
interval: 600000
});
this.updateHandler.promise
.then(res => {
this.setState({
queries: res.queries.toLocaleString(),
filesize: res.filesize.toLocaleString(),
sqlite_version: res.sqlite_version
});
})
.catch(ignoreCancel)
.catch(() => {
this.setState({
queries: "-!-",
filesize: "-!-",
sqlite_version: "-!-"
});
}
);
this.setState({
queries: "-!-",
filesize: "-!-",
sqlite_version: "-!-"
});
});
}
componentDidMount() {
@@ -57,9 +60,11 @@ class FTLInfo extends Component {
return (
<pre>
{t("Queries")}: {this.state.queries}<br/>
{t("Filesize")}: {this.state.filesize} B<br/>
{t("SQLite version")}: {this.state.sqlite_version}<br/>
{t("Queries")}: {this.state.queries}
<br />
{t("Filesize")}: {this.state.filesize} B<br />
{t("SQLite version")}: {this.state.sqlite_version}
<br />
</pre>
);
}
+37 -17
View File
@@ -27,15 +27,19 @@ class NetworkInfo extends Component {
}
updateNetInfo() {
this.updateHandler = makeCancelable(api.getNetworkInfo(), { repeat: this.updateNetInfo, interval: 600000 });
this.updateHandler.promise.then(res => {
this.setState({
interface: res.interface,
ipv4_address: res.ipv4_address,
ipv6_address: res.ipv6_address,
hostname: res.hostname
});
})
this.updateHandler = makeCancelable(api.getNetworkInfo(), {
repeat: this.updateNetInfo,
interval: 600000
});
this.updateHandler.promise
.then(res => {
this.setState({
interface: res.interface,
ipv4_address: res.ipv4_address,
ipv6_address: res.ipv6_address,
hostname: res.hostname
});
})
.catch(ignoreCancel)
.catch(() => {
this.setState({
@@ -62,27 +66,43 @@ class NetworkInfo extends Component {
<Fragment>
<Form>
<FormGroup row>
<Label className="bold" for="interface" sm={4}>{t("Interface")}</Label>
<Label className="bold" for="interface" sm={4}>
{t("Interface")}
</Label>
<Col sm={8}>
<Input plaintext id="interface">{this.state.interface}</Input>
<Input plaintext id="interface">
{this.state.interface}
</Input>
</Col>
</FormGroup>
<FormGroup row>
<Label className="bold" for="ipv4_address" sm={4}>{t("IPv4 address")}</Label>
<Label className="bold" for="ipv4_address" sm={4}>
{t("IPv4 address")}
</Label>
<Col sm={8}>
<Input plaintext id="ipv4_address">{this.state.ipv4_address}</Input>
<Input plaintext id="ipv4_address">
{this.state.ipv4_address}
</Input>
</Col>
</FormGroup>
<FormGroup row>
<Label className="bold" for="ipv6_address" sm={4}>{t("IPv6 address")}</Label>
<Label className="bold" for="ipv6_address" sm={4}>
{t("IPv6 address")}
</Label>
<Col sm={8}>
<Input plaintext id="ipv6_address">{this.state.ipv6_address}</Input>
<Input plaintext id="ipv6_address">
{this.state.ipv6_address}
</Input>
</Col>
</FormGroup>
<FormGroup row>
<Label className="bold" for="hostname" sm={4}>{t("Hostname")}</Label>
<Label className="bold" for="hostname" sm={4}>
{t("Hostname")}
</Label>
<Col sm={8}>
<Input plaintext id="hostname">{this.state.hostname}</Input>
<Input plaintext id="hostname">
{this.state.hostname}
</Input>
</Col>
</FormGroup>
</Form>
+12 -7
View File
@@ -25,22 +25,27 @@ class VersionCard extends Component {
<div className="card border-0 bg-primary">
<div className="card-block">
<div className="card-icon">
<i className={this.props.icon}/>
<i className={this.props.icon} />
</div>
</div>
<div className="card-body">
<h3>{this.props.name}</h3>
<p>
{t("Version")}: {this.props.branch === "master" ? this.props.tag : "vDev"}
{t("Version")}:{" "}
{this.props.branch === "master" ? this.props.tag : "vDev"}
</p>
<button className="btn btn-secondary"
type="button"
onClick={() => this.setState({ collapsed: !this.state.collapsed })}>
<button
className="btn btn-secondary"
type="button"
onClick={() => this.setState({ collapsed: !this.state.collapsed })}
>
{t("Details")}
</button>
<Collapse isOpen={!this.state.collapsed}>
<table className="table table-borderless table-sm table-condensed"
style={{ marginTop: "1rem", marginBottom: "0" }}>
<table
className="table table-borderless table-sm table-condensed"
style={{ marginTop: "1rem", marginBottom: "0" }}
>
<tbody>
<tr>
<td>{t("Branch")}</td>
+40 -12
View File
@@ -43,10 +43,14 @@ class VersionInfo extends Component {
}
updateVersionInfo() {
this.updateHandler = makeCancelable(api.getVersion(), { repeat: this.updateVersionInfo, interval: 600000 });
this.updateHandler.promise.then(res => {
this.setState(res);
})
this.updateHandler = makeCancelable(api.getVersion(), {
repeat: this.updateVersionInfo,
interval: 600000
});
this.updateHandler.promise
.then(res => {
this.setState(res);
})
.catch(ignoreCancel)
.catch(() => {
this.setState({
@@ -88,20 +92,44 @@ class VersionInfo extends Component {
return (
<div className="row">
<div className="col-xl-3 col-md-6 col-xs-12">
<VersionCard name={t("Core")} icon="fa fa-dot-circle-o fa-2x"
branch={this.state.core.branch} hash={this.state.core.hash} tag={this.state.core.tag} t={t}/>
<VersionCard
name={t("Core")}
icon="fa fa-dot-circle-o fa-2x"
branch={this.state.core.branch}
hash={this.state.core.hash}
tag={this.state.core.tag}
t={t}
/>
</div>
<div className="col-xl-3 col-md-6 col-xs-12">
<VersionCard name={t("FTL")} icon="fa fa-industry fa-2x"
branch={this.state.ftl.branch} hash={this.state.ftl.hash} tag={this.state.ftl.tag} t={t}/>
<VersionCard
name={t("FTL")}
icon="fa fa-industry fa-2x"
branch={this.state.ftl.branch}
hash={this.state.ftl.hash}
tag={this.state.ftl.tag}
t={t}
/>
</div>
<div className="col-xl-3 col-md-6 col-xs-12">
<VersionCard name={t("API")} icon="fa fa-bullseye fa-2x"
branch={this.state.api.branch} hash={this.state.api.hash} tag={this.state.api.tag} t={t}/>
<VersionCard
name={t("API")}
icon="fa fa-bullseye fa-2x"
branch={this.state.api.branch}
hash={this.state.api.hash}
tag={this.state.api.tag}
t={t}
/>
</div>
<div className="col-xl-3 col-md-6 col-xs-12">
<VersionCard name={t("Web")} icon="fa fa-list-alt fa-2x"
branch={this.state.web.branch} hash={this.state.web.hash} tag={this.state.web.tag} t={t}/>
<VersionCard
name={t("Web")}
icon="fa fa-list-alt fa-2x"
branch={this.state.web.branch}
hash={this.state.web.hash}
tag={this.state.web.tag}
t={t}
/>
</div>
</div>
);
+1 -1
View File
@@ -11,4 +11,4 @@
export default {
developmentMode: true,
fakeAPI: false
};
};
+6 -9
View File
@@ -8,16 +8,13 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import developmentConfig from './config.development';
import productionConfig from './config.production';
import developmentConfig from "./config.development";
import productionConfig from "./config.production";
let config;
if(process.env.NODE_ENV === "development")
config = developmentConfig;
else
config = productionConfig;
if (process.env.NODE_ENV === "development") config = developmentConfig;
else config = productionConfig;
if(process.env.REACT_APP_FAKE_API)
config.fakeAPI = true;
if (process.env.REACT_APP_FAKE_API) config.fakeAPI = true;
export default config;
export default config;
+1 -1
View File
@@ -11,4 +11,4 @@
export default {
developmentMode: false,
fakeAPI: false
};
};
+24 -15
View File
@@ -8,29 +8,29 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from 'react';
import { Switch, Route, Redirect } from 'react-router-dom'
import Header, { mobileSidebarHide } from '../components/common/Header';
import Sidebar from '../components/common/Sidebar';
import Footer from '../components/common/Footer';
import React from "react";
import { Switch, Route, Redirect } from "react-router-dom";
import Header, { mobileSidebarHide } from "../components/common/Header";
import Sidebar from "../components/common/Sidebar";
import Footer from "../components/common/Footer";
import { api } from "../utils";
import { nav } from "../routes";
export default props => (
<div className="app">
<Header/>
<Header />
<div className="app-body">
<Sidebar items={nav} {...props}/>
<Sidebar items={nav} {...props} />
<main className="main" onClick={mobileSidebarHide}>
<div className="container-fluid" style={{"marginTop": "1.5rem"}}>
<div className="container-fluid" style={{ marginTop: "1.5rem" }}>
<Switch>
<Redirect exact from="/" to="/dashboard"/>
<Redirect exact from="/" to="/dashboard" />
{nav.map(createRoute)}
</Switch>
</div>
</main>
</div>
<Footer/>
<Footer />
</div>
);
@@ -41,13 +41,23 @@ export default props => (
* @param routeData the route data (see routes.js)
*/
const createRoute = routeData => {
if(routeData.children) {
if (routeData.children) {
return routeData.children.map(createRoute);
}
return routeData.auth
? <AuthRoute key={routeData.url} path={routeData.url} component={routeData.component}/>
: <Route key={routeData.url} path={routeData.url} component={routeData.component}/>;
return routeData.auth ? (
<AuthRoute
key={routeData.url}
path={routeData.url}
component={routeData.component}
/>
) : (
<Route
key={routeData.url}
path={routeData.url}
component={routeData.component}
/>
);
};
/**
@@ -76,4 +86,3 @@ const AuthRoute = ({ component: Component, ...rest }) => (
}
/>
);
+15 -7
View File
@@ -1,8 +1,8 @@
import i18n from 'i18next';
import XHR from 'i18next-xhr-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { reactI18nextModule } from 'react-i18next';
import config from './config';
import i18n from "i18next";
import XHR from "i18next-xhr-backend";
import LanguageDetector from "i18next-browser-languagedetector";
import { reactI18nextModule } from "react-i18next";
import config from "./config";
export function setupI18n() {
i18n
@@ -11,9 +11,17 @@ export function setupI18n() {
.use(reactI18nextModule)
.init({
fallbackLng: "en",
ns: ['common'],
ns: ["common"],
defaultNS: "common",
fallbackNS: ["dashboard", "footer", "lists", "location", "login", "query-log", "settings"],
fallbackNS: [
"dashboard",
"footer",
"lists",
"location",
"login",
"query-log",
"settings"
],
nsSeparator: false,
keySeparator: false,
debug: config.developmentMode,
+12 -14
View File
@@ -8,16 +8,16 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route, Switch } from 'react-router-dom'
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import "ionicons/dist/css/ionicons.min.css";
import "font-awesome/css/font-awesome.min.css";
import "simple-line-icons/css/simple-line-icons.css";
import './scss/style.css';
import Full from './containers/Full';
import "./scss/style.css";
import Full from "./containers/Full";
import { api } from "./utils";
import { setupI18n } from './i18n';
import { setupI18n } from "./i18n";
// Before rendering anything, check if there is a session cookie.
// Note: the user could have an old session, so the first API call
@@ -27,12 +27,10 @@ api.loggedIn = document.cookie.includes("user_id=");
setupI18n();
ReactDOM.render(
(
<BrowserRouter>
<Switch>
<Route path="/" name="Home" component={Full}/>
</Switch>
</BrowserRouter>
),
document.getElementById('root')
<BrowserRouter>
<Switch>
<Route path="/" name="Home" component={Full} />
</Switch>
</BrowserRouter>,
document.getElementById("root")
);
+42 -42
View File
@@ -19,96 +19,96 @@ import Login from "./views/Login";
import Logout from "./views/Logout";
export const routes = t => ({
'/dashboard': t('Dashboard'),
'/query-log': t('Query Log'),
'/whitelist': t('Whitelist'),
'/blacklist/exact': `${t('Blacklist')} (${t('Exact')})`,
'/blacklist/regex': `${t('Blacklist')} (${t('Regex')})`,
'/settings/versions': t('Versions'),
'/settings/networking': t('Networking'),
'/login': t('Login'),
'/logout': t('Logout')
"/dashboard": t("Dashboard"),
"/query-log": t("Query Log"),
"/whitelist": t("Whitelist"),
"/blacklist/exact": `${t("Blacklist")} (${t("Exact")})`,
"/blacklist/regex": `${t("Blacklist")} (${t("Regex")})`,
"/settings/versions": t("Versions"),
"/settings/networking": t("Networking"),
"/login": t("Login"),
"/logout": t("Logout")
});
export const nav = [
{
name: 'Dashboard',
url: '/dashboard',
name: "Dashboard",
url: "/dashboard",
component: Dashboard,
icon: 'fa fa-dashboard',
icon: "fa fa-dashboard",
auth: false
},
{
name: 'Query Log',
url: '/query-log',
name: "Query Log",
url: "/query-log",
component: QueryLog,
icon: 'fa fa-database',
icon: "fa fa-database",
auth: true
},
{
name: 'Whitelist',
url: '/whitelist',
name: "Whitelist",
url: "/whitelist",
component: Whitelist,
icon: 'fa fa-check-circle-o',
icon: "fa fa-check-circle-o",
auth: false
},
{
name: 'Blacklist',
name: "Blacklist",
url: "/blacklist",
icon: 'fa fa-ban',
icon: "fa fa-ban",
auth: false,
children: [
{
name: 'Exact',
url: '/blacklist/exact',
name: "Exact",
url: "/blacklist/exact",
component: Blacklist,
icon: 'fa fa-ban',
icon: "fa fa-ban",
auth: false
},
{
name: 'Regex',
url: '/blacklist/regex',
name: "Regex",
url: "/blacklist/regex",
component: Regexlist,
icon: 'fa fa-ban',
icon: "fa fa-ban",
auth: false
}
]
},
{
name: 'Settings',
name: "Settings",
url: "/settings",
icon: 'fa fa-wrench',
icon: "fa fa-wrench",
auth: true,
children: [
{
name: 'Versions',
url: '/settings/versions',
{
name: "Versions",
url: "/settings/versions",
component: Versions,
icon: 'fa fa-download',
icon: "fa fa-download",
auth: true
},
{
name: 'Networking',
url: '/settings/networking',
name: "Networking",
url: "/settings/networking",
component: Networking,
icon: 'fa fa-sitemap',
auth: true,
icon: "fa fa-sitemap",
auth: true
}
]
},
{
name: 'Login',
url: '/login',
name: "Login",
url: "/login",
component: Login,
icon: 'fa fa-user',
icon: "fa fa-user",
auth: false,
authStrict: true
},
{
name: 'Logout',
url: '/logout',
name: "Logout",
url: "/logout",
component: Logout,
icon: 'fa fa-user-times',
icon: "fa fa-user-times",
auth: true,
authStrict: true
}
+5 -5
View File
@@ -8,9 +8,9 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import { configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import 'jest-enzyme';
import { configure } from "enzyme";
import Adapter from "enzyme-adapter-react-16";
import "jest-enzyme";
import { api } from "./utils";
import fetchMock from "fetch-mock";
@@ -18,12 +18,12 @@ import fetchMock from "fetch-mock";
configure({ adapter: new Adapter() });
// Mock out react-i18next
jest.mock('react-i18next', () => ({
jest.mock("react-i18next", () => ({
// This mock makes sure any components using the translate HoC receive the t function as a prop
translate: () => component => {
component.defaultProps = { ...component.defaultProps, t: key => key };
return component;
},
}
}));
// Temporary fix to reset logged in state for each test.
+34 -39
View File
@@ -8,27 +8,26 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import config from './config';
import config from "./config";
export const padNumber = (num) => {
return ("00" + num).substr(-2,2);
export const padNumber = num => {
return ("00" + num).substr(-2, 2);
};
export const makeCancelable = (promise, { repeat = null, interval = 0 } = {}) => {
export const makeCancelable = (
promise,
{ repeat = null, interval = 0 } = {}
) => {
let hasCanceled = false;
let repeatId = null;
const handle = (resolve, reject, val, isError) => {
if(hasCanceled)
reject({ isCanceled: true });
if (hasCanceled) reject({ isCanceled: true });
else {
if(isError)
reject(val);
else
resolve(val);
if (isError) reject(val);
else resolve(val);
if(repeat)
repeatId = setTimeout(repeat, interval);
if (repeat) repeatId = setTimeout(repeat, interval);
}
};
@@ -44,13 +43,12 @@ export const makeCancelable = (promise, { repeat = null, interval = 0 } = {}) =>
cancel() {
clearTimeout(repeatId);
hasCanceled = true;
},
}
};
};
export const ignoreCancel = err => {
if(!err.isCanceled)
throw err;
if (!err.isCanceled) throw err;
};
export const api = {
@@ -73,13 +71,13 @@ export const api = {
return api.get("stats/overTime/history");
},
getClientsGraph() {
return api.get("stats/overTime/clients")
return api.get("stats/overTime/clients");
},
getQueryTypes() {
return api.get("stats/query_types")
return api.get("stats/query_types");
},
getForwardDestinations() {
return api.get("stats/forward_destinations")
return api.get("stats/forward_destinations");
},
getTopDomains() {
return api.get("stats/top_domains");
@@ -103,13 +101,13 @@ export const api = {
return api.get("dns/regexlist");
},
addWhitelist(domain) {
return api.post("dns/whitelist", { "domain": domain });
return api.post("dns/whitelist", { domain: domain });
},
addBlacklist(domain) {
return api.post("dns/blacklist", { "domain": domain });
return api.post("dns/blacklist", { domain: domain });
},
addRegexlist(domain) {
return api.post("dns/regexlist", { "domain": domain });
return api.post("dns/regexlist", { domain: domain });
},
removeWhitelist(domain) {
return api.delete("dns/whitelist/" + domain);
@@ -121,7 +119,7 @@ export const api = {
return api.delete("dns/regexlist/" + encodeURIComponent(domain));
},
getStatus() {
return api.get("dns/status")
return api.get("dns/status");
},
get(url) {
return fetch(api.urlFor(url), {
@@ -152,42 +150,41 @@ export const api = {
.then(api.checkForErrors);
},
getNetworkInfo() {
return api.get("settings/network")
return api.get("settings/network");
},
getVersion() {
return api.get("version")
return api.get("version");
},
getFTLdb() {
return api.get("settings/ftldb")
return api.get("settings/ftldb");
},
getDNSInfo() {
return api.get("settings/dns")
return api.get("settings/dns");
},
getDHCPInfo() {
return api.get("settings/dhcp")
return api.get("settings/dhcp");
},
/**
* If the user is logged in, check if the user's session has lapsed.
* If so, log them out and refresh the page.
*/
checkIfLoggedOut(response) {
if(api.loggedIn && response.status === 401) {
if (api.loggedIn && response.status === 401) {
// Clear the user's old session and refresh the page
document.cookie = 'user_id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
document.cookie =
"user_id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;";
window.location.reload();
return Promise.reject({ isCanceled: true })
return Promise.reject({ isCanceled: true });
}
return Promise.resolve(response);
},
async convertJSON(data) {
if(!data.ok)
return Promise.reject({ data, json: await data.json() });
else
return data.json();
if (!data.ok) return Promise.reject({ data, json: await data.json() });
else return data.json();
},
checkForErrors(data) {
if(data.error) {
if (data.error) {
return Promise.reject(data.error);
}
return Promise.resolve(data);
@@ -195,10 +192,8 @@ export const api = {
urlFor(endpoint) {
let apiLocation;
if(config.fakeAPI)
apiLocation = process.env.PUBLIC_URL + "/fakeAPI";
else
apiLocation = "/admin/api";
if (config.fakeAPI) apiLocation = process.env.PUBLIC_URL + "/fakeAPI";
else apiLocation = "/admin/api";
return apiLocation + "/" + endpoint;
},
+2 -3
View File
@@ -12,13 +12,12 @@ import { api } from "./utils";
// This is a dumb test used to set up the next test,
// which checks that the logged in state is reset before each test
it('sets logged in to true', () => {
it("sets logged in to true", () => {
api.loggedIn = true;
expect(api.loggedIn).toBeTruthy();
});
it('resets the logged in state for each test', () => {
it("resets the logged in state for each test", () => {
expect(api.loggedIn).toBeFalsy();
});
+3 -3
View File
@@ -8,8 +8,8 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from 'react';
import { translate } from 'react-i18next';
import React from "react";
import { translate } from "react-i18next";
import { api } from "../utils";
import ListPage from "../components/list/ListPage";
import { isValidDomain } from "../validate";
@@ -28,7 +28,7 @@ const Blacklist = props => {
validationErrorMsg={t("Not a valid domain")}
{...props}
/>
)
);
};
export default translate("location")(Blacklist);
+34 -36
View File
@@ -8,56 +8,54 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React, { Fragment } from 'react';
import SummaryStats from '../components/dashboard/SummaryStats';
import QueriesGraph from '../components/dashboard/QueriesGraph';
import ClientsGraph from '../components/dashboard/ClientsGraph';
import React, { Fragment } from "react";
import SummaryStats from "../components/dashboard/SummaryStats";
import QueriesGraph from "../components/dashboard/QueriesGraph";
import ClientsGraph from "../components/dashboard/ClientsGraph";
import QueryTypesChart from "../components/dashboard/QueryTypesChart";
import ForwardDestinationsChart from "../components/dashboard/ForwardDestinationsChart";
import TopDomains from '../components/dashboard/TopDomains';
import TopBlocked from '../components/dashboard/TopBlocked';
import TopClients from '../components/dashboard/TopClients';
import TopDomains from "../components/dashboard/TopDomains";
import TopBlocked from "../components/dashboard/TopBlocked";
import TopClients from "../components/dashboard/TopClients";
import { api } from "../utils";
export default () => (
<div className="animated fadeIn">
<div className="row">
<SummaryStats/>
<SummaryStats />
</div>
<div className="row">
<div className="col-md-12">
<QueriesGraph/>
<QueriesGraph />
</div>
</div>
{
api.loggedIn ?
<Fragment>
<div className="row">
<div className="col-md-12">
<ClientsGraph/>
</div>
{api.loggedIn ? (
<Fragment>
<div className="row">
<div className="col-md-12">
<ClientsGraph />
</div>
<div className="row">
<div className="col-md-6">
<QueryTypesChart/>
</div>
<div className="col-md-6">
<ForwardDestinationsChart/>
</div>
</div>
<div className="row">
<div className="col-md-6">
<QueryTypesChart />
</div>
<div className="row">
<div className="col-md-6 col-xl-4">
<TopDomains/>
</div>
<div className="col-md-6 col-xl-4">
<TopBlocked/>
</div>
<div className="col-md-6 col-xl-4">
<TopClients/>
</div>
<div className="col-md-6">
<ForwardDestinationsChart />
</div>
</Fragment>
: null
}
</div>
<div className="row">
<div className="col-md-6 col-xl-4">
<TopDomains />
</div>
<div className="col-md-6 col-xl-4">
<TopBlocked />
</div>
<div className="col-md-6 col-xl-4">
<TopClients />
</div>
</div>
</Fragment>
) : null}
</div>
);
+72 -60
View File
@@ -27,8 +27,7 @@ class Login extends Component {
componentWillMount() {
// Check if cookies are enabled
if(navigator.cookieEnabled)
this.setState({ cookiesEnabled: true });
if (navigator.cookieEnabled) this.setState({ cookiesEnabled: true });
}
/**
@@ -39,10 +38,8 @@ class Login extends Component {
* @param e the event
*/
handlePasswordChange = e => {
if(e.keyCode === 13)
this.authenticate();
else
this.setState({ password: e.target.value });
if (e.keyCode === 13) this.authenticate();
else this.setState({ password: e.target.value });
};
/**
@@ -53,17 +50,22 @@ class Login extends Component {
e.preventDefault();
// Hash the password twice before sending to the API
let hashedPassword = sha("sha256").update(this.state.password).digest("hex");
hashedPassword = sha("sha256").update(hashedPassword).digest("hex");
let hashedPassword = sha("sha256")
.update(this.state.password)
.digest("hex");
hashedPassword = sha("sha256")
.update(hashedPassword)
.digest("hex");
// Clear the state
this.setState({ password: "", error: false });
// Send the password to the API to authenticate the user
api.authenticate(hashedPassword)
api
.authenticate(hashedPassword)
.then(data => {
// Verify status
if(data.status !== "success") {
if (data.status !== "success") {
console.log("Failed to log in:");
console.log(data);
return;
@@ -71,7 +73,7 @@ class Login extends Component {
api.loggedIn = true;
if(config.fakeAPI) {
if (config.fakeAPI) {
// When using the fake API, set the cookie ourselves
document.cookie = "user_id=;";
}
@@ -86,76 +88,86 @@ class Login extends Component {
render() {
// If the user is already logged in, then go to the home page
if(api.loggedIn)
return <Redirect to="/"/>;
if (api.loggedIn) return <Redirect to="/" />;
const { t } = this.props;
return (
<div className="mainbox col-md-8 offset-md-2 col-lg-6 offset-lg-3" style={{ "float": "none" }}>
<div
className="mainbox col-md-8 offset-md-2 col-lg-6 offset-lg-3"
style={{ float: "none" }}
>
<div className="card">
<div className="card-header">
<div style={{ "textAlign": "center" }}>
<img src={logo} alt="Logo" width="30%"/>
<div style={{ textAlign: "center" }}>
<img src={logo} alt="Logo" width="30%" />
</div>
<br/>
<br />
<div className="card-title text-center" style={{ "marginBottom": 0 }}>
<span className="logo-lg" style={{ "fontSize": "25px" }}>
Pi-<b>hole</b>
<div className="card-title text-center" style={{ marginBottom: 0 }}>
<span className="logo-lg" style={{ fontSize: "25px" }}>
Pi-
<b>hole</b>
</span>
</div>
<div className="login-box-msg">
{t("Sign in to start your session")}
{
// If the user tried to go to a protected page and is not logged in,
// tell them they will be redirected once login is successful
this.props.location.state && this.props.location.state.from.pathname in routes(t) ?
(
<Fragment>
<br/>
{t(
"You will be transferred to the \"{{page}}\" page",
{ page: routes(t)[this.props.location.state.from.pathname] }
)}
</Fragment>
) : null
}
{
// If cookies are not enabled (or detected), show a warning
!this.state.cookiesEnabled ?
<div className="text-center" style={{ "color": "#F00" }}>
{t("Verify that cookies are allowed for {{host}}", { host: window.location.host })}
</div>
: null
}
</div>
{
this.state.error
?
<div className="form-group has-error login-box-msg">
<label className="control-label">
<i className="fa fa-times-circle-o"/> {t("Wrong Password!")}
</label>
{// If the user tried to go to a protected page and is not logged in,
// tell them they will be redirected once login is successful
this.props.location.state &&
this.props.location.state.from.pathname in routes(t) ? (
<Fragment>
<br />
{t('You will be transferred to the "{{page}}" page', {
page: routes(t)[this.props.location.state.from.pathname]
})}
</Fragment>
) : null}
{// If cookies are not enabled (or detected), show a warning
!this.state.cookiesEnabled ? (
<div className="text-center" style={{ color: "#F00" }}>
{t("Verify that cookies are allowed for {{host}}", {
host: window.location.host
})}
</div>
: null
}
) : null}
</div>
{this.state.error ? (
<div className="form-group has-error login-box-msg">
<label className="control-label">
<i className="fa fa-times-circle-o" /> {t("Wrong Password!")}
</label>
</div>
) : null}
</div>
<div className="card-body">
<form id="loginform">
<div className={"input-group" + (this.state.error ? " has-error" : "")}>
<input type="password" className="form-control"
value={this.state.password} onChange={this.handlePasswordChange}
placeholder={t("Password")} autoFocus/>
<div
className={
"input-group" + (this.state.error ? " has-error" : "")
}
>
<input
type="password"
className="form-control"
value={this.state.password}
onChange={this.handlePasswordChange}
placeholder={t("Password")}
autoFocus
/>
<div className="input-group-append">
<button type="submit" className="btn btn-primary" onClick={this.authenticate}>
<button
type="submit"
className="btn btn-primary"
onClick={this.authenticate}
>
{t("Log in")}
</button>
</div>
</div>
<br/>
<ForgotPassword error={this.state.error}/>
<br />
<ForgotPassword error={this.state.error} />
</form>
</div>
</div>
@@ -164,4 +176,4 @@ class Login extends Component {
}
}
export default translate(["login", "location"])(Login);
export default translate(["login", "location"])(Login);
+6 -5
View File
@@ -8,8 +8,8 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React, { Component } from 'react';
import { Redirect } from 'react-router-dom';
import React, { Component } from "react";
import { Redirect } from "react-router-dom";
import { api } from "../utils";
import config from "../config";
@@ -17,16 +17,17 @@ export default class Logout extends Component {
componentWillMount() {
api.loggedIn = false;
if(config.fakeAPI) {
if (config.fakeAPI) {
// When using the fake API, don't try deleting the resource
// (it results in an error). Instead, delete the cookie.
document.cookie = 'user_id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
document.cookie =
"user_id=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;";
} else {
api.logout();
}
}
render() {
return <Redirect to="/"/>;
return <Redirect to="/" />;
}
}
+10 -9
View File
@@ -27,7 +27,7 @@ class Networking extends Component {
* @param tab the tab ID to switch to
*/
setTab = tab => {
if(this.state.activeTab !== tab) {
if (this.state.activeTab !== tab) {
this.setState({
activeTab: tab
});
@@ -43,7 +43,10 @@ class Networking extends Component {
*/
tab = (id, name) => (
<NavItem>
<NavLink active={this.state.activeTab === id} onClick={() => this.setTab(id)}>
<NavLink
active={this.state.activeTab === id}
onClick={() => this.setTab(id)}
>
{name}
</NavLink>
</NavItem>
@@ -56,9 +59,7 @@ class Networking extends Component {
* @param component the component to render in the tab
* @returns {TabPane} the tab content component
*/
tabContent = (id, component) => (
<TabPane tabId={id}>{component}</TabPane>
);
tabContent = (id, component) => <TabPane tabId={id}>{component}</TabPane>;
render() {
const { t } = this.props;
@@ -72,10 +73,10 @@ class Networking extends Component {
{this.tab("ftl", t("FTL"))}
</Nav>
<TabContent activeTab={this.state.activeTab}>
{this.tabContent("network", <NetworkInfo/>)}
{this.tabContent("dhcp", <DHCPInfo/>)}
{this.tabContent("dns", <DNSInfo/>)}
{this.tabContent("ftl", <FTLInfo/>)}
{this.tabContent("network", <NetworkInfo />)}
{this.tabContent("dhcp", <DHCPInfo />)}
{this.tabContent("dns", <DNSInfo />)}
{this.tabContent("ftl", <FTLInfo />)}
</TabContent>
</div>
);
+2 -2
View File
@@ -8,8 +8,8 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from 'react';
import { translate } from 'react-i18next';
import React from "react";
import { translate } from "react-i18next";
import ListPage from "../components/list/ListPage";
import { api } from "../utils";
+3 -3
View File
@@ -8,13 +8,13 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from 'react';
import VersionInfo from '../components/settings/VersionInfo';
import React from "react";
import VersionInfo from "../components/settings/VersionInfo";
export default () => (
<div className="animated fadeIn">
<div className="container-flex">
<VersionInfo/>
<VersionInfo />
</div>
</div>
);
+3 -3
View File
@@ -8,8 +8,8 @@
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React from 'react';
import { translate } from 'react-i18next';
import React from "react";
import { translate } from "react-i18next";
import ListPage from "../components/list/ListPage";
import { api } from "../utils";
import { isValidDomain } from "../validate";
@@ -28,7 +28,7 @@ const Whitelist = props => {
validationErrorMsg={t("Not a valid domain")}
{...props}
/>
)
);
};
export default translate(["location", "lists"])(Whitelist);