diff --git a/src/components/dashboard/ClientsGraph.tsx b/src/components/dashboard/ClientsGraph.tsx
index 2bf1888..c6ac9da 100644
--- a/src/components/dashboard/ClientsGraph.tsx
+++ b/src/components/dashboard/ClientsGraph.tsx
@@ -14,7 +14,7 @@ import { Line } from "react-chartjs-2";
import { WithTranslation, withTranslation } from "react-i18next";
import moment from "moment";
import { getIntervalForRange } from "../../util/graphUtils";
-import api from "../../util/api";
+import api, { ApiClient } from "../../util/api";
import ChartTooltip from "./ChartTooltip";
import { WithAPIData } from "../common/WithAPIData";
import { ChartDataSets, ChartOptions, TimeUnit } from "chart.js";
@@ -242,17 +242,23 @@ export const TranslatedClientsGraph = withTranslation([
"time-ranges"
])(ClientsGraph);
-export const ClientsGraphContainer = () => (
+export interface ClientsGraphContainerProps {
+ apiClient: ApiClient;
+}
+
+export const ClientsGraphContainer = ({
+ apiClient
+}: ClientsGraphContainerProps) => (
{context => (
context.range
- ? api.getClientsGraphDb(
+ ? apiClient.getClientsGraphDb(
context.range,
getIntervalForRange(context.range)
)
- : api.getClientsGraph()
+ : apiClient.getClientsGraph()
}
repeatOptions={
context.range
@@ -271,3 +277,7 @@ export const ClientsGraphContainer = () => (
)}
);
+
+ClientsGraphContainer.defaultProps = {
+ apiClient: api
+};
diff --git a/src/components/dashboard/TopBlockedClients.tsx b/src/components/dashboard/TopBlockedClients.tsx
index 0413c6f..c547014 100644
--- a/src/components/dashboard/TopBlockedClients.tsx
+++ b/src/components/dashboard/TopBlockedClients.tsx
@@ -17,7 +17,7 @@ import { TimeRangeContext } from "../common/context/TimeRangeContext";
export interface TopBlockedClientsData {
blockedQueries: number;
- topClients: Array;
+ topClients: Array;
}
/**
diff --git a/src/components/dashboard/TopClients.tsx b/src/components/dashboard/TopClients.tsx
index 7c4998a..1a90c9d 100644
--- a/src/components/dashboard/TopClients.tsx
+++ b/src/components/dashboard/TopClients.tsx
@@ -17,7 +17,7 @@ import { TimeRangeContext } from "../common/context/TimeRangeContext";
export interface TopClientsData {
totalQueries: number;
- topClients: Array;
+ topClients: Array;
}
/**
diff --git a/src/components/dashboard/__tests__/ClientsGraph.test.tsx b/src/components/dashboard/__tests__/ClientsGraph.test.tsx
index e717795..281e3b0 100644
--- a/src/components/dashboard/__tests__/ClientsGraph.test.tsx
+++ b/src/components/dashboard/__tests__/ClientsGraph.test.tsx
@@ -11,10 +11,21 @@
import React from "react";
import { shallow } from "enzyme";
import {
+ ClientsGraph,
+ ClientsGraphContainer,
loadingProps,
transformData,
TranslatedClientsGraph
} from "../ClientsGraph";
+import {
+ TimeRange,
+ TimeRangeContext,
+ TimeRangeContextType
+} from "../../common/context/TimeRangeContext";
+import moment from "moment";
+import { Line } from "react-chartjs-2";
+import { ChartData, ChartTooltipItem } from "chart.js";
+import { ApiClient } from "../../../util/api";
const fakeData: ApiClientsGraph = {
over_time: [
@@ -38,6 +49,8 @@ const fakeData: ApiClientsGraph = {
]
};
+const tick = global.tick;
+
it("shows loading indicator correctly", () => {
const wrapper = shallow().dive();
@@ -62,3 +75,188 @@ it("loads API data correctly", async () => {
expect(props.datasets[1].label).toEqual(fakeData.clients[1].ip);
expect(props.datasets[0].data!.length).toEqual(fakeData.over_time.length - 1);
});
+
+it("should use all of the data if there is a time range set", () => {
+ const range: TimeRange = {
+ name: "test",
+ from: moment().subtract(1, "hour"),
+ until: moment()
+ };
+ const props = transformData(fakeData, range);
+
+ expect(props.datasets[0].data!.length).toEqual(fakeData.over_time.length);
+});
+
+it("should use hour as the time unit if the time range is less than a day", () => {
+ const range: TimeRange = {
+ name: "test",
+ from: moment().subtract(1, "hour"),
+ until: moment()
+ };
+ const props = transformData(fakeData, range);
+
+ expect(props.timeUnit).toEqual("hour");
+});
+
+it("should use day as the time unit if the time range is more than a day", () => {
+ const range: TimeRange = {
+ name: "test",
+ from: moment().subtract(2, "days"),
+ until: moment()
+ };
+ const props = transformData(fakeData, range);
+
+ expect(props.timeUnit).toEqual("day");
+});
+
+it("should show the date range in the tooltip title", () => {
+ const t = jest.fn(key => key);
+ const wrapper = shallow(
+ // @ts-ignore Ignore the missing i18n props
+
+ );
+
+ const titleFunc = wrapper.find(Line).props().options!.tooltips!.callbacks!
+ .title!;
+ const tooltipItem: ChartTooltipItem = {
+ datasetIndex: 0,
+ index: 0,
+ xLabel: "04:02",
+ yLabel: ""
+ };
+
+ const result = titleFunc([tooltipItem], {});
+
+ expect(result).toEqual("Client activity from {{from}} to {{to}}");
+ expect(t).toHaveBeenCalledWith("Client activity from {{from}} to {{to}}", {
+ from: "03:57:00",
+ to: "04:06:59"
+ });
+});
+
+it("should show the client and count in the tooltip label", () => {
+ const wrapper = shallow(
+
+ ).dive();
+
+ const labelFunc = wrapper.find(Line).props().options!.tooltips!.callbacks!
+ .label!;
+ const tooltipItem: ChartTooltipItem = {
+ datasetIndex: 0,
+ index: 0,
+ xLabel: "xLabel",
+ yLabel: "yLabel"
+ };
+ const data: ChartData = {
+ datasets: [
+ {
+ label: "datasetLabel"
+ }
+ ]
+ };
+
+ expect(labelFunc(tooltipItem, data)).toEqual("datasetLabel: yLabel");
+});
+
+it("should use the normal API call when there is no time range", () => {
+ const context: TimeRangeContextType = {
+ range: null,
+ update: () => {}
+ };
+ const apiClient = ({
+ getClientsGraph: jest.fn(() => Promise.reject({ isCanceled: true })),
+ getClientsGraphDb: jest.fn(() => Promise.reject({ isCanceled: true }))
+ } as any) as ApiClient;
+
+ shallow(
+
+
+
+ )
+ .dive()
+ .dive()
+ .dive();
+
+ expect(apiClient.getClientsGraph).toHaveBeenCalled();
+ expect(apiClient.getClientsGraphDb).not.toHaveBeenCalled();
+});
+
+it("should use the DB API call when there is a time range", () => {
+ const context: TimeRangeContextType = {
+ range: {
+ name: "test",
+ from: moment().subtract(1, "day"),
+ until: moment()
+ },
+ update: () => {}
+ };
+ const apiClient = ({
+ getClientsGraph: jest.fn(() => Promise.reject({ isCanceled: true })),
+ getClientsGraphDb: jest.fn(() => Promise.reject({ isCanceled: true }))
+ } as any) as ApiClient;
+
+ shallow(
+
+
+
+ )
+ .dive()
+ .dive()
+ .dive();
+
+ expect(apiClient.getClientsGraph).not.toHaveBeenCalled();
+ expect(apiClient.getClientsGraphDb).toHaveBeenCalledWith(context.range, 600);
+});
+
+it("should transform the data and render if the API returns data", async () => {
+ const context: TimeRangeContextType = {
+ range: null,
+ update: () => {}
+ };
+ const apiClient = ({
+ getClientsGraph: () => Promise.resolve(fakeData)
+ } as any) as ApiClient;
+
+ const wrapper = shallow(
+
+
+
+ )
+ .dive()
+ .dive()
+ .dive();
+
+ // Let the API call resolve
+ await tick();
+ wrapper.update();
+
+ const actualProps = wrapper.find(TranslatedClientsGraph).props();
+ const expectedProps = transformData(fakeData, null);
+ expect(actualProps).toEqual(expectedProps);
+});
+
+it("should render as loading if the API fails to return data", async () => {
+ const context: TimeRangeContextType = {
+ range: null,
+ update: () => {}
+ };
+ const apiClient = ({
+ getClientsGraph: () => Promise.reject({ error: {} })
+ } as any) as ApiClient;
+
+ const wrapper = shallow(
+
+
+
+ )
+ .dive()
+ .dive()
+ .dive();
+
+ // Let the API call resolve
+ await tick();
+ wrapper.update();
+
+ const actualProps = wrapper.find(TranslatedClientsGraph).props();
+ expect(actualProps).toEqual(loadingProps);
+});
diff --git a/src/types/api.d.ts b/src/types/api.d.ts
index bd1f2cb..e0493f1 100644
--- a/src/types/api.d.ts
+++ b/src/types/api.d.ts
@@ -188,7 +188,7 @@ interface ApiTopDomains {
}
interface ApiTopClients {
- top_clients: Array;
+ top_clients: Array;
total_queries: number;
}
@@ -197,7 +197,7 @@ interface ApiTopBlockedClients {
blocked_queries: number;
}
-interface ApiClient {
+interface ApiClientData {
name: string;
ip: string;
count: number;