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',
|
||||
'add_remote_schema',
|
||||
'update_scope_of_collection_in_allowlist',
|
||||
'drop_collection_from_allowlist',
|
||||
'add_collection_from_allowlist',
|
||||
'bulk',
|
||||
] as const;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user