mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-14 17:02:49 +03:00
console: develop carded table for role based allow list permission
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/5648 Co-authored-by: Daniele Cammareri <5709409+dancamma@users.noreply.github.com> GitOrigin-RevId: c1cdd6899e5c50032e097496696097874f5abfd5
This commit is contained in:
parent
00aeb57adf
commit
e0c9e2802c
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Story, Meta } from '@storybook/react';
|
||||||
|
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
|
||||||
|
import {
|
||||||
|
AllowListPermissions,
|
||||||
|
AllowListPermissionsTabProps,
|
||||||
|
} from './AllowListPermissions';
|
||||||
|
import { handlers } from '../../hooks/AllowListPermissions/mock/handlers.mocks';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Features/Allow List Permission/CardedTable',
|
||||||
|
component: AllowListPermissions,
|
||||||
|
decorators: [ReactQueryDecorator()],
|
||||||
|
parameters: {
|
||||||
|
msw: handlers(),
|
||||||
|
},
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
export const Default: Story<AllowListPermissionsTabProps> = args => {
|
||||||
|
return <AllowListPermissions {...args} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
Default.args = {
|
||||||
|
collectionName: 'allowed-queries',
|
||||||
|
};
|
@ -0,0 +1,134 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Switch } from '@/new-components/Switch';
|
||||||
|
import { FaCheck } from 'react-icons/fa';
|
||||||
|
import { useSetRoleToAllowListPermission } from '../../hooks/AllowListPermissions/useSetRoleToAllowListPermissions';
|
||||||
|
import { useEnabledRolesFromAllowListState } from '../../hooks/AllowListPermissions/useEnabledRolesFromAllowListState';
|
||||||
|
|
||||||
|
export interface AllowListPermissionsTabProps {
|
||||||
|
collectionName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AllowListPermissions: React.FC<AllowListPermissionsTabProps> = ({
|
||||||
|
collectionName,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
allAvailableRoles,
|
||||||
|
newRoles,
|
||||||
|
setNewRoles,
|
||||||
|
enabledRoles,
|
||||||
|
setEnabledRoles,
|
||||||
|
} = useEnabledRolesFromAllowListState(collectionName);
|
||||||
|
|
||||||
|
const { setRoleToAllowListPermission } =
|
||||||
|
useSetRoleToAllowListPermission(collectionName);
|
||||||
|
|
||||||
|
const handleToggle = (roleName: string, index: number) => {
|
||||||
|
let newEnabledRoles = [];
|
||||||
|
// add roleName to enabledRoles, remove duplicates
|
||||||
|
if (enabledRoles.includes(roleName)) {
|
||||||
|
newEnabledRoles = Array.from(
|
||||||
|
new Set(enabledRoles.filter(role => role !== roleName))
|
||||||
|
);
|
||||||
|
setEnabledRoles(newEnabledRoles);
|
||||||
|
} else {
|
||||||
|
newEnabledRoles = Array.from(new Set([...enabledRoles, roleName]));
|
||||||
|
// remove enabled role from newRoles
|
||||||
|
setNewRoles(
|
||||||
|
newRoles.length > 1
|
||||||
|
? newRoles.filter(role => role !== newRoles[index])
|
||||||
|
: newRoles
|
||||||
|
);
|
||||||
|
setEnabledRoles(newEnabledRoles);
|
||||||
|
}
|
||||||
|
|
||||||
|
setRoleToAllowListPermission(newEnabledRoles);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNewRole = (value: string, index: number) => {
|
||||||
|
const newAddedRoles = [...newRoles];
|
||||||
|
newAddedRoles[index] = value;
|
||||||
|
|
||||||
|
// if last item is not an empty string, add empty string as last index
|
||||||
|
if (newAddedRoles[newAddedRoles.length - 1] !== '') {
|
||||||
|
newAddedRoles.push('');
|
||||||
|
}
|
||||||
|
// drop last one
|
||||||
|
if (newAddedRoles[newAddedRoles.length - 2] === '') {
|
||||||
|
newAddedRoles.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
setNewRoles(newAddedRoles);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="p-md">
|
||||||
|
<div className="overflow-x-auto border border-gray-300 rounded mb-md">
|
||||||
|
<table className="min-w-full divide-y divide-gray-300 text-left">
|
||||||
|
<thead>
|
||||||
|
<tr className="divide-x divide-gray-300">
|
||||||
|
<th className="w-0 bg-gray-50 px-sm py-xs text-sm font-semibold text-muted uppercase tracking-wider">
|
||||||
|
Role
|
||||||
|
</th>
|
||||||
|
<th className="text-center bg-gray-50 px-sm py-xs text-sm font-semibold text-muted uppercase tracking-wider">
|
||||||
|
Access
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody className="bg-white divide-y divide-gray-300">
|
||||||
|
<tr className="group divide-x divide-gray-300">
|
||||||
|
<td className="w-0 bg-gray-50 p-sm font-semibold text-muted">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<label className="flex items-center">admin</label>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="text-center p-sm whitespace-nowrap cursor-not-allowed">
|
||||||
|
<FaCheck className="text-green-600" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{allAvailableRoles.map((roleName, index) => (
|
||||||
|
<tr className="divide-x divide-gray-300">
|
||||||
|
<td className="w-0 bg-gray-50 p-sm font-semibold text-muted">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<label>{roleName}</label>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="group relative text-center p-sm whitespace-nowrap cursor-pointer">
|
||||||
|
<Switch
|
||||||
|
checked={enabledRoles?.includes(roleName)}
|
||||||
|
onCheckedChange={() => handleToggle(roleName, index)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
{newRoles.map((newRole, index) => (
|
||||||
|
<tr className="divide-x divide-gray-300">
|
||||||
|
<td className="w-0 bg-gray-50 p-2 font-semibold text-muted">
|
||||||
|
<input
|
||||||
|
x-model="newRole"
|
||||||
|
type="text"
|
||||||
|
className="block w-full h-input min-w-max shadow-sm rounded border border-gray-300 hover:border-gray-400 focus:outline-0 focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400"
|
||||||
|
placeholder="Create New Role..."
|
||||||
|
value={newRole}
|
||||||
|
onChange={e => handleNewRole(e.target.value, index)}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td className="group relative text-center p-sm whitespace-nowrap cursor-pointer">
|
||||||
|
<Switch
|
||||||
|
checked={enabledRoles.includes(newRole)}
|
||||||
|
onCheckedChange={
|
||||||
|
newRole !== ''
|
||||||
|
? () => handleToggle(newRole, index)
|
||||||
|
: () => {}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,24 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useRoles } from '@/features/MetadataAPI';
|
||||||
|
import { useEnabledRolesFromAllowList } from './useEnabledRolesFromAllowList';
|
||||||
|
|
||||||
|
export const useEnabledRolesFromAllowListState = (collectionName: string) => {
|
||||||
|
const { data: allAvailableRoles } = useRoles();
|
||||||
|
const { data: allowListRoles } = useEnabledRolesFromAllowList(collectionName);
|
||||||
|
const [enabledRoles, setEnabledRoles] = React.useState<string[]>([]);
|
||||||
|
const [newRoles, setNewRoles] = React.useState<string[]>(['']);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (allowListRoles && allowListRoles !== enabledRoles) {
|
||||||
|
setEnabledRoles(allowListRoles);
|
||||||
|
}
|
||||||
|
}, [allowListRoles]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
allAvailableRoles,
|
||||||
|
newRoles,
|
||||||
|
setNewRoles,
|
||||||
|
enabledRoles,
|
||||||
|
setEnabledRoles,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,92 @@
|
|||||||
|
import {
|
||||||
|
REQUEST_SUCCESS,
|
||||||
|
updateSchemaInfo,
|
||||||
|
} from '@/components/Services/Data/DataActions';
|
||||||
|
import {
|
||||||
|
allowedMetadataTypes,
|
||||||
|
useMetadata,
|
||||||
|
useMetadataMigration,
|
||||||
|
} from '@/features/MetadataAPI';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import { AnyAction } from 'redux';
|
||||||
|
import { ReduxState } from '@/types';
|
||||||
|
import { ThunkDispatch } from 'redux-thunk';
|
||||||
|
import { useFireNotification } from '@/new-components/Notifications';
|
||||||
|
|
||||||
|
export const useSetRoleToAllowListPermission = (collectionName: string) => {
|
||||||
|
const { fireNotification } = useFireNotification();
|
||||||
|
const { data: metadata } = useMetadata();
|
||||||
|
const dispatch: ThunkDispatch<ReduxState, unknown, AnyAction> = useDispatch();
|
||||||
|
|
||||||
|
// to check if collection is present in allowList
|
||||||
|
const isAllowList = metadata?.metadata.allowlist?.find(
|
||||||
|
qs => qs.collection === collectionName
|
||||||
|
);
|
||||||
|
|
||||||
|
const mutation = useMetadataMigration({
|
||||||
|
onSuccess: () => {
|
||||||
|
dispatch({ type: REQUEST_SUCCESS });
|
||||||
|
dispatch(updateSchemaInfo()).then(() => {
|
||||||
|
fireNotification({
|
||||||
|
title: 'Success!',
|
||||||
|
message: 'Permission added',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: (error: Error) => {
|
||||||
|
fireNotification({
|
||||||
|
title: 'Error',
|
||||||
|
message: error?.message ?? 'Error while adding permission',
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const setRoleToAllowListPermission = (roles: string[]): void => {
|
||||||
|
// drop collection from allow list if we are disabling the last role
|
||||||
|
if (roles.length === 0) {
|
||||||
|
const type: allowedMetadataTypes = 'drop_collection_from_allowlist';
|
||||||
|
mutation.mutate({
|
||||||
|
query: {
|
||||||
|
type,
|
||||||
|
args: {
|
||||||
|
collection: collectionName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (!isAllowList) {
|
||||||
|
// add collection to allow list and add roles if we are enabling the 1st role
|
||||||
|
const type: allowedMetadataTypes = 'add_collection_to_allowlist';
|
||||||
|
mutation.mutate({
|
||||||
|
query: {
|
||||||
|
type,
|
||||||
|
args: {
|
||||||
|
collection: collectionName,
|
||||||
|
scope: {
|
||||||
|
global: false,
|
||||||
|
roles,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const type: allowedMetadataTypes =
|
||||||
|
'update_scope_of_collection_in_allowlist';
|
||||||
|
mutation.mutate({
|
||||||
|
query: {
|
||||||
|
type,
|
||||||
|
args: {
|
||||||
|
collection: collectionName,
|
||||||
|
scope: {
|
||||||
|
global: false,
|
||||||
|
roles,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return { setRoleToAllowListPermission };
|
||||||
|
};
|
@ -40,6 +40,8 @@ export const allowedMetadataTypesArr = [
|
|||||||
'delete_remote_schema_remote_relationship',
|
'delete_remote_schema_remote_relationship',
|
||||||
'add_remote_schema',
|
'add_remote_schema',
|
||||||
'update_scope_of_collection_in_allowlist',
|
'update_scope_of_collection_in_allowlist',
|
||||||
|
'drop_collection_from_allowlist',
|
||||||
|
'add_collection_from_allowlist',
|
||||||
'bulk',
|
'bulk',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user