Skip to content

Commit

Permalink
UI work
Browse files Browse the repository at this point in the history
  • Loading branch information
wardviaene committed Sep 24, 2024
1 parent d084dd6 commit 17651d9
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 9 deletions.
16 changes: 14 additions & 2 deletions pkg/observability/logs.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bufio"
"fmt"
"math"
"sort"
"strings"
"time"
)
Expand All @@ -13,9 +14,11 @@ func (o *Observability) getLogs(fromDate, endDate time.Time, pos int64, maxLogLi
Enabled: true,
Environments: []string{"dev", "qa", "prod"},
LogEntries: []LogEntry{},
Keys: make(map[KeyValue]int),
Keys: KeyValueInt{},
}

keys := make(map[KeyValue]int)

logFiles := []string{}

if maxLogLines == 0 {
Expand Down Expand Up @@ -65,7 +68,7 @@ func (o *Observability) getLogs(fromDate, endDate time.Time, pos int64, maxLogLi
logEntryResponse.LogEntries = append(logEntryResponse.LogEntries, logEntry)
for k, v := range logMessage.Data {
if k != "log" {
logEntryResponse.Keys[KeyValue{Key: k, Value: v}] += 1
keys[KeyValue{Key: k, Value: v}] += 1
}
}
}
Expand All @@ -81,6 +84,15 @@ func (o *Observability) getLogs(fromDate, endDate time.Time, pos int64, maxLogLi
logEntryResponse.NextPos = pos
}

for k, v := range keys {
logEntryResponse.Keys = append(logEntryResponse.Keys, KeyValueTotal{
Key: k.Key,
Value: k.Value,
Total: v,
})
}
sort.Sort(logEntryResponse.Keys)

return logEntryResponse, nil
}

