diff --git a/backend/dataall/modules/s3_datasets_shares/tasks/dataset_subscription_task.py b/backend/dataall/modules/s3_datasets_shares/tasks/dataset_subscription_task.py index ac37d633d..e382b05ef 100644 --- a/backend/dataall/modules/s3_datasets_shares/tasks/dataset_subscription_task.py +++ b/backend/dataall/modules/s3_datasets_shares/tasks/dataset_subscription_task.py @@ -18,6 +18,9 @@ from dataall.modules.s3_datasets_shares.tasks.subscriptions import poll_queues from dataall.modules.s3_datasets.db.dataset_repositories import DatasetRepository from dataall.modules.s3_datasets.db.dataset_models import DatasetStorageLocation, DatasetTable, S3Dataset +from dataall.modules.datasets_base.db.dataset_models import DatasetBase +from dataall.modules.shares_base.db.share_object_models import ShareObject +from dataall.modules.shares_base.services.share_notification_service import DataSharingNotificationType root = logging.getLogger() root.setLevel(logging.INFO) @@ -130,15 +133,25 @@ def publish_sns_message(self, session, message, dataset, share_items, prefix, ta response = sns_client.publish_dataset_message(message) log.info(f'SNS update publish response {response}') - notifications = ShareNotificationService( - session=session, dataset=dataset, share=share_object - ).notify_new_data_available_from_owners(s3_prefix=prefix) + notifications = self.notify_new_data_available_from_owners( + session=session, dataset=dataset, share=share_object, s3_prefix=prefix + ) log.info(f'Notifications for share owners {notifications}') except ClientError as e: log.error(f'Failed to deliver message {message} due to: {e}') + @staticmethod + def notify_new_data_available_from_owners(session, dataset: DatasetBase, share: ShareObject, s3_prefix: str): + msg = ( + f'New data (at {s3_prefix}) is available from dataset {dataset.datasetUri} shared by owner {dataset.owner}' + ) + notifications = ShareNotificationService(session=session, dataset=dataset, share=share).register_notifications( + notification_type=DataSharingNotificationType.DATASET_VERSION.value, msg=msg + ) + return notifications + if __name__ == '__main__': ENVNAME = os.environ.get('envname', 'local') diff --git a/backend/dataall/modules/shares_base/db/share_object_repositories.py b/backend/dataall/modules/shares_base/db/share_object_repositories.py index f77da2841..84b2f90d4 100644 --- a/backend/dataall/modules/shares_base/db/share_object_repositories.py +++ b/backend/dataall/modules/shares_base/db/share_object_repositories.py @@ -67,7 +67,7 @@ def get_share_item_by_uri(session, uri): return share_item @staticmethod - def get_share_item_details(session, share_type_model, item_uri): # TODO CHECK THAT IT WORKS + def get_share_item_details(session, share_type_model, item_uri): return session.query(share_type_model).get(item_uri) @staticmethod diff --git a/backend/dataall/modules/shares_base/db/share_state_machines_repositories.py b/backend/dataall/modules/shares_base/db/share_state_machines_repositories.py index ddb5d9ae9..1b9905dcf 100644 --- a/backend/dataall/modules/shares_base/db/share_state_machines_repositories.py +++ b/backend/dataall/modules/shares_base/db/share_state_machines_repositories.py @@ -48,6 +48,17 @@ def get_share_items_states(session, share_uri, item_uris=None): query = query.filter(ShareObjectItem.shareItemUri.in_(item_uris)) return [item.status for item in query.distinct(ShareObjectItem.status)] + @staticmethod + def get_share_items_health_states(session, share_uri, item_uris=None): + query = session.query(ShareObjectItem).filter( + and_( + ShareObjectItem.shareUri == share_uri, + ) + ) + if item_uris: + query = query.filter(ShareObjectItem.shareItemUri.in_(item_uris)) + return [item.healthStatus for item in query.distinct(ShareObjectItem.healthStatus)] + @staticmethod def update_share_object_status(session, share_uri: str, status: str) -> ShareObject: share = ShareObjectRepository.get_share_by_uri(session, share_uri) @@ -78,7 +89,7 @@ def update_share_item_status_batch( and_(ShareObjectItem.shareUri == share_uri, ShareObjectItem.status == old_status) ) if share_item_type: - query = query.filter(ShareObjectItem.shareableType == share_item_type.value) + query = query.filter(ShareObjectItem.itemType == share_item_type.value) query.update( { diff --git a/backend/dataall/modules/shares_base/services/share_item_service.py b/backend/dataall/modules/shares_base/services/share_item_service.py index a4d231978..9f27b6121 100644 --- a/backend/dataall/modules/shares_base/services/share_item_service.py +++ b/backend/dataall/modules/shares_base/services/share_item_service.py @@ -81,6 +81,9 @@ def revoke_items_share_object(uri, revoked_uris): share = ShareObjectRepository.get_share_by_uri(session, uri) dataset = DatasetBaseRepository.get_dataset_by_uri(session, share.datasetUri) revoked_items_states = ShareStatusRepository.get_share_items_states(session, uri, revoked_uris) + revoked_items_health_states = ShareStatusRepository.get_share_items_health_states( + session, uri, revoked_uris + ) revoked_items = [ShareObjectRepository.get_share_item_by_uri(session, uri) for uri in revoked_uris] if not revoked_items_states: @@ -89,6 +92,12 @@ def revoke_items_share_object(uri, revoked_uris): message='Nothing to be revoked.', ) + if ShareItemHealthStatus.PendingReApply.value in revoked_items_health_states: + raise UnauthorizedOperation( + action='Revoke Items from Share Object', + message='Cannot revoke while reapply pending for one or more items.', + ) + share_sm = ShareObjectSM(share.status) new_share_state = share_sm.run_transition(ShareObjectActions.RevokeItems.value) @@ -124,26 +133,16 @@ def add_shared_item(uri: str, data: dict = None): item_type = data.get('itemType') item_uri = data.get('itemUri') share = ShareObjectRepository.get_share_by_uri(session, uri) - target_environment = EnvironmentService.get_environment_by_uri(session, share.environmentUri) share_sm = ShareObjectSM(share.status) new_share_state = share_sm.run_transition(ShareItemActions.AddItem.value) share_sm.update_state(session, share, new_share_state) + processor = ShareProcessorManager.get_processor_by_item_type(item_type) item = ShareObjectRepository.get_share_item_details(session, processor.shareable_type, item_uri) if not item: raise ObjectNotFound('ShareObjectItem', item_uri) - if ( - item_type == ShareableType.Table.value and item.region != target_environment.region - ): # TODO Part10: remove from here (we might be able to remove get_share_item_details entirely - raise UnauthorizedOperation( - action=ADD_ITEM, - message=f'Lake Formation cross region sharing is not supported. ' - f'Table {item.itemUri} is in {item.region} and target environment ' - f'{target_environment.name} is in {target_environment.region} ', - ) - share_item: ShareObjectItem = ShareObjectRepository.find_sharable_item(session, uri, item_uri) if not share_item: @@ -163,17 +162,6 @@ def add_shared_item(uri: str, data: dict = None): def remove_shared_item(uri: str): with get_context().db_engine.scoped_session() as session: share_item = ShareObjectRepository.get_share_item_by_uri(session, uri) - if ( - share_item.itemType == ShareableType.Table.value # TODO Part10 - REMOVE - and share_item.status == ShareItemStatus.Share_Failed.value - ): - share = ShareObjectRepository.get_share_by_uri(session, share_item.shareUri) - ResourcePolicyService.delete_resource_policy( - session=session, - group=share.groupUri, - resource_uri=share_item.itemUri, - ) - item_sm = ShareItemSM(share_item.status) item_sm.run_transition(ShareItemActions.RemoveItem.value) ShareObjectRepository.remove_share_object_item(session, share_item) @@ -184,9 +172,7 @@ def remove_shared_item(uri: str): def resolve_shared_item(uri, item: ShareObjectItem): with get_context().db_engine.scoped_session() as session: processor = ShareProcessorManager.get_processor_by_item_type(item.itemType) - return ShareObjectRepository.get_share_item_details( - session, processor.shareable_type, item.itemUri - ) # TODO - check it works + return ShareObjectRepository.get_share_item_details(session, processor.shareable_type, item.itemUri) @staticmethod def check_existing_shared_items(share): diff --git a/backend/dataall/modules/shares_base/services/share_notification_service.py b/backend/dataall/modules/shares_base/services/share_notification_service.py index 197b706f4..765138af9 100644 --- a/backend/dataall/modules/shares_base/services/share_notification_service.py +++ b/backend/dataall/modules/shares_base/services/share_notification_service.py @@ -49,7 +49,7 @@ def notify_share_object_submission(self, email_id: str): subject = f'Data.all | Share Request Submitted for {self.dataset.label}' email_notification_msg = msg + share_link_text - notifications = self._register_notifications( + notifications = self.register_notifications( notification_type=DataSharingNotificationType.SHARE_OBJECT_SUBMITTED.value, msg=msg ) @@ -64,7 +64,7 @@ def notify_share_object_approval(self, email_id: str): subject = f'Data.all | Share Request Approved for {self.dataset.label}' email_notification_msg = msg + share_link_text - notifications = self._register_notifications( + notifications = self.register_notifications( notification_type=DataSharingNotificationType.SHARE_OBJECT_APPROVED.value, msg=msg ) @@ -86,21 +86,13 @@ def notify_share_object_rejection(self, email_id: str): subject = f'Data.all | Share Request Rejected / Revoked for {self.dataset.label}' email_notification_msg = msg + share_link_text - notifications = self._register_notifications( + notifications = self.register_notifications( notification_type=DataSharingNotificationType.SHARE_OBJECT_REJECTED.value, msg=msg ) self._create_notification_task(subject=subject, msg=email_notification_msg) return notifications - def notify_new_data_available_from_owners(self, s3_prefix): # TODO part10: remove, this is specific for S3 - msg = f'New data (at {s3_prefix}) is available from dataset {self.dataset.datasetUri} shared by owner {self.dataset.owner}' - - notifications = self._register_notifications( - notification_type=DataSharingNotificationType.DATASET_VERSION.value, msg=msg - ) - return notifications - def _get_share_object_targeted_users(self): targeted_users = list() targeted_users.append(self.dataset.SamlAdminGroupName) @@ -109,7 +101,7 @@ def _get_share_object_targeted_users(self): targeted_users.append(self.share.groupUri) return targeted_users - def _register_notifications(self, notification_type, msg): + def register_notifications(self, notification_type, msg): """ Notifications sent to: - dataset.SamlAdminGroupName diff --git a/deploy/custom_resources/custom_authorizer/requirements.txt b/deploy/custom_resources/custom_authorizer/requirements.txt index 6eca03d80..930779231 100644 --- a/deploy/custom_resources/custom_authorizer/requirements.txt +++ b/deploy/custom_resources/custom_authorizer/requirements.txt @@ -7,4 +7,4 @@ python-jose==3.3.0 requests==2.32.2 rsa==4.9 six==1.16.0 -urllib3==1.26.18 \ No newline at end of file +urllib3==1.26.19 \ No newline at end of file diff --git a/frontend/src/design/components/ShareHealthStatus.js b/frontend/src/design/components/ShareHealthStatus.js new file mode 100644 index 000000000..983536f67 --- /dev/null +++ b/frontend/src/design/components/ShareHealthStatus.js @@ -0,0 +1,72 @@ +import VerifiedUserOutlinedIcon from '@mui/icons-material/VerifiedUserOutlined'; +import GppBadOutlinedIcon from '@mui/icons-material/GppBadOutlined'; +import PendingOutlinedIcon from '@mui/icons-material/PendingOutlined'; +import DangerousOutlinedIcon from '@mui/icons-material/DangerousOutlined'; +import * as PropTypes from 'prop-types'; +import { Typography } from '@mui/material'; +import { Label } from './Label'; + +export const ShareHealthStatus = (props) => { + const { status, healthStatus, lastVerificationTime } = props; + + const isShared = ['Revoke_Failed', 'Share_Succeeded'].includes(status); + const isHealthPending = ['PendingReApply', 'PendingVerify', null].includes( + healthStatus + ); + const setStatus = () => { + if (!healthStatus) return 'Undefined'; + return healthStatus; + }; + + const setColor = () => { + if (!healthStatus) return 'info'; + if (['Healthy'].includes(healthStatus)) return 'success'; + if (['Unhealthy'].includes(healthStatus)) return 'error'; + if (isHealthPending) return 'warning'; + return 'info'; + }; + + const setIcon = () => { + if (!healthStatus) return ; + if (['Healthy'].includes(healthStatus)) + return ; + if (['Unhealthy'].includes(healthStatus)) + return ; + if (['PendingReApply', 'PendingVerify'].includes(healthStatus)) + return ; + return ; + }; + + if (!isShared) { + return ( + + {'Item is not Shared'} + + ); + } + + return ( +
+ {setIcon()} + + {!isHealthPending && ( + + {(lastVerificationTime && + '(' + + lastVerificationTime.substring( + 0, + lastVerificationTime.indexOf('.') + ) + + ')') || + ''} + + )} +
+ ); +}; + +ShareHealthStatus.propTypes = { + status: PropTypes.string.isRequired, + healthStatus: PropTypes.string, + lastVerificationTime: PropTypes.string +}; diff --git a/frontend/src/design/components/index.js b/frontend/src/design/components/index.js index cd01380bb..8b5ce3bc0 100644 --- a/frontend/src/design/components/index.js +++ b/frontend/src/design/components/index.js @@ -19,6 +19,7 @@ export * from './Scrollbar'; export * from './SearchInput'; export * from './SettingsDrawer'; export * from './ShareStatus'; +export * from './ShareHealthStatus'; export * from './SplashScreen'; export * from './StackStatus'; export * from './TagsInput'; diff --git a/frontend/src/modules/Dashboards/views/DashboardImportForm.js b/frontend/src/modules/Dashboards/views/DashboardImportForm.js index 50fbc3ac4..83afbc2c9 100644 --- a/frontend/src/modules/Dashboards/views/DashboardImportForm.js +++ b/frontend/src/modules/Dashboards/views/DashboardImportForm.js @@ -8,12 +8,10 @@ import { CardContent, CardHeader, Chip, - CircularProgress, Container, FormHelperText, Grid, Link, - MenuItem, TextField, Typography } from '@mui/material'; @@ -31,13 +29,9 @@ import { useSettings } from 'design'; import { SET_ERROR, useDispatch } from 'globalErrors'; -import { - listEnvironmentGroups, - listValidEnvironments, - searchGlossary, - useClient -} from 'services'; +import { searchGlossary, useClient } from 'services'; import { importDashboard } from '../services'; +import { EnvironmentTeamDropdown } from 'modules/Shared'; const DashboardImportForm = (props) => { const navigate = useNavigate(); @@ -45,31 +39,8 @@ const DashboardImportForm = (props) => { const dispatch = useDispatch(); const client = useClient(); const { settings } = useSettings(); - const [loading, setLoading] = useState(true); - const [groupOptions, setGroupOptions] = useState([]); - const [environmentOptions, setEnvironmentOptions] = useState([]); const [selectableTerms, setSelectableTerms] = useState([]); - const fetchEnvironments = useCallback(async () => { - setLoading(true); - const response = await client.query( - listValidEnvironments({ - filter: Defaults.selectListFilter - }) - ); - if (!response.errors) { - setEnvironmentOptions( - response.data.listValidEnvironments.nodes.map((e) => ({ - ...e, - value: e.environmentUri, - label: e.label - })) - ); - } else { - dispatch({ type: SET_ERROR, error: response.errors[0].message }); - } - setLoading(false); - }, [client, dispatch]); const fetchTerms = useCallback(async () => { const response = await client.query( searchGlossary(Defaults.selectListFilter) @@ -95,37 +66,11 @@ const DashboardImportForm = (props) => { }, [client, dispatch]); useEffect(() => { if (client) { - fetchEnvironments().catch((e) => - dispatch({ type: SET_ERROR, error: e.message }) - ); fetchTerms().catch((e) => dispatch({ type: SET_ERROR, error: e.message }) ); } - }, [client, fetchTerms, fetchEnvironments, dispatch]); - - const fetchGroups = async (environmentUri) => { - try { - const response = await client.query( - listEnvironmentGroups({ - filter: Defaults.selectListFilter, - environmentUri - }) - ); - if (!response.errors) { - setGroupOptions( - response.data.listEnvironmentGroups.nodes.map((g) => ({ - value: g.groupUri, - label: g.groupUri - })) - ); - } else { - dispatch({ type: SET_ERROR, error: response.errors[0].message }); - } - } catch (e) { - dispatch({ type: SET_ERROR, error: e.message }); - } - }; + }, [client, fetchTerms, dispatch]); async function submit(values, setStatus, setSubmitting, setErrors) { try { @@ -136,7 +81,7 @@ const DashboardImportForm = (props) => { dashboardId: values.dashboardId, environmentUri: values.environment.environmentUri, description: values.description, - SamlGroupName: values.SamlGroupName, + SamlGroupName: values.SamlAdminGroupName, tags: values.tags, terms: values.terms.nodes ? values.terms.nodes.map((t) => t.nodeUri) @@ -168,9 +113,6 @@ const DashboardImportForm = (props) => { dispatch({ type: SET_ERROR, error: err.message }); } } - if (loading) { - return ; - } return ( <> @@ -239,7 +181,7 @@ const DashboardImportForm = (props) => { label: '', dashboardId: '', description: '', - SamlGroupName: '', + SamlAdminGroupName: '', environment: '', tags: [], terms: [] @@ -252,7 +194,7 @@ const DashboardImportForm = (props) => { .max(255) .required('*QuickSight dashboard identifier is required'), description: Yup.string().max(5000), - SamlGroupName: Yup.string() + SamlAdminGroupName: Yup.string() .max(255) .required('*Team is required'), environment: Yup.object().required('*Environment is required'), @@ -392,94 +334,13 @@ const DashboardImportForm = (props) => { - - - - { - setFieldValue('SamlGroupName', ''); - fetchGroups( - event.target.value.environmentUri - ).catch((e) => - dispatch({ type: SET_ERROR, error: e.message }) - ); - setFieldValue('environment', event.target.value); - }} - select - value={values.environment} - variant="outlined" - > - {environmentOptions.map((environment) => ( - - {environment.label} - - ))} - - - - - - - - - - - {groupOptions.map((group) => ( - - {group.label} - - ))} - - - + {errors.submit && ( {errors.submit} diff --git a/frontend/src/modules/DatasetsBase/components/DatasetListItem.js b/frontend/src/modules/DatasetsBase/components/DatasetListItem.js index 432883398..85726a435 100644 --- a/frontend/src/modules/DatasetsBase/components/DatasetListItem.js +++ b/frontend/src/modules/DatasetsBase/components/DatasetListItem.js @@ -41,7 +41,7 @@ export const DatasetListItem = (props) => { variant="h6" onClick={() => { navigate( - dataset.datasetType === 'DatasetType.S3' + dataset.datasetType === 'DatasetTypes.S3' ? `/console/s3-datasets/${dataset.datasetUri}` : '-' ); @@ -208,7 +208,7 @@ export const DatasetListItem = (props) => { color="primary" component={RouterLink} to={ - dataset.datasetType === 'DatasetType.S3' + dataset.datasetType === 'DatasetTypes.S3' ? `/console/s3-datasets/${dataset.datasetUri}` : '-' } diff --git a/frontend/src/modules/Environments/components/EnvironmentOwnedDatasets.js b/frontend/src/modules/Environments/components/EnvironmentOwnedDatasets.js index 1ed875a3f..5ef920184 100644 --- a/frontend/src/modules/Environments/components/EnvironmentOwnedDatasets.js +++ b/frontend/src/modules/Environments/components/EnvironmentOwnedDatasets.js @@ -150,7 +150,7 @@ export const EnvironmentOwnedDatasets = ({ environment }) => { { navigate( - dataset.datasetType === 'DatasetType.S3' + dataset.datasetType === 'DatasetTypes.S3' ? `/console/s3-datasets/${dataset.datasetUri}` : '-' ); diff --git a/frontend/src/modules/Environments/views/EnvironmentCreateForm.js b/frontend/src/modules/Environments/views/EnvironmentCreateForm.js index 17de06d94..bbe55e522 100644 --- a/frontend/src/modules/Environments/views/EnvironmentCreateForm.js +++ b/frontend/src/modules/Environments/views/EnvironmentCreateForm.js @@ -17,7 +17,6 @@ import { Grid, IconButton, Link, - MenuItem, Switch, TextField, Typography @@ -73,9 +72,7 @@ const EnvironmentCreateForm = (props) => { const [trustedAccount, setTrustedAccount] = useState(null); const [pivotRoleName, setPivotRoleName] = useState(null); const [loading, setLoading] = useState(true); - const groupOptions = groups - ? groups.map((g) => ({ value: g, label: g })) - : []; + const fetchItem = useCallback(async () => { setLoading(true); const response = await client.query(getOrganization(params.uri)); @@ -175,7 +172,7 @@ const EnvironmentCreateForm = (props) => { organizationUri: organization.organizationUri, AwsAccountId: values.AwsAccountId, label: values.label, - SamlGroupName: values.SamlGroupName, + SamlGroupName: values.SamlAdminGroupName, tags: values.tags, description: values.description, region: values.region, @@ -506,7 +503,7 @@ const EnvironmentCreateForm = (props) => { initialValues={{ label: '', description: '', - SamlGroupName: '', + SamlAdminGroupName: '', AwsAccountId: '', region: '', tags: [], @@ -525,7 +522,7 @@ const EnvironmentCreateForm = (props) => { .max(255) .required('*Environment name is required'), description: Yup.string().max(5000), - SamlGroupName: Yup.string() + SamlAdminGroupName: Yup.string() .max(255) .required('*Team is required'), AwsAccountId: Yup.number( @@ -873,27 +870,37 @@ const EnvironmentCreateForm = (props) => { /> - { + if (value) { + setFieldValue('SamlAdminGroupName', value); + } else { + setFieldValue('SamlAdminGroupName', ''); + } + }} + inputValue={values.SamlAdminGroupName} + renderInput={(params) => ( + )} - helperText={ - touched.SamlGroupName && errors.SamlGroupName - } - onChange={handleChange} - select - value={values.SamlGroupName} - variant="outlined" - > - {groupOptions.map((group) => ( - - {group.label} - - ))} - + /> { const client = useClient(); const { settings } = useSettings(); const groups = useGroups(); - const groupOptions = groups - ? groups.map((g) => ({ value: g, label: g })) - : []; async function submit(values, setStatus, setSubmitting, setErrors) { try { @@ -42,7 +39,7 @@ const GlossaryCreateForm = (props) => { createGlossary({ label: values.label, readme: values.readme, - admin: values.admin + admin: values.SamlAdminGroupName }) ); @@ -212,21 +209,32 @@ const GlossaryCreateForm = (props) => { option.value)} + id="SamlAdminGroupName" + disablePortal + options={groups} onChange={(event, value) => { - setFieldValue('admin', value); + if (value) { + setFieldValue('SamlAdminGroupName', value); + } else { + setFieldValue('SamlAdminGroupName', ''); + } }} + inputValue={values.SamlAdminGroupName} renderInput={(params) => ( )} diff --git a/frontend/src/modules/MLStudio/views/MLStudioCreateForm.js b/frontend/src/modules/MLStudio/views/MLStudioCreateForm.js index 0f4c13e34..74206dc88 100644 --- a/frontend/src/modules/MLStudio/views/MLStudioCreateForm.js +++ b/frontend/src/modules/MLStudio/views/MLStudioCreateForm.js @@ -6,18 +6,15 @@ import { Card, CardContent, CardHeader, - CircularProgress, Container, FormHelperText, Grid, Link, - MenuItem, TextField, Typography } from '@mui/material'; import { Formik } from 'formik'; import { useSnackbar } from 'notistack'; -import { useCallback, useEffect, useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { Link as RouterLink, useNavigate } from 'react-router-dom'; import * as Yup from 'yup'; @@ -25,17 +22,13 @@ import { ArrowLeftIcon, ChevronRightIcon, ChipInput, - Defaults, useSettings } from 'design'; import { SET_ERROR, useDispatch } from 'globalErrors'; -import { - listEnvironmentGroups, - listValidEnvironments, - useClient -} from 'services'; +import { useClient } from 'services'; import { createSagemakerStudioUser } from '../services'; +import { EnvironmentTeamDropdown } from 'modules/Shared'; const MLStudioCreateForm = (props) => { const navigate = useNavigate(); @@ -43,57 +36,6 @@ const MLStudioCreateForm = (props) => { const dispatch = useDispatch(); const client = useClient(); const { settings } = useSettings(); - const [loading, setLoading] = useState(true); - const [groupOptions, setGroupOptions] = useState([]); - const [environmentOptions, setEnvironmentOptions] = useState([]); - const fetchEnvironments = useCallback(async () => { - setLoading(true); - const response = await client.query( - listValidEnvironments({ filter: Defaults.selectListFilter }) - ); - if (!response.errors) { - setEnvironmentOptions( - response.data.listValidEnvironments.nodes.map((e) => ({ - ...e, - value: e.environmentUri, - label: e.label - })) - ); - } else { - dispatch({ type: SET_ERROR, error: response.errors[0].message }); - } - setLoading(false); - }, [client, dispatch]); - const fetchGroups = async (environmentUri) => { - try { - const response = await client.query( - listEnvironmentGroups({ - filter: Defaults.selectListFilter, - environmentUri - }) - ); - if (!response.errors) { - setGroupOptions( - response.data.listEnvironmentGroups.nodes.map((g) => ({ - value: g.groupUri, - label: g.groupUri - })) - ); - } else { - dispatch({ type: SET_ERROR, error: response.errors[0].message }); - } - } catch (e) { - dispatch({ type: SET_ERROR, error: e.message }); - } - }; - useEffect(() => { - if (client) { - fetchEnvironments().catch((e) => - dispatch({ type: SET_ERROR, error: e.message }) - ); - } - }, [client, dispatch, fetchEnvironments]); - async function submit(values, setStatus, setSubmitting, setErrors) { try { const response = await client.mutate( @@ -130,10 +72,6 @@ const MLStudioCreateForm = (props) => { setSubmitting(false); } } - if (loading) { - return ; - } - return ( <> @@ -282,31 +220,6 @@ const MLStudioCreateForm = (props) => { - - - {groupOptions.map((group) => ( - - {group.label} - - ))} - - { - - - - { - setFieldValue('SamlGroupName', ''); - fetchGroups( - event.target.value.environmentUri - ).catch((e) => - dispatch({ type: SET_ERROR, error: e.message }) - ); - setFieldValue('environment', event.target.value); - }} - select - value={values.environment} - variant="outlined" - > - {environmentOptions.map((environment) => ( - - {environment.label} - - ))} - - - - - - - - - + {errors.submit && ( {errors.submit} diff --git a/frontend/src/modules/Notebooks/views/NotebookCreateForm.js b/frontend/src/modules/Notebooks/views/NotebookCreateForm.js index 2d256fc2d..b433b5fe2 100644 --- a/frontend/src/modules/Notebooks/views/NotebookCreateForm.js +++ b/frontend/src/modules/Notebooks/views/NotebookCreateForm.js @@ -394,45 +394,52 @@ const NotebookCreateForm = (props) => { - { + option)} + onChange={(event, value) => { setFieldValue('SamlAdminGroupName', ''); - fetchGroups( - event.target.value.environmentUri - ).catch((e) => - dispatch({ type: SET_ERROR, error: e.message }) - ); - setFieldValue('environment', event.target.value); - setVpcOptions( - event.target.value.networks.map((v) => ({ - ...v, - value: v, - label: v.VpcId - })) - ); + if (value && value.environmentUri) { + setFieldValue('environment', value); + fetchGroups(value.environmentUri).catch((e) => + dispatch({ + type: SET_ERROR, + error: e.message + }) + ); + setVpcOptions( + value.networks.map((v) => ({ + ...v, + value: v, + label: v.VpcId + })) + ); + } else { + setFieldValue('environment', ''); + setVpcOptions([]); + setGroupOptions([]); + } }} - select - value={values.environment} - variant="outlined" - > - {environmentOptions.map((environment) => ( - - {environment.label} - - ))} - + renderInput={(params) => ( + + )} + /> { label="Region" name="region" value={ - values.environment + values.environment && values.environment.region ? values.environment.region : '' } @@ -455,7 +462,8 @@ const NotebookCreateForm = (props) => { label="Organization" name="organization" value={ - values.environment + values.environment && + values.environment.organization ? values.environment.organization.label : '' } @@ -463,29 +471,59 @@ const NotebookCreateForm = (props) => { /> - option)} + onChange={(event, value) => { + if (value && value.value) { + setFieldValue( + 'SamlAdminGroupName', + value.value + ); + } else { + setFieldValue('SamlAdminGroupName', ''); + } + }} + inputValue={values.SamlAdminGroupName} + renderInput={(params) => ( + + {groupOptions.length > 0 ? ( + + ) : ( + + )} + )} - helperText={ - touched.SamlAdminGroupName && - errors.SamlAdminGroupName - } - label="Team" - name="SamlAdminGroupName" - onChange={handleChange} - select - value={values.SamlAdminGroupName} - variant="outlined" - > - {groupOptions.map((group) => ( - - {group.label} - - ))} - + /> @@ -558,7 +596,6 @@ const NotebookCreateForm = (props) => { - {errors.submit && ( {errors.submit} diff --git a/frontend/src/modules/Omics/views/OmicsRunCreateForm.js b/frontend/src/modules/Omics/views/OmicsRunCreateForm.js index cdf5dc4e3..fbf62acad 100644 --- a/frontend/src/modules/Omics/views/OmicsRunCreateForm.js +++ b/frontend/src/modules/Omics/views/OmicsRunCreateForm.js @@ -14,22 +14,17 @@ import { FormHelperText, Grid, Link, - MenuItem, TextField, Typography } from '@mui/material'; import { Helmet } from 'react-helmet-async'; import { LoadingButton } from '@mui/lab'; import React, { useCallback, useEffect, useState } from 'react'; -import { - useClient, - listEnvironmentGroups, - listValidEnvironments, - listS3DatasetsOwnedByEnvGroup -} from 'services'; +import { useClient } from 'services'; import { getOmicsWorkflow, createOmicsRun } from '../services'; -import { ArrowLeftIcon, ChevronRightIcon, Defaults, useSettings } from 'design'; +import { ArrowLeftIcon, ChevronRightIcon, useSettings } from 'design'; import { SET_ERROR, useDispatch } from 'globalErrors'; +import { EnvironmentTeamDatasetsDropdown } from 'modules/Shared'; const OmicsRunCreateForm = (props) => { const params = useParams(); @@ -54,88 +49,6 @@ const OmicsRunCreateForm = (props) => { setLoading(false); }, [client, dispatch, params.uri]); - const [groupOptions, setGroupOptions] = useState([]); - const [environmentOptions, setEnvironmentOptions] = useState([]); - const [currentEnv, setCurrentEnv] = useState(''); - const [datasetOptions, setDatasetOptions] = useState([]); - const fetchEnvironments = useCallback(async () => { - setLoading(true); - const response = await client.query( - listValidEnvironments({ filter: Defaults.SelectListFilter }) - ); - if (!response.errors) { - setEnvironmentOptions( - response.data.listValidEnvironments.nodes.map((e) => ({ - ...e, - value: e.environmentUri, - label: e.label - })) - ); - } else { - dispatch({ type: SET_ERROR, error: response.errors[0].message }); - } - setLoading(false); - }, [client, dispatch]); - - const fetchGroups = async (environmentUri) => { - setCurrentEnv(environmentUri); - try { - const response = await client.query( - listEnvironmentGroups({ - filter: Defaults.SelectListFilter, - environmentUri - }) - ); - if (!response.errors) { - setGroupOptions( - response.data.listEnvironmentGroups.nodes.map((g) => ({ - value: g.groupUri, - label: g.groupUri - })) - ); - } else { - dispatch({ type: SET_ERROR, error: response.errors[0].message }); - } - } catch (e) { - dispatch({ type: SET_ERROR, error: e.message }); - } - }; - - const fetchDatasets = async (groupUri) => { - let ownedDatasets = []; - try { - const response = await client.query( - listS3DatasetsOwnedByEnvGroup({ - filter: Defaults.SelectListFilter, - environmentUri: currentEnv, - groupUri: groupUri - }) - ); - if (!response.errors) { - ownedDatasets = response.data.listS3DatasetsOwnedByEnvGroup.nodes?.map( - (dataset) => ({ - value: dataset.datasetUri, - label: dataset.label - }) - ); - } else { - dispatch({ type: SET_ERROR, error: response.errors[0].message }); - } - } catch (e) { - dispatch({ type: SET_ERROR, error: e.message }); - } - setDatasetOptions(ownedDatasets); - }; - - useEffect(() => { - if (client) { - fetchEnvironments().catch((e) => - dispatch({ type: SET_ERROR, error: e.message }) - ); - fetchItem().catch((e) => dispatch({ type: SET_ERROR, error: e.message })); - } - }, [client, dispatch, fetchEnvironments, fetchItem]); - useEffect(() => { if (client) { fetchItem().catch((e) => dispatch({ type: SET_ERROR, error: e.message })); @@ -151,7 +64,7 @@ const OmicsRunCreateForm = (props) => { workflowUri: omicsWorkflow.workflowUri, parameterTemplate: values.parameterTemplate, SamlAdminGroupName: values.SamlAdminGroupName, - destination: values.destination + destination: values.dataset }) ); setStatus({ success: true }); @@ -252,7 +165,7 @@ const OmicsRunCreateForm = (props) => { label: '', SamlAdminGroupName: '', environment: '', - destination: '', + dataset: '', parameterTemplate: omicsWorkflow.parameterTemplate }} validationSchema={Yup.object().shape({ @@ -267,9 +180,7 @@ const OmicsRunCreateForm = (props) => { .max(255) .required('*Team is required'), environment: Yup.object().required('*Environment is required'), - destination: Yup.string() - .max(255) - .required('*Destination is required') + dataset: Yup.string().max(255).required('*Dataset is required') })} onSubmit={async ( values, @@ -316,114 +227,13 @@ const OmicsRunCreateForm = (props) => { variant="outlined" /> - - { - setFieldValue('SamlAdminGroupName', ''); - fetchGroups( - event.target.value.environmentUri - ).catch((e) => - dispatch({ type: SET_ERROR, error: e.message }) - ); - setFieldValue('environment', event.target.value); - }} - select - value={values.environment} - variant="outlined" - > - {environmentOptions.map((environment) => ( - - {environment.label} - - ))} - - - - - - - { - setFieldValue('destination', ''); - fetchDatasets(event.target.value).catch((e) => - dispatch({ type: SET_ERROR, error: e.message }) - ); - setFieldValue( - 'SamlAdminGroupName', - event.target.value - ); - }} - select - value={values.SamlAdminGroupName} - variant="outlined" - > - {groupOptions.map((group) => ( - - {group.label} - - ))} - - - - - {datasetOptions.map((dataset) => ( - - {dataset.label} - - ))} - - + diff --git a/frontend/src/modules/Organizations/views/OrganizationCreateForm.js b/frontend/src/modules/Organizations/views/OrganizationCreateForm.js index 6936e1948..ca5f8e62f 100644 --- a/frontend/src/modules/Organizations/views/OrganizationCreateForm.js +++ b/frontend/src/modules/Organizations/views/OrganizationCreateForm.js @@ -1,5 +1,6 @@ import { LoadingButton } from '@mui/lab'; import { + Autocomplete, Box, Breadcrumbs, Button, @@ -10,7 +11,6 @@ import { FormHelperText, Grid, Link, - MenuItem, TextField, Typography } from '@mui/material'; @@ -36,9 +36,6 @@ const OrganizationCreateForm = (props) => { const client = useClient(); const groups = useGroups(); const { settings } = useSettings(); - const groupOptions = groups - ? groups.map((g) => ({ value: g, label: g })) - : []; async function submit(values, setStatus, setSubmitting, setErrors) { try { @@ -46,7 +43,7 @@ const OrganizationCreateForm = (props) => { createOrganization({ label: values.label, description: values.description, - SamlGroupName: values.SamlGroupName, + SamlGroupName: values.SamlAdminGroupName, tags: values.tags }) ); @@ -147,7 +144,7 @@ const OrganizationCreateForm = (props) => { initialValues={{ label: '', description: '', - SamlGroupName: '', + SamlAdminGroupName: '', tags: [] }} validationSchema={Yup.object().shape({ @@ -155,7 +152,7 @@ const OrganizationCreateForm = (props) => { .max(255) .required('*Organization name is required'), description: Yup.string().max(5000), - SamlGroupName: Yup.string() + SamlAdminGroupName: Yup.string() .max(255) .required('*Team is required'), tags: Yup.array().nullable() @@ -230,27 +227,37 @@ const OrganizationCreateForm = (props) => { - { + if (value) { + setFieldValue('SamlAdminGroupName', value); + } else { + setFieldValue('SamlAdminGroupName', ''); + } + }} + inputValue={values.SamlAdminGroupName} + renderInput={(params) => ( + )} - helperText={ - touched.SamlGroupName && errors.SamlGroupName - } - fullWidth - label="Team" - name="SamlGroupName" - onChange={handleChange} - select - value={values.SamlGroupName} - variant="outlined" - > - {groupOptions.map((group) => ( - - {group.label} - - ))} - + /> diff --git a/frontend/src/modules/S3_Datasets/views/DatasetCreateForm.js b/frontend/src/modules/S3_Datasets/views/DatasetCreateForm.js index cc88bf175..565afe50c 100644 --- a/frontend/src/modules/S3_Datasets/views/DatasetCreateForm.js +++ b/frontend/src/modules/S3_Datasets/views/DatasetCreateForm.js @@ -120,7 +120,7 @@ const DatasetCreateForm = (props) => { owner: '', stewards: values.stewards, label: values.label, - SamlAdminGroupName: values.SamlGroupName, + SamlAdminGroupName: values.SamlAdminGroupName, tags: values.tags, description: values.description, topics: values.topics ? values.topics.map((t) => t.value) : [], @@ -220,7 +220,7 @@ const DatasetCreateForm = (props) => { environment: '', stewards: '', confidentiality: '', - SamlGroupName: '', + SamlAdminGroupName: '', tags: [], topics: [], autoApprovalEnabled: false @@ -230,7 +230,7 @@ const DatasetCreateForm = (props) => { .max(255) .required('*Dataset name is required'), description: Yup.string().max(5000), - SamlGroupName: Yup.string() + SamlAdminGroupName: Yup.string() .max(255) .required('*Owners team is required'), topics: isFeatureEnabled('datasets_base', 'topics_dropdown') @@ -429,38 +429,45 @@ const DatasetCreateForm = (props) => { - { - setFieldValue('SamlGroupName', ''); - fetchGroups( - event.target.value.environmentUri - ).catch((e) => - dispatch({ type: SET_ERROR, error: e.message }) - ); - setFieldValue('environment', event.target.value); + option)} + onChange={(event, value) => { + setFieldValue('SamlAdminGroupName', ''); + setFieldValue('stewards', ''); + if (value && value.environmentUri) { + setFieldValue('environment', value); + fetchGroups(value.environmentUri).catch((e) => + dispatch({ + type: SET_ERROR, + error: e.message + }) + ); + } else { + setFieldValue('environment', ''); + setGroupOptions([]); + } }} - select - value={values.environment} - variant="outlined" - > - {environmentOptions.map((environment) => ( - - {environment.label} - - ))} - + renderInput={(params) => ( + + )} + /> { label="Region" name="region" value={ - values.environment + values.environment && values.environment.region ? values.environment.region : '' } @@ -483,7 +490,8 @@ const DatasetCreateForm = (props) => { label="Organization" name="organization" value={ - values.environment + values.environment && + values.environment.organization ? values.environment.organization.label : '' } @@ -494,45 +502,105 @@ const DatasetCreateForm = (props) => { - option)} + onChange={(event, value) => { + if (value && value.value) { + setFieldValue( + 'SamlAdminGroupName', + value.value + ); + } else { + setFieldValue('SamlAdminGroupName', ''); + } + }} + inputValue={values.SamlAdminGroupName} + renderInput={(params) => ( + + {groupOptions.length > 0 ? ( + + ) : ( + + )} + )} - helperText={ - touched.SamlGroupName && errors.SamlGroupName - } - label="Owners" - name="SamlGroupName" - onChange={handleChange} - select - value={values.SamlGroupName} - variant="outlined" - > - {groupOptions.map((group) => ( - - {group.label} - - ))} - + /> option.value)} + disablePortal + options={groupOptions.map((option) => option)} onChange={(event, value) => { - setFieldValue('stewards', value); + if (value && value.value) { + setFieldValue('stewards', value.value); + } else { + setFieldValue('stewards', ''); + } }} - renderInput={(renderParams) => ( - + inputValue={values.stewards} + renderInput={(params) => ( + + {groupOptions.length > 0 ? ( + + ) : ( + + )} + )} /> diff --git a/frontend/src/modules/S3_Datasets/views/DatasetImportForm.js b/frontend/src/modules/S3_Datasets/views/DatasetImportForm.js index c73a7cfbc..21454003f 100644 --- a/frontend/src/modules/S3_Datasets/views/DatasetImportForm.js +++ b/frontend/src/modules/S3_Datasets/views/DatasetImportForm.js @@ -119,7 +119,7 @@ const DatasetImportForm = (props) => { environmentUri: values.environment.environmentUri, owner: '', label: values.label, - SamlAdminGroupName: values.SamlGroupName, + SamlAdminGroupName: values.SamlAdminGroupName, tags: values.tags, description: values.description, topics: values.topics ? values.topics.map((t) => t.value) : [], @@ -223,7 +223,7 @@ const DatasetImportForm = (props) => { environment: '', businessOwnerEmail: '', businessOwnerDelegationEmails: [], - SamlGroupName: '', + SamlAdminGroupName: '', stewards: '', tags: [], topics: [], @@ -238,7 +238,7 @@ const DatasetImportForm = (props) => { .max(255) .required('*Dataset name is required'), description: Yup.string().max(5000), - SamlGroupName: Yup.string() + SamlAdminGroupName: Yup.string() .max(255) .required('*Team is required'), topics: isFeatureEnabled('datasets_base', 'topics_dropdown') @@ -442,38 +442,45 @@ const DatasetImportForm = (props) => { - { - setFieldValue('SamlGroupName', ''); - fetchGroups( - event.target.value.environmentUri - ).catch((e) => - dispatch({ type: SET_ERROR, error: e.message }) - ); - setFieldValue('environment', event.target.value); + option)} + onChange={(event, value) => { + setFieldValue('SamlAdminGroupName', ''); + setFieldValue('stewards', ''); + if (value && value.environmentUri) { + setFieldValue('environment', value); + fetchGroups(value.environmentUri).catch((e) => + dispatch({ + type: SET_ERROR, + error: e.message + }) + ); + } else { + setFieldValue('environment', ''); + setGroupOptions([]); + } }} - select - value={values.environment} - variant="outlined" - > - {environmentOptions.map((environment) => ( - - {environment.label} - - ))} - + renderInput={(params) => ( + + )} + /> { label="Region" name="region" value={ - values.environment + values.environment && values.environment.region ? values.environment.region : '' } @@ -496,7 +503,8 @@ const DatasetImportForm = (props) => { label="Organization" name="organization" value={ - values.environment + values.environment && + values.environment.organization ? values.environment.organization.label : '' } @@ -558,45 +566,105 @@ const DatasetImportForm = (props) => { - option)} + onChange={(event, value) => { + if (value && value.value) { + setFieldValue( + 'SamlAdminGroupName', + value.value + ); + } else { + setFieldValue('SamlAdminGroupName', ''); + } + }} + inputValue={values.SamlAdminGroupName} + renderInput={(params) => ( + + {groupOptions.length > 0 ? ( + + ) : ( + + )} + )} - helperText={ - touched.SamlGroupName && errors.SamlGroupName - } - label="Team" - name="SamlGroupName" - onChange={handleChange} - select - value={values.SamlGroupName} - variant="outlined" - > - {groupOptions.map((group) => ( - - {group.label} - - ))} - + /> option.value)} + disablePortal + options={groupOptions.map((option) => option)} onChange={(event, value) => { - setFieldValue('stewards', value); + if (value && value.value) { + setFieldValue('stewards', value.value); + } else { + setFieldValue('stewards', ''); + } }} - renderInput={(renderParams) => ( - + inputValue={values.stewards} + renderInput={(params) => ( + + {groupOptions.length > 0 ? ( + + ) : ( + + )} + )} /> diff --git a/frontend/src/modules/Shared/EnvironmentGroupSelect/EnvironmentTeamDatasetsDropdown.js b/frontend/src/modules/Shared/EnvironmentGroupSelect/EnvironmentTeamDatasetsDropdown.js new file mode 100644 index 000000000..515182243 --- /dev/null +++ b/frontend/src/modules/Shared/EnvironmentGroupSelect/EnvironmentTeamDatasetsDropdown.js @@ -0,0 +1,282 @@ +import { + Autocomplete, + Box, + Card, + CardContent, + CardHeader, + CircularProgress, + TextField +} from '@mui/material'; +import React, { useCallback, useEffect, useState } from 'react'; +import { Defaults } from 'design'; +import { SET_ERROR, useDispatch } from 'globalErrors'; +import { + listEnvironmentGroups, + listValidEnvironments, + listS3DatasetsOwnedByEnvGroup, + useClient +} from 'services'; + +import PropTypes from 'prop-types'; + +export const EnvironmentTeamDatasetsDropdown = (props) => { + const { setFieldValue, handleChange, values, touched, errors } = props; + const dispatch = useDispatch(); + const client = useClient(); + const [loading, setLoading] = useState(true); + const [groupOptions, setGroupOptions] = useState([]); + const [environmentOptions, setEnvironmentOptions] = useState([]); + const [currentEnv, setCurrentEnv] = useState(''); + const [datasetOptions, setDatasetOptions] = useState([]); + const fetchEnvironments = useCallback(async () => { + setLoading(true); + const response = await client.query( + listValidEnvironments({ filter: Defaults.SelectListFilter }) + ); + if (!response.errors) { + setEnvironmentOptions( + response.data.listValidEnvironments.nodes.map((e) => ({ + ...e, + value: e.environmentUri, + label: e.label + })) + ); + } else { + dispatch({ type: SET_ERROR, error: response.errors[0].message }); + } + setLoading(false); + }, [client, dispatch]); + const fetchGroups = async (environmentUri) => { + setCurrentEnv(environmentUri); + try { + const response = await client.query( + listEnvironmentGroups({ + filter: Defaults.SelectListFilter, + environmentUri + }) + ); + if (!response.errors) { + setGroupOptions( + response.data.listEnvironmentGroups.nodes.map((g) => ({ + value: g.groupUri, + label: g.groupUri + })) + ); + } else { + dispatch({ type: SET_ERROR, error: response.errors[0].message }); + } + } catch (e) { + dispatch({ type: SET_ERROR, error: e.message }); + } + }; + const fetchDatasets = async (groupUri) => { + let ownedDatasets = []; + try { + const response = await client.query( + listS3DatasetsOwnedByEnvGroup({ + filter: Defaults.SelectListFilter, + environmentUri: currentEnv, + groupUri: groupUri + }) + ); + if (!response.errors) { + ownedDatasets = response.data.listS3DatasetsOwnedByEnvGroup.nodes?.map( + (dataset) => ({ + value: dataset.datasetUri, + label: dataset.label + }) + ); + } else { + dispatch({ type: SET_ERROR, error: response.errors[0].message }); + } + } catch (e) { + dispatch({ type: SET_ERROR, error: e.message }); + } + setDatasetOptions(ownedDatasets); + }; + + useEffect(() => { + if (client) { + fetchEnvironments().catch((e) => + dispatch({ type: SET_ERROR, error: e.message }) + ); + } + }, [client, dispatch, fetchEnvironments]); + + if (loading) { + return ; + } + + return ( + + + + + option)} + onChange={(event, value) => { + setFieldValue('SamlAdminGroupName', ''); + setFieldValue('dataset', ''); + if (value && value.environmentUri) { + setFieldValue('environment', value); + fetchGroups(value.environmentUri).catch((e) => + dispatch({ + type: SET_ERROR, + error: e.message + }) + ); + } else { + setFieldValue('environment', ''); + setGroupOptions([]); + setDatasetOptions([]); + } + }} + renderInput={(params) => ( + + )} + /> + + + + + + + + + option)} + onChange={(event, value) => { + setFieldValue('dataset', ''); + if (value && value.value) { + setFieldValue('SamlAdminGroupName', value.value); + fetchDatasets(value).catch((e) => + dispatch({ type: SET_ERROR, error: e.message }) + ); + } else { + setFieldValue('SamlAdminGroupName', ''); + setDatasetOptions([]); + } + }} + inputValue={values.SamlAdminGroupName} + renderInput={(params) => ( + + {groupOptions.length > 0 ? ( + + ) : ( + + )} + + )} + /> + + + option)} + onChange={(event, value) => { + if (value && value.value) { + setFieldValue('dataset', value.value); + } else { + setFieldValue('dataset', ''); + } + }} + inputValue={values.dataset} + renderInput={(params) => ( + + {datasetOptions.length > 0 ? ( + + ) : ( + + )} + + )} + /> + + + + ); +}; + +EnvironmentTeamDatasetsDropdown.propTypes = { + setFieldValue: PropTypes.func.isRequired, + handleChange: PropTypes.func.isRequired, + values: PropTypes.object.isRequired, + touched: PropTypes.object.isRequired, + errors: PropTypes.object.isRequired +}; diff --git a/frontend/src/modules/Shared/EnvironmentGroupSelect/EnvironmentTeamDropdown.js b/frontend/src/modules/Shared/EnvironmentGroupSelect/EnvironmentTeamDropdown.js new file mode 100644 index 000000000..6862c8baf --- /dev/null +++ b/frontend/src/modules/Shared/EnvironmentGroupSelect/EnvironmentTeamDropdown.js @@ -0,0 +1,205 @@ +import { + Autocomplete, + Box, + Card, + CardContent, + CardHeader, + CircularProgress, + TextField +} from '@mui/material'; +import React, { useCallback, useEffect, useState } from 'react'; +import { Defaults } from 'design'; +import { SET_ERROR, useDispatch } from 'globalErrors'; +import { + listEnvironmentGroups, + listValidEnvironments, + useClient +} from 'services'; + +import PropTypes from 'prop-types'; + +export const EnvironmentTeamDropdown = (props) => { + const { setFieldValue, handleChange, values, touched, errors } = props; + const dispatch = useDispatch(); + const client = useClient(); + const [loading, setLoading] = useState(true); + const [groupOptions, setGroupOptions] = useState([]); + const [environmentOptions, setEnvironmentOptions] = useState([]); + const fetchEnvironments = useCallback(async () => { + setLoading(true); + const response = await client.query( + listValidEnvironments({ filter: Defaults.selectListFilter }) + ); + if (!response.errors) { + setEnvironmentOptions( + response.data.listValidEnvironments.nodes.map((e) => ({ + ...e, + value: e.environmentUri, + label: e.label + })) + ); + } else { + dispatch({ type: SET_ERROR, error: response.errors[0].message }); + } + setLoading(false); + }, [client, dispatch]); + const fetchGroups = async (environmentUri) => { + try { + const response = await client.query( + listEnvironmentGroups({ + filter: Defaults.selectListFilter, + environmentUri + }) + ); + if (!response.errors) { + setGroupOptions( + response.data.listEnvironmentGroups.nodes.map((g) => ({ + value: g.groupUri, + label: g.groupUri + })) + ); + } else { + dispatch({ type: SET_ERROR, error: response.errors[0].message }); + } + } catch (e) { + dispatch({ type: SET_ERROR, error: e.message }); + } + }; + useEffect(() => { + if (client) { + fetchEnvironments().catch((e) => + dispatch({ type: SET_ERROR, error: e.message }) + ); + } + }, [client, dispatch, fetchEnvironments]); + + if (loading) { + return ; + } + + return ( + + + + + option)} + onChange={(event, value) => { + setFieldValue('SamlAdminGroupName', ''); + if (value && value.environmentUri) { + setFieldValue('environment', value); + fetchGroups(value.environmentUri).catch((e) => + dispatch({ + type: SET_ERROR, + error: e.message + }) + ); + } else { + setFieldValue('environment', ''); + setGroupOptions([]); + } + }} + renderInput={(params) => ( + + )} + /> + + + + + + + + + option)} + onChange={(event, value) => { + if (value && value.value) { + setFieldValue('SamlAdminGroupName', value.value); + } else { + setFieldValue('SamlAdminGroupName', ''); + } + }} + inputValue={values.SamlAdminGroupName} + renderInput={(params) => ( + + {groupOptions.length > 0 ? ( + + ) : ( + + )} + + )} + /> + + + + ); +}; + +EnvironmentTeamDropdown.propTypes = { + setFieldValue: PropTypes.func.isRequired, + handleChange: PropTypes.func.isRequired, + values: PropTypes.object.isRequired, + touched: PropTypes.object.isRequired, + errors: PropTypes.object.isRequired +}; diff --git a/frontend/src/modules/Shared/EnvironmentGroupSelect/index.js b/frontend/src/modules/Shared/EnvironmentGroupSelect/index.js new file mode 100644 index 000000000..c89c2caf3 --- /dev/null +++ b/frontend/src/modules/Shared/EnvironmentGroupSelect/index.js @@ -0,0 +1,2 @@ +export * from './EnvironmentTeamDatasetsDropdown'; +export * from './EnvironmentTeamDropdown'; diff --git a/frontend/src/modules/Shared/Shares/ShareEditForm.js b/frontend/src/modules/Shared/Shares/ShareEditForm.js index e357ece47..5ad742a53 100644 --- a/frontend/src/modules/Shared/Shares/ShareEditForm.js +++ b/frontend/src/modules/Shared/Shares/ShareEditForm.js @@ -12,7 +12,7 @@ import { TextField, Typography } from '@mui/material'; -import { Defaults, Pager, ShareStatus } from '../../../design'; +import { Defaults, Pager, ShareHealthStatus, ShareStatus } from 'design'; import SendIcon from '@mui/icons-material/Send'; import React, { useCallback, useEffect, useState } from 'react'; import { @@ -50,7 +50,10 @@ const ItemRow = (props) => { item.status === 'Share_Failed' ) return 'Delete'; - if (item.status === 'Share_Succeeded' || item.status === 'Revoke_Failed') + if ( + (item.status === 'Share_Succeeded' || item.status === 'Revoke_Failed') && + item.healthStatus !== 'PendingReApply' + ) return 'Revoke'; return 'Nothing'; }; @@ -135,6 +138,17 @@ const ItemRow = (props) => { {item.status ? : 'Not requested'} + + {item.status ? ( + + ) : ( + 'Not requested' + )} + {(shareStatus === 'Draft' || shareStatus === 'Processed' || shareStatus === 'Rejected' || @@ -173,7 +187,7 @@ const ItemRow = (props) => { )} {possibleAction === 'Nothing' && ( - Wait until this item is processed + Wait until this item is processed and/or re-apply task is complete )} @@ -376,6 +390,7 @@ export const ShareEditForm = (props) => { Type Name Status + Health Status {(shareStatus === 'Draft' || shareStatus === 'Processed' || shareStatus === 'Rejected' || diff --git a/frontend/src/modules/Shared/index.js b/frontend/src/modules/Shared/index.js index ccffdd8d7..04647f029 100644 --- a/frontend/src/modules/Shared/index.js +++ b/frontend/src/modules/Shared/index.js @@ -1,3 +1,4 @@ export * from './Comments'; +export * from './EnvironmentGroupSelect'; export * from './KeyValueTags'; export * from './Stack'; diff --git a/frontend/src/modules/Shares/components/NavigateShareViewModal.js b/frontend/src/modules/Shares/components/NavigateShareViewModal.js new file mode 100644 index 000000000..7f3effd2b --- /dev/null +++ b/frontend/src/modules/Shares/components/NavigateShareViewModal.js @@ -0,0 +1,60 @@ +import { Box, Dialog, Divider, Typography, Button } from '@mui/material'; +import PropTypes from 'prop-types'; +import { Link as RouterLink } from 'react-router-dom'; + +export const NavigateShareViewModal = (props) => { + const { dataset, onApply, onClose, open, ...other } = props; + + return ( + + + + Dataset: {dataset.label} - Share Object Verification Task(s) Started + + + Navigate to the Share View Page and select the desired share object to + view the progress of each of verification task + + + + + + + ); +}; + +NavigateShareViewModal.propTypes = { + shares: PropTypes.array.isRequired, + dataset: PropTypes.object.isRequired, + onApply: PropTypes.func, + onClose: PropTypes.func, + open: PropTypes.bool.isRequired +}; diff --git a/frontend/src/modules/Shares/components/ShareBoxList.js b/frontend/src/modules/Shares/components/ShareBoxList.js index d795141ba..063a7524d 100644 --- a/frontend/src/modules/Shares/components/ShareBoxList.js +++ b/frontend/src/modules/Shares/components/ShareBoxList.js @@ -28,6 +28,7 @@ import { getShareRequestsFromMe, listOwnedDatasets } from '../services'; import { ShareBoxListItem } from './ShareBoxListItem'; import { ShareObjectSelectorModal } from './ShareObjectSelectorModal'; +import { NavigateShareViewModal } from './NavigateShareViewModal'; import { ShareStatusList } from '../constants'; import { RefreshRounded } from '@mui/icons-material'; import { reApplyShareObjectItemsOnDataset } from '../services/reApplyShareObjectItemsOnDataset'; @@ -54,6 +55,8 @@ export const ShareBoxList = (props) => { const [datasets, setDatasets] = useState([]); const [isVerifyObjectItemsModalOpen, setIsVerifyObjectItemsModalOpen] = useState(false); + const [isNavigateShareViewModalOpen, setIsNavigateShareViewModalOpen] = + useState(false); const statusOptions = ShareStatusList; const { enqueueSnackbar } = useSnackbar(); @@ -62,6 +65,13 @@ export const ShareBoxList = (props) => { }; const handleVerifyObjectItemsModalClose = () => { setIsVerifyObjectItemsModalOpen(false); + if (dataset) { + setIsNavigateShareViewModalOpen(true); + } + }; + + const handleNavigateShareViewModalClose = () => { + setIsNavigateShareViewModalOpen(false); }; const handlePageChange = async (event, value) => { @@ -571,6 +581,14 @@ export const ShareBoxList = (props) => { open={isVerifyObjectItemsModalOpen} /> )} + {isNavigateShareViewModalOpen && ( + + )} ); }; diff --git a/frontend/src/modules/Shares/components/index.js b/frontend/src/modules/Shares/components/index.js index e00ca86a8..86373e838 100644 --- a/frontend/src/modules/Shares/components/index.js +++ b/frontend/src/modules/Shares/components/index.js @@ -6,3 +6,4 @@ export * from './ShareUpdateReject'; export * from './ShareUpdateRequest'; export * from './ShareItemsSelectorModal'; export * from './ShareObjectSelectorModal'; +export * from './NavigateShareViewModal'; diff --git a/frontend/src/modules/Shares/views/ShareView.js b/frontend/src/modules/Shares/views/ShareView.js index 16a5c311f..7bb05feda 100644 --- a/frontend/src/modules/Shares/views/ShareView.js +++ b/frontend/src/modules/Shares/views/ShareView.js @@ -6,10 +6,7 @@ import { DeleteOutlined, RefreshRounded } from '@mui/icons-material'; -import VerifiedUserIcon from '@mui/icons-material/VerifiedUser'; -import GppBadIcon from '@mui/icons-material/GppBad'; import SecurityIcon from '@mui/icons-material/Security'; -import PendingIcon from '@mui/icons-material/Pending'; import { LoadingButton } from '@mui/lab'; import { Box, @@ -49,6 +46,7 @@ import { PencilAltIcon, Scrollbar, ShareStatus, + ShareHealthStatus, TextAvatar, useSettings } from 'design'; @@ -489,33 +487,11 @@ export function SharedItem(props) { )} -
- {item.healthStatus === 'Unhealthy' ? ( - {item.healthStatus}}> - - - ) : item.healthStatus === 'Healthy' ? ( - {item.healthStatus}}> - - - ) : ( - {item.healthStatus || 'Undefined'} - } - > - - - )} - - {(item.lastVerificationTime && - item.lastVerificationTime.substring( - 0, - item.lastVerificationTime.indexOf('.') - )) || - ''} - -
+
{item.healthMessage ? ( diff --git a/frontend/src/modules/Worksheets/views/WorksheetCreateForm.js b/frontend/src/modules/Worksheets/views/WorksheetCreateForm.js index 0a307559f..fab25da6f 100644 --- a/frontend/src/modules/Worksheets/views/WorksheetCreateForm.js +++ b/frontend/src/modules/Worksheets/views/WorksheetCreateForm.js @@ -1,5 +1,6 @@ import { LoadingButton } from '@mui/lab'; import { + Autocomplete, Box, Breadcrumbs, Button, @@ -10,7 +11,6 @@ import { FormHelperText, Grid, Link, - MenuItem, TextField, Typography } from '@mui/material'; @@ -36,9 +36,6 @@ const WorksheetCreateForm = (props) => { const client = useClient(); const groups = useGroups(); const { settings } = useSettings(); - const groupOptions = groups - ? groups.map((g) => ({ value: g, label: g })) - : []; async function submit(values, setStatus, setSubmitting, setErrors) { try { @@ -46,7 +43,7 @@ const WorksheetCreateForm = (props) => { createWorksheet({ label: values.label, description: values.description, - SamlAdminGroupName: values.SamlGroupName, + SamlAdminGroupName: values.SamlAdminGroupName, tags: values.tags }) ); @@ -141,7 +138,7 @@ const WorksheetCreateForm = (props) => { initialValues={{ label: '', description: '', - SamlGroupName: '', + SamlAdminGroupName: '', tags: [] }} validationSchema={Yup.object().shape({ @@ -149,7 +146,7 @@ const WorksheetCreateForm = (props) => { .max(255) .required('*Worksheet name is required'), description: Yup.string().max(5000), - SamlGroupName: Yup.string() + SamlAdminGroupName: Yup.string() .max(255) .required('* Team is required'), tags: Yup.array().nullable() @@ -224,27 +221,37 @@ const WorksheetCreateForm = (props) => { - { + if (value) { + setFieldValue('SamlAdminGroupName', value); + } else { + setFieldValue('SamlAdminGroupName', ''); + } + }} + inputValue={values.SamlAdminGroupName} + renderInput={(params) => ( + )} - helperText={ - touched.SamlGroupName && errors.SamlGroupName - } - label="Team" - name="SamlGroupName" - onChange={handleChange} - select - value={values.SamlGroupName} - variant="outlined" - > - {groupOptions.map((group) => ( - - {group.label} - - ))} - + /> diff --git a/tests/modules/s3_datasets_shares/test_share.py b/tests/modules/s3_datasets_shares/test_share.py index 22ca395ee..988b14f5e 100644 --- a/tests/modules/s3_datasets_shares/test_share.py +++ b/tests/modules/s3_datasets_shares/test_share.py @@ -317,6 +317,15 @@ def share3_processed( if delete_share_object_response.data.deleteShareObject == True: return + # Revert healthStatus back to healthy + with db.scoped_session() as session: + ShareStatusRepository.update_share_item_health_status_batch( + session=session, + share_uri=share3.shareUri, + old_status=ShareItemHealthStatus.PendingReApply.value, + new_status=ShareItemHealthStatus.Healthy.value, + ) + # Given share item in shared states get_share_object_response = get_share_object( client=client, @@ -1522,7 +1531,7 @@ def test_reapply_items_share_request(db, client, user, group, share3_processed, client=client, user=user, group=group, shareUri=share3_processed.shareUri, reapply_items_uris=reapply_items_uris ) - # Then share item health Status changes to PendingVerify + # Then share item health Status changes to PendingReApply get_share_object_response = get_share_object( client=client, user=user,