mirror of
https://github.com/gosticks/web.git
synced 2026-10-06 14:47:32 +00:00
Merge pull request #95 from pi-hole/ci/formatter
Use a Formatter for Consistent Code Style
This commit is contained in:
@@ -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"
|
||||
|
||||
Generated
+3
-7
@@ -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
@@ -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%",
|
||||
|
||||
@@ -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}>
|
||||
×
|
||||
</button>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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&hosted_button_id=3J2L3Z4DHW9UY">
|
||||
{t("Donate")}
|
||||
|
||||
{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>
|
||||
|
||||
@@ -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 />);
|
||||
});
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
☰
|
||||
</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}
|
||||
>
|
||||
☰
|
||||
</button>
|
||||
</li>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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"/> {t("Enabled")}</span>
|
||||
:
|
||||
<span><i className="fa fa-circle text-danger"/> {t("Disabled")}</span>
|
||||
return this.state.status === "enabled" ? (
|
||||
<span>
|
||||
<i className="fa fa-circle text-success" />
|
||||
|
||||
{t("Enabled")}
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<i className="fa fa-circle text-danger" />
|
||||
|
||||
{t("Disabled")}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default translate("common")(StatusBadge);
|
||||
export default translate("common")(StatusBadge);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -11,4 +11,4 @@
|
||||
export default {
|
||||
developmentMode: true,
|
||||
fakeAPI: false
|
||||
};
|
||||
};
|
||||
|
||||
+6
-9
@@ -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;
|
||||
|
||||
@@ -11,4 +11,4 @@
|
||||
export default {
|
||||
developmentMode: false,
|
||||
fakeAPI: false
|
||||
};
|
||||
};
|
||||
|
||||
+24
-15
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user