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

Kevin yoo/hmp 157 add error messages to workspace creation dialog 10 datasets protected datasets selected #3215

Conversation

kevinyooky
Copy link
Contributor

@kevinyooky kevinyooky commented Aug 4, 2023

This branch is the main branch for this feature. I have chained other branches from this one to prevent PRs from getting too large.

image

Copy link
Contributor Author

@kevinyooky kevinyooky Aug 4, 2023

Choose a reason for hiding this comment

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

The additional space in between the quotes was creating an empty gap in the dialog.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was to fix the build error which required the provider wrapping around savedList in order to access the store.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The SelectableTableProvider component should be usable in place of the raw Provider and createStore calls: https://github.com/hubmapconsortium/portal-ui/blob/main/context/app/static/js/shared-styles/tables/SelectableTableProvider/SelectableTableProvider.jsx#L6

In general though, I'm pretty sure this should be the same store instance that the selected list is saved in; otherwise, you have two instances of the state that may not necessarily be synchronized.

The instance of the zustand store that the saved entities table uses is created as part of the default export: https://github.com/hubmapconsortium/portal-ui/tree/main/context/app/static/js/components/savedLists/SavedEntitiesTable/SavedEntitiesTable.jsx#L130

I think it would make sense to import the version of SavedEntitiesTable that doesn't have that provider exported with it in this file so that you can guarantee the list and error logic are using the same store instance.

