Open Bobgy opened 3 years ago
/cc @zijianjoy @StefanoFioravanzo
Another partially related problem is the Page abstraction: https://github.com/kubeflow/pipelines/blob/1f32e90ecd1fe8657c084d623b2fcd23ead13c48/frontend/src/pages/Page.tsx#L40-L99
This is a standard KFP page:
Some UI elements are common:
In order to reuse this design and logic for all KFP pages, they are built as root of router in https://github.com/kubeflow/pipelines/blob/1f32e90ecd1fe8657c084d623b2fcd23ead13c48/frontend/src/components/Router.tsx#L230.
This is a reasonable choice, because in the DOM tree, these items are close to the root. So callbacks to control these common elements are passed using the Page interface to each page in a route.
updateToolbar
, updateBanner
are passed to Page. The interface for controlling toolbar state is imperative, rather than declarative. After each state update, we need to call updateToolbar
in a child component. e.g. _selectionChanged handler.Because
Therefore, I have never thought a refactoring is of enough priority, considering the number of other more meaningful ways we can improve KFP.
From my past experience, more idiomatic way of implementing these is:
Benefits:
Note, I think these only apply to Banners, Toolbars, Breadcrumbs and Title. Snackbar and dialogs might need to be rendered across pages, so they are probably not a good fit.
If we agree on the ideas, what we can do is building new pages using this more idiomatic paradigm. I still don't think it's worth it to rewrite the entire KFP UI just for this refactoring. If there are other reasons we need to build/rebuild pages, that will be good chances to start applying the new practices.
Thank you for the detailed explanation of the concept! I agree with using render props for reusable and portable components on the common UI elements on the top of pages. One question: The UI elements listed are unified across different pages, except buttons in toolbar. Each page will have different set of buttons, and based on user action on children element, these buttons might change on the same page. How does each layer look like if we apply render props for button elements?
@zijianjoy I built a quick demo: https://codesandbox.io/s/magical-violet-hwfxb?file=/src/App.js
Notice how the Layout component defines layout of the page, while App component can inject stuff into the Layout component dynamically using either render prop or react elements as props.
So that, back to KFP UI, we can define a common PageLayout component that gets reused across each Page, and the pages can control what buttons to render in layout slots.
@Bobgy Thank you so much Yuan for building the sample demo!
To confirm if I understand it correctly, here is a simplified example for KFP UI - RunList:
<Page>
<ExperimentDetail>
<PageLayout />
<RunsList>
<CustomTable />
</RunList>
</ExperimentDetail>
</Page>
In the above layout, RunsList
determines what buttons to show (for example, Archive/Activate/Clone runs), and CustomTable
determines which buttons are enabled/disabled because of user selection, and RunsList
collects run selection callback from CustomTable
. ExperimentDetail
Collects button information from RunsList
callbacks, and updates PageLayout
accordingly. In such case, can I assume that ExperimentDetail
determines what buttons should show in PageLayout
? Because ExperimentDetail
collects all button related information from children, and use render props to render PageLayout
.
As reference, the Page
component looks like below:
render() {
return (
<div>
<ExperimentDetail render={buttonState => (
<PageLayout buttonState={buttonState} />
)}/>
</div>
);
}
<ExperimentDetail>
<PageLayout />
<RunsList>
<CustomTable />
</RunList>
</ExperimentDetail>
Page is unnecessary, files in the pages folder are already pages.
The following understanding has a key difference, because button management can be very dynamic, the most declarative way is to move state that can affect buttons up to the page level and render the buttons in layout from e.g. move state in RunList up to ExperimentDetails's state, and render buttons directly from ExperimentDetails. https://reactjs.org/docs/lifting-state-up.html
Moving state up might sound daunting at first, but React has also built React hooks, they allow making abstractions on state logic, so that moving some state can be as simple as moving a hook call from child to parent and pass needed props to the child.
I agree with moving the state up to parent so the parent has all the information it needs to determine buttons arrangement. Thank you Yuan!
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
/lifecycle frozen
https://reactjs.org/docs/refs-and-the-dom.html It's recommended to avoid using ref when possible, because the data/event flow mechanism conflicts with React's recommendation of one-way data flow philosophy (see https://reactjs.org/docs/thinking-in-react.html).
Similar to this example, I think to avoid exposing
refresh()
as a public method, we could have a prop called sth likerefreshCounter: number
, the child components can do a refresh whenever it sees an update in refreshCounter -- usingcomponentDidUpdate
oruseEffect react hook
. In this way, we no longer need to add refs all the way and trigger a method to refresh components, one refresh would be as simple as a state update to the refreshCounter. We'd also need aonLoad
callback, that will be triggered by child components when a page finishes loading.After avoiding using ref to get component instances, we can write components using functions and react hooks: https://reactjs.org/docs/hooks-overview.html. It's a nice way to make abstraction on state changes (and a lot more!).
_Originally posted by @Bobgy in https://github.com/kubeflow/pipelines/pull/5040#discussion_r569956232_