Skip to content

Commit

Permalink
Make ui filters generic
Browse files Browse the repository at this point in the history
Signed-off-by: Mohamed Abokammer <[email protected]>
  • Loading branch information
mahmednabil109 committed Sep 7, 2022
1 parent 7fc1455 commit aa296ee
Show file tree
Hide file tree
Showing 11 changed files with 4,029 additions and 3,779 deletions.
55 changes: 55 additions & 0 deletions cmds/admin_server/server/descripe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package server

import (
"fmt"
"reflect"
"strings"
)

const (
INT = "int"
UINT = "uint"
STRING = "string"
TIME = "time"
ENUM = "enum"
)

type FieldMetaData struct {
Name string `json:"name"`
Type string `json:"type"`
Values []string `json:"values"`
}

type Description []FieldMetaData

/*
DescribeEntity returns a Description for some entity fields
to help frontend renders appropiate filters and tables.
it depends on the tags assigned to the entity fields to create the needed metadata.
it expects tags: filter, values
*/
func DescribeEntity(entity interface{}) (Description, error) {
var describtion Description
t := reflect.TypeOf(entity)

for i := 0; i < t.NumField(); i++ {
sf := t.Field(i)
if sf.Anonymous {
return nil, fmt.Errorf("Error Anonymous Field")
}

name := sf.Tag.Get("json")
tagValue := sf.Tag.Get("filter")
var values []string
if tagValue == ENUM {
values = strings.Split(sf.Tag.Get("values"), ",")
}
describtion = append(describtion, FieldMetaData{
Name: name,
Type: tagValue,
Values: values,
})
}

return describtion, nil
}
30 changes: 24 additions & 6 deletions cmds/admin_server/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,15 @@ var (
DefaultDBAccessTimeout time.Duration = 10 * time.Second
)

/*
Query should have the same names as the fields in the entity that it queries(in this case Log).
To generalize filtering time fields (range filtering), it should have two fields with prefixes `start_<name>`, `end_<name>`
to make the frontend generate the query programmatically.
e.g. (Log.Date `date` -> Query.StartDate `start_date`, Query.EndDate `end_date`)
*/
type Query struct {
JobID *uint64 `form:"job_id"`
Text *string `form:"text"`
LogData *string `form:"log_data"`
LogLevel *string `form:"log_level"`
StartDate *time.Time `form:"start_date" time_format:"2006-01-02T15:04:05.000Z07:00"`
EndDate *time.Time `form:"end_date" time_format:"2006-01-02T15:04:05.000Z07:00"`
Expand All @@ -40,7 +46,7 @@ func (q *Query) ToStorageQuery() storage.Query {
}

storageQuery.JobID = q.JobID
storageQuery.Text = q.Text
storageQuery.LogData = q.LogData
storageQuery.LogLevel = q.LogLevel
storageQuery.StartDate = q.StartDate
storageQuery.EndDate = q.EndDate
Expand All @@ -57,10 +63,10 @@ func (q *Query) ToStorageQuery() storage.Query {
}

type Log struct {
JobID uint64 `json:"job_id"`
LogData string `json:"log_data"`
Date time.Time `json:"date"`
LogLevel string `json:"log_level"`
JobID uint64 `json:"job_id" filter:"uint"`
LogData string `json:"log_data" filter:"string"`
Date time.Time `json:"date" filter:"time"`
LogLevel string `json:"log_level" filter:"enum" values:"info,debug,error,fatal,panic,warning"`
}

func (l *Log) ToStorageLog() storage.Log {
Expand Down Expand Up @@ -159,6 +165,17 @@ func (r *RouteHandler) status(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"status": "live"})
}

//
func (r *RouteHandler) describeLog(c *gin.Context) {
res, err := DescribeEntity(Log{})
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"status": "err", "msg": "error while getting the storage descirbtion"})
return
}

c.JSON(http.StatusOK, res)
}

// addLogs inserts log's batches into the database
func (r *RouteHandler) addLogs(c *gin.Context) {
var logs []Log
Expand Down Expand Up @@ -277,6 +294,7 @@ func initRouter(ctx xcontext.Context, rh RouteHandler, middlewares []gin.Handler
r.GET("/log", rh.getLogs)
r.GET("/tag", rh.getTags)
r.GET("/tag/:name/jobs", rh.getJobs)
r.GET("/log-description", rh.describeLog)

// serve the frontend app
r.StaticFS("/app", FS(false))
Expand Down
7,360 changes: 3,725 additions & 3,635 deletions cmds/admin_server/server/static.go

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions cmds/admin_server/storage/mongo/mongo.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ func toMongoQuery(query storage.Query) bson.D {
})
}