Expand Down
4 changes: 2 additions & 2 deletions pkg/observability/logs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ func TestFloatToDate(t *testing.T) {

func TestKeyValue(t *testing.T) {
logEntryResponse := LogEntryResponse{
Keys: map[KeyValue]int{
{Key: "k", Value: "v"}: 4,
Keys: KeyValueInt{
{Key: "k", Value: "v", Total: 4},
},
}
out, err := json.Marshal(logEntryResponse)
Expand Down
24 changes: 21 additions & 3 deletions pkg/observability/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,37 @@ type LogEntry struct {
Data string `json:"data"`
}

type KeyValueInt map[KeyValue]int
type KeyValueInt []KeyValueTotal

type KeyValueTotal struct {
Key string
Value string
Total int
}
type KeyValue struct {
Key string
Value string
}

func (kv KeyValueInt) MarshalJSON() ([]byte, error) {
res := "["
for k, v := range kv {
res += `{ "key" : "` + k.Key + `", "value": "` + k.Value + `", "total": ` + strconv.Itoa(v) + ` },`
for _, v := range kv {
res += `{ "key" : "` + v.Key + `", "value": "` + v.Value + `", "total": ` + strconv.Itoa(v.Total) + ` },`
}
res = strings.TrimRight(res, ",")
res += "]"
return []byte(res), nil
}

func (kv KeyValueInt) Len() int {
return len(kv)
}
func (kv KeyValueInt) Less(i, j int) bool {
if kv[i].Key == kv[j].Key {
return kv[i].Value < kv[j].Value
}
return kv[i].Key < kv[j].Key
}
func (kv KeyValueInt) Swap(i, j int) {
kv[i], kv[j] = kv[j], kv[i]
}
4 changes: 2 additions & 2 deletions webapp/.gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Logs
logs
/logs
*.log
npm-debug.log*
yarn-debug.log*
Expand Down Expand Up @@ -129,4 +129,4 @@ dist
.yarn/install-state.gz
.pnp.*

.DS_Store
.DS_Store
211 changes: 211 additions & 0 deletions webapp/src/Routes/Logs/Logs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { Card, Container, Text, Table, Title, Button, Grid, Select, Popover, Group, TextInput, rem, ActionIcon, Checkbox, Highlight} from "@mantine/core";
import { AppSettings } from "../../Constants/Constants";
import { useInfiniteQuery } from "@tanstack/react-query";
import { useAuthContext } from "../../Auth/Auth";
import { Link, useSearchParams } from "react-router-dom";
import { TbArrowRight, TbSearch, TbSettings } from "react-icons/tb";
import { DatePickerInput } from "@mantine/dates";
import { useEffect, useState } from "react";
import React from "react";

type LogsDataResponse = {
enabled: boolean;
logEntries: LogEntry[];
environments: string[];
nextPos: number;
keys: Keys[];
}
type LogEntry = {
data: string;
timestamp: string;
}
type Keys = {
key: string;
value: string;
total: number;
}
type Tag = {
key: string;
value: string;
}

function getDate(date:Date) {
var dd = String(date.getDate()).padStart(2, '0');
var mm = String(date.getMonth() + 1).padStart(2, '0'); //January is 0!
var yyyy = date.getFullYear();
return yyyy + "-" + mm + '-' + dd;
}

export function Logs() {
const {authInfo} = useAuthContext();
const timezoneOffset = new Date().getTimezoneOffset() * -1
const [currentQueryParameters] = useSearchParams();
const dateParam = currentQueryParameters.get("date")
const environmentParam = currentQueryParameters.get("environment")
const [tags, setTags] = useState<Tag[]>([])
const [search, setSearch] = useState<string>("")
const [searchParam, setSearchParam] = useState<string>("")
const [logsDate, setLogsDate] = useState<Date | null>(dateParam === null ? new Date() : new Date(dateParam));
const [environment, setEnvironment] = useState<string>(environmentParam === null ? "all" : environmentParam)
const { isPending, fetchNextPage, hasNextPage, error, data } = useInfiniteQuery<LogsDataResponse>({
queryKey: ['logs', environment, logsDate, tags, searchParam],
queryFn: async ({ pageParam }) =>
fetch(AppSettings.url + '/observability/logs?environment='+(environment === undefined || environment === "" ? "all" : environment)+'&fromDate='+(logsDate == undefined ? getDate(new Date()) : getDate(logsDate)) + '&endDate='+(logsDate == undefined ? getDate(new Date()) : getDate(logsDate)) + "&pos="+pageParam+"&offset="+timezoneOffset+"&tags="+encodeURIComponent(tags.map(t => t.key + "=" + t.value).join(","))+"&search="+encodeURIComponent(searchParam), {
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + authInfo.token
},
}).then((res) => {
return res.json()
}
),
initialPageParam: 0,
getNextPageParam: (lastRequest) => lastRequest.nextPos === -1 ? null : lastRequest.nextPos,
})

const captureEnter = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Enter") {
setSearchParam(search)
}
}

useEffect(() => {
const handleScroll = () => {
const { scrollTop, clientHeight, scrollHeight } =
document.documentElement;
if (scrollTop + clientHeight >= scrollHeight - 20) {
fetchNextPage();
}
};

window.addEventListener("scroll", handleScroll);
return () => {
window.removeEventListener("scroll", handleScroll);
};
}, [fetchNextPage])

if(isPending) return "Loading..."
if(error) return 'A backend error has occurred: ' + error.message

if(data.pages.length === 0 || !data.pages[0].enabled) { // show disabled page if not enabled
return (
<Container my={40}>
<Title ta="center" style={{marginBottom: 20}}>
Logs
</Title>
<Card withBorder radius="md" padding="xl" bg="var(--mantine-color-body)">
<Text fz="xs" tt="uppercase" fw={700} c="dimmed">
{ !data.pages[0].enabled ?
"Logs are not enabled."
:
null
}
</Text>
<Card.Section inheritPadding mt="sm" pb="md">
<Link to="/setup/vpn">
<Button leftSection={<TbSettings size={14} />} fz="sm" mt="md" radius="md" variant="default" size="sm">
Logs Settings
</Button>
</Link>
</Card.Section>
</Card>
</Container>
)
}

const rows = data.pages.map((group, groupIndex) => (
<React.Fragment key={groupIndex}>
{group.logEntries.map((row, i) => (
<Table.Tr key={i}>
<Table.Td>{row.timestamp}</Table.Td>
<Table.Td>{searchParam === "" ? row.data : <Highlight color="lime" highlight={searchParam}>{row.data}</Highlight>}</Table.Td>
</Table.Tr>
))}
</React.Fragment>
));
return (
<Container my={40} size="80rem">
<Title ta="center" style={{marginBottom: 20}}>
Logs
</Title>
<Grid>
<Grid.Col span={4}>
<TextInput
placeholder="Search..."
rightSectionWidth={30}
size="xs"
leftSection={<TbSearch style={{ width: rem(18), height: rem(18) }} />}
rightSection={
<ActionIcon size={18} radius="xl" variant="filled" onClick={() => setSearchParam(search)}>
<TbArrowRight style={{ width: rem(14), height: rem(14) }} />
</ActionIcon>
}
onKeyDown={(e) => captureEnter(e)}
onChange={(e) => setSearch(e.currentTarget.value)}
value={search}
/>
</Grid.Col>
<Grid.Col span={4}>
<DatePickerInput
value={logsDate}
onChange={setLogsDate}
size="xs"
/>
</Grid.Col>
<Grid.Col span={2}>
<Select
data={data.pages[0].environments.map((key, index) => {
return {
label: key,
value: index.toString(),
}
})}
size="xs"
withCheckIcon={false}
value={environment}
onChange={(_value) => setEnvironment(_value === null ? "" : _value)}
placeholder="Environment"
/>
</Grid.Col>
<Grid.Col span={2}>
<Popover width={300} position="bottom" withArrow shadow="md">
<Popover.Target>
<Button variant="default" size="xs">Filter</Button>
</Popover.Target>
<Popover.Dropdown>
{data.pages[0].keys.map((element) => {
return (
<Checkbox
key={element.key +"="+element.value}
label={element.key + " = " + element.value.substring(0, 10) + (element.value.length > 10 ? "..." : "") + " (" + element.total + ")"}
radius="xs"
size="xs"
style={{marginBottom: 3}}
onChange={(event) => event.currentTarget.checked ? setTags([...tags, {key: element.key, value: element.value }]) : setTags(tags.filter((tag) => { return tag.key !== element.key || tag.value !== element.value } ))}
checked={tags.some((tag) => tag.key === element.key && tag.value === element.value)}
/>
)
})}
</Popover.Dropdown>
</Popover>
</Grid.Col>
</Grid>
<Table>
<Table.Thead>
<Table.Tr key="heading">
<Table.Th>Timestamp</Table.Th>
<Table.Th>Log</Table.Th>
</Table.Tr>
</Table.Thead>
<Table.Tbody>
{rows}
</Table.Tbody>
</Table>
<Group justify="center">
{hasNextPage ? <Button onClick={() => fetchNextPage()} variant="default">Loading more...</Button> : null}
</Group>

</Container>

)
}

0 comments on commit 17651d9

Please sign in to comment.