-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Move filters' source of truth to the backend #148
base: main
Are you sure you want to change the base?
Conversation
ac21969
to
b3db303
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please:
- add PR description of the problem you're solving (not just the title), and how you're solving it
- add some screenshots since youre also modifying UI
- reconsider the code design in the UI (or overall) since using
any
all over the place completely denies the type safety of typescript
type Query struct { | ||
JobID *uint64 `form:"job_id"` | ||
Text *string `form:"text"` | ||
LogData *string `form:"log_data"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what was wrong with text
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the frontend will form the query dynamically based on the metadata that is sent from the backend. so it's easier ( for the frontend ) to make the query fields match the log struct fields.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i actually dont understand your answer. Are there hidden dependencies in this chain? Do you expect to have some field names matching stuff that it shouldn't? (based on your "it's easier for the frontend to make the query fields match")
@@ -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) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this probably sits better under /log/metadata
or similar
@@ -0,0 +1,55 @@ | |||
package server |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
filename typo
} | ||
|
||
name := sf.Tag.Get("json") | ||
tagValue := sf.Tag.Get("filter") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you figure out these data types automatically? you already have that access.
also "filter" isnt really conducive to this usage. you might want to use a different name, if it's still necessary (as per point above)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you figure out these data types automatically? you already have that access.
the tag fitler:"<type>"
does not necessary match the field type, it refers to the type of frontend component that should be rendered to apply filters on that field. e.g. we could have x map[string]int `filter:"string"
to search on the map keys for example.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
you should document this. it's not obvious. also not obvious how that map/string example works.
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"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how do you make sure these hardcoded values actually match the data? what if the logged data has "INFORMATION" as level, for example; and you only give the user the option "info" as per this values tag
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
okay, i will make a db distinct query to get the values for the enum filters.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
make sure you understand what the cost of that is. you may end up hammering the db with metadata related requests, combined with missing indices and you probably get a whole server crash or something.
caching would solve some things here (with write-thru), but it's probably too much to do in the remaining time span.
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"}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
c.JSON(http.StatusInternalServerError, gin.H{"status": "err", "msg": "error while getting the storage descirbtion"}) | |
c.JSON(http.StatusInternalServerError, gin.H{"status": "err", "msg": "error while getting the storage description"}) |
}) | ||
} | ||
|
||
return describtion, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
return describtion, nil | |
return description, nil |
type FieldMetaData struct { | ||
Name string `json:"name"` | ||
Type string `json:"type"` | ||
Values []string `json:"values"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
to note, in this design, the Values
field only makes sense for a particular value of Type
. you might wanna think about using different FieldMetaData
struct variants or some other design without this possibility of invalid state (as in, having a non-nil Values
when Type=="int"
is invalid)
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> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
usage of any
really isnt ok without any kind of structural context. can you write this in a different way? (page and page_size seem to be constant, so those need to exist from the start in the type).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the query will be constructed based on the meta date that is fetched from the backend. i don't know how i can modify that to not use any ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it should be an array of arbitrary fields+values instead of any.
[just thinking out loud] I actually like GraphQL, because everything is already invented there and we do not need to reinvent a query language and reimplement it in there. But on the other hand it is much more complicated.
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"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
warning
This all requires description.
/* | ||
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 | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nitpicking
DescribeEntity returns a Description for some entity fields to help frontend renders appropiate filters and tables.
Would be nice to have an example of such entity (which is supposed to be used here).
Signed-off-by: Mohamed Abokammer <mahmednabil109@gmail.com>
b3db303
to
aa296ee
Compare
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
opinion
I understand the initial idea (of why we decided to use reflection in the first place) is that this is generic logic (not coupled with the server
), thus it seems like it should be a dedicated package. The same thought from another angle: basically the whole problem with pushing the source of truth of available (for filtering) values to the backend is pretty typical; may be the solution of this specific problem should be agnostic of ConTest-specifics (including of these admin_server
specifics) and thus be separated from package server
.
useEffect(() => { | ||
(async () => { | ||
try { | ||
let res = await getLogDescription(); | ||
setDescription(res); | ||
} catch (err) { | ||
console.log(err); | ||
showMsg('error', err?.message); | ||
} | ||
})(); | ||
}, []); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As @mimir-d already mentioned, it feels a bit dangerous. It could overload the DB. Would be nice to find some server-side solution for that (like caching DB response).
Signed-off-by: Mohamed Abokammer mahmednabil109@gmail.com
Goal:
log) that we are applying the filters on, So that when any change is to be made to the backend log structure (like adding new fields to the log struct) the frontend will not need to be modified.
Frontend: