Skip to content
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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

mahmednabil109
Copy link
Contributor

@mahmednabil109 mahmednabil109 commented Sep 6, 2022

Signed-off-by: Mohamed Abokammer mahmednabil109@gmail.com

Goal:

  • to make the filters in the frontend rendered dynamically based on the backend entity (in this case
    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:

  • the rendered ui does not have much changes.

Screenshot 2022-09-07 at 11 12 15

@mahmednabil109 mahmednabil109 changed the title Make ui filters generic Move filters' SOT to the backend Sep 6, 2022
@mahmednabil109 mahmednabil109 force-pushed the generic-filtering branch 3 times, most recently from ac21969 to b3db303 Compare September 6, 2022 14:44
@mahmednabil109 mahmednabil109 marked this pull request as ready for review September 6, 2022 14:59
@mahmednabil109 mahmednabil109 changed the title Move filters' SOT to the backend Move filters' source of truth to the backend Sep 6, 2022
Copy link
Member

@mimir-d mimir-d left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please:

  1. add PR description of the problem you're solving (not just the title), and how you're solving it
  2. add some screenshots since youre also modifying UI
  3. 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"`
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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)
Copy link
Member

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
Copy link
Member

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")
Copy link
Member

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)

Copy link
Contributor Author

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.

Copy link
Member

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"`
Copy link
Member

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

Copy link
Contributor Author

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.

Copy link
Member

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"})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return describtion, nil
return description, nil

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

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> {
Copy link
Member

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).

Copy link
Contributor Author

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 ?

Copy link
Member

@xaionaro xaionaro Sep 7, 2022

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.

Comment on lines +9 to +20
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"`
Copy link
Member

@xaionaro xaionaro Sep 6, 2022

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.

Comment on lines +25 to +30
/*
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
*/
Copy link
Member

@xaionaro xaionaro Sep 6, 2022

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>
Comment on lines +31 to +54
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
Copy link
Member

@xaionaro xaionaro Sep 7, 2022

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.

Comment on lines +24 to +34
useEffect(() => {
(async () => {
try {
let res = await getLogDescription();
setDescription(res);
} catch (err) {
console.log(err);
showMsg('error', err?.message);
}
})();
}, []);
Copy link
Member

@xaionaro xaionaro Sep 7, 2022

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants