Skip to content

Commit

Permalink
feat: filter heartbeat by time
Browse files Browse the repository at this point in the history
  • Loading branch information
chamanbravo committed Jan 30, 2024
1 parent 6f83b82 commit 043d54f
Show file tree
Hide file tree
Showing 14 changed files with 151 additions and 70 deletions.
17 changes: 14 additions & 3 deletions controllers/monitor_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ func CreateMonitor(c *fiber.Ctx) error {
newMonitor := new(serializers.AddMonitorIn)
if err := c.BodyParser(newMonitor); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid body",
"message": err.Error(),
})
}
Expand Down Expand Up @@ -98,7 +97,6 @@ func UpdateMonitor(c *fiber.Ctx) error {
monitor := new(serializers.AddMonitorIn)
if err := c.BodyParser(monitor); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"error": "Invalid body",
"message": err.Error(),
})
}
Expand Down Expand Up @@ -247,6 +245,7 @@ func MonitorsList(c *fiber.Ctx) error {
// @Accept json
// @Produce json
// @Param id path string true "Monitor ID"
// @Param startTime query time.Time true "Start Time" format(json)
// @Success 200 {object} serializers.HeartbeatsOut
// @Success 400 {object} serializers.ErrorResponse
// @Router /api/monitors/heartbeat/{id} [get]
Expand All @@ -265,7 +264,19 @@ func RetrieveHeartbeat(c *fiber.Ctx) error {
})
}

heartbeat, err := queries.RetrieveHeartbeats(id, 10)
query := new(serializers.RetrieveHeartbeatIn)
if err := c.QueryParser(query); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
"message": err.Error(),
})
}

errors := utils.BodyValidator.Validate(query)
if len(errors) > 0 {
return c.Status(400).JSON(errors)
}