if query.Text != nil {
if query.LogData != nil {
q = append(q, bson.E{
Key: "log_data",
Value: bson.M{
"$regex": primitive.Regex{Pattern: *query.Text, Options: "ig"},
"$regex": primitive.Regex{Pattern: *query.LogData, Options: "ig"},
},
})
}
Expand Down
2 changes: 1 addition & 1 deletion cmds/admin_server/storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ type Log struct {
// Query defines the different options to filter with
type Query struct {
JobID *uint64
Text *string
LogData *string
LogLevel *string
StartDate *time.Time
EndDate *time.Time
Expand Down
47 changes: 22 additions & 25 deletions cmds/admin_server/ui/src/api/logs.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,36 @@
import superagent from 'superagent';

// TODO: remove the hardcoded levels
// logLevels is the possible levels for logs
export const Levels = ['panic', 'fatal', 'error', 'warning', 'info', 'debug'];

// Log defines the expected log entry returned from the api
export interface Log {
job_id: number;
log_data: string;
log_level: string;
date: string;
}

// Query defines all the possible filters to form a query
export interface Query {
job_id?: number;
text?: string;
log_level?: string;
start_date?: string;
end_date?: string;
page: number;
page_size: number;
}

// Result defines the structure of the api response to a query
export interface Result {
logs: Log[] | null;
logs: any;
count: number;
page: number;
page_size: number;
}

// getLogs returns Result that contains logs fetched according to the Query
export async function getLogs(query: Query): Promise<Result> {
export async function getLogs(query: any): Promise<Result> {
let result: superagent.Response = await superagent.get('/log').query(query);

return result.body;
}

export enum FieldType {
INT = 'int',
UINT = 'uint',
STRING = 'string',
TIME = 'time',
ENUM = 'enum',
}

export interface FieldMetaData {
name: string;
type: string;
values: string[];
}

export async function getLogDescription(): Promise<FieldMetaData[]> {
let result: superagent.Response = await superagent.get('/log-description');

return result.body;
}
62 changes: 24 additions & 38 deletions cmds/admin_server/ui/src/search_logs/log_table/log_table.tsx
Original file line number Diff line number Diff line change
@@ -1,34 +1,37 @@
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { Table, Pagination, Button, useToaster, Message } from 'rsuite';
import { Column, Cell, HeaderCell } from 'rsuite-table';
import { StandardProps } from 'rsuite-table/lib/@types/common';
import { getLogs, Log, Result } from '../../api/logs';
import { TypeAttributes } from 'rsuite/esm/@types/common';
import DateCell from './date_cell/date_cell';
import { renderColumn } from './render_columns';
import {
getLogs,
getLogDescription,
Result,
FieldMetaData,
} from '../../api/logs';
import 'rsuite/dist/rsuite.min.css';
import './log_table.scss';

export interface LogTableProps extends StandardProps {
logLevels?: string;
queryText?: string;
jobID?: number;
startDate?: Date;
endDate?: Date;
columns?: FieldMetaData[];
filters?: any;
}

export default function LogTable({
logLevels,
queryText,
jobID,
startDate,
endDate,
}: LogTableProps) {
export default function LogTable({ columns, filters }: LogTableProps) {
const [loading, setLoading] = useState<boolean>(false);
const [logs, setLogs] = useState<Log[] | null>([]);
const [logs, setLogs] = useState<any[] | null>([]);
const [count, setCount] = useState<number>(0);
const [page, setPage] = useState<number>(0);
const [limit, setLimit] = useState<number>(20);

const renderedColumns = useMemo(
() =>
columns
?.sort((a, b) => (a.type.length > b.type.length ? 1 : -1))
.map((c, idx) => renderColumn(c, idx)),
[columns]
);

const toaster = useToaster();
const pageSizes = [20, 50, 100];

Expand All @@ -41,16 +44,14 @@ export default function LogTable({
);
};
const updateLogsTable = async (page: number, limit: number) => {
getLogDescription();

setLoading(true);
try {
let result: Result = await getLogs({
job_id: jobID ?? undefined,
text: queryText,
page: page,
page_size: limit,
log_level: logLevels,
start_date: startDate?.toJSON(),
end_date: endDate?.toJSON(),
...filters,
});

setLogs(result.logs);
Expand Down Expand Up @@ -83,22 +84,7 @@ export default function LogTable({
wordWrap="break-word"
rowHeight={30}
>
<Column width={80} align="center" fixed>
<HeaderCell>JobID</HeaderCell>
<Cell className="log-table__cell" dataKey="job_id" />
</Column>
<Column width={250} align="center" fixed>
<HeaderCell>Date</HeaderCell>
<DateCell className="log-table__cell" dataKey="date" />
</Column>
<Column width={80} align="center" fixed>
<HeaderCell>Level</HeaderCell>
<Cell className="log-table__cell" dataKey="log_level" />
</Column>
<Column width={600} align="left" flexGrow={1}>
<HeaderCell>Data</HeaderCell>
<Cell className="log-table__cell" dataKey="log_data" />
</Column>
{renderedColumns}
</Table>
<div>
<Pagination
Expand Down
35 changes: 35 additions & 0 deletions cmds/admin_server/ui/src/search_logs/log_table/render_columns.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React from 'react';
import { FieldMetaData, FieldType } from '../../api/logs';
import { Column, Cell, HeaderCell } from 'rsuite-table';
import DateCell from './date_cell/date_cell';

export function renderColumn(field: FieldMetaData, key: any) {
switch (field.type) {
case FieldType.INT:
case FieldType.UINT:
case FieldType.ENUM:
return (
<Column width={80} align="center" fixed key={key}>
<HeaderCell>{field.name}</HeaderCell>
<Cell className="log-table__cell" dataKey={field.name} />
</Column>
);
case FieldType.STRING:
return (
<Column width={600} align="left" flexGrow={1} key={key}>
<HeaderCell>{field.name}</HeaderCell>
<Cell className="log-table__cell" dataKey={field.name} />
</Column>
);
case FieldType.TIME:
return (
<Column width={250} align="center" fixed key={key}>
<HeaderCell>{field.name}</HeaderCell>
<DateCell
className="log-table__cell"
dataKey={field.name}
/>
</Column>
);
}
}
Loading

0 comments on commit aa296ee

Please sign in to comment.