@@ -28,6 +30,11 @@ function DialogModal({
</Typography>
</StyledDialogTitle>
<DialogContent>
{errorMessage && (
<Alert $marginBottom={20} severity="error">
Copy link
Contributor Author

@kevinyooky kevinyooky Aug 4, 2023

Choose a reason for hiding this comment

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

sx prop for marginBottom did not work so passed in marginBottom prop for Alert

Copy link
Collaborator

Choose a reason for hiding this comment

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

Setting the marginBottom works fine here, but I'd like to shed some light on why sx didn't work here and how our upcoming changes will make it work in more places.

The imported Alert component is a styled-component with an explicit $marginBottom prop. The sx prop's style is thus overwritten by the styles defined for that component (if marginBottom is not in the props, then the margin-bottom: 0px style is still applied as it's currently implemented).

https://github.com/hubmapconsortium/portal-ui/tree/main/context/app/static/js/shared-styles/alerts/index.js

Once we convert these sorts of components to be handled via MUI's styled util or via theme, we'll be able to use the sx prop in more places.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes sense.

@kevinyooky kevinyooky requested a review from NickAkhmetov August 4, 2023 23:45
Copy link
Collaborator

@NickAkhmetov NickAkhmetov left a comment

Choose a reason for hiding this comment

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

Great progress! A few minor notes on my end.

@@ -28,6 +30,11 @@ function DialogModal({
</Typography>
</StyledDialogTitle>
<DialogContent>
{errorMessage && (
<Alert $marginBottom={20} severity="error">
Copy link
Collaborator

Choose a reason for hiding this comment

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

Setting the marginBottom works fine here, but I'd like to shed some light on why sx didn't work here and how our upcoming changes will make it work in more places.

The imported Alert component is a styled-component with an explicit $marginBottom prop. The sx prop's style is thus overwritten by the styles defined for that component (if marginBottom is not in the props, then the margin-bottom: 0px style is still applied as it's currently implemented).

https://github.com/hubmapconsortium/portal-ui/tree/main/context/app/static/js/shared-styles/alerts/index.js

Once we convert these sorts of components to be handled via MUI's styled util or via theme, we'll be able to use the sx prop in more places.

Comment on lines 46 to 48
selectedRows.size > 10
? `You have selected ${selectedRows.size} datasets. Workspaces currently only supports up to 10 datasets. Please unselect datasets.`
: null
Copy link
Collaborator

Choose a reason for hiding this comment

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

Great job on this change - this should make it a lot easier for us to provide future error messages/feedback in this style.

On a somewhat more optional note: consider defining errorMessage (and other conditional props in the future) as a variable on the line right after you get selectedRows:

const { selectedRows } = useStore();
const errorMessage = selectedRows.size > 10 ? 'you have selected ...' : null;
  errorMessage={errorMessage}

This approach helps with readability when ternary statements are longer than a single line.

Copy link
Collaborator

Choose a reason for hiding this comment

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

The SelectableTableProvider component should be usable in place of the raw Provider and createStore calls: https://github.com/hubmapconsortium/portal-ui/blob/main/context/app/static/js/shared-styles/tables/SelectableTableProvider/SelectableTableProvider.jsx#L6

In general though, I'm pretty sure this should be the same store instance that the selected list is saved in; otherwise, you have two instances of the state that may not necessarily be synchronized.

The instance of the zustand store that the saved entities table uses is created as part of the default export: https://github.com/hubmapconsortium/portal-ui/tree/main/context/app/static/js/components/savedLists/SavedEntitiesTable/SavedEntitiesTable.jsx#L130

I think it would make sense to import the version of SavedEntitiesTable that doesn't have that provider exported with it in this file so that you can guarantee the list and error logic are using the same store instance.

…space-creation-dialog-10-datasets-protected-datasets-selected
@NickAkhmetov
Copy link
Collaborator

Updated demos

Create Workspace dialog with both errors visible:

image

Dismissing the errors reactivates the workspace name field and the launch button:

2023-08-18.workspace.error.workflow.demo.mov

Storybook demo for the four snackbar variants, which can be triggered from anywhere on the site:

2023-08-18.snackbar.story.demo.mov

Selecting a single protected dataset properly handles the non-plural case in the error message:

image

@NickAkhmetov
Copy link
Collaborator

TODO: Switch over vitessce visualization toasts from sharing config and from viewing vitessce in firefox

Copy link
Collaborator

@john-conroy john-conroy left a comment

Choose a reason for hiding this comment

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

Thanks for picking this up! The only blocking comment is the one made in CreateWorkspaceDialog - everything else you can take or leave.

Comment on lines 5 to 8
'../app/static/js/shared-styles/**/*.stories.js',
'../app/static/js/shared-styles/**/*.stories.tsx',
'../app/static/js/components/**/*.stories.js',
'../app/static/js/components/**/*.stories.tsx',
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
'../app/static/js/shared-styles/**/*.stories.js',
'../app/static/js/shared-styles/**/*.stories.tsx',
'../app/static/js/components/**/*.stories.js',
'../app/static/js/components/**/*.stories.tsx',
'../app/static/js/**/*.stories.@(js|jsx|ts|tsx)'

Perhaps this could simplify it?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Good call!

Comment on lines 42 to 47
datasets: (rowCount) =>
`You have selected ${rowCount} datasets. Workspaces currently only supports up to 10 datasets. Please unselect datasets.`,
protectedDataset: (rows) =>
`You have selected a protected dataset (${rows[0]._source.hubmap_id}). Workspaces currently only supports published public datasets. To remove the protected dataset from workspace creation, click the “Remove Protected Datasets” button below or return to the previous screen to manually remove this dataset.`,
protectedDatasets: (rows) =>
`You have selected ${rows.length} protected datasets. Workspaces currently only supports published public datasets. To remove protected datasets from this workspace creation, click the “Remove Protected Datasets” button below or return to the previous screen to manually remove those datasets.`,
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we just pass rows for consistency? Both datasets and protectedDatasets depend on the length of the rows array.

Copy link
Collaborator

Choose a reason for hiding this comment

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

I decided to pass the size here initially because selectedRows is a set, while protectedRows is an array; I didn't like rows.size in one case and rows.length in the other.

I've applied this revision but modified the arg names and added an explanatory comment to ensure clarity re: why this difference is here.

Comment on lines 55 to 71
const { selectedRows } = useSelectableTableStore();
const protectedRows = useDatasetsAccessLevel(selectedRows.size > 0 ? [...selectedRows] : []).datasets;
const containsProtectedDataset = protectedRows.length > 0;

const errorMessages = [];

if (selectedRows.size > 10) {
errorMessages.push(errorHelper.datasets(selectedRows.size));
}

if (containsProtectedDataset) {
if (protectedRows.length === 1) {
errorMessages.push(errorHelper.protectedDataset(protectedRows));
} else {
errorMessages.push(errorHelper.protectedDatasets(protectedRows));
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

We could add a wrapper component around CreateWorkspaceDialog and push all of that logic there.

@@ -220,6 +220,8 @@ function createSearchkitFacet({ field, identifier, label, type, ...rest }) {
});
}

const defaultFields = createField({ fieldName: 'mapped_data_access_level', label: 'mapped_data_access_level' });
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we still need this? I remember Kevin and I introduced this at some point, but later moved towards making a request with useDatasetAccessLevel.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for the context - I initially left this intact since I wasn't familiar with what this was doing, and have removed it after testing that it doesn't impact functionality.

Comment on lines 23 to 32

const protectedHubmapIds = protectedRows?.map((row) => row._source.hubmap_id).join(', ');
const { toastSuccess } = useSnackbarStore();
const { deselectRows } = useSelectableTableStore();

const removeProtectedDatasets = () => {
deselectRows(protectedRows.map((r) => r._id));
toastSuccess('Protected datasets successfully removed from selection.');
};

Copy link
Collaborator

Choose a reason for hiding this comment

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

CreateWorkspaceDialog is used outside of the search page on both the workspaces page and datasets pages. I don't believe this component will have access to the SelectableTableProvider store. We should move this to a component around CreateWorkspaceDialog for use on the search page.

Copy link
Collaborator

@NickAkhmetov NickAkhmetov Aug 22, 2023

Choose a reason for hiding this comment

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

I've addressed this and the MetadataMenu comment accordingly; I've created a CreateWorkspaceWithDatasetsDialog component which wraps the CreateWorkspaceDialog component and performs error checking. The field is then passed in to the CreateWorkspaceDialog via a renderProp which will also allow us to add any other additional fields we may require in the future.

@NickAkhmetov
Copy link
Collaborator

Following the discussion during yesterday's planning meeting, I have converted the Vitessce snackbars to use the new toast mechanism for consistency. I was not able to trigger the long URL condition for the vitessce copy functionality (to my understanding, modern browsers all support 8k+ URL lengths and this functionality dates back to IE only allowing 2048 characters) but the logic in the code is set to trigger a warning if a long URL is detected.

I've also set the Vitessce Firefox performance warning to only pop up once for each user via a localStorage key. This functionality was particularly annoying on publication pages where it would pop up every time the expanded vignette changed.

2023-08-22.vitessce.snackbar.updates.mov

I'll start work on addressing John's feedback.

Comment on lines +55 to +63
const useSnackbarActions = () => {
return useStore((store) => ({
toastError: store.toastError,
toastInfo: store.toastInfo,
toastWarning: store.toastWarning,
toastSuccess: store.toastSuccess,
closeSnackbar: store.closeSnackbar,
}));
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

I lean towards not selecting the entire state by default as each component will update when any of the variables change even if the component does not depend on them. We could implement something like https://docs.pmnd.rs/zustand/guides/auto-generating-selectors if writing selectors ourselves is too much boilerplate.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for the heads up! I agree completely about not selecting the entire state (since that would be more or less identical to using context). The auto generating selectors functionality seems very useful, and would be a solid improvement for us to make once we're on zustand v4.

TL;DR: - I can take this helper out and replace it with specific selectors where needed, but I'm not sure how much benefit that would bring in this case since these are just functions.

To my understanding, since the keys accessed/returned by this helper are functions, they shouldn't be updating at all; the idea was that this helper would let us skip making a separate selector for each case where we just need to be able to send a snackbar message without necessarily needing to read it. Additionally, if zustand functions are regenerated on each state update, wouldn't they all be regenerated regardless of whether they're accessed separately or all together in a helper like this?

Comment on lines +70 to +83
// Show a warning if the user is using Firefox.
useEffect(() => {
if (sniffBrowser() === 'Firefox' && !localStorage.getItem(localStorageFirefoxWarningKey)) {
toastError(FIREFOX_WARNING);
localStorage.setItem(localStorageFirefoxWarningKey, true);
}
}, [toastError]);

// Show instructions for exiting full screen mode when the user enters full screen mode.
useEffect(() => {
if (vizIsFullscreen) {
toastInfo('Press [esc] to exit full window.');
}
}, [vizIsFullscreen, toastInfo]);
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think I prefer to pull related hooks together into a single hook, but it's entirely a preference. Do you prefer to have everything in a single file?

Copy link
Collaborator

Choose a reason for hiding this comment

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

It's on a case-by-case basis for me, but I lean towards keeping hooks focused on handling a single functionality/behavior:

  • If there's an interdependent process where separating it leads to the output of one function being fed into another function, I do like to unify those into one on at least some level (e.g. if the SWR output from one fetch is used to set the params of a second fetch, it's helpful to have a single hook which abstracts that detail out from the component, but still keep the other two SWR calls as their own hooks)
  • If it's two different conditions that trigger similar effects, I still keep them separated in order to keep the code maintainable and prevent unnecessary computations
    • RE: maintainable - as time goes on, combining more and more business logic into one hook/into one function leads to big fragile monoliths where
    • RE: unnecessary computations - in this case, combining these two useEffects into one would lead to extra sniffBrowser executions/potential extra localStorage reads whenever the visualization is full-screened; these are trivial operations, but in other circumstances these could be e.g. lookups from an API.

john-conroy
john-conroy previously approved these changes Sep 11, 2023
…space-creation-dialog-10-datasets-protected-datasets-selected
@NickAkhmetov
Copy link
Collaborator

Note: the fixes in this branch coincide with the changes in #3258 as I initially didn't realize the lint issues came from upstream. If this branch is merged in, we can close that PR.

…space-creation-dialog-10-datasets-protected-datasets-selected
john-conroy
john-conroy previously approved these changes Sep 19, 2023
…space-creation-dialog-10-datasets-protected-datasets-selected
@NickAkhmetov NickAkhmetov merged commit 98a59f5 into main Sep 19, 2023
6 of 7 checks passed
@NickAkhmetov NickAkhmetov deleted the kevin-yoo/HMP-157-Add-error-messages-to-workspace-creation-dialog-10-datasets-protected-datasets-selected branch September 19, 2023 16:51
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