heartbeat, err := queries.RetrieveHeartbeatsByTime(id, query.StartTime)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"message": err.Error(),
Expand Down
10 changes: 10 additions & 0 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,16 @@ const docTemplate = `
"format": "string",
"description": "Monitor ID"
}
},
{
"name": "startTime",
"in": "query",
"description": "Start Time",
"required": true,
"schema": {
"type": "string",
"format": "date-time"
}
}
]
}
Expand Down
10 changes: 10 additions & 0 deletions docs/swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,16 @@
"format": "string",
"description": "Monitor ID"
}
},
{
"name": "startTime",
"in": "query",
"description": "Start Time",
"required": true,
"schema": {
"type": "string",
"format": "date-time"
}
}
]
}
Expand Down
40 changes: 40 additions & 0 deletions queries/heartbeat.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package queries

import (
"log"
"time"

"github.com/chamanbravo/upstat/database"
"github.com/chamanbravo/upstat/models"
Expand Down Expand Up @@ -64,3 +65,42 @@ func SaveHeartbeat(heartbeat *models.Heartbeat) error {

return nil
}

func RetrieveHeartbeatsByTime(id int, startTime time.Time) ([]*models.Heartbeat, error) {
stmt, err := database.DB.Prepare("SELECT * FROM heartbeats WHERE monitor_id = $1 AND timestamp >= $2 ORDER BY timestamp ASC")
if err != nil {
log.Println("Error when trying to prepare statement")
log.Println(err)
return nil, err
}
defer stmt.Close()

rows, err := stmt.Query(id, startTime)
if err != nil {
log.Println("Error when trying to execute query")
log.Println(err)
return nil, err
}
defer rows.Close()

var heartbeats []*models.Heartbeat

for rows.Next() {
heartbeat := new(models.Heartbeat)
err := rows.Scan(&heartbeat.ID, &heartbeat.MonitorId, &heartbeat.Timestamp, &heartbeat.Status, &heartbeat.Latency, &heartbeat.Message)
if err != nil {
log.Println("Error when trying to scan row")
log.Println(err)
return nil, err
}
heartbeats = append(heartbeats, heartbeat)
}

if err := rows.Err(); err != nil {
log.Println("Error during iteration over rows")
log.Println(err)
return nil, err
}

return heartbeats, nil
}
6 changes: 6 additions & 0 deletions serializers/serializers.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package serializers

import (
"time"

"github.com/chamanbravo/upstat/models"
)

Expand Down Expand Up @@ -80,3 +82,7 @@ type MonitorInfoOut struct {
SuccessResponse
Monitor models.Monitor `json:"monitor"`
}

type RetrieveHeartbeatIn struct {
StartTime time.Time `query:"startTime"`
}
1 change: 0 additions & 1 deletion web/src/components/CommandMenu.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as React from "react";
import { useNavigate } from "react-router-dom";
import { DialogProps } from "@radix-ui/react-alert-dialog";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
CommandDialog,
Expand Down
23 changes: 6 additions & 17 deletions web/src/components/DateDropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import {
} from "@/components/ui/dropdown-menu";
import { CaretSortIcon } from "@radix-ui/react-icons";
import { Button } from "./ui/button";
import { subDays, subHours, subMonths, subYears } from "date-fns";
import { subDays, subHours, subMonths } from "date-fns";
import {
formatAsDateHour,
formatAsDayDateHour,
formatAsMonthDate,
formatAsMonthDateHour,
formatAsMonthYear,
formatAsYear,
} from "@/lib/utils";

export type DateItem = {
Expand All @@ -39,7 +39,7 @@ const data: DateItem[] = [
new Date() instanceof Date
? subHours(new Date(), 24).toString()
: new Date().toISOString(),
formatter: formatAsMonthDateHour,
formatter: formatAsDateHour,
},
{
value: "7_day",
Expand All @@ -50,7 +50,7 @@ const data: DateItem[] = [
new Date() instanceof Date
? subDays(new Date(), 7).toString()
: new Date().toISOString(),
formatter: formatAsMonthDate,
formatter: formatAsDayDateHour,
},
{
value: "14_day",
Expand All @@ -61,7 +61,7 @@ const data: DateItem[] = [
new Date() instanceof Date
? subDays(new Date(), 14).toString()
: new Date().toISOString(),
formatter: formatAsMonthDate,
formatter: formatAsDayDateHour,
},
{
value: "30_day",
Expand All @@ -85,17 +85,6 @@ const data: DateItem[] = [
: new Date().toISOString(),
formatter: formatAsMonthYear,
},
{
value: "10_year",
label: "Last Decade",
interval: "year",
count: 10,
getStartTimeStamp:
new Date() instanceof Date
? subYears(new Date(), 10).toString()
: new Date().toISOString(),
formatter: formatAsYear,
},
];

const dataMap = new Map(data.map((i) => [i.value, i]));
Expand Down
4 changes: 3 additions & 1 deletion web/src/components/RedirectOnUser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ interface Props {

interface JwtPayload {
username: string;
firstname: string;
lastname: string;
}

export default function RedirectOnUser({ children }: Props) {
Expand All @@ -27,7 +29,7 @@ export default function RedirectOnUser({ children }: Props) {
if (isValidToken(accessToken)) {
const payload: JwtPayload | null = jwtDecode(accessToken);
if (payload) {
setUser(payload.username);
setUser(payload.username, payload.firstname, payload.lastname);
return navigate("/app/monitors");
}
} else {
Expand Down
4 changes: 4 additions & 0 deletions web/src/lib/api/v1.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ export interface paths {
/** Monitor ID */
id: string;
};
query: {
/** Start Time */
startTime: string;
};
};
responses: {
200: {
Expand Down
12 changes: 7 additions & 5 deletions web/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,14 @@ export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

export const formatAsMonthDateHour = (value: string) =>
export const formatAsDateHour = (value: string) =>
new Date(value).toLocaleString(undefined, {
hour: "numeric",
minute: "numeric",
});

export const formatAsDayDateHour = (value: string) =>
new Date(value).toLocaleString(undefined, {
month: "short",
day: "2-digit",
hour: "numeric",
minute: "numeric",
Expand All @@ -27,9 +32,6 @@ export const formatAsMonthYear = (value: string) =>
year: "numeric",
});

export const formatAsYear = (value: string) =>
new Date(value).toLocaleString(undefined, { year: "numeric" });

export const isValidToken = (token: string) => {
if (!token) {
return false;
Expand Down
58 changes: 49 additions & 9 deletions web/src/pages/MonitorItem/LineChart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,26 @@ import {
NameType,
ValueType,
} from "recharts/types/component/DefaultTooltipContent";
import { DateDropdown, DateItem, defaultDate } from "@/components/DateDropdown";
import { useEffect, useState } from "react";
import { components } from "@/lib/api/v1";
import { client } from "@/lib/utils";
import { useParams } from "react-router";

interface Props {
data: {}[];
}
const { GET } = client;

const CustomTooltip = ({
label,
active,
payload,
labelFormatter,
}: TooltipProps<ValueType, NameType>) => {
if (active && payload && payload.length) {
return (
<div className="bg-card p-2 border border-border rounded-sm">
<p className="text-card-foreground">{label}</p>
<p className="text-card-foreground">
{labelFormatter ? labelFormatter(label, payload) : label}
</p>
{payload.map((item) => (
<p
key={item.name}
Expand All @@ -40,20 +46,54 @@ const CustomTooltip = ({
return null;
};

export default function GenericLineChart({ data }: Props) {
export default function GenericLineChart() {
const { id } = useParams();
const [date, setDate] = useState<DateItem>(defaultDate);
const [monitorData, setMonitorData] = useState<
components["schemas"]["HeartbeatsOut"]["heartbeat"]
>([]);

const fetchHeartbeat = async (signal: AbortSignal) => {
if (!id) return;
try {
const { response, data } = await GET(`/api/monitors/heartbeat/{id}`, {
params: {
path: {
id: `${id}`,
},
query: {
startTime: new Date(date.getStartTimeStamp).toISOString(),
},
},
signal,
});
if (response.ok) {
data?.heartbeat && setMonitorData(data?.heartbeat);
}
} catch (error) {}
};

useEffect(() => {
const controller = new AbortController();
fetchHeartbeat(controller.signal);
}, [date.value]);

return (
<div className="bg-card p-2 px-4 rounded-lg flex flex-col gap-8 border">
<h3 className="font-medium text--xl">Response time in the last day</h3>
<div className="flex flex-col gap-2 md:flex-row md:justify-between md:items-center">
<h3 className="font-medium text--xl">Response time</h3>
<DateDropdown date={date} onChange={setDate} />
</div>
<div className="flex flex-col w-full max-w-full h-[325px]">
<div className="flex flex-grow justify-center items-center">
<ResponsiveContainer width="100%" height="100%">
<LineChart data={data} margin={{ top: 8 }}>
<LineChart data={monitorData} margin={{ top: 8 }}>
<CartesianGrid
stroke="hsl(var(--muted-foreground)/0.2)"
strokeDasharray="5 5"
/>
<Tooltip
content={<CustomTooltip />}
content={<CustomTooltip labelFormatter={date.formatter} />}
cursor={{
stroke: "hsl(var(--muted-foreground)/0.4)",
strokeWidth: 1.5,
Expand Down Expand Up @@ -90,7 +130,7 @@ export default function GenericLineChart({ data }: Props) {
tick={{
fill: "hsl(var(--muted-foreground))",
}}
tickFormatter={(value) => new Date(value).toLocaleTimeString()}
tickFormatter={date.formatter}
/>
</LineChart>
</ResponsiveContainer>
Expand Down
5 changes: 0 additions & 5 deletions web/src/pages/MonitorItem/Summary.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import { Card, CardTitle } from "@/components/ui/card";
import { useState } from "react";

export default function Summary() {
const [monitorSummary, setMonitorSummary] = useState();

const fetchMonitorSummary = () => {};

return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
<Card className="px-4 py-5 flex flex-col gap-3">
Expand Down
Loading

0 comments on commit 043d54f

Please sign in to comment.