-
Notifications
You must be signed in to change notification settings - Fork 3
React Animal History - Search By Id #1085
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
base: develop
Are you sure you want to change the base?
Conversation
| 8. Permissions: Folder Read Permission and dataset read permissions | ||
| 9. Metrics: Report and filter usage. |
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.
Are these requirements or non-requiremnents? I'm unclear on the break between them and the first seven.
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.
requirements
| - Shows read-only summary: "Viewing {count} animal(s): {subject1}, {subject2}, ..." | ||
| - Shows "Modify Search" button that switches to ID Search mode with current subjects pre-populated | ||
| - Reports filter by URL subjects without requiring ID resolution | ||
| - No ID limit applies (URL-provided subjects are assumed already validated/resolved) |
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.
If it's worth capping the limit in "normal" usage (for performance, I guess?), it seems like we shouldn't exempt URL-provided values. But I'm also not really clear on why this is a useful mode.
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 is the same UI as participant history which completely derives the filters from the URL. This is pretty much only used when clicking on links generated in the EHR, so we assume we don't need to validate the parameters. In the normal search we're just trying to scope the amount of Ids that can be filtered, but may need to revisit this as we get more testing done.
| }, [activeReport]); | ||
|
|
||
| const handleFilterChange = useCallback(( | ||
| newFilterType: 'idSearch' | 'all' | 'aliveAtCenter' | 'urlParams', |
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.
Seems like a lot of separate hard-codings of these 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.
These were moved to variables.
| 2. Likelihood: Medium | ||
| 3. Mitigation: | ||
| 1. Id resolution feedback for in UI. | ||
| 2. Implemented as experimental feature to be able to view old and new animal history results side-by-side. |
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 does this feature flag control? Being able to open the new and old versions without toggling an experimental flag seems like it would be more convenient. Or does the flag control which is the target URL by default for existing UI?
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 updated the spec. This is going to just be hidden with link in admin page for now.
| - Repeat with tab-separated and semicolon-separated lists | ||
|
|
||
| 4. **Multi-Animal Search (Mixed Separators)** | ||
| - Enter IDs using multiple separators in single input: "ID1, ID2\nID3;ID4\tID5" |
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.
Because IDs can contain special characters the test should try IDs with separator values (e.g. ID123,A).
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.
added
| - Verify validation error appears: "Maximum of 100 animal IDs allowed. You entered 101 IDs." | ||
| - Verify "Update Report" button is disabled | ||
| - Remove one ID to get back to 100 | ||
| - Verify error clears and "Update Report" button re-enables |
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.
Are IDs stored as string values? Would another boundary case be an ID that is a very long string, or MAXINT + 1?
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.
added
| 4. **Mixed Valid/Invalid IDs:** Resolve valid IDs; show invalid in "Not Found" section | ||
| 5. **All IDs Not Found:** Display "Not Found" section only; reports panel shows no data message | ||
| 6. **Alias Resolves to Same ID:** If multiple input values resolve to the same animal ID, show all in "Resolved" section but pass de-duplicated list to reports | ||
| 7. **Special Characters in IDs:** Support IDs with hyphens, underscores, and other special characters |
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.
It might be worthwhile to call out some explicit special characters to test. For example, any characters that could be interpreted as a comment(// ' #), string(" ') or something could have special meaning in a SQL query or a url (? @ :).
Also worthwhile to consider where in the ID the special character is. An ID that starts with @123, or '123 or an ID that looks like a link / email address ([email protected])
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.
added
| - Close browser, reopen bookmark | ||
| - Verify exactly 50 animals display correctly | ||
| - Verify no ID limit validation (URL Params mode bypasses 100 limit) | ||
| - Verify URL hash length doesn't cause browser issues |
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.
Another boundary case could be 10 IDs that are 1K characters long (or some other ridiculous size) and bookmark that.
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.
added
| private void clickFilterButton(String buttonText) | ||
| { | ||
| clickButton(buttonText); // "All Records", "Alive at Center", or "ID Search" | ||
| sleep(500); // Allow mode transition |
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'm not opposed to using sleeps, if they work that's great. Sometimes waiting for an element to go stale, the button or some element to show up can make the test a little more reliable and have a quicker run time. Something like:
shortWait().until(ExpectedConditions.stalenessOf(button));
Not sure if that will be applicable, but just a thought.
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 need to work through a few of these.
labkey-tchad
left a comment
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.
asserts are out of scope for page (and component) classes. Page classes should provide methods to inspect and control the page but test classes should assert functionality.
If these assertions would be useful elsewhere, you could put them into a separate helper class, maybe.
EHR_App/test/src/org/labkey/test/pages/ReactAnimalHistoryPage.java
Outdated
Show resolved
Hide resolved
EHR_App/test/src/org/labkey/test/pages/ReactAnimalHistoryPage.java
Outdated
Show resolved
Hide resolved
EHR_App/test/src/org/labkey/test/pages/ReactAnimalHistoryPage.java
Outdated
Show resolved
Hide resolved
EHR_App/test/src/org/labkey/test/pages/ReactAnimalHistoryPage.java
Outdated
Show resolved
Hide resolved
EHR_App/test/src/org/labkey/test/pages/ReactAnimalHistoryPage.java
Outdated
Show resolved
Hide resolved
EHR_App/test/src/org/labkey/test/pages/ReactAnimalHistoryPage.java
Outdated
Show resolved
Hide resolved
EHR_App/test/src/org/labkey/test/pages/ReactAnimalHistoryPage.java
Outdated
Show resolved
Hide resolved
…java Co-authored-by: Trey Chadick <[email protected]>
Thanks @labkey-tchad . I believe all your suggestions are implemented now or the code was factored out. Let me know if you have any other feedback. |
labkey-tchad
left a comment
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.
Looks like this change is causing some test failures. ONPRC tests are failing folder import.
VALIDATION ERROR: Query study.aliasIdMatches failed validation!
org.labkey.api.query.QueryNotFoundException: Error on line 8: Query or table not found: study.alias
labkey-alan
left a comment
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've left a lot of feedback. But in general we need to ensure that:
- We are properly memoizing things (especially functions used as callbacks)
- Our tests are actually testing things. A lot of the tests in this PR are not actually testing anything. I reviewed most tests, but not all, I suspect the tests I did not review are suffering from the same problems as the ones I did review.
- Interfaces should not be defined inline
- Prop types should be correct. Props that are always passed should be considered required, even if sometimes they're undefined. If something is sometimes undefined, say that in the type definition. If something is always defined, then we shouldn't treat is as sometimes not being defined (there's a lot of code that looks like
foo?.bar?.baz.()). Sometimes it's better to not render a component until all of the necessary props are defined, then you don't need any defensive code in the component, and there are less edge cases to handle.
We also need to rethink the component structure of the ReportTab and related components.
I also think that we shouldn't retain backwards compatibility with the hash URL, it adds a lot of code that we have to maintain, for very little benefit. We should use URL search params as they are intended to be used, not invent our own thing.
I also personally find this UI confusing. It doesn't make a lot of sense to have a text area with three buttons below it, but two of the buttons ignore the textarea. It seems to me like there should be something like a dropdown, or a radio group, where you select the filter mode. We should only render the textarea if the user selected "Search By Ids".
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 don't think there is any value in having a models folder with just an index.ts file with models defined. I would either:
- Get granular and split these model definitions up as needed into separate files in this folder
- Alternatively, get rid of the
modelsdirectory, instead move this file up one directory and rename it tomodels.ts
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.
Yeah since these are shared across files I just wanted them in a common place. I foresee more models being added in future stories and breaking up this file into multiple files. That's why I have it in a separate folder.
labkey-ui-ehr/src/ParticipantHistory/TabbedReportPanel/JSReportWrapper.tsx
Outdated
Show resolved
Hide resolved
| const { resolved, notFound } = resolutionResult; | ||
|
|
||
| // Separate direct matches from alias matches | ||
| const directMatches = resolved.filter(r => r.resolvedBy === 'direct'); |
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.
These variables should be memoized, you're creating a new array object every render cycle when filter is called, even if the resolved variable hasn't changed.
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 think memoizing this would actually raise the overhead. This is a pretty small calculation.
Also it's a pretty narrow use case (if at all) that this component would re-render with the same prop values (which would be the dependency on the useMemo). Most likely resolutionResult will have changed and these values will need to be recalculated any way.
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 component re-renders, and these values are re-computed, on every keystroke in the textarea, I have manually verified this. So it is actually called with the same value repeatedly, which is why I had suggested to use useMemo here. You could also use memo on the component itself, which should have the same effect as useMemo, since the incoming props would be compared against their previous values and only render when they change.
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.
Made component a memo
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'm not seeing that, you may need to push, but that is probably the best solution.
Thanks for the great review @labkey-alan. This is a large PR, I appreciate the time on it. Most of your feedback has been incorporated. I commented on anything that wasn't. For your specific points.
I commented on the structure of ReportTab and related components below. I think given the integration with JS and ExtJS outside the react lifecycle and the setup required to render the non-react reports, this is a clean approach. Some of the components are not typical react components, but it's not a typical react interface as it extends outside the normal lifecycle. The patterns used, render-props and headless components, are all established patterns in react. On the URL I think you're right. We probably just need to break compatibility. That may be a story all on it's own to ensure we get it all correct and all links in EHR and otherwise updated. Thanks for the feedback on the UI. I will look into making that more clear in the next story. |
|
I have a bunch of pending review comments, but I wanted to leave a comment at the top level here because it's less likely to get lost in the noise of everything else.
Here is the math I am doing in my head to determine if we should
Because the performance hit from the overhead is effectively never disruptive to us you are, generally speaking, better off just memoizing the thing. If you are in the habit of doing that, then the likelihood that you'll introduce a performance issue is low. It also saves you the cognitive burden of constantly having to think to yourself "is this case one that could get out of hand and cause issues down the road?". You can absolutely write your code such that you only ever memoize stuff when you know for a fact that you'll get a benefit that outweighs the (most definitely negligible) overhead cost of memoizing, but in order to write that code you'll need to manually verify every component you write, and I don't really think that is worth the effort, when the cost is so low. You also need to ensure that when you're verifying your code you're doing it with realistic data, which is definitely easier said than done. An example of this: we had a performance issue in the Domain Designer, but it wasn't noticeable until you added something like 15-20 fields to a domain. Once you broke that threshold typing in any input on a Domain Field was noticeably slow. We didn't know about this issue until someone had a local domain with enough fields to reproduce the problem. |
Yeah I understand where you're coming from on that. I added it as a topic in our next front end meeting so we're consistent in that approach. It's a good time to formalize some of these standards across the team. |
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 code looks better, especially the new useReportTab hook which I think accomplishes a clearer component hierarchy, and is more idiomatic. I left a bunch of specific feedback, but I have some general feedback as well:
- There are a ton of comments in the code. In general I am a fan of code comments, and think that LabKey as a whole (myself included) do a poor job of commenting our code. That said, I think you should consider taking a pass to look at all of the comments across the files in this PR and trim the very obvious ones, and especially trim the ones that say something like "extracted from ", I don't think those are useful at all. I commented on a few of these, but there are a lot more that I didn't comment on.
- There are still a lot of tests that setup what seems like valid scenarios, but then fail to assert anything useful. If we cannot properly assert something, then I struggle to believe the test is useful.
Edit: some of my comments are on outdated code because you pushed changes while I was reviewing, but GitHub then hid those comments from me when it came to review submission time, so I couldn't remove them. I suspect some of those comments are already completely irrelevant.
| * Report Type Constants | ||
| * Literal types for the discriminated union | ||
| */ | ||
| export type ReportType = 'js' | 'query' | 'report'; |
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 type isn't used, so you could remove it. You could also use it on ReportConfigBase by defining reportType: ReportType and the interfaces below it that extend ReportConfigBase would still be valid. I don't have a strong preference, other than unused stuff should be removed.
| notFound: [], | ||
| }; | ||
|
|
||
| render(<IdResolutionFeedback isVisible={true} resolutionResult={resolutionResult} />); |
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.
Need to update these tests to no longer pass the flag now that it's been removed.
| } | ||
|
|
||
| // Ensure the DOM element exists before rendering | ||
| const targetElement = document.getElementById(targetId); |
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.
Yes and the way to do that within a react app is generally to use a ref and check if ref.current is not undefined. I'm sure this works, it's just not what I would call idiomatic react code.
| @@ -43,4 +43,8 @@ export const QueryReportWrapper: FC<{ report: ReportConfig; tab: any }> = memo(( | |||
| }, [tab, report, container]); | |||
|
|
|||
| return null; | |||
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.
Hard disagree that it is an established or clean pattern to create components that never render anything. I have never seen that before. I have asked other people, and they have never seen that before. Happy to discuss.
|
|
||
| const ReportTab: FC<{ children: (tab: any) => React.ReactNode; filters: any; report: ReportConfig }> = ({ | ||
| report, | ||
| const TabbedReportPanelComponent: FC<TabbedReportPanelProps> = ({ |
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.
Yes it is a style we have across many of our components, and the advantage is that you don't need to have code that destructures your props on multiple lines, you can condense it into a single line.
| import { useReportTab } from './useReportTab'; | ||
| import { getTitleSuffix } from './JSReportWrapper'; | ||
|
|
||
| /** Props for OtherReportWrapper component */ |
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 don't think this comment, or any of the ones like it (QueryReportWrapper, JSReportWrapper, probably elsewhere), are useful. It's just noise.
|
|
||
| /** | ||
| * Hook that creates and manages an ExtJS report tab container. | ||
| * Extracted from the former ReportTab render-prop component. |
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.
In general I am in favor of comments like this that explain what a hook (or component) does. However, in this particular case I think it's not useful to provide Extracted from the former ReportTab render-prop component.. The rest of it seems appropriate.
| // The component should render the TabbedReportPanel with EHR.reports namespace | ||
| // This is verified indirectly by successful render | ||
| expect(screen.getByText('Loading reports...')).toBeVisible(); | ||
| await waitForReportsToLoad(); |
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.
Most of the tests in this file (34/55) only rely on await waitForReportsToLoad();, despite all of them having test names implying they're testing something specific, and some even having additional comments implying even more specific behavior is being tested. These two tests in particular (props passed to TabbedReportPanel) have the exact same test body, if you ignore the comments, I don't think that's particularly useful. We should be asserting something different for each test. The passes correct reportNamespace prop test in particular says the thing being tested is verified indirectly by successful render, but surely we could actually assert that the TabbedReportPanel is visible right?
Another example is the test case for readOnly mode, it's not asserting anything different than most of the tests in this file, but if you look at the implementation of the component we could easily assert that the SearchByIdPanel should not be visible, since we hide that in readOnly mode.
The tests in this file (and everywhere else) need to actually assert something. I don't think it is a useful test if the only thing being asserted is that the text "General" has appeared on the page somewhere. The coverage numbers don't matter if you aren't asserting anything in your tests.
If you can't actually assert something in the body of a test, then the test is probably worthless; maybe unit tests aren't appropriate for the particular test case.
| return context; | ||
| }; | ||
| // Fetch all visible reports once on mount | ||
| // This consolidates the query that was previously in TabbedReportPanel |
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 file has a lot of comments that feel overly verbose, but this one in particular I think is not helpful.
| } | ||
|
|
||
| .update-report-button { | ||
| padding: 10px 20px; |
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.
Something to consider: these button styles don't match any other button styles within LabKey. I think they look good, but it's odd that we have completely different styles for these buttons. I would consider using the bootstrap styles that align with what you're doing e.g. btn btn-primary, btn btn-default, etc.
Rationale
Implements the first phase of React conversion of Animal History. This is currently a hidden view until we get to MVP. Link in the ehrAdmin page. Participant history is available if you have the experimental feature on, you'll get a link at the top of the old participant view which comes up when you click on an Id.
Spec is in this PR
Changes