Merge branch 'development' into isdampe_ftl_style

This commit is contained in:
Mark Drobnak
2019-06-15 22:15:09 -04:00
committed by GitHub
19 changed files with 492 additions and 101 deletions
+6 -1
View File
@@ -8,7 +8,7 @@ version: 2
.test: &test
run:
name: "Test"
command: npm test
command: npm run coverage -- --maxWorkers=4
.store_version: &store_version
run:
@@ -50,6 +50,11 @@ jobs:
chmod +x ./FTL-client
./FTL-client "$FOLDER" pihole-web.tar.gz "$FTL_SECRET"
rm ./FTL-client
- run:
name: "Upload Code Coverage"
command: |
npm install codecov -g
codecov
- save_cache:
key: v1-build-{{ checksum "package-lock.json" }}
paths:
+3
View File
@@ -9,6 +9,9 @@ node_modules
build
src/scss/style.css
# Test Files
coverage
# Generated Files
src/languages.json
+61 -42
View File
@@ -1177,9 +1177,9 @@
}
},
"@coreui/coreui": {
"version": "2.1.11",
"resolved": "https://registry.npmjs.org/@coreui/coreui/-/coreui-2.1.11.tgz",
"integrity": "sha512-qGgRcNstB6bqW+D5UMUFQHPVUD4gfTmsPsZpiQYNLvAKWCszq2LhilTIWAAJHxs9yf2iTer0s30MjccTsqjXOA==",
"version": "2.1.12",
"resolved": "https://registry.npmjs.org/@coreui/coreui/-/coreui-2.1.12.tgz",
"integrity": "sha512-wO2olnHWyvzW6CHaiU4oRq/tomuuHItiifcyU40UBN00vACjBUH4kpxfhZGGjwM031GRZRL6LnAWzwZb9wg8Lw==",
"requires": {
"@coreui/coreui-plugin-npm-postinstall": "^1.0.2",
"bootstrap": "^4.3.1",
@@ -1300,9 +1300,9 @@
"dev": true
},
"@fortawesome/fontawesome-free": {
"version": "5.8.2",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.8.2.tgz",
"integrity": "sha512-E4fDUF4fbu9AxKpaQQqCN3XBnNzb/5e0Gvd9OaQsYkK574LVI57v/EqqPfIm/mC7jYbxaPNrhvT5AF+Yzwyizg=="
"version": "5.9.0",
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.9.0.tgz",
"integrity": "sha512-g795BBEzM/Hq2SYNPm/NQTIp3IWd4eXSH0ds87Na2jnrAUFX3wkyZAI4Gwj9DOaWMuz2/01i8oWI7P7T/XLkhg=="
},
"@hapi/address": {
"version": "2.0.0",
@@ -2610,9 +2610,9 @@
}
},
"@types/jest": {
"version": "24.0.13",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.13.tgz",
"integrity": "sha512-3m6RPnO35r7Dg+uMLj1+xfZaOgIHHHut61djNjzwExXN4/Pm9has9C6I1KMYSfz7mahDhWUOVg4HW/nZdv5Pww==",
"version": "24.0.14",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-24.0.14.tgz",
"integrity": "sha512-IxS2AO0nOr4zrpKfRCxobQUb1bSK6ejodZ7odCzHXMjsASCI8J10N8qVQhrCjvJTc3bUjGGeuD+ytKZqyhajqQ==",
"dev": true,
"requires": {
"@types/jest-diff": "*"
@@ -2648,10 +2648,19 @@
"@types/lodash": "*"
}
},
"@types/lodash.isequal": {
"version": "4.5.5",
"resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.5.tgz",
"integrity": "sha512-4IKbinG7MGP131wRfceK6W4E/Qt3qssEFLF30LnJbjYiSfHGGRU/Io8YxXrZX109ir+iDETC8hw8QsDijukUVg==",
"dev": true,
"requires": {
"@types/lodash": "*"
}
},
"@types/node": {
"version": "12.0.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.4.tgz",
"integrity": "sha512-j8YL2C0fXq7IONwl/Ud5Kt0PeXw22zGERt+HSSnwbKOJVsAGkEz3sFCYwaF9IOuoG1HOtE0vKCj6sXF7Q0+Vaw==",
"version": "12.0.8",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.0.8.tgz",
"integrity": "sha512-b8bbUOTwzIY3V5vDTY1fIJ+ePKDUBqt2hC2woVGotdQQhG/2Sh62HOKHrT7ab+VerXAcPyAiTEipPu/FsreUtg==",
"dev": true
},
"@types/prop-types": {
@@ -9990,13 +9999,6 @@
"integrity": "sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA==",
"requires": {
"react-is": "^16.7.0"
},
"dependencies": {
"react-is": {
"version": "16.8.6",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
"integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA=="
}
}
},
"hosted-git-info": {
@@ -10517,9 +10519,9 @@
"dev": true
},
"i18next": {
"version": "17.0.1",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-17.0.1.tgz",
"integrity": "sha512-pg+RxJWoqEt4JtueOkoxXshivtmsf8u0SuTYQTSB5cUTNWUyw/Py3zF//A/vd5FEUaiooyPCNNJ94leQ8J4JEQ==",
"version": "17.0.3",
"resolved": "https://registry.npmjs.org/i18next/-/i18next-17.0.3.tgz",
"integrity": "sha512-vQyW6a4ZLt3Dxnd6GXSnhbW5DwGYC4uLPKk1MFE5pfFbR9CEiNatdwwUZDQfrcNOh2x0eOGDFYeCEyLlkLvDQA==",
"requires": {
"@babel/runtime": "^7.3.1"
},
@@ -14892,8 +14894,7 @@
"lodash.isequal": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=",
"dev": true
"integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA="
},
"lodash.isfunction": {
"version": "3.0.9",
@@ -15250,6 +15251,31 @@
"integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==",
"dev": true
},
"mini-create-react-context": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz",
"integrity": "sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw==",
"requires": {
"@babel/runtime": "^7.4.0",
"gud": "^1.0.0",
"tiny-warning": "^1.0.2"
},
"dependencies": {
"@babel/runtime": {
"version": "7.4.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.4.5.tgz",
"integrity": "sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==",
"requires": {
"regenerator-runtime": "^0.13.2"
}
},
"regenerator-runtime": {
"version": "0.13.2",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz",
"integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA=="
}
}
},
"mini-css-extract-plugin": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.5.0.tgz",
@@ -17875,9 +17901,9 @@
"dev": true
},
"prettier": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.17.1.tgz",
"integrity": "sha512-TzGRNvuUSmPgwivDqkZ9tM/qTGW9hqDKWOE9YHiyQdixlKbv7kvEqsmDPrcHJTKwthU774TQwZXVtaQ/mMsvjg==",
"version": "1.18.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz",
"integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==",
"dev": true
},
"pretty-bytes": {
@@ -18719,39 +18745,32 @@
}
},
"react-router": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.0.0.tgz",
"integrity": "sha512-6EQDakGdLG/it2x9EaCt9ZpEEPxnd0OCLBHQ1AcITAAx7nCnyvnzf76jKWG1s2/oJ7SSviUgfWHofdYljFexsA==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-5.0.1.tgz",
"integrity": "sha512-EM7suCPNKb1NxcTZ2LEOWFtQBQRQXecLxVpdsP4DW4PbbqYWeRiLyV/Tt1SdCrvT2jcyXAXmVTmzvSzrPR63Bg==",
"requires": {
"@babel/runtime": "^7.1.2",
"create-react-context": "^0.2.2",
"history": "^4.9.0",
"hoist-non-react-statics": "^3.1.0",
"loose-envify": "^1.3.1",
"mini-create-react-context": "^0.3.0",
"path-to-regexp": "^1.7.0",
"prop-types": "^15.6.2",
"react-is": "^16.6.0",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
},
"dependencies": {
"react-is": {
"version": "16.8.6",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.8.6.tgz",
"integrity": "sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA=="
}
}
},
"react-router-dom": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.0.0.tgz",
"integrity": "sha512-wSpja5g9kh5dIteZT3tUoggjnsa+TPFHSMrpHXMpFsaHhQkm/JNVGh2jiF9Dkh4+duj4MKCkwO6H08u6inZYgQ==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.0.1.tgz",
"integrity": "sha512-zaVHSy7NN0G91/Bz9GD4owex5+eop+KvgbxXsP/O+iW1/Ln+BrJ8QiIR5a6xNPtrdTvLkxqlDClx13QO1uB8CA==",
"requires": {
"@babel/runtime": "^7.1.2",
"history": "^4.9.0",
"loose-envify": "^1.3.1",
"prop-types": "^15.6.2",
"react-router": "5.0.0",
"react-router": "5.0.1",
"tiny-invariant": "^1.0.2",
"tiny-warning": "^1.0.0"
}
+10 -7
View File
@@ -8,9 +8,10 @@
"@types/enzyme": "^3.9.3",
"@types/enzyme-adapter-react-16": "^1.0.5",
"@types/fetch-mock": "^7.3.0",
"@types/jest": "^24.0.13",
"@types/jest": "^24.0.14",
"@types/lodash.debounce": "^4.0.6",
"@types/node": "^12.0.4",
"@types/lodash.isequal": "^4.5.5",
"@types/node": "^12.0.8",
"@types/react": "^16.8.19",
"@types/react-bootstrap-daterangepicker": "0.0.26",
"@types/react-bootstrap-typeahead": "^3.4.5",
@@ -30,24 +31,25 @@
"node-fetch": "^2.6.0",
"node-sass-chokidar": "^1.3.5",
"npm-run-all": "^4.1.5",
"prettier": "^1.17.1",
"prettier": "^1.18.2",
"react-scripts": "^3.0.1",
"react-test-renderer": "^16.8.6",
"typescript": "^3.5.1"
},
"dependencies": {
"@coreui/coreui": "^2.1.11",
"@coreui/coreui": "^2.1.12",
"@coreui/icons": "0.3.0",
"@coreui/react": "^2.5.1",
"@fortawesome/fontawesome-free": "^5.8.2",
"@fortawesome/fontawesome-free": "^5.9.0",
"bootstrap": "^4.3.1",
"bootstrap-daterangepicker": "^3.0.5",
"chart.js": "^2.8.0",
"i18next": "^17.0.1",
"i18next": "^17.0.3",
"i18next-browser-languagedetector": "^3.0.1",
"i18next-xhr-backend": "^3.0.0",
"ionicons": "^4.5.8",
"lodash.debounce": "^4.0.8",
"lodash.isequal": "^4.5.0",
"moment": "^2.24.0",
"prop-types": "^15.7.2",
"react": "^16.8.6",
@@ -56,7 +58,7 @@
"react-chartjs-2": "^2.7.6",
"react-dom": "^16.8.6",
"react-i18next": "^10.11.0",
"react-router-dom": "^5.0.0",
"react-router-dom": "^5.0.1",
"react-table": "^6.10.0",
"reactstrap": "^8.0.0",
"sha.js": "^2.4.11",
@@ -74,6 +76,7 @@
"generate-language-list": "node scripts/generate-language-list.js",
"make-fake-data": "node scripts/make-fake-data.js",
"test": "react-scripts test --env=jsdom",
"coverage": "react-scripts test --env=jsdom --coverage",
"eject": "react-scripts eject",
"format": "prettier --write \"src/**/*.tsx\"",
"check-format": "prettier --list-different \"src/**/*.tsx\""
+1 -1
View File
@@ -40,7 +40,7 @@
<!-- End Single Page Apps for GitHub Pages -->
</head>
<body class="app header-fixed sidebar-fixed sidebar-show sidebar-lg-show boxcontainer background-image">
<body class="app header-fixed sidebar-fixed sidebar-lg-show boxcontainer background-image">
<noscript>
<p align="center">Please enable JavaScript! This web interface will not work without it.</p>
</noscript>
+11 -11
View File
@@ -12,19 +12,19 @@ import React from "react";
import { WithTranslation, withTranslation } from "react-i18next";
import { Link } from "react-router-dom";
const FooterUpdateStatus = (props: WithTranslation) => {
const { t } = props;
const updateAvailable = false;
if (updateAvailable) {
return (
<div className="ml-auto">
<Link to="/settings/versions">{t("Update Available")}</Link>
</div>
);
} else {
const FooterUpdateStatus = ({
updateAvailable = false,
t
}: { updateAvailable: boolean } & WithTranslation) => {
if (!updateAvailable) {
return null;
}
return (
<div className="ml-auto">
<Link to="/settings/versions">{t("Update Available")}</Link>
</div>
);
};
export default withTranslation("footer")(FooterUpdateStatus);
+2 -6
View File
@@ -15,14 +15,10 @@ import React, { MouseEventHandler } from "react";
export interface NavButtonProps {
name: string;
icon: string;
onClick: MouseEventHandler;
onClick?: MouseEventHandler;
}
export default ({
name,
icon,
onClick = e => e.preventDefault()
}: NavButtonProps) => (
export default ({ name, icon, onClick = () => {} }: NavButtonProps) => (
<li className="nav-item">
<a
href="#"
@@ -84,8 +84,7 @@ describe("EnableDisable", () => {
wrapper
.find(NavButton)
.at(buttonIndex)
.props()
.onClick({} as MouseEvent);
.props().onClick!({} as MouseEvent);
// Wait for the setStatus promise to resolve
await tick();
@@ -129,10 +128,7 @@ describe("EnableDisable", () => {
wrapper.setState({ processing: true });
wrapper
.find(NavButton)
.props()
.onClick({} as MouseEvent);
wrapper.find(NavButton).props().onClick!({} as MouseEvent);
expect(setStatus).not.toHaveBeenCalled();
});
@@ -151,10 +147,7 @@ describe("EnableDisable", () => {
/>
);
wrapper
.find(NavButton)
.props()
.onClick({} as MouseEvent);
wrapper.find(NavButton).props().onClick!({} as MouseEvent);
wrapper.unmount();
@@ -175,10 +168,7 @@ describe("EnableDisable", () => {
/>
);
wrapper
.find(NavButton)
.props()
.onClick({} as MouseEvent);
wrapper.find(NavButton).props().onClick!({} as MouseEvent);
expect(wrapper.state().processing).toBeTruthy();
@@ -226,8 +216,7 @@ describe("EnableDisable", () => {
wrapper
.find(NavButton)
.last()
.props()
.onClick({} as MouseEvent);
.props().onClick!({} as MouseEvent);
expect(wrapper.find(Modal).props().isOpen).toBeTruthy();
});
@@ -0,0 +1,26 @@
/* Pi-hole: A black hole for Internet advertisements
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* Web Interface
* FooterUpdateStatus component test
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import { shallow } from "enzyme";
import FooterUpdateStatus from "../FooterUpdateStatus";
import * as React from "react";
import { Link } from "react-router-dom";
it("renders as null if no update is available", () => {
const wrapper = shallow(<FooterUpdateStatus updateAvailable={false} />);
expect(wrapper).toBeEmptyRender();
});
it("renders a link to the versions page if there is an update", () => {
const wrapper = shallow(<FooterUpdateStatus updateAvailable={true} />);
expect(wrapper.find(Link).props().to).toEqual("/settings/versions");
});
@@ -0,0 +1,69 @@
/* Pi-hole: A black hole for Internet advertisements
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* Web Interface
* Header component test
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import React, { MouseEvent } from "react";
import api from "../../../util/api";
import { shallow } from "enzyme";
import Header from "../Header";
import { TimeRangeSelectorContainer } from "../../dashboard/TimeRangeSelector";
it("shows the time range selector on the dashboard", () => {
api.loggedIn = true;
history.pushState({}, "", "/dashboard");
const wrapper = shallow(<Header />);
expect(wrapper.find(TimeRangeSelectorContainer)).toExist();
});
it("does not show the time range selector when not logged in", () => {
api.loggedIn = false;
history.pushState({}, "", "/dashboard");
const wrapper = shallow(<Header />);
expect(wrapper.find(TimeRangeSelectorContainer)).not.toExist();
});
it("does not show the time range selector on non-dashboard pages", () => {
api.loggedIn = true;
history.pushState({}, "", "/whitelist");
const wrapper = shallow(<Header />);
expect(wrapper.find(TimeRangeSelectorContainer)).not.toExist();
});
it("toggle the sidebar when clicked", () => {
const wrapper = shallow(<Header />);
expect(document.body.classList).not.toContain("sidebar-minimized");
expect(document.body.classList).not.toContain("brand-minimized");
wrapper
.find(".sidebar-toggler")
.simulate("click", { preventDefault: () => {} });
expect(document.body.classList).toContain("sidebar-minimized");
expect(document.body.classList).toContain("brand-minimized");
});
it("toggle the sidebar on mobile when clicked", () => {
const wrapper = shallow(<Header />);
expect(document.body.classList).not.toContain("sidebar-show");
wrapper
.find(".navbar-toggler")
.first()
.simulate("click", { preventDefault: () => {} });
expect(document.body.classList).toContain("sidebar-show");
});
@@ -0,0 +1,60 @@
/* Pi-hole: A black hole for Internet advertisements
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* Web Interface
* LanguageApplier component test
*
* This file is copyright under the latest version of the EUPL.
* Please see LICENSE file for your rights under this license. */
import { mount } from "enzyme";
import * as React from "react";
import LanguageApplier from "../LanguageApplier";
import {
PreferencesContext,
PreferencesContextType
} from "../context/PreferencesContext";
import i18n from "i18next";
it("does not update i18next if the language is already set correctly", () => {
const settings: PreferencesContextType = {
settings: {
language: "en",
layout: "boxed"
},
refresh: () => {}
};
i18n.language = "en";
i18n.changeLanguage = jest.fn();
mount(
<PreferencesContext.Provider value={settings}>
<LanguageApplier />
</PreferencesContext.Provider>
);
expect(i18n.changeLanguage).not.toHaveBeenCalled();
});
it("updates i18next with the language if it was using a different one", () => {
const settings: PreferencesContextType = {
settings: {
language: "es",
layout: "boxed"
},
refresh: () => {}
};
i18n.language = "en";
i18n.changeLanguage = jest.fn();
mount(
<PreferencesContext.Provider value={settings}>
<LanguageApplier />
</PreferencesContext.Provider>
);
expect(i18n.changeLanguage).toHaveBeenCalledWith("es");
});
@@ -0,0 +1,63 @@
/* Pi-hole: A black hole for Internet advertisements
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* Web Interface
* LayoutApplier component test
*
* 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 { mount } from "enzyme";
import LayoutApplier from "../LayoutApplier";
import {
PreferencesContext,
PreferencesContextType
} from "../context/PreferencesContext";
it("adds box layout CSS when the layout is box", () => {
const settings: PreferencesContextType = {
settings: {
language: "en",
layout: "boxed"
},
refresh: () => {}
};
expect(document.body.classList).not.toContain("boxcontainer");
expect(document.body.classList).not.toContain("background-image");
mount(
<PreferencesContext.Provider value={settings}>
<LayoutApplier />
</PreferencesContext.Provider>
);
expect(document.body.classList).toContain("boxcontainer");
expect(document.body.classList).toContain("background-image");
});
it("removes box layout CSS when the layout is traditional", () => {
const settings: PreferencesContextType = {
settings: {
language: "en",
layout: "traditional"
},
refresh: () => {}
};
document.body.classList.add("boxcontainer", "background-image");
expect(document.body.classList).toContain("boxcontainer");
expect(document.body.classList).toContain("background-image");
mount(
<PreferencesContext.Provider value={settings}>
<LayoutApplier />
</PreferencesContext.Provider>
);
expect(document.body.classList).not.toContain("boxcontainer");
expect(document.body.classList).not.toContain("background-image");
});
@@ -0,0 +1,43 @@
/* Pi-hole: A black hole for Internet advertisements
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* Web Interface
* NavButton component test
*
* 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 NavButton from "../NavButton";
it("renders with the given icon and name", () => {
const name = "testName";
const icon = "testIcon";
const wrapper = shallow(<NavButton name={name} icon={icon} />);
expect(wrapper).toHaveText(name);
expect(wrapper.find("i")).toHaveClassName(icon);
});
it("calls onClick when clicked", () => {
const onClick = jest.fn();
const event = { preventDefault: jest.fn(), test: true };
const wrapper = shallow(
<NavButton name="name" icon="icon" onClick={onClick} />
);
wrapper.find("a").simulate("click", event);
expect(onClick).toHaveBeenCalledWith(event);
});
it("has a default onClick", () => {
const event = { preventDefault: jest.fn() };
const wrapper = shallow(<NavButton name="name" icon="icon" />);
wrapper.find("a").simulate("click", event);
expect(event.preventDefault).toHaveBeenCalled();
});
@@ -0,0 +1,57 @@
/* Pi-hole: A black hole for Internet advertisements
* (c) 2019 Pi-hole, LLC (https://pi-hole.net)
* Network-wide ad blocking via your own hardware.
*
* Web Interface
* NavDropdown component test
*
* 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 NavDropdown from "../NavDropdown";
it("renders as open when isOpen is true", () => {
const wrapper = shallow(
<NavDropdown name="test" icon="test" isOpen={true}>
{}
</NavDropdown>
);
expect(wrapper.find("li")).toHaveClassName("open");
});
it("renders as closed when isOpen is false", () => {
const wrapper = shallow(
<NavDropdown name="test" icon="test" isOpen={false}>
{}
</NavDropdown>
);
expect(wrapper.find("li")).not.toHaveClassName("open");
});
it("toggles the dropdown when clicked", () => {
const toggle = jest.fn();
const wrapper = shallow(
<NavDropdown name="test" icon="test" isOpen={true}>
{}
</NavDropdown>
);
expect(wrapper.find("li")).toHaveClassName("open");
wrapper.find("button").simulate("click", {
preventDefault: () => {},
target: {
parentElement: {
classList: {
toggle
}
}
}
});
expect(toggle).toHaveBeenCalledWith("open");
});
@@ -14,6 +14,7 @@ import Sidebar, { NavList, PiholeNavDropdown, PiholeNavItem } from "../Sidebar";
import api from "../../../util/api";
import { RouteCustomItem, RouteGroup } from "../../../routes";
import NavDropdown from "../NavDropdown";
import { NavLink } from "react-router-dom";
const t = global.t;
@@ -192,3 +193,25 @@ it("renders the NavList in the sidebar", () => {
expect(props.items).toEqual([item]);
expect(props.location).toEqual(location);
});
it("should hide the sidebar on mobile when an item is clicked", () => {
const item = {
url: "/testUrl",
icon: "test-icon",
name: "testName",
auth: false,
component: () => <div />
};
const wrapper = shallow(<PiholeNavItem item={item} t={global.t} />);
document.body.classList.add("sidebar-show");
expect(document.body.classList).toContain("sidebar-show");
// @ts-ignore
wrapper
.find(NavLink)
.props()
.onClick();
expect(document.body.classList).not.toContain("sidebar-show");
});
@@ -9,8 +9,9 @@
* Please see LICENSE file for your rights under this license. */
import React from "react";
import { shallow } from "enzyme";
import { TranslatedStatusBadge } from "../StatusBadge";
import { mount, shallow } from "enzyme";
import StatusBadge, { TranslatedStatusBadge } from "../StatusBadge";
import { StatusContext, StatusContextType } from "../context/StatusContext";
it("shows green enabled message if API returns enabled", async () => {
const wrapper = shallow(<TranslatedStatusBadge status="enabled" />);
@@ -25,3 +26,19 @@ it("shows red disabled message if API doesn't return enabled", async () => {
expect(wrapper.childAt(2)).toHaveText("Disabled");
expect(wrapper.childAt(0)).toHaveClassName("text-danger");
});
it("uses context to get status", () => {
const context: StatusContextType = {
status: "enabled",
refresh: () => {}
};
const wrapper = mount(
<StatusContext.Provider value={context}>
<StatusBadge />
</StatusContext.Provider>
);
expect(wrapper.find(TranslatedStatusBadge).props().status).toEqual(
context.status
);
});
+20 -11
View File
@@ -19,6 +19,7 @@ import i18n from "i18next";
import i18next from "i18next";
import { WithTranslation, withTranslation } from "react-i18next";
import debounce from "lodash.debounce";
import isEqual from "lodash.isequal";
import moment from "moment";
import { padNumber } from "../../util/graphUtils";
import api from "../../util/api";
@@ -201,17 +202,15 @@ class QueryLog extends Component<WithTranslation, QueryLogState> {
* @param pageSize The number of queries in the page
*/
fetchQueries = ({ page, pageSize }: { page: number; pageSize: number }) => {
// Check if we've reached the end of the queries, or are still waiting for
// the last fetch to finish
if (this.state.atEnd || this.state.loading) {
return;
}
// Check if the filters are the same and we already have this page and the
// next page.
// Don't fetch the queries if:
// - We've reached the end of the queries
// - We are still waiting for the last fetch to finish
// - Filters are the same and we already have this page and the next
if (
!this.state.filtersChanged &&
this.state.history.length >= (page + 2) * pageSize
this.state.atEnd ||
this.state.loading ||
(!this.state.filtersChanged &&
this.state.history.length >= (page + 2) * pageSize)
) {
return;
}
@@ -255,7 +254,7 @@ class QueryLog extends Component<WithTranslation, QueryLogState> {
data={this.state.history}
loading={this.state.loading}
onFetchData={state => {
if (state.filtered === this.state.filters) {
if (isEqual(state.filtered, this.state.filters)) {
// If the filters have not changed, do not debounce the fetch.
// This allows fetching the next page to happen without waiting for
// the debounce.
@@ -267,6 +266,10 @@ class QueryLog extends Component<WithTranslation, QueryLogState> {
}
}}
onFilteredChange={debounce(filters => {
if (isEqual(filters, this.state.filters)) {
return;
}
this.setState({
filters,
filtersChanged: true,
@@ -276,6 +279,12 @@ class QueryLog extends Component<WithTranslation, QueryLogState> {
history: []
});
}, 300)}
defaultFiltered={[
{
id: "time",
value: getDefaultRange(t)
}
]}
getTrProps={this.getRowProps}
ofText={this.state.atEnd ? "of" : "of at least"}
// Pad empty rows to have the same height as filled rows
+6
View File
@@ -219,3 +219,9 @@ body {
display: none;
}
}
@media (max-width: 576px) {
.modal-dialog {
margin: 0.5rem auto;
}
}
+7 -4
View File
@@ -12,6 +12,9 @@ import { padNumber, getIntervalForRange } from "../graphUtils";
import { TimeRange } from "../../components/common/context/TimeRangeContext";
import moment from "moment";
// A timestamp used for testing
const timestamp = 1559614028;
describe("padNumber", () => {
it("pads 0 to 00", () => {
expect(padNumber(0)).toEqual("00");
@@ -30,8 +33,8 @@ describe("getIntervalForRange", () => {
it("returns 10 minutes for 24 hours", () => {
const range: TimeRange = {
name: "24 Hours",
from: moment().subtract(1, "day"),
until: moment()
from: moment(timestamp).subtract(1, "day"),
until: moment(timestamp)
};
expect(getIntervalForRange(range)).toEqual(10 * 60);
@@ -42,8 +45,8 @@ describe("getIntervalForRange", () => {
name: "144 days",
// Use seconds instead of days to ensure the difference in epoch time is
// equal to 144 days
from: moment().subtract(144 * 24 * 60 * 60, "seconds"),
until: moment()
from: moment(timestamp).subtract(144 * 24 * 60 * 60, "seconds"),
until: moment(timestamp)
};
expect(getIntervalForRange(range)).toEqual(24 * 60 * 60);