mirror of
https://github.com/gosticks/web.git
synced 2026-10-05 22:26:57 +00:00
Convert dashboard components to TypeScript
Also fixed the format check to use TypeScript files. Signed-off-by: Mcat12 <newtoncat12@yahoo.com>
This commit is contained in:
Generated
+6
@@ -1378,6 +1378,12 @@
|
||||
"loader-utils": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"@types/chart.js": {
|
||||
"version": "2.7.42",
|
||||
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.7.42.tgz",
|
||||
"integrity": "sha512-+0v+PV2J9wsV7u36Vqt5Oke7ugtiZqNeYJgNk5mgNMkEkqtjxo2Q9N5Q9znHlgmXeOTfQL6e1sfcAbTnPHAzlA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/cheerio": {
|
||||
"version": "0.22.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.10.tgz",
|
||||
|
||||
+3
-2
@@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"homepage": "/admin",
|
||||
"devDependencies": {
|
||||
"@types/chart.js": "^2.7.42",
|
||||
"@types/enzyme": "^3.1.15",
|
||||
"@types/enzyme-adapter-react-16": "^1.0.3",
|
||||
"@types/fetch-mock": "^7.2.1",
|
||||
@@ -76,8 +77,8 @@
|
||||
"update-translations": "node update-translations.js",
|
||||
"test": "react-scripts test --env=jsdom",
|
||||
"eject": "react-scripts eject",
|
||||
"format": "prettier --write \"src/**/*.js\"",
|
||||
"check-format": "prettier --list-different \"src/**/*.js\""
|
||||
"format": "prettier --write \"src/**/*.tsx\"",
|
||||
"check-format": "prettier --list-different \"src/**/*.tsx\""
|
||||
},
|
||||
"browserslist": [
|
||||
">0.2%",
|
||||
|
||||
@@ -13,9 +13,9 @@ import React, { FunctionComponent } from "react";
|
||||
export type AlertType = "info" | "success" | "danger";
|
||||
|
||||
export interface AlertProps {
|
||||
type: AlertType,
|
||||
onClick: () => void,
|
||||
message: string
|
||||
type: AlertType;
|
||||
onClick: () => void;
|
||||
message: string;
|
||||
}
|
||||
|
||||
const Alert: FunctionComponent<AlertProps> = (props: AlertProps) => {
|
||||
|
||||
@@ -17,7 +17,12 @@ import { mobileSidebarHide } from "./Header";
|
||||
import api from "../../util/api";
|
||||
import StatusBadge from "./StatusBadge";
|
||||
import NavDropdown from "./NavDropdown";
|
||||
import { RouteCustomItem, RouteData, RouteGroup, RouteItem } from "../../routes";
|
||||
import {
|
||||
RouteCustomItem,
|
||||
RouteData,
|
||||
RouteGroup,
|
||||
RouteItem
|
||||
} from "../../routes";
|
||||
import i18next from "i18next";
|
||||
|
||||
export const isDropdownOpen = (
|
||||
@@ -88,11 +93,7 @@ export interface SidebarProps extends WithNamespaces {
|
||||
location: Location;
|
||||
}
|
||||
|
||||
const Sidebar = ({
|
||||
items,
|
||||
t,
|
||||
location
|
||||
}: SidebarProps) => {
|
||||
const Sidebar = ({ items, t, location }: SidebarProps) => {
|
||||
return (
|
||||
<div className="sidebar">
|
||||
<nav className="sidebar-nav">
|
||||
|
||||
@@ -35,7 +35,7 @@ it("creates nav items with correct data", () => {
|
||||
icon: "test-icon",
|
||||
name: "testName",
|
||||
auth: false,
|
||||
component: () => <div/>
|
||||
component: () => <div />
|
||||
};
|
||||
const key = "testKey";
|
||||
const wrapper = shallow(
|
||||
|
||||
+24
-18
@@ -8,11 +8,21 @@
|
||||
* 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, RefObject } from "react";
|
||||
import { ChartTooltipOptions } from "chart.js";
|
||||
import { Line } from "react-chartjs-2";
|
||||
|
||||
class ChartTooltip extends Component {
|
||||
state = {
|
||||
export interface ChartTooltipProps {
|
||||
chart: RefObject<Line>;
|
||||
handler: ChartTooltipOptions;
|
||||
}
|
||||
|
||||
export interface ChartTooltipState {
|
||||
tooltip: any | null;
|
||||
}
|
||||
|
||||
class ChartTooltip extends Component<ChartTooltipProps, ChartTooltipState> {
|
||||
state: ChartTooltipState = {
|
||||
tooltip: null
|
||||
};
|
||||
|
||||
@@ -43,12 +53,15 @@ class ChartTooltip extends Component {
|
||||
return null;
|
||||
|
||||
// Get the graph's position data so we can offset the tooltip
|
||||
const position = this.props.chart.current.chartInstance.canvas.getBoundingClientRect();
|
||||
const position = this.props.chart.current.chartInstance.canvas!.getBoundingClientRect();
|
||||
let width = tooltip.caretX;
|
||||
|
||||
// Prevent compression of the tooltip at the right edge of the screen
|
||||
if (document.offsetWidth - tooltip.caretX < 400)
|
||||
// @ts-ignore
|
||||
if (document.offsetWidth - tooltip.caretX < 400) {
|
||||
// @ts-ignore
|
||||
width = document.offsetWidth - 400;
|
||||
}
|
||||
|
||||
// Prevent tooltip disappearing behind the sidebar
|
||||
if (tooltip.caretX < 100) width = 100;
|
||||
@@ -68,13 +81,13 @@ class ChartTooltip extends Component {
|
||||
let data = [];
|
||||
if (tooltip.body) {
|
||||
data = tooltip.body
|
||||
.map(body => body.lines)
|
||||
.map((item, i) => ({
|
||||
.map((body: any) => body.lines)
|
||||
.map((item: any, i: number) => ({
|
||||
data: item[0],
|
||||
colors: tooltip.labelColors[i]
|
||||
}));
|
||||
}
|
||||
data.sort((a, b) =>
|
||||
data.sort((a: any, b: any) =>
|
||||
a.data.split(": ")[0].localeCompare(b.data.split(": ")[0])
|
||||
);
|
||||
|
||||
@@ -82,14 +95,14 @@ class ChartTooltip extends Component {
|
||||
<div className="chartjs-tooltip" style={style}>
|
||||
<table>
|
||||
<thead>
|
||||
{tooltip.title.map((title, i) => (
|
||||
{tooltip.title.map((title: string, i: number) => (
|
||||
<tr key={i}>
|
||||
<th>{title}</th>
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.map((item, i) => (
|
||||
{data.map((item: any, i: number) => (
|
||||
<tr key={i}>
|
||||
<td>
|
||||
<span
|
||||
@@ -111,11 +124,4 @@ class ChartTooltip extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
ChartTooltip.propTypes = {
|
||||
chart: PropTypes.object.isRequired,
|
||||
handler: PropTypes.shape({
|
||||
custom: PropTypes.func.isRequired
|
||||
}).isRequired
|
||||
};
|
||||
|
||||
export default ChartTooltip;
|
||||
+30
-24
@@ -8,24 +8,26 @@
|
||||
* 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 React, { Component, RefObject } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import PropTypes from "prop-types";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import { withNamespaces } from "react-i18next";
|
||||
import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import { padNumber } from "../../util";
|
||||
import api from "../../util/api";
|
||||
import api, { ApiClientsGraph } from "../../util/api";
|
||||
import ChartTooltip from "./ChartTooltip";
|
||||
import { WithAPIData } from "../common/WithAPIData";
|
||||
import { ChartDataSets, ChartOptions } from "chart.js";
|
||||
|
||||
class ClientsGraph extends Component {
|
||||
static propTypes = {
|
||||
loading: PropTypes.bool.isRequired,
|
||||
labels: PropTypes.array.isRequired,
|
||||
datasets: PropTypes.array.isRequired
|
||||
};
|
||||
export interface ClientsGraphProps {
|
||||
loading: boolean;
|
||||
labels: Array<string>;
|
||||
datasets: Array<ChartDataSets>;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
class ClientsGraph extends Component<ClientsGraphProps & WithNamespaces, {}> {
|
||||
private readonly graphRef: RefObject<Line>;
|
||||
|
||||
constructor(props: ClientsGraphProps & WithNamespaces) {
|
||||
super(props);
|
||||
this.graphRef = React.createRef();
|
||||
}
|
||||
@@ -33,19 +35,20 @@ class ClientsGraph extends Component {
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
const options = {
|
||||
const options: ChartOptions = {
|
||||
tooltips: {
|
||||
enabled: false,
|
||||
mode: "x-axis",
|
||||
custom: () => "placeholder",
|
||||
itemSort: function(a, b) {
|
||||
itemSort: (a, b) => {
|
||||
// @ts-ignore
|
||||
return b.yLabel - a.yLabel;
|
||||
},
|
||||
callbacks: {
|
||||
title: tooltipItem => {
|
||||
const time = tooltipItem[0].xLabel.match(/(\d?\d):?(\d?\d?)/);
|
||||
const hour = parseInt(time[1], 10);
|
||||
const minute = parseInt(time[2], 10) || 0;
|
||||
const time = tooltipItem[0].xLabel!.match(/(\d?\d):?(\d?\d?)/);
|
||||
const hour = parseInt(time![1], 10);
|
||||
const minute = parseInt(time![2], 10) || 0;
|
||||
const from = padNumber(hour) + ":" + padNumber(minute - 5) + ":00";
|
||||
const to = padNumber(hour) + ":" + padNumber(minute + 4) + ":59";
|
||||
|
||||
@@ -53,7 +56,7 @@ class ClientsGraph extends Component {
|
||||
},
|
||||
label: (tooltipItems, data) => {
|
||||
return (
|
||||
data.datasets[tooltipItems.datasetIndex].label +
|
||||
data.datasets![tooltipItems.datasetIndex!].label +
|
||||
": " +
|
||||
tooltipItems.yLabel
|
||||
);
|
||||
@@ -117,7 +120,7 @@ class ClientsGraph extends Component {
|
||||
|
||||
{// Now you're thinking with portals!
|
||||
ReactDOM.createPortal(
|
||||
<ChartTooltip chart={this.graphRef} handler={options.tooltips} />,
|
||||
<ChartTooltip chart={this.graphRef} handler={options.tooltips!} />,
|
||||
document.body
|
||||
)}
|
||||
</div>
|
||||
@@ -132,7 +135,7 @@ class ClientsGraph extends Component {
|
||||
* @returns {{labels: Date[], datasets: Array, loading: boolean}} ClientsGraph
|
||||
* props
|
||||
*/
|
||||
export const transformData = data => {
|
||||
export const transformData = (data: ApiClientsGraph) => {
|
||||
// Remove last data point as it's not yet finished
|
||||
const overTime = data.over_time.slice(0, -1);
|
||||
|
||||
@@ -146,7 +149,7 @@ export const transformData = data => {
|
||||
"#b0bec5"
|
||||
];
|
||||
const labels = overTime.map(step => new Date(1000 * step.timestamp));
|
||||
const datasets = [];
|
||||
const datasets: Array<ChartDataSets> = [];
|
||||
|
||||
// Fill in dataset metadata
|
||||
let i = 0;
|
||||
@@ -175,11 +178,13 @@ export const transformData = data => {
|
||||
for (let step of overTime) {
|
||||
for (let destination in datasets) {
|
||||
if (datasets.hasOwnProperty(destination))
|
||||
datasets[destination].data.push(step.data[destination]);
|
||||
(datasets[destination].data as Array<number>).push(
|
||||
step.data[destination]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
datasets.sort((a, b) => a.label.localeCompare(b.label));
|
||||
datasets.sort((a, b) => a.label!.localeCompare(b.label!));
|
||||
|
||||
return { labels, datasets, loading: false };
|
||||
};
|
||||
@@ -195,11 +200,12 @@ export const loadingProps = {
|
||||
|
||||
export const TranslatedClientsGraph = withNamespaces("dashboard")(ClientsGraph);
|
||||
|
||||
export default props => (
|
||||
export default (props: any) => (
|
||||
<WithAPIData
|
||||
apiCall={api.getClientsGraph}
|
||||
repeatOptions={{
|
||||
interval: 10 * 60 * 1000
|
||||
interval: 10 * 60 * 1000,
|
||||
ignoreCancel: true
|
||||
}}
|
||||
renderInitial={() => (
|
||||
<TranslatedClientsGraph {...loadingProps} {...props} />
|
||||
+63
-39
@@ -8,28 +8,33 @@
|
||||
* 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 React, { Component, MouseEvent, RefObject } from "react";
|
||||
import { Doughnut } from "react-chartjs-2";
|
||||
import PropTypes from "prop-types";
|
||||
import { WithAPIData } from "../common/WithAPIData";
|
||||
import { ChartOptions } from "chart.js";
|
||||
|
||||
export class GenericDoughnutChart extends Component {
|
||||
static propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
loading: PropTypes.bool.isRequired,
|
||||
data: PropTypes.array.isRequired,
|
||||
colors: PropTypes.array.isRequired,
|
||||
labels: PropTypes.array.isRequired
|
||||
};
|
||||
export interface GenericDoughnutChartProps {
|
||||
title: string;
|
||||
loading: boolean;
|
||||
data: Array<number>;
|
||||
colors: Array<string>;
|
||||
labels: Array<string>;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
export class GenericDoughnutChart extends Component<
|
||||
GenericDoughnutChartProps,
|
||||
{}
|
||||
> {
|
||||
private readonly chartRef: RefObject<Doughnut>;
|
||||
|
||||
constructor(props: GenericDoughnutChartProps) {
|
||||
super(props);
|
||||
this.chartRef = React.createRef();
|
||||
}
|
||||
|
||||
handleClick = (e, index) => {
|
||||
handleClick = (e: MouseEvent, index: number) => {
|
||||
// Hide the entry by clicking on the internal legend item
|
||||
const chart = this.chartRef.current.chartInstance;
|
||||
const chart: any = this.chartRef.current!.chartInstance;
|
||||
chart.legend.options.onClick.call(
|
||||
chart,
|
||||
e,
|
||||
@@ -41,17 +46,20 @@ export class GenericDoughnutChart extends Component {
|
||||
};
|
||||
|
||||
render() {
|
||||
const options = {
|
||||
const options: ChartOptions = {
|
||||
legend: { display: false },
|
||||
tooltips: {
|
||||
enabled: true,
|
||||
callbacks: {
|
||||
title: () => this.props.title,
|
||||
label: (tooltipItems, data) => {
|
||||
const dataset = data.datasets[tooltipItems.datasetIndex];
|
||||
const label = data.labels[tooltipItems.index];
|
||||
const dataset = data.datasets![tooltipItems.datasetIndex!];
|
||||
const label = data.labels![tooltipItems.index!];
|
||||
return (
|
||||
label + ": " + dataset.data[tooltipItems.index].toFixed(1) + "%"
|
||||
label +
|
||||
": " +
|
||||
(dataset.data![tooltipItems.index!] as number).toFixed(1) +
|
||||
"%"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -132,14 +140,19 @@ export class GenericDoughnutChart extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export interface ChartItem {
|
||||
name: string;
|
||||
ip?: string;
|
||||
percent: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the API data into props for GenericDoughnutChart
|
||||
*
|
||||
* @param apiData the API data
|
||||
* @returns {{loading: boolean, data: Array, colors: Array, labels: Array}}
|
||||
* GenericDoughnutChart props
|
||||
* @returns GenericDoughnutChartProps
|
||||
*/
|
||||
export const transformData = apiData => {
|
||||
export const transformData = (apiData: Array<ChartItem>) => {
|
||||
const colors = [
|
||||
"#20a8d8",
|
||||
"#f86c6b",
|
||||
@@ -157,7 +170,7 @@ export const transformData = apiData => {
|
||||
let i = 0;
|
||||
for (let entry of apiData) {
|
||||
data.push(entry.percent);
|
||||
labels.push(entry.name.length !== 0 ? entry.name : entry.ip);
|
||||
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
|
||||
@@ -189,21 +202,32 @@ export const loadingProps = {
|
||||
labels: []
|
||||
};
|
||||
|
||||
export default ({ apiCall, title, apiHandler, ...props }) => (
|
||||
<WithAPIData
|
||||
apiCall={apiCall}
|
||||
renderInitial={() => (
|
||||
<GenericDoughnutChart title={title} {...loadingProps} {...props} />
|
||||
)}
|
||||
renderOk={data => (
|
||||
<GenericDoughnutChart
|
||||
title={title}
|
||||
{...transformData(apiHandler(data))}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
renderErr={() => (
|
||||
<GenericDoughnutChart title={title} {...loadingProps} {...props} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
export default function<T>({
|
||||
apiCall,
|
||||
title,
|
||||
apiHandler,
|
||||
...props
|
||||
}: {
|
||||
apiCall: () => Promise<T>;
|
||||
title: string;
|
||||
apiHandler: (data: T) => Array<ChartItem>;
|
||||
}) {
|
||||
return (
|
||||
<WithAPIData
|
||||
apiCall={apiCall}
|
||||
renderInitial={() => (
|
||||
<GenericDoughnutChart title={title} {...loadingProps} {...props} />
|
||||
)}
|
||||
renderOk={data => (
|
||||
<GenericDoughnutChart
|
||||
title={title}
|
||||
{...transformData(apiHandler(data))}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
renderErr={() => (
|
||||
<GenericDoughnutChart title={title} {...loadingProps} {...props} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+31
-29
@@ -9,25 +9,26 @@
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import React, { Component } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import { withNamespaces } from "react-i18next";
|
||||
import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import { padNumber } from "../../util";
|
||||
import api from "../../util/api";
|
||||
import api, { ApiHistoryGraphItem } from "../../util/api";
|
||||
import { WithAPIData } from "../common/WithAPIData";
|
||||
import { ChartData, ChartOptions } from "chart.js";
|
||||
import { Line } from "react-chartjs-2";
|
||||
|
||||
class QueriesGraph extends Component {
|
||||
static propTypes = {
|
||||
loading: PropTypes.bool.isRequired,
|
||||
labels: PropTypes.array.isRequired,
|
||||
domains_over_time: PropTypes.array.isRequired,
|
||||
blocked_over_time: PropTypes.array.isRequired
|
||||
};
|
||||
export interface QueriesGraphProps {
|
||||
loading: boolean;
|
||||
labels: Array<Date>;
|
||||
domains_over_time: Array<number>;
|
||||
blocked_over_time: Array<number>;
|
||||
}
|
||||
|
||||
class QueriesGraph extends Component<QueriesGraphProps & WithNamespaces, {}> {
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
const data = {
|
||||
const data: ChartData = {
|
||||
// @ts-ignore
|
||||
labels: this.props.labels,
|
||||
datasets: [
|
||||
{
|
||||
@@ -57,15 +58,15 @@ class QueriesGraph extends Component {
|
||||
]
|
||||
};
|
||||
|
||||
const options = {
|
||||
const options: ChartOptions = {
|
||||
tooltips: {
|
||||
enabled: true,
|
||||
mode: "x-axis",
|
||||
callbacks: {
|
||||
title: tooltipItem => {
|
||||
const time = tooltipItem[0].xLabel.match(/(\d?\d):?(\d?\d?)/);
|
||||
const hour = parseInt(time[1], 10);
|
||||
const minute = parseInt(time[2], 10) || 0;
|
||||
const time = tooltipItem[0].xLabel!.match(/(\d?\d):?(\d?\d?)/);
|
||||
const hour = parseInt(time![1], 10);
|
||||
const minute = parseInt(time![2], 10) || 0;
|
||||
const from = padNumber(hour) + ":" + padNumber(minute - 5) + ":00";
|
||||
const to = padNumber(hour) + ":" + padNumber(minute + 4) + ":59";
|
||||
|
||||
@@ -74,19 +75,17 @@ class QueriesGraph extends Component {
|
||||
label: (tooltipItems, data) => {
|
||||
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 = data.datasets![0].data![
|
||||
tooltipItems.index!
|
||||
] as number;
|
||||
const blocked = data.datasets![1].data![
|
||||
tooltipItems.index!
|
||||
] as number;
|
||||
|
||||
if (total > 0) percentage = (100.0 * blocked) / total;
|
||||
|
||||
return (
|
||||
data.datasets[tooltipItems.datasetIndex].label +
|
||||
data.datasets![tooltipItems.datasetIndex].label +
|
||||
": " +
|
||||
tooltipItems.yLabel +
|
||||
" (" +
|
||||
@@ -95,7 +94,7 @@ class QueriesGraph extends Component {
|
||||
);
|
||||
} else
|
||||
return (
|
||||
data.datasets[tooltipItems.datasetIndex].label +
|
||||
data.datasets![tooltipItems.datasetIndex!].label +
|
||||
": " +
|
||||
tooltipItems.yLabel
|
||||
);
|
||||
@@ -157,7 +156,9 @@ class QueriesGraph extends Component {
|
||||
* @returns {{loading: boolean, labels: Date[], domains_over_time: *,
|
||||
* blocked_over_time: any[]}} QueriesGraph props
|
||||
*/
|
||||
export const transformData = data => {
|
||||
export const transformData = (
|
||||
data: Array<ApiHistoryGraphItem>
|
||||
): QueriesGraphProps => {
|
||||
// Remove last data point as it's not yet finished
|
||||
data = data.slice(0, -1);
|
||||
|
||||
@@ -185,11 +186,12 @@ export const loadingProps = {
|
||||
|
||||
export const TranslatedQueriesGraph = withNamespaces("dashboard")(QueriesGraph);
|
||||
|
||||
export default props => (
|
||||
export default (props: any) => (
|
||||
<WithAPIData
|
||||
apiCall={api.getHistoryGraph}
|
||||
repeatOptions={{
|
||||
interval: 10 * 60 * 1000
|
||||
interval: 10 * 60 * 1000,
|
||||
ignoreCancel: true
|
||||
}}
|
||||
renderInitial={() => (
|
||||
<TranslatedQueriesGraph {...loadingProps} {...props} />
|
||||
+2
-2
@@ -9,11 +9,11 @@
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import React from "react";
|
||||
import { withNamespaces } from "react-i18next";
|
||||
import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import api from "../../util/api";
|
||||
import GenericDoughnutChart from "./GenericDoughnutChart";
|
||||
|
||||
const QueryTypesChart = ({ t }) => (
|
||||
const QueryTypesChart = ({ t }: WithNamespaces) => (
|
||||
<GenericDoughnutChart
|
||||
title={t("Query Types")}
|
||||
apiCall={api.getQueryTypes}
|
||||
+17
-19
@@ -9,21 +9,19 @@
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import React, { Component, Fragment } from "react";
|
||||
import PropTypes from "prop-types";
|
||||
import { withNamespaces } from "react-i18next";
|
||||
import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import { WithAPIData } from "../common/WithAPIData";
|
||||
import api from "../../util/api";
|
||||
import api, { ApiSummary } from "../../util/api";
|
||||
|
||||
class SummaryStats extends Component {
|
||||
static propTypes = {
|
||||
totalQueries: PropTypes.string.isRequired,
|
||||
blockedQueries: PropTypes.string.isRequired,
|
||||
percentBlocked: PropTypes.string.isRequired,
|
||||
gravityDomains: PropTypes.string.isRequired,
|
||||
uniqueClients: PropTypes.oneOfType([PropTypes.number, PropTypes.string])
|
||||
.isRequired
|
||||
};
|
||||
export interface SummaryStatsProps {
|
||||
totalQueries: string;
|
||||
blockedQueries: string;
|
||||
percentBlocked: string;
|
||||
gravityDomains: string;
|
||||
uniqueClients: number;
|
||||
}
|
||||
|
||||
class SummaryStats extends Component<SummaryStatsProps & WithNamespaces, {}> {
|
||||
render() {
|
||||
const { t } = this.props;
|
||||
|
||||
@@ -96,7 +94,7 @@ class SummaryStats extends Component {
|
||||
* @param data the API data
|
||||
* @returns {*} the transformed props
|
||||
*/
|
||||
export const transformData = data => ({
|
||||
export const transformData = (data: ApiSummary): SummaryStatsProps => ({
|
||||
totalQueries: Object.keys(data.total_queries)
|
||||
.reduce((total, queryType) => total + data.total_queries[queryType], 0)
|
||||
.toLocaleString(),
|
||||
@@ -110,33 +108,33 @@ export const transformData = data => ({
|
||||
* The props the summary stats should use when it fails to get the API data
|
||||
* (it does not need the error object)
|
||||
*/
|
||||
export const errorProps = {
|
||||
export const errorProps: SummaryStatsProps = {
|
||||
totalQueries: "Lost",
|
||||
blockedQueries: "Connection",
|
||||
percentBlocked: "To",
|
||||
gravityDomains: "API",
|
||||
uniqueClients: ""
|
||||
uniqueClients: 0
|
||||
};
|
||||
|
||||
/**
|
||||
* The props used to show a loading state
|
||||
*/
|
||||
export const initialProps = {
|
||||
export const initialProps: SummaryStatsProps = {
|
||||
blockedQueries: "---",
|
||||
totalQueries: "---",
|
||||
percentBlocked: "---",
|
||||
gravityDomains: "---",
|
||||
uniqueClients: "---"
|
||||
uniqueClients: 0
|
||||
};
|
||||
|
||||
export const TranslatedSummaryStats = withNamespaces(["common", "dashboard"])(
|
||||
SummaryStats
|
||||
);
|
||||
|
||||
export default props => (
|
||||
export default (props: any) => (
|
||||
<WithAPIData
|
||||
apiCall={api.getSummary}
|
||||
repeatOptions={{ interval: 5000 }}
|
||||
repeatOptions={{ interval: 5000, ignoreCancel: true }}
|
||||
renderInitial={() => (
|
||||
<TranslatedSummaryStats {...initialProps} {...props} />
|
||||
)}
|
||||
@@ -9,9 +9,15 @@
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import React from "react";
|
||||
import { withNamespaces } from "react-i18next";
|
||||
import api from "../../util/api";
|
||||
import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import api, { ApiTopBlocked, ApiTopDomainItem } from "../../util/api";
|
||||
import TopTable from "./TopTable";
|
||||
import i18next from "i18next";
|
||||
|
||||
export interface TopBlockedData {
|
||||
totalBlocked: number;
|
||||
topBlocked: Array<ApiTopDomainItem>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the API data into the form used in generateRows
|
||||
@@ -19,7 +25,7 @@ import TopTable from "./TopTable";
|
||||
* @param data the API data
|
||||
* @returns {{totalBlocked: number, topBlocked: *}} the parsed data
|
||||
*/
|
||||
export const transformData = data => ({
|
||||
export const transformData = (data: ApiTopBlocked): TopBlockedData => ({
|
||||
totalBlocked: data.blocked_queries,
|
||||
topBlocked: data.top_domains
|
||||
});
|
||||
@@ -30,7 +36,9 @@ export const transformData = data => ({
|
||||
* @param t the translation function
|
||||
* @returns {function(*): any[]} a function to generate rows of top blocked
|
||||
*/
|
||||
export const generateRows = t => data => {
|
||||
export const generateRows = (t: i18next.TranslationFunction) => (
|
||||
data: TopBlockedData
|
||||
) => {
|
||||
return data.topBlocked.map(item => {
|
||||
const percentage = (item.count / data.totalBlocked) * 100;
|
||||
|
||||
@@ -57,7 +65,7 @@ export const generateRows = t => data => {
|
||||
});
|
||||
};
|
||||
|
||||
const TopBlocked = ({ t, ...props }) => (
|
||||
const TopBlocked = ({ t, ...props }: WithNamespaces) => (
|
||||
<TopTable
|
||||
{...props}
|
||||
title={t("Top Blocked Domains")}
|
||||
@@ -9,9 +9,15 @@
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import React from "react";
|
||||
import { withNamespaces } from "react-i18next";
|
||||
import api from "../../util/api";
|
||||
import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import api, { ApiClient, ApiTopClients } from "../../util/api";
|
||||
import TopTable from "./TopTable";
|
||||
import i18next from "i18next";
|
||||
|
||||
export interface TopClientsData {
|
||||
totalQueries: number;
|
||||
topClients: Array<ApiClient>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the API data into the form used in generateRows
|
||||
@@ -19,7 +25,7 @@ import TopTable from "./TopTable";
|
||||
* @param data the API data
|
||||
* @returns {{totalQueries: number, topClients: *}} the parsed data
|
||||
*/
|
||||
export const transformData = data => ({
|
||||
export const transformData = (data: ApiTopClients): TopClientsData => ({
|
||||
totalQueries: data.total_queries,
|
||||
topClients: data.top_clients
|
||||
});
|
||||
@@ -30,7 +36,9 @@ export const transformData = data => ({
|
||||
* @param t the translation function
|
||||
* @returns {function(*): any[]} a function to generate rows of top clients
|
||||
*/
|
||||
export const generateRows = t => data => {
|
||||
export const generateRows = (t: i18next.TranslationFunction) => (
|
||||
data: TopClientsData
|
||||
) => {
|
||||
return data.topClients.map(item => {
|
||||
const percentage = (item.count / data.totalQueries) * 100;
|
||||
|
||||
@@ -57,7 +65,7 @@ export const generateRows = t => data => {
|
||||
});
|
||||
};
|
||||
|
||||
const TopClients = ({ t, ...props }) => (
|
||||
const TopClients = ({ t, ...props }: WithNamespaces) => (
|
||||
<TopTable
|
||||
{...props}
|
||||
title={t("Top Clients")}
|
||||
@@ -9,9 +9,15 @@
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import React from "react";
|
||||
import { withNamespaces } from "react-i18next";
|
||||
import api from "../../util/api";
|
||||
import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import api, { ApiTopDomainItem, ApiTopDomains } from "../../util/api";
|
||||
import TopTable from "./TopTable";
|
||||
import i18next from "i18next";
|
||||
|
||||
export interface TopDomainsData {
|
||||
totalQueries: number;
|
||||
topDomains: Array<ApiTopDomainItem>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the API data into the form used in generateRows
|
||||
@@ -19,7 +25,7 @@ import TopTable from "./TopTable";
|
||||
* @param data the API data
|
||||
* @returns {{totalQueries: number, topDomains: *}} the parsed data
|
||||
*/
|
||||
export const transformData = data => ({
|
||||
export const transformData = (data: ApiTopDomains): TopDomainsData => ({
|
||||
totalQueries: data.total_queries,
|
||||
topDomains: data.top_domains
|
||||
});
|
||||
@@ -30,7 +36,9 @@ export const transformData = data => ({
|
||||
* @param t the translation function
|
||||
* @returns {function(*): any[]} a function to generate rows of top domains
|
||||
*/
|
||||
export const generateRows = t => data => {
|
||||
export const generateRows = (t: i18next.TranslationFunction) => (
|
||||
data: TopDomainsData
|
||||
) => {
|
||||
return data.topDomains.map(item => {
|
||||
const percentage = (item.count / data.totalQueries) * 100;
|
||||
|
||||
@@ -57,7 +65,7 @@ export const generateRows = t => data => {
|
||||
});
|
||||
};
|
||||
|
||||
const TopDomains = ({ t, ...props }) => (
|
||||
const TopDomains = ({ t, ...props }: WithNamespaces) => (
|
||||
<TopTable
|
||||
{...props}
|
||||
title={t("Top Permitted Domains")}
|
||||
@@ -8,21 +8,21 @@
|
||||
* 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, ReactNode } from "react";
|
||||
import { WithAPIData } from "../common/WithAPIData";
|
||||
import { Subtract } from "react-i18next";
|
||||
|
||||
export class TopTable extends Component {
|
||||
static propTypes = {
|
||||
loading: PropTypes.bool.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
data: PropTypes.object.isRequired,
|
||||
headers: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
emptyMessage: PropTypes.string.isRequired,
|
||||
isEmpty: PropTypes.func.isRequired,
|
||||
generateRows: PropTypes.func.isRequired
|
||||
};
|
||||
export interface TopTableInnerProps<T> {
|
||||
loading: boolean;
|
||||
title: string;
|
||||
data: T;
|
||||
headers: Array<string>;
|
||||
emptyMessage: string;
|
||||
isEmpty: (data: T) => boolean;
|
||||
generateRows: (data: T) => ReactNode;
|
||||
}
|
||||
|
||||
export class TopTable<T> extends Component<TopTableInnerProps<T>, {}> {
|
||||
static defaultProps = {
|
||||
loading: true,
|
||||
title: "",
|
||||
@@ -86,7 +86,14 @@ export class TopTable extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
export default ({
|
||||
export interface TopTableProps<T, D>
|
||||
extends Subtract<TopTableInnerProps<T>, { loading: boolean; data: T }> {
|
||||
apiCall: () => Promise<D>;
|
||||
initialData: T;
|
||||
apiHandler: (data: D) => T;
|
||||
}
|
||||
|
||||
export default function<T, D>({
|
||||
title,
|
||||
apiCall,
|
||||
initialData,
|
||||
@@ -96,47 +103,50 @@ export default ({
|
||||
apiHandler,
|
||||
generateRows,
|
||||
...props
|
||||
}) => (
|
||||
<WithAPIData
|
||||
apiCall={apiCall}
|
||||
repeatOptions={{
|
||||
interval: 10 * 60 * 1000
|
||||
}}
|
||||
renderInitial={() => (
|
||||
<TopTable
|
||||
title={title}
|
||||
headers={headers}
|
||||
emptyMessage={emptyMessage}
|
||||
isEmpty={isEmpty}
|
||||
generateRows={generateRows}
|
||||
data={initialData}
|
||||
loading={true}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
renderOk={data => (
|
||||
<TopTable
|
||||
title={title}
|
||||
headers={headers}
|
||||
emptyMessage={emptyMessage}
|
||||
isEmpty={isEmpty}
|
||||
generateRows={generateRows}
|
||||
data={apiHandler(data)}
|
||||
loading={false}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
renderErr={() => (
|
||||
<TopTable
|
||||
title={title}
|
||||
headers={headers}
|
||||
emptyMessage={emptyMessage}
|
||||
isEmpty={isEmpty}
|
||||
generateRows={generateRows}
|
||||
data={initialData}
|
||||
loading={true}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}: TopTableProps<T, D>) {
|
||||
return (
|
||||
<WithAPIData
|
||||
apiCall={apiCall}
|
||||
repeatOptions={{
|
||||
interval: 10 * 60 * 1000,
|
||||
ignoreCancel: true
|
||||
}}
|
||||
renderInitial={() => (
|
||||
<TopTable
|
||||
title={title}
|
||||
headers={headers}
|
||||
emptyMessage={emptyMessage}
|
||||
isEmpty={isEmpty}
|
||||
generateRows={generateRows}
|
||||
data={initialData}
|
||||
loading={true}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
renderOk={data => (
|
||||
<TopTable
|
||||
title={title}
|
||||
headers={headers}
|
||||
emptyMessage={emptyMessage}
|
||||
isEmpty={isEmpty}
|
||||
generateRows={generateRows}
|
||||
data={apiHandler(data)}
|
||||
loading={false}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
renderErr={() => (
|
||||
<TopTable
|
||||
title={title}
|
||||
headers={headers}
|
||||
emptyMessage={emptyMessage}
|
||||
isEmpty={isEmpty}
|
||||
generateRows={generateRows}
|
||||
data={initialData}
|
||||
loading={true}
|
||||
{...props}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+2
-2
@@ -9,11 +9,11 @@
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import React from "react";
|
||||
import { withNamespaces } from "react-i18next";
|
||||
import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import api from "../../util/api";
|
||||
import GenericDoughnutChart from "./GenericDoughnutChart";
|
||||
|
||||
const UpstreamsChart = ({ t }) => (
|
||||
const UpstreamsChart = ({ t }: WithNamespaces) => (
|
||||
<GenericDoughnutChart
|
||||
title={t("Queries Answered By Destination")}
|
||||
apiCall={api.getUpstreams}
|
||||
+5
-4
@@ -11,12 +11,13 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import {
|
||||
TranslatedClientsGraph,
|
||||
loadingProps,
|
||||
transformData,
|
||||
loadingProps
|
||||
TranslatedClientsGraph
|
||||
} from "../ClientsGraph";
|
||||
import { ApiClientsGraph } from "../../../util/api";
|
||||
|
||||
const fakeData = {
|
||||
const fakeData: ApiClientsGraph = {
|
||||
over_time: [
|
||||
{ timestamp: 1513218354, data: [48476, 35688, 95153, 56971, 83497] },
|
||||
{ timestamp: 1513218954, data: [53603, 88146, 17471, 3039, 33678] },
|
||||
@@ -60,5 +61,5 @@ it("loads API data correctly", async () => {
|
||||
);
|
||||
expect(data.datasets[2].label).toEqual(fakeData.clients[2].name);
|
||||
expect(data.datasets[1].label).toEqual(fakeData.clients[1].ip);
|
||||
expect(data.datasets[0].data.length).toEqual(fakeData.over_time.length - 1);
|
||||
expect(data.datasets[0].data!.length).toEqual(fakeData.over_time.length - 1);
|
||||
});
|
||||
+4
-3
@@ -11,12 +11,13 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import {
|
||||
ChartItem,
|
||||
GenericDoughnutChart,
|
||||
transformData,
|
||||
loadingProps
|
||||
loadingProps,
|
||||
transformData
|
||||
} from "../GenericDoughnutChart";
|
||||
|
||||
const fakeData = [
|
||||
const fakeData: Array<ChartItem> = [
|
||||
{ name: "roberta.net", ip: "8.239.48.32", percent: 0.38411761010240625 },
|
||||
{ name: "", ip: "89.60.252.186", percent: 0.2830935477791041 },
|
||||
{ name: "christop.net", ip: "181.219.42.222", percent: 0.6249293208519193 }
|
||||
+6
-3
@@ -11,12 +11,15 @@
|
||||
import React from "react";
|
||||
import { shallow } from "enzyme";
|
||||
import {
|
||||
TranslatedSummaryStats,
|
||||
errorProps,
|
||||
transformData,
|
||||
errorProps
|
||||
TranslatedSummaryStats
|
||||
} from "../SummaryStats";
|
||||
import { ApiSummary } from "../../../util/api";
|
||||
|
||||
const fakeData = {
|
||||
const tick = global.tick;
|
||||
|
||||
const fakeData: ApiSummary = {
|
||||
active_clients: 2,
|
||||
blocked_queries: 2281,
|
||||
cached_queries: 3573,
|
||||
+3
-2
@@ -9,9 +9,10 @@
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import React from "react";
|
||||
import { transformData, generateRows } from "../TopBlocked";
|
||||
import { generateRows, transformData } from "../TopBlocked";
|
||||
import { ApiTopBlocked } from "../../../util/api";
|
||||
|
||||
const fakeData = {
|
||||
const fakeData: ApiTopBlocked = {
|
||||
top_domains: [
|
||||
{ domain: "jaron.info", count: 54316 },
|
||||
{ domain: "candelario.info", count: 47470 },
|
||||
+3
-2
@@ -9,9 +9,10 @@
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import React from "react";
|
||||
import { transformData, generateRows } from "../TopClients";
|
||||
import { generateRows, transformData } from "../TopClients";
|
||||
import { ApiTopClients } from "../../../util/api";
|
||||
|
||||
const fakeData = {
|
||||
const fakeData: ApiTopClients = {
|
||||
top_clients: [
|
||||
{ name: "", ip: "21.43.61.185", count: 37586 },
|
||||
{ name: "", ip: "224.77.182.60", count: 28664 },
|
||||
+2
-1
@@ -10,8 +10,9 @@
|
||||
|
||||
import React from "react";
|
||||
import { generateRows, transformData } from "../TopDomains";
|
||||
import { ApiTopDomains } from "../../../util/api";
|
||||
|
||||
const fakeData = {
|
||||
const fakeData: ApiTopDomains = {
|
||||
top_domains: [
|
||||
{ domain: "willow.com", count: 13739 },
|
||||
{ domain: "maurine.info", count: 13397 },
|
||||
@@ -14,8 +14,8 @@ import api from "../../util/api";
|
||||
import { Button } from "reactstrap";
|
||||
|
||||
export interface DomainListProps extends WithNamespaces {
|
||||
domains: string[],
|
||||
onRemove: (domain: string) => void
|
||||
domains: string[];
|
||||
onRemove: (domain: string) => void;
|
||||
}
|
||||
|
||||
const DomainList = ({ domains, onRemove, t }: DomainListProps) => {
|
||||
|
||||
@@ -16,20 +16,20 @@ import DomainList from "./DomainList";
|
||||
import { CancelablePromise, ignoreCancel, makeCancelable } from "../../util";
|
||||
|
||||
export interface ListPageProps extends WithNamespaces {
|
||||
title: string,
|
||||
note?: {} | string,
|
||||
placeholder: string,
|
||||
add: (domain: string) => Promise<any | never>,
|
||||
refresh: () => Promise<any | never>,
|
||||
remove: (domain: string) => Promise<any | never>,
|
||||
isValid: (domain: string) => boolean,
|
||||
validationErrorMsg: string
|
||||
title: string;
|
||||
note?: {} | string;
|
||||
placeholder: string;
|
||||
add: (domain: string) => Promise<any | never>;
|
||||
refresh: () => Promise<any | never>;
|
||||
remove: (domain: string) => Promise<any | never>;
|
||||
isValid: (domain: string) => boolean;
|
||||
validationErrorMsg: string;
|
||||
}
|
||||
|
||||
export interface ListPageState {
|
||||
domains: string[],
|
||||
message: string,
|
||||
messageType: AlertType
|
||||
domains: string[];
|
||||
message: string;
|
||||
messageType: AlertType;
|
||||
}
|
||||
|
||||
export class ListPage extends Component<ListPageProps, ListPageState> {
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
|
||||
import React from "react";
|
||||
import { shallow, ShallowWrapper } from "enzyme";
|
||||
import DomainInput, { DomainInputProps, DomainInputState } from "../DomainInput";
|
||||
import DomainInput, {
|
||||
DomainInputProps,
|
||||
DomainInputState
|
||||
} from "../DomainInput";
|
||||
import api from "../../../util/api";
|
||||
import { isValidDomain } from "../../../util/validate";
|
||||
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
|
||||
import React from "react";
|
||||
import { shallow, ShallowWrapper } from "enzyme";
|
||||
import ListPage, { ListPage as ListPageType, ListPageProps, ListPageState } from "../ListPage";
|
||||
import ListPage, {
|
||||
ListPage as ListPageType,
|
||||
ListPageProps,
|
||||
ListPageState
|
||||
} from "../ListPage";
|
||||
|
||||
const ignoreAPI = global.ignoreAPI;
|
||||
const tick = global.tick;
|
||||
|
||||
@@ -9,7 +9,12 @@
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import React, { Component, Fragment } from "react";
|
||||
import ReactTable, { Filter, ReactTableFunction, RowInfo, RowRenderProps } from "react-table";
|
||||
import ReactTable, {
|
||||
Filter,
|
||||
ReactTableFunction,
|
||||
RowInfo,
|
||||
RowRenderProps
|
||||
} from "react-table";
|
||||
import DateRangePicker from "react-bootstrap-daterangepicker";
|
||||
import { Button } from "reactstrap";
|
||||
import i18n from "i18next";
|
||||
@@ -17,7 +22,12 @@ import i18next from "i18next";
|
||||
import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import debounce from "lodash.debounce";
|
||||
import moment, { Moment } from "moment";
|
||||
import { CancelablePromise, ignoreCancel, makeCancelable, padNumber } from "../../util";
|
||||
import {
|
||||
CancelablePromise,
|
||||
ignoreCancel,
|
||||
makeCancelable,
|
||||
padNumber
|
||||
} from "../../util";
|
||||
import api, { ApiHistoryResponse, ApiQuery } from "../../util/api";
|
||||
import "react-table/react-table.css";
|
||||
import "bootstrap-daterangepicker/daterangepicker.css";
|
||||
|
||||
@@ -13,17 +13,17 @@ import { Col, FormGroup, Input, Label } from "reactstrap";
|
||||
import i18next from "i18next";
|
||||
|
||||
export interface ConditionalForwardingObject {
|
||||
enabled: boolean,
|
||||
routerIp: string,
|
||||
domain: string
|
||||
enabled: boolean;
|
||||
routerIp: string;
|
||||
domain: string;
|
||||
}
|
||||
|
||||
export interface ConditionalForwardingSettingsProps {
|
||||
settings: ConditionalForwardingObject,
|
||||
onUpdate: (settings: ConditionalForwardingObject) => void,
|
||||
settings: ConditionalForwardingObject;
|
||||
onUpdate: (settings: ConditionalForwardingObject) => void;
|
||||
isRouterIpValid: boolean;
|
||||
isDomainValid: boolean;
|
||||
t: i18next.TranslationFunction
|
||||
t: i18next.TranslationFunction;
|
||||
}
|
||||
|
||||
const ConditionalForwardingSettings = ({
|
||||
|
||||
@@ -12,7 +12,16 @@ import React, { ChangeEvent, Component, FormEvent } from "react";
|
||||
import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import { CancelablePromise, ignoreCancel, makeCancelable } from "../../util";
|
||||
import api, { ApiDhcpSettings, ApiResultResponse } from "../../util/api";
|
||||
import { Button, Col, Form, FormGroup, Input, InputGroup, InputGroupAddon, Label } from "reactstrap";
|
||||
import {
|
||||
Button,
|
||||
Col,
|
||||
Form,
|
||||
FormGroup,
|
||||
Input,
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
Label
|
||||
} from "reactstrap";
|
||||
import { isValidHostname, isValidIpv4 } from "../../util/validate";
|
||||
import Alert, { AlertType } from "../common/Alert";
|
||||
|
||||
|
||||
@@ -14,7 +14,9 @@ import { CancelablePromise, ignoreCancel, makeCancelable } from "../../util";
|
||||
import api, { ApiDnsSettings, ApiResultResponse } from "../../util/api";
|
||||
import DnsList from "./DnsList";
|
||||
import { Button, Col, Form, FormGroup } from "reactstrap";
|
||||
import ConditionalForwardingSettings, { ConditionalForwardingObject } from "./ConditionalForwardingSettings";
|
||||
import ConditionalForwardingSettings, {
|
||||
ConditionalForwardingObject
|
||||
} from "./ConditionalForwardingSettings";
|
||||
import DnsOptionSettings, { DnsOptionsObject } from "./DnsOptionSettings";
|
||||
import Alert, { AlertType } from "../common/Alert";
|
||||
import { isValidHostname, isValidIpv4 } from "../../util/validate";
|
||||
|
||||
@@ -12,7 +12,7 @@ import React from "react";
|
||||
import { Button, ListGroupItem } from "reactstrap";
|
||||
|
||||
export interface DnsListItemProps {
|
||||
address: string,
|
||||
address: string;
|
||||
onRemove: () => void;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,10 @@ import React, { Component, RefObject } from "react";
|
||||
import { Button, InputGroup, InputGroupAddon, ListGroupItem } from "reactstrap";
|
||||
import { Typeahead } from "react-bootstrap-typeahead";
|
||||
import { WithNamespaces, withNamespaces } from "react-i18next";
|
||||
import { PreconfiguredUpstreamOption, preconfiguredUpstreamOptions } from "./preconfiguredUpstreams";
|
||||
import {
|
||||
PreconfiguredUpstreamOption,
|
||||
preconfiguredUpstreamOptions
|
||||
} from "./preconfiguredUpstreams";
|
||||
|
||||
export interface DnsListNewItemProps {
|
||||
onAdd: (address: string) => void;
|
||||
|
||||
@@ -14,9 +14,9 @@ import api, { ApiFtlDbResponse } from "../../util/api";
|
||||
import { WithAPIData } from "../common/WithAPIData";
|
||||
|
||||
export interface FTLInfoProps {
|
||||
fileSize: number,
|
||||
queries: number,
|
||||
sqliteVersion: string
|
||||
fileSize: number;
|
||||
queries: number;
|
||||
sqliteVersion: string;
|
||||
}
|
||||
|
||||
class FTLInfo extends Component<FTLInfoProps & WithNamespaces, {}> {
|
||||
|
||||
@@ -21,7 +21,7 @@ export interface VersionCardProps extends WithNamespaces {
|
||||
}
|
||||
|
||||
export interface VersionCardState {
|
||||
collapsed: boolean
|
||||
collapsed: boolean;
|
||||
}
|
||||
|
||||
class VersionCard extends Component<VersionCardProps, VersionCardState> {
|
||||
|
||||
@@ -14,7 +14,13 @@ import Header, { mobileSidebarHide } from "../components/common/Header";
|
||||
import Sidebar from "../components/common/Sidebar";
|
||||
import Footer from "../components/common/Footer";
|
||||
import api from "../util/api";
|
||||
import { nav, RouteCustomItem, RouteData, RouteGroup, RouteItem } from "../routes";
|
||||
import {
|
||||
nav,
|
||||
RouteCustomItem,
|
||||
RouteData,
|
||||
RouteGroup,
|
||||
RouteItem
|
||||
} from "../routes";
|
||||
import { GlobalContextProvider } from "../components/common/context";
|
||||
import LayoutApplier from "../components/common/LayoutApplier";
|
||||
|
||||
|
||||
+4
-1
@@ -21,7 +21,10 @@ configure({ adapter: new Adapter() });
|
||||
jest.mock("react-i18next", () => ({
|
||||
// This mock makes sure any components using the withNamespaces HoC receive the t function as a prop
|
||||
withNamespaces: () => (component: any) => {
|
||||
component.defaultProps = { ...component.defaultProps, t: (key: string) => key };
|
||||
component.defaultProps = {
|
||||
...component.defaultProps,
|
||||
t: (key: string) => key
|
||||
};
|
||||
return component;
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -8,7 +8,13 @@
|
||||
* This file is copyright under the latest version of the EUPL.
|
||||
* Please see LICENSE file for your rights under this license. */
|
||||
|
||||
import { isPositiveNumber, isValidDomain, isValidIpv4, isValidIpv4OptionalPort, isValidRegex } from "../validate";
|
||||
import {
|
||||
isPositiveNumber,
|
||||
isValidDomain,
|
||||
isValidIpv4,
|
||||
isValidIpv4OptionalPort,
|
||||
isValidRegex
|
||||
} from "../validate";
|
||||
|
||||
describe("Testing the validation functions", () => {
|
||||
describe("isValidDomain", () => {
|
||||
|
||||
+100
-8
@@ -108,6 +108,98 @@ export interface ApiStatus {
|
||||
status: Status;
|
||||
}
|
||||
|
||||
export interface ApiClientsGraph {
|
||||
over_time: Array<ApiClientOverTime>;
|
||||
clients: Array<ApiClientGraphInfo>;
|
||||
}
|
||||
|
||||
export interface ApiClientOverTime {
|
||||
timestamp: number;
|
||||
data: Array<number>;
|
||||
}
|
||||
|
||||
export interface ApiClientGraphInfo {
|
||||
name: string;
|
||||
ip: string;
|
||||
}
|
||||
|
||||
export interface ApiHistoryGraphItem {
|
||||
timestamp: number;
|
||||
total_queries: number;
|
||||
blocked_queries: number;
|
||||
}
|
||||
|
||||
export interface ApiQueryType {
|
||||
name: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ApiSummary {
|
||||
gravity_size: number;
|
||||
total_queries: {
|
||||
A: number;
|
||||
AAAA: number;
|
||||
ANY: number;
|
||||
SRV: number;
|
||||
SOA: number;
|
||||
PTR: number;
|
||||
TXT: number;
|
||||
[key: string]: number;
|
||||
};
|
||||
blocked_queries: number;
|
||||
percent_blocked: number;
|
||||
unique_domains: number;
|
||||
forwarded_queries: number;
|
||||
cached_queries: number;
|
||||
reply_types: {
|
||||
IP: number;
|
||||
CNAME: number;
|
||||
DOMAIN: number;
|
||||
NODATA: number;
|
||||
NXDOMAIN: number;
|
||||
[key: string]: number;
|
||||
};
|
||||
total_clients: number;
|
||||
active_clients: number;
|
||||
status: string;
|
||||
}
|
||||
|
||||
export interface ApiUpstreams {
|
||||
upstreams: Array<{
|
||||
name: string;
|
||||
ip: string;
|
||||
count: number;
|
||||
}>;
|
||||
forwarded_queries: number;
|
||||
total_queries: number;
|
||||
}
|
||||
|
||||
export interface ApiTopDomainItem {
|
||||
domain: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export interface ApiTopBlocked {
|
||||
top_domains: Array<ApiTopDomainItem>;
|
||||
blocked_queries: number;
|
||||
}
|
||||
|
||||
export interface ApiTopDomains {
|
||||
top_domains: Array<ApiTopDomainItem>;
|
||||
total_queries: number;
|
||||
}
|
||||
|
||||
export interface ApiTopClients {
|
||||
top_clients: Array<ApiClient>;
|
||||
total_queries: number;
|
||||
}
|
||||
|
||||
export interface ApiClient {
|
||||
name: string;
|
||||
ip: string;
|
||||
count: number;
|
||||
}
|
||||
|
||||
export default {
|
||||
loggedIn: false,
|
||||
authenticate(key: string) {
|
||||
@@ -118,25 +210,25 @@ export default {
|
||||
logout() {
|
||||
return http.delete("auth");
|
||||
},
|
||||
getSummary() {
|
||||
getSummary(): Promise<ApiSummary> {
|
||||
return http.get("stats/summary");
|
||||
},
|
||||
getHistoryGraph() {
|
||||
getHistoryGraph(): Promise<Array<ApiHistoryGraphItem>> {
|
||||
return http.get("stats/overTime/history");
|
||||
},
|
||||
getClientsGraph() {
|
||||
getClientsGraph(): Promise<ApiClientsGraph> {
|
||||
return http.get("stats/overTime/clients");
|
||||
},
|
||||
getQueryTypes() {
|
||||
getQueryTypes(): Promise<Array<ApiQueryType>> {
|
||||
return http.get("stats/query_types");
|
||||
},
|
||||
getUpstreams() {
|
||||
getUpstreams(): Promise<ApiUpstreams> {
|
||||
return http.get("stats/upstreams");
|
||||
},
|
||||
getTopDomains() {
|
||||
getTopDomains(): Promise<ApiTopDomains> {
|
||||
return http.get("stats/top_domains");
|
||||
},
|
||||
getTopBlocked() {
|
||||
getTopBlocked(): Promise<ApiTopBlocked> {
|
||||
// The API uses a GET parameter to differentiate top domains from top
|
||||
// blocked, but the fake API is not able to handle GET parameters right now.
|
||||
const url = config.fakeAPI
|
||||
@@ -145,7 +237,7 @@ export default {
|
||||
|
||||
return http.get(url);
|
||||
},
|
||||
getTopClients() {
|
||||
getTopClients(): Promise<ApiTopClients> {
|
||||
return http.get("stats/top_clients");
|
||||
},
|
||||
getHistory(params: any): Promise<ApiHistoryResponse> {
|
||||
|
||||
+11
-3
@@ -8,7 +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, { ChangeEvent, Component, FormEvent, Fragment, KeyboardEvent } from "react";
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
Component,
|
||||
FormEvent,
|
||||
Fragment,
|
||||
KeyboardEvent
|
||||
} from "react";
|
||||
import { Redirect } from "react-router-dom";
|
||||
import sha from "sha.js";
|
||||
import api from "../util/api";
|
||||
@@ -20,7 +26,7 @@ import config from "../config";
|
||||
import { History, LocationDescriptorObject } from "history";
|
||||
|
||||
export interface LoginProps extends WithNamespaces {
|
||||
location: LocationDescriptorObject<{ from: Location }>
|
||||
location: LocationDescriptorObject<{ from: Location }>;
|
||||
history: History;
|
||||
}
|
||||
|
||||
@@ -97,7 +103,9 @@ class Login extends Component<LoginProps, LoginState> {
|
||||
}
|
||||
|
||||
// Redirect to the page the user was originally going to, or if that doesn't exist, go to home
|
||||
const locationState = this.props.location.state || { from: { pathname: "/" } };
|
||||
const locationState = this.props.location.state || {
|
||||
from: { pathname: "/" }
|
||||
};
|
||||
this.props.history.push(locationState.from.pathname);
|
||||
})
|
||||
// If there was an error, tell the user they used the wrong password
|
||||
|
||||
Reference in New Issue
Block a user