diff --git a/backend/dataall/core/environment/api/input_types.py b/backend/dataall/core/environment/api/input_types.py index 27188f4ed..8662408f4 100644 --- a/backend/dataall/core/environment/api/input_types.py +++ b/backend/dataall/core/environment/api/input_types.py @@ -118,3 +118,11 @@ class EnvironmentSortField(GraphQLEnumMapper): gql.Argument(name='groupUri', type=gql.String), ], ) + +UpdateConsumptionRoleInput = gql.InputType( + name='UpdateConsumptionRoleInput', + arguments=[ + gql.Argument('consumptionRoleName', gql.String), + gql.Argument('groupUri', gql.String), + ], +) diff --git a/backend/dataall/core/environment/api/mutations.py b/backend/dataall/core/environment/api/mutations.py index 0b60a92a5..8c36c2e62 100644 --- a/backend/dataall/core/environment/api/mutations.py +++ b/backend/dataall/core/environment/api/mutations.py @@ -5,7 +5,8 @@ NewEnvironmentInput, EnableDataSubscriptionsInput, InviteGroupOnEnvironmentInput, - AddConsumptionRoleToEnvironmentInput + AddConsumptionRoleToEnvironmentInput, + UpdateConsumptionRoleInput, ) from dataall.core.environment.api.resolvers import * @@ -110,3 +111,14 @@ resolver=disable_subscriptions, type=gql.Boolean, ) + +updateConsumptionRole = gql.MutationField( + name='updateConsumptionRole', + args=[ + gql.Argument('environmentUri', type=gql.NonNullableType(gql.String)), + gql.Argument('consumptionRoleUri', type=gql.NonNullableType(gql.String)), + gql.Argument('input', type=UpdateConsumptionRoleInput), + ], + type=gql.Ref('ConsumptionRole'), + resolver=update_consumption_role, +) diff --git a/backend/dataall/core/environment/api/resolvers.py b/backend/dataall/core/environment/api/resolvers.py index 097e0a405..458b577e9 100644 --- a/backend/dataall/core/environment/api/resolvers.py +++ b/backend/dataall/core/environment/api/resolvers.py @@ -222,6 +222,17 @@ def remove_consumption_role(context: Context, source, environmentUri=None, consu return status +def update_consumption_role(context: Context, source, environmentUri=None, consumptionRoleUri=None, input={}): + with context.engine.scoped_session() as session: + status = EnvironmentService.update_consumption_role( + session=session, + uri=consumptionRoleUri, + env_uri=environmentUri, + input=input, + ) + return status + + def list_environment_invited_groups( context: Context, source, environmentUri=None, filter=None ): diff --git a/backend/dataall/core/environment/services/environment_service.py b/backend/dataall/core/environment/services/environment_service.py index f7d2b3d7e..12f360a9c 100644 --- a/backend/dataall/core/environment/services/environment_service.py +++ b/backend/dataall/core/environment/services/environment_service.py @@ -459,6 +459,32 @@ def remove_consumption_role(session, uri, env_uri): ) return True + @staticmethod + @has_tenant_permission(permissions.MANAGE_ENVIRONMENTS) + @has_resource_permission(permissions.REMOVE_ENVIRONMENT_CONSUMPTION_ROLE) + def update_consumption_role(session, uri, env_uri, input): + role_query = session.query(ConsumptionRole).filter( + ( + and_( + ConsumptionRole.consumptionRoleUri == uri, + ConsumptionRole.environmentUri == env_uri, + ) + ) + ) + consumption_role = role_query.first() + if consumption_role: + ResourcePolicy.update_resource_policy( + session=session, + resource_uri=uri, + resource_type=ConsumptionRole.__name__, + old_group=consumption_role.groupUri, + new_group=input['groupUri'], + new_permissions=permissions.CONSUMPTION_ROLE_ALL + ) + role_query.update(input) + session.commit() + return role_query.first() + @staticmethod def query_user_environments(session, username, groups, filter) -> Query: query = ( @@ -699,7 +725,7 @@ def query_user_environment_consumption_roles(session, groups, uri, filter) -> Qu ConsumptionRole.groupUri == group, ) ) - return query + return query.order_by(ConsumptionRole.consumptionRoleUri) @staticmethod @has_resource_permission(permissions.LIST_ENVIRONMENT_CONSUMPTION_ROLES) @@ -731,7 +757,7 @@ def query_all_environment_consumption_roles(session, uri, filter) -> Query: ConsumptionRole.groupUri == group, ) ) - return query + return query.order_by(ConsumptionRole.consumptionRoleUri) @staticmethod @has_resource_permission(permissions.LIST_ENVIRONMENT_CONSUMPTION_ROLES) diff --git a/backend/dataall/core/permissions/db/resource_policy_repositories.py b/backend/dataall/core/permissions/db/resource_policy_repositories.py index 7c93011b5..b3d4e4a18 100644 --- a/backend/dataall/core/permissions/db/resource_policy_repositories.py +++ b/backend/dataall/core/permissions/db/resource_policy_repositories.py @@ -122,6 +122,29 @@ def find_resource_policy( ) return resource_policy + @staticmethod + def update_resource_policy( + session, + resource_uri: str, + resource_type: str, + old_group: str, + new_group: str, + new_permissions: [str] + ) -> models.ResourcePolicy: + ResourcePolicy.delete_resource_policy( + session=session, + group=old_group, + resource_uri=resource_uri, + resource_type=resource_type, + ) + return ResourcePolicy.attach_resource_policy( + session=session, + group=new_group, + resource_uri=resource_uri, + permissions=new_permissions, + resource_type=resource_type, + ) + @staticmethod def attach_resource_policy( session, diff --git a/frontend/src/modules/Environments/components/EnvironmentRoleAddForm.js b/frontend/src/modules/Environments/components/EnvironmentRoleAddForm.js index 0eed7f6d7..137722a74 100644 --- a/frontend/src/modules/Environments/components/EnvironmentRoleAddForm.js +++ b/frontend/src/modules/Environments/components/EnvironmentRoleAddForm.js @@ -12,53 +12,18 @@ import { import { Formik } from 'formik'; import { useSnackbar } from 'notistack'; import PropTypes from 'prop-types'; -import React, { useEffect, useState } from 'react'; +import React from 'react'; import * as Yup from 'yup'; -import { Defaults } from 'design'; import { SET_ERROR, useDispatch } from 'globalErrors'; -import { listEnvironmentGroups, useClient } from 'services'; +import { useClient } from 'services'; import { addConsumptionRoleToEnvironment } from '../services'; +import { useFetchGroups } from '../../../utils/api'; + export const EnvironmentRoleAddForm = (props) => { const { environment, onClose, open, reloadRoles, ...other } = props; const { enqueueSnackbar } = useSnackbar(); const dispatch = useDispatch(); const client = useClient(); - const [loadingGroups, setLoadingGroups] = useState(true); - const [groupOptions, setGroupOptions] = useState([]); - - const fetchGroups = async (environmentUri) => { - try { - setLoadingGroups(true); - 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 }); - } finally { - setLoadingGroups(false); - } - }; - - useEffect(() => { - if (client && environment) { - fetchGroups(environment.environmentUri).catch((e) => - dispatch({ type: SET_ERROR, error: e.message }) - ); - } - }, [client, environment, dispatch]); async function submit(values, setStatus, setSubmitting, setErrors) { try { @@ -98,6 +63,8 @@ export const EnvironmentRoleAddForm = (props) => { } } + let { groupOptions, loadingGroups } = useFetchGroups(environment); + if (!environment) { return null; } diff --git a/frontend/src/modules/Environments/components/EnvironmentTeams.js b/frontend/src/modules/Environments/components/EnvironmentTeams.js index e094ff484..e33c9cc6a 100644 --- a/frontend/src/modules/Environments/components/EnvironmentTeams.js +++ b/frontend/src/modules/Environments/components/EnvironmentTeams.js @@ -1,6 +1,5 @@ import { CopyAllOutlined, - DeleteOutlined, GroupAddOutlined, SupervisedUserCircleRounded } from '@mui/icons-material'; @@ -13,7 +12,6 @@ import { Chip, Divider, Grid, - IconButton, InputAdornment, Table, TableBody, @@ -22,6 +20,10 @@ import { TableRow, TextField } from '@mui/material'; +import EditIcon from '@mui/icons-material/Edit'; +import DeleteIcon from '@mui/icons-material/DeleteOutlined'; +import SaveIcon from '@mui/icons-material/Save'; +import CancelIcon from '@mui/icons-material/Close'; import CircularProgress from '@mui/material/CircularProgress'; import { useTheme } from '@mui/styles'; import { useSnackbar } from 'notistack'; @@ -46,12 +48,15 @@ import { listAllEnvironmentConsumptionRoles, listAllEnvironmentGroups, removeConsumptionRoleFromEnvironment, - removeGroupFromEnvironment + removeGroupFromEnvironment, + updateConsumptionRole } from '../services'; import { EnvironmentRoleAddForm } from './EnvironmentRoleAddForm'; import { EnvironmentTeamInviteEditForm } from './EnvironmentTeamInviteEditForm'; import { EnvironmentTeamInviteForm } from './EnvironmentTeamInviteForm'; import { isFeatureEnabled } from '../../../utils'; +import { DataGrid, GridActionsCellItem, GridRowModes } from '@mui/x-data-grid'; +import { useFetchGroups } from '../../../utils/api'; function TeamRow({ team, environment, fetchItems }) { const client = useClient(); @@ -243,6 +248,7 @@ export const EnvironmentTeams = ({ environment }) => { const [filter, setFilter] = useState(Defaults.filter); const [filterRoles, setFilterRoles] = useState(Defaults.filter); const [loading, setLoading] = useState(true); + const [loadingRoles, setLoadingRoles] = useState(true); const [inputValue, setInputValue] = useState(''); const [inputValueRoles, setInputValueRoles] = useState(''); const [isTeamInviteModalOpen, setIsTeamInviteModalOpen] = useState(false); @@ -282,6 +288,7 @@ export const EnvironmentTeams = ({ environment }) => { const fetchRoles = useCallback(async () => { try { + setLoadingRoles(true); const response = await client.query( listAllEnvironmentConsumptionRoles({ environmentUri: environment.environmentUri, @@ -296,7 +303,7 @@ export const EnvironmentTeams = ({ environment }) => { } catch (e) { dispatch({ type: SET_ERROR, error: e.message }); } finally { - setLoading(false); + setLoadingRoles(false); } }, [client, dispatch, environment, filterRoles]); @@ -325,6 +332,31 @@ export const EnvironmentTeams = ({ environment }) => { } }; + const updateConsumptionRoleHandler = async (newRow) => { + const response = await client.mutate( + updateConsumptionRole({ + environmentUri: environment.environmentUri, + consumptionRoleUri: newRow.consumptionRoleUri, + input: { + groupUri: newRow.groupUri, + consumptionRoleName: newRow.consumptionRoleName + } + }) + ); + if (!response.errors) { + enqueueSnackbar('Consumption Role was updated', { + anchorOrigin: { + horizontal: 'right', + vertical: 'top' + }, + variant: 'success' + }); + fetchRoles(); + } else { + throw new Error(response.errors[0].message); + } + }; + useEffect(() => { if (client) { fetchItems().catch((e) => @@ -368,12 +400,49 @@ export const EnvironmentTeams = ({ environment }) => { } }; - const handlePageChangeRoles = async (event, value) => { - if (value <= roles.pages && value !== roles.page) { - await setFilterRoles({ ...filterRoles, page: value }); + const handlePageChangeRoles = async (page) => { + page += 1; //expecting 1-indexing + if (page <= roles.pages && page !== roles.page) { + await setFilterRoles({ ...filterRoles, page: page }); } }; + const [rowModesModel, setRowModesModel] = React.useState({}); + + const handleRowEditStart = (params, event) => { + event.defaultMuiPrevented = true; + }; + + const handleRowEditStop = (params, event) => { + event.defaultMuiPrevented = true; + }; + + const handleEditClick = (id) => () => { + setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } }); + }; + + const handleSaveClick = (id) => () => { + setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } }); + }; + + const handleDeleteClick = (id) => () => { + removeConsumptionRole(id); + }; + + const handleCancelClick = (id) => () => { + setRowModesModel({ + ...rowModesModel, + [id]: { mode: GridRowModes.View, ignoreModifications: true } + }); + }; + + const processRowUpdate = async (newRow, oldRow) => { + await updateConsumptionRoleHandler(newRow); + return newRow; + }; + + let { groupOptions } = useFetchGroups(environment); + return ( @@ -554,52 +623,101 @@ export const EnvironmentTeams = ({ environment }) => { - - - - Name - IAM Role - Role Owner - Action - - - {loading ? ( - - ) : ( - - {roles.nodes.length > 0 ? ( - roles.nodes.map((role) => ( - - {role.consumptionRoleName} - {role.IAMRoleArn} - {role.groupUri} - - - removeConsumptionRole(role.consumptionRoleUri) - } - > - - - - - )) - ) : ( - - No Consumption IAM Role added - - )} - - )} -
- {!loading && roles.nodes.length > 0 && ( - - )} + node.consumptionRoleUri} + rows={roles.nodes} + columns={[ + { field: 'id', hide: true }, + { + field: 'consumptionRoleName', + headerName: 'Name', + flex: 0.5, + editable: true + }, + { + field: 'IAMRoleArn', + headerName: 'IAM Role', + flex: 1 + }, + { + field: 'groupUri', + headerName: 'Role Owner', + flex: 0.5, + editable: true, + type: 'singleSelect', + valueOptions: groupOptions.map((group) => group.label) + }, + { + field: 'actions', + headerName: 'Actions', + flex: 0.5, + type: 'actions', + cellClassName: 'actions', + getActions: ({ id }) => { + const isInEditMode = + rowModesModel[id]?.mode === GridRowModes.Edit; + + if (isInEditMode) { + return [ + } + label="Save" + sx={{ + color: 'primary.main' + }} + onClick={handleSaveClick(id)} + />, + } + label="Cancel" + className="textPrimary" + onClick={handleCancelClick(id)} + color="inherit" + /> + ]; + } + return [ + } + label="Edit" + className="textPrimary" + onClick={handleEditClick(id)} + color="inherit" + />, + } + label="Delete" + onClick={handleDeleteClick(id)} + color="inherit" + /> + ]; + } + } + ]} + editMode="row" + rowModesModel={rowModesModel} + onRowModesModelChange={setRowModesModel} + onRowEditStart={handleRowEditStart} + onRowEditStop={handleRowEditStop} + processRowUpdate={processRowUpdate} + onProcessRowUpdateError={(error) => + dispatch({ type: SET_ERROR, error: error.message }) + } + experimentalFeatures={{ newEditingApi: true }} + rowCount={roles.count} + page={roles.page - 1} + pageSize={filterRoles.pageSize} + paginationMode="server" + onPageChange={handlePageChangeRoles} + loading={loadingRoles} + onPageSizeChange={(pageSize) => { + setFilterRoles({ ...filterRoles, pageSize: pageSize }); + }} + getRowHeight={() => 'auto'} + disableSelectionOnClick + sx={{ wordWrap: 'break-word' }} + />
diff --git a/frontend/src/modules/Environments/services/index.js b/frontend/src/modules/Environments/services/index.js index 14f5b659f..d0895efa5 100644 --- a/frontend/src/modules/Environments/services/index.js +++ b/frontend/src/modules/Environments/services/index.js @@ -22,3 +22,4 @@ export * from './removeConsumptionRole'; export * from './removeGroup'; export * from './updateEnvironment'; export * from './updateGroupEnvironmentPermissions'; +export * from './updateConsumptionRole'; diff --git a/frontend/src/modules/Environments/services/updateConsumptionRole.js b/frontend/src/modules/Environments/services/updateConsumptionRole.js new file mode 100644 index 000000000..3cc9610bb --- /dev/null +++ b/frontend/src/modules/Environments/services/updateConsumptionRole.js @@ -0,0 +1,33 @@ +import { gql } from 'apollo-boost'; + +export const updateConsumptionRole = ({ + environmentUri, + consumptionRoleUri, + input +}) => ({ + variables: { + environmentUri, + consumptionRoleUri, + input + }, + mutation: gql` + mutation updateConsumptionRole( + $environmentUri: String! + $consumptionRoleUri: String! + $input: UpdateConsumptionRoleInput! + ) { + updateConsumptionRole( + environmentUri: $environmentUri + consumptionRoleUri: $consumptionRoleUri + input: $input + ) { + consumptionRoleUri + consumptionRoleName + environmentUri + groupUri + IAMRoleName + IAMRoleArn + } + } + ` +}); diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js new file mode 100644 index 000000000..5d27fbfea --- /dev/null +++ b/frontend/src/utils/api.js @@ -0,0 +1,50 @@ +import { listEnvironmentGroups, useClient } from '../services'; +import { useEffect, useState } from 'react'; +import { SET_ERROR, useDispatch } from '../globalErrors'; +import { Defaults } from '../design'; + +// TODO DRY fetchGroup usages using this func +export const useFetchGroups = (environment) => { + const client = useClient(); + const [loadingGroups, setLoadingGroups] = useState(true); + const [groupOptions, setGroupOptions] = useState([]); + const dispatch = useDispatch(); + const fetchGroups = async (environmentUri) => { + try { + setLoadingGroups(true); + 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 }); + } finally { + setLoadingGroups(false); + } + }; + + useEffect(() => { + if (client && environment) { + fetchGroups(environment.environmentUri).catch((e) => + dispatch({ + type: SET_ERROR, + error: e.message + }) + ); + } + }, [client, environment, dispatch]); + + return { groupOptions, loadingGroups }; +};