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;