mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-12-15 09:22:43 +03:00
Logical Model Permissions
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/9276 Co-authored-by: Erik Magnusson <32518962+ejkkan@users.noreply.github.com> GitOrigin-RevId: ee1dddf813fac3f3a29c5444644e589a8e67d3cb
This commit is contained in:
parent
6102270f11
commit
921b148dc8
@ -1,4 +1,4 @@
|
|||||||
import { IndexRedirect, IndexRoute, Route } from 'react-router';
|
import { IndexRedirect, IndexRoute, Redirect, Route } from 'react-router';
|
||||||
|
|
||||||
import { SERVER_CONSOLE_MODE } from '../../../constants';
|
import { SERVER_CONSOLE_MODE } from '../../../constants';
|
||||||
import globals from '../../../Globals';
|
import globals from '../../../Globals';
|
||||||
@ -39,6 +39,7 @@ import { ModifyTableContainer } from './TableModify/ModifyTableContainer';
|
|||||||
import { LandingPageRoute as NativeQueries } from '../../../features/Data/LogicalModels/LandingPage/LandingPage';
|
import { LandingPageRoute as NativeQueries } from '../../../features/Data/LogicalModels/LandingPage/LandingPage';
|
||||||
|
|
||||||
import { TrackStoredProcedureRoute } from '../../../features/Data/LogicalModels/StoredProcedures/StoredProcedureWidget.route';
|
import { TrackStoredProcedureRoute } from '../../../features/Data/LogicalModels/StoredProcedures/StoredProcedureWidget.route';
|
||||||
|
import { LogicalModelPermissionsRoute } from '../../../features/Data/LogicalModels/LogicalModelPermissions/LogicalModelPermissionsPage';
|
||||||
import { ManageFunction } from '../../../features/Data/ManageFunction/ManageFunction';
|
import { ManageFunction } from '../../../features/Data/ManageFunction/ManageFunction';
|
||||||
import {
|
import {
|
||||||
UpdateNativeQueryRoute,
|
UpdateNativeQueryRoute,
|
||||||
@ -90,7 +91,16 @@ const makeDataRouter = (
|
|||||||
path="native-query/:source/:name"
|
path="native-query/:source/:name"
|
||||||
component={UpdateNativeQueryRoute}
|
component={UpdateNativeQueryRoute}
|
||||||
/>
|
/>
|
||||||
<Route path="logical-models" component={NativeQueries} />
|
<Route path="logical-models">
|
||||||
|
<IndexRoute component={NativeQueries} />
|
||||||
|
<Redirect from=":source" to="/data/native-queries/logical-models" />
|
||||||
|
<Route path=":source">
|
||||||
|
<Route
|
||||||
|
path=":name/permissions"
|
||||||
|
component={LogicalModelPermissionsRoute}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
|
</Route>
|
||||||
<Route path="stored-procedures" component={NativeQueries} />
|
<Route path="stored-procedures" component={NativeQueries} />
|
||||||
<Route
|
<Route
|
||||||
path="stored-procedures/track"
|
path="stored-procedures/track"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { InjectedRouter, Link, withRouter } from 'react-router';
|
import { InjectedRouter, Link, withRouter } from 'react-router';
|
||||||
import { useDestructiveAlert } from '../../../../new-components/Alert';
|
import { useDestructiveAlert } from '../../../../new-components/Alert';
|
||||||
import { Button } from '../../../../new-components/Button';
|
import { Button } from '../../../../new-components/Button';
|
||||||
@ -9,14 +9,14 @@ import { useMetadata } from '../../../hasura-metadata-api';
|
|||||||
import { useTrackLogicalModel } from '../../hooks/useTrackLogicalModel';
|
import { useTrackLogicalModel } from '../../hooks/useTrackLogicalModel';
|
||||||
import { useTrackNativeQuery } from '../../hooks/useTrackNativeQuery';
|
import { useTrackNativeQuery } from '../../hooks/useTrackNativeQuery';
|
||||||
import { LogicalModelWidget } from '../LogicalModelWidget/LogicalModelWidget';
|
import { LogicalModelWidget } from '../LogicalModelWidget/LogicalModelWidget';
|
||||||
import { RouteWrapper } from '../components/RouteWrapper';
|
|
||||||
|
|
||||||
import { LogicalModelWithSource, NativeQueryWithSource } from '../types';
|
import { LogicalModelWithSource, NativeQueryWithSource } from '../types';
|
||||||
import { extractModelsAndQueriesFromMetadata } from '../utils';
|
|
||||||
import { ListLogicalModels } from './components/ListLogicalModels';
|
import { ListLogicalModels } from './components/ListLogicalModels';
|
||||||
import { ListNativeQueries } from './components/ListNativeQueries';
|
import { ListNativeQueries } from './components/ListNativeQueries';
|
||||||
import { ListStoredProcedures } from './components/ListStoredProcedures';
|
import { ListStoredProcedures } from './components/ListStoredProcedures';
|
||||||
import { NATIVE_QUERY_ROUTES } from '../constants';
|
import { NATIVE_QUERY_ROUTES } from '../constants';
|
||||||
|
import { extractModelsAndQueriesFromMetadata } from '../../../hasura-metadata-api/selectors';
|
||||||
|
import { RouteWrapper } from '../components/RouteWrapper';
|
||||||
|
|
||||||
export const LandingPage = ({ pathname }: { pathname: string }) => {
|
export const LandingPage = ({ pathname }: { pathname: string }) => {
|
||||||
const push = usePushRoute();
|
const push = usePushRoute();
|
||||||
@ -149,8 +149,10 @@ export const LandingPage = ({ pathname }: { pathname: string }) => {
|
|||||||
<ListLogicalModels
|
<ListLogicalModels
|
||||||
logicalModels={logicalModels}
|
logicalModels={logicalModels}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
onEditClick={() => {
|
onEditClick={model => {
|
||||||
push?.('/data/native-queries/logical-models/permissions');
|
push?.(
|
||||||
|
`/data/native-queries/logical-models/${model.source.name}/${model.name}/permissions`
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
onRemoveClick={handleRemoveLogicalModel}
|
onRemoveClick={handleRemoveLogicalModel}
|
||||||
/>
|
/>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { StoryObj, Meta } from '@storybook/react';
|
import { StoryObj, Meta } from '@storybook/react';
|
||||||
import { buildMetadata } from '../../mocks/metadata';
|
import { buildMetadata } from '../../mocks/metadata';
|
||||||
import { extractModelsAndQueriesFromMetadata } from '../../utils';
|
|
||||||
import { ListLogicalModels } from './ListLogicalModels';
|
import { ListLogicalModels } from './ListLogicalModels';
|
||||||
|
import { extractModelsAndQueriesFromMetadata } from '../../../../hasura-metadata-api/selectors';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
component: ListLogicalModels,
|
component: ListLogicalModels,
|
||||||
|
@ -39,8 +39,9 @@ export const ListLogicalModels = ({
|
|||||||
header: 'Actions',
|
header: 'Actions',
|
||||||
cell: ({ cell, row }) => (
|
cell: ({ cell, row }) => (
|
||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
{/* Re add once we implement Edit functionality */}
|
<Button onClick={() => onEditClick(row.original)}>
|
||||||
{/* <Button onClick={() => onEditClick(row.original)}>Edit</Button> */}
|
Edit Permissions
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
mode="destructive"
|
mode="destructive"
|
||||||
onClick={() => onRemoveClick(row.original)}
|
onClick={() => onRemoveClick(row.original)}
|
||||||
|
@ -2,7 +2,7 @@ import { StoryObj, Meta } from '@storybook/react';
|
|||||||
|
|
||||||
import { ListNativeQueries } from './ListNativeQueries';
|
import { ListNativeQueries } from './ListNativeQueries';
|
||||||
import { buildMetadata } from '../../mocks/metadata';
|
import { buildMetadata } from '../../mocks/metadata';
|
||||||
import { extractModelsAndQueriesFromMetadata } from '../../utils';
|
import { extractModelsAndQueriesFromMetadata } from '../../../../hasura-metadata-api/selectors';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
component: ListNativeQueries,
|
component: ListNativeQueries,
|
||||||
|
@ -0,0 +1,173 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { LogicalModelPermissions } from './LogicalModelPermissions';
|
||||||
|
import { comparators } from '../../../Permissions/PermissionsForm/components/RowPermissionsBuilder/components/__tests__/fixtures/comparators';
|
||||||
|
import { userEvent, within } from '@storybook/testing-library';
|
||||||
|
import { expect } from '@storybook/jest';
|
||||||
|
import { ComponentProps } from 'react';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Features/Permissions/Form/Logical Model Permissions',
|
||||||
|
component: LogicalModelPermissions,
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof LogicalModelPermissions>;
|
||||||
|
|
||||||
|
const noPermissionArgs: Partial<
|
||||||
|
ComponentProps<typeof LogicalModelPermissions>
|
||||||
|
> = {
|
||||||
|
logicalModels: [
|
||||||
|
{
|
||||||
|
source: {
|
||||||
|
name: 'default',
|
||||||
|
kind: 'postgres',
|
||||||
|
configuration: {},
|
||||||
|
tables: [],
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{ name: 'one', nullable: false, type: 'text' },
|
||||||
|
{ name: 'two', nullable: false, type: 'text' },
|
||||||
|
],
|
||||||
|
name: 'hello_world',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
logicalModelName: 'hello_world',
|
||||||
|
comparators,
|
||||||
|
};
|
||||||
|
|
||||||
|
const existingPermissionArgs: Partial<
|
||||||
|
ComponentProps<typeof LogicalModelPermissions>
|
||||||
|
> = {
|
||||||
|
logicalModels: [
|
||||||
|
{
|
||||||
|
source: {
|
||||||
|
name: 'default',
|
||||||
|
kind: 'postgres',
|
||||||
|
configuration: {},
|
||||||
|
tables: [],
|
||||||
|
},
|
||||||
|
fields: [
|
||||||
|
{ name: 'one', nullable: false, type: 'text' },
|
||||||
|
{ name: 'two', nullable: false, type: 'text' },
|
||||||
|
],
|
||||||
|
name: 'hello_world',
|
||||||
|
select_permissions: [
|
||||||
|
{
|
||||||
|
permission: {
|
||||||
|
columns: ['one'],
|
||||||
|
filter: { one: { _eq: 'eqone' } },
|
||||||
|
},
|
||||||
|
role: 'user',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
logicalModelName: 'hello_world',
|
||||||
|
comparators,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NewPermission: Story = {
|
||||||
|
args: noPermissionArgs,
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
// Should allow writing new role name and then clicking select should open form
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
await userEvent.type(canvas.getByTestId('new-role-input'), 'reader');
|
||||||
|
await userEvent.click(canvas.getByTestId('reader-select-permissions-cell'));
|
||||||
|
await expect(canvas.getByTestId('permissions-form')).toBeInTheDocument();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ExistingPermission: Story = {
|
||||||
|
args: existingPermissionArgs,
|
||||||
|
play: async ({ canvasElement, args: { onSave } }) => {
|
||||||
|
// Clicking select on existing permission should open form with correct role (user) and action (select)
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
await userEvent.click(canvas.getByTestId('user-select-permissions-cell'));
|
||||||
|
await expect(canvas.getByTestId('permissions-form')).toBeInTheDocument();
|
||||||
|
await expect((await canvas.findByTestId('role-pill')).textContent).toBe(
|
||||||
|
'user'
|
||||||
|
);
|
||||||
|
await expect((await canvas.findByTestId('action-pill')).textContent).toBe(
|
||||||
|
'select'
|
||||||
|
);
|
||||||
|
|
||||||
|
// It should send correct existing permission (`{"one":{"_eq":"eqone"}}`) when clicking save
|
||||||
|
await userEvent.click(canvas.getByTestId('save-permissions-button'));
|
||||||
|
await expect(onSave).toHaveBeenCalledWith({
|
||||||
|
roleName: 'user',
|
||||||
|
filter: {
|
||||||
|
one: {
|
||||||
|
_eq: 'eqone',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
columns: ['one'],
|
||||||
|
action: 'select',
|
||||||
|
isNew: false,
|
||||||
|
source: 'default',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UpdatingPermissions: Story = {
|
||||||
|
args: existingPermissionArgs,
|
||||||
|
play: async ({ canvasElement, args: { onSave } }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
await userEvent.click(canvas.getByTestId('user-select-permissions-cell'));
|
||||||
|
|
||||||
|
// Clicking "Without any checks" should set permission to `{}`
|
||||||
|
await userEvent.click(canvas.getByTestId('without-filter'));
|
||||||
|
// Clicking `two` should select that column
|
||||||
|
await userEvent.click(canvas.getByTestId('column-two-checkbox'));
|
||||||
|
|
||||||
|
await userEvent.click(canvas.getByTestId('save-permissions-button'));
|
||||||
|
await expect(onSave).toHaveBeenCalledWith({
|
||||||
|
roleName: 'user',
|
||||||
|
filter: {},
|
||||||
|
columns: ['one', 'two'],
|
||||||
|
action: 'select',
|
||||||
|
isNew: false,
|
||||||
|
source: 'default',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ToggleAllColumns: Story = {
|
||||||
|
args: existingPermissionArgs,
|
||||||
|
play: async ({ canvasElement, args: { onSave } }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
await userEvent.click(canvas.getByTestId('user-select-permissions-cell'));
|
||||||
|
|
||||||
|
await userEvent.click(canvas.getByTestId('toggle-all-columns'));
|
||||||
|
|
||||||
|
await userEvent.click(canvas.getByTestId('save-permissions-button'));
|
||||||
|
await expect(onSave).toHaveBeenCalledWith({
|
||||||
|
roleName: 'user',
|
||||||
|
filter: { one: { _eq: 'eqone' } },
|
||||||
|
columns: ['one', 'two'],
|
||||||
|
action: 'select',
|
||||||
|
isNew: false,
|
||||||
|
source: 'default',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const UntoggleAllColumns: Story = {
|
||||||
|
args: existingPermissionArgs,
|
||||||
|
play: async ({ canvasElement, args: { onSave } }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
await userEvent.click(canvas.getByTestId('user-select-permissions-cell'));
|
||||||
|
|
||||||
|
await userEvent.click(canvas.getByTestId('toggle-all-columns'));
|
||||||
|
await userEvent.click(canvas.getByTestId('toggle-all-columns'));
|
||||||
|
|
||||||
|
await userEvent.click(canvas.getByTestId('save-permissions-button'));
|
||||||
|
await expect(onSave).toHaveBeenCalledWith({
|
||||||
|
roleName: 'user',
|
||||||
|
filter: { one: { _eq: 'eqone' } },
|
||||||
|
columns: [],
|
||||||
|
action: 'select',
|
||||||
|
isNew: false,
|
||||||
|
source: 'default',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,125 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import {
|
||||||
|
Comparators,
|
||||||
|
RowPermissionsInput,
|
||||||
|
} from '../../../Permissions/PermissionsForm/components/RowPermissionsBuilder/components';
|
||||||
|
import {
|
||||||
|
LogicalModelWithPermissions,
|
||||||
|
OnDelete,
|
||||||
|
OnSave,
|
||||||
|
Permission,
|
||||||
|
RowSelectPermissionsType,
|
||||||
|
} from './components/types';
|
||||||
|
import {
|
||||||
|
PermissionsTable,
|
||||||
|
PermissionsTableProps,
|
||||||
|
} from './components/PermissionsTable';
|
||||||
|
import { PermissionsForm } from './components/PermissionsForm';
|
||||||
|
import { usePermissionsFormContext } from './hooks/usePermissionForm';
|
||||||
|
import { LogicalModelPermissionsFormProvider } from './components/LogicalModelPermissionsFormProvider';
|
||||||
|
|
||||||
|
export type LogicalModelPermissionsProps = {
|
||||||
|
logicalModelName: string;
|
||||||
|
logicalModels: LogicalModelWithPermissions[];
|
||||||
|
onSave: OnSave;
|
||||||
|
onDelete: OnDelete;
|
||||||
|
comparators: Comparators;
|
||||||
|
isCreating?: boolean;
|
||||||
|
isRemoving?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LogicalModelPermissionsState = {
|
||||||
|
permissions: Permission[];
|
||||||
|
activePermission: number | null;
|
||||||
|
rowSelectPermissions: RowSelectPermissionsType;
|
||||||
|
columns: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LogicalModelPermissions = ({
|
||||||
|
logicalModelName,
|
||||||
|
logicalModels,
|
||||||
|
onSave,
|
||||||
|
onDelete,
|
||||||
|
comparators,
|
||||||
|
isCreating,
|
||||||
|
isRemoving,
|
||||||
|
}: LogicalModelPermissionsProps) => {
|
||||||
|
const logicalModel = logicalModels.find(
|
||||||
|
model => model.name === logicalModelName
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LogicalModelPermissionsFormProvider logicalModel={logicalModel}>
|
||||||
|
<PureLogicalModelPermissions
|
||||||
|
onSave={onSave}
|
||||||
|
onDelete={onDelete}
|
||||||
|
isCreating={isCreating}
|
||||||
|
isRemoving={isRemoving}
|
||||||
|
comparators={comparators}
|
||||||
|
logicalModelName={logicalModelName}
|
||||||
|
logicalModels={logicalModels}
|
||||||
|
/>
|
||||||
|
</LogicalModelPermissionsFormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const PureLogicalModelPermissions = ({
|
||||||
|
onSave: onSaveProp,
|
||||||
|
onDelete: onDeleteProp,
|
||||||
|
logicalModelName,
|
||||||
|
comparators,
|
||||||
|
logicalModels,
|
||||||
|
isCreating,
|
||||||
|
isRemoving,
|
||||||
|
}: LogicalModelPermissionsProps) => {
|
||||||
|
const { activePermission, permissions, setPermission } =
|
||||||
|
usePermissionsFormContext();
|
||||||
|
const permission =
|
||||||
|
activePermission === null ? null : permissions[activePermission];
|
||||||
|
const onSave = useCallback(async () => {
|
||||||
|
if (!permission) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await onSaveProp(permission);
|
||||||
|
}, [onSaveProp, permission]);
|
||||||
|
const onDelete = useCallback(async () => {
|
||||||
|
if (!permission) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await onDeleteProp(permission);
|
||||||
|
}, [onDeleteProp, permission]);
|
||||||
|
const allowedActions: PermissionsTableProps['allowedActions'] = ['select'];
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<div className="grid gap-4">
|
||||||
|
<PermissionsTable
|
||||||
|
allowedActions={allowedActions}
|
||||||
|
permissions={permissions}
|
||||||
|
/>
|
||||||
|
{permission && (
|
||||||
|
<PermissionsForm
|
||||||
|
permission={permission}
|
||||||
|
onSave={onSave}
|
||||||
|
onDelete={onDelete}
|
||||||
|
isCreating={isCreating}
|
||||||
|
isRemoving={isRemoving}
|
||||||
|
PermissionsInput={
|
||||||
|
<RowPermissionsInput
|
||||||
|
table={undefined}
|
||||||
|
forbidden={['exists']}
|
||||||
|
tables={[]}
|
||||||
|
onPermissionsChange={permissionFilter => {
|
||||||
|
setPermission(permission.roleName, permissionFilter);
|
||||||
|
}}
|
||||||
|
logicalModel={logicalModelName}
|
||||||
|
logicalModels={logicalModels}
|
||||||
|
permissions={permission.filter}
|
||||||
|
comparators={comparators}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,106 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { LogicalModelPermissionsPage } from './LogicalModelPermissionsPage';
|
||||||
|
import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query';
|
||||||
|
import { RouteWrapper } from '../components/RouteWrapper';
|
||||||
|
import { handlers, deleteHandlers } from './mocks';
|
||||||
|
|
||||||
|
const name = 'LogicalModel';
|
||||||
|
const source = 'Postgres';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Features/Permissions/Form/Logical Model Permissions Page',
|
||||||
|
component: params => {
|
||||||
|
return (
|
||||||
|
<RouteWrapper
|
||||||
|
route={
|
||||||
|
'/data/native-queries/logical-models/{{source}}/{{name}}/permissions'
|
||||||
|
}
|
||||||
|
itemSourceName={source}
|
||||||
|
itemName={name}
|
||||||
|
>
|
||||||
|
<LogicalModelPermissionsPage {...params} />
|
||||||
|
</RouteWrapper>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
decorators: [ReactQueryDecorator()],
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof LogicalModelPermissionsPage>;
|
||||||
|
|
||||||
|
export const SavePermission: Story = {
|
||||||
|
args: {
|
||||||
|
name,
|
||||||
|
source,
|
||||||
|
},
|
||||||
|
// Play function deleted because it was failing in chromatic
|
||||||
|
// It works locally and in storybook, but not in chromatic
|
||||||
|
// TODO: Figure out the issue
|
||||||
|
// play: async ({ canvasElement }) => {
|
||||||
|
// const canvas = within(canvasElement);
|
||||||
|
// // Wait for both tabs and loading to be present because otherwise story fails in chromatic
|
||||||
|
// expect(
|
||||||
|
// await canvas.findByTestId(
|
||||||
|
// 'logical-model-permissions-tab',
|
||||||
|
// {},
|
||||||
|
// {
|
||||||
|
// timeout: 10000,
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// ).toBeInTheDocument();
|
||||||
|
// expect(
|
||||||
|
// await canvas.findByTestId('loading-logical-model-permissions')
|
||||||
|
// ).toBeInTheDocument();
|
||||||
|
// await canvas.findByTestId('permissions-table');
|
||||||
|
// await userEvent.click(
|
||||||
|
// await canvas.findByTestId('editor-select-permissions-cell')
|
||||||
|
// );
|
||||||
|
// await userEvent.click(await canvas.findByTestId('save-permissions-button'));
|
||||||
|
// await expect(
|
||||||
|
// await canvas.findByText('Permissions saved successfully!')
|
||||||
|
// ).toBeInTheDocument();
|
||||||
|
// },
|
||||||
|
parameters: {
|
||||||
|
msw: handlers(),
|
||||||
|
consoleType: 'pro',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DeletePermission: Story = {
|
||||||
|
args: {
|
||||||
|
name,
|
||||||
|
source,
|
||||||
|
},
|
||||||
|
// Play function deleted because it was failing in chromatic
|
||||||
|
// It works locally and in storybook, but not in chromatic
|
||||||
|
// TODO: Figure out the issue
|
||||||
|
// play: async ({ canvasElement }) => {
|
||||||
|
// const canvas = within(canvasElement);
|
||||||
|
// // Wait for both tabs and loading to be present because otherwise story fails in chromatic
|
||||||
|
// expect(
|
||||||
|
// await canvas.findByTestId(
|
||||||
|
// 'logical-model-permissions-tab',
|
||||||
|
// {},
|
||||||
|
// {
|
||||||
|
// timeout: 10000,
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
// expect(
|
||||||
|
// await canvas.findByTestId('loading-logical-model-permissions')
|
||||||
|
// ).toBeInTheDocument();
|
||||||
|
// await canvas.findByTestId('permissions-table');
|
||||||
|
// await userEvent.click(
|
||||||
|
// await canvas.findByTestId('editor-select-permissions-cell')
|
||||||
|
// );
|
||||||
|
// await userEvent.click(
|
||||||
|
// await canvas.findByTestId('delete-permissions-button')
|
||||||
|
// );
|
||||||
|
// await expect(
|
||||||
|
// await canvas.findByText('Permissions successfully deleted!')
|
||||||
|
// ).toBeInTheDocument();
|
||||||
|
// },
|
||||||
|
parameters: {
|
||||||
|
msw: deleteHandlers(),
|
||||||
|
consoleType: 'pro',
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,109 @@
|
|||||||
|
import { InjectedRouter, withRouter } from 'react-router';
|
||||||
|
import { RouteWrapper } from '../components/RouteWrapper';
|
||||||
|
import { Tabs } from '../../../../new-components/Tabs';
|
||||||
|
import { LogicalModelPermissions } from './LogicalModelPermissions';
|
||||||
|
import { useCreateLogicalModelsPermissions } from './hooks/useCreateLogicalModelsPermissions';
|
||||||
|
import { useRemoveLogicalModelsPermissions } from './hooks/useRemoveLogicalModelsPermissions';
|
||||||
|
import { useMetadata } from '../../../hasura-metadata-api';
|
||||||
|
import { usePermissionComparators } from '../../../Permissions/PermissionsForm/components/RowPermissionsBuilder/hooks/usePermissionComparators';
|
||||||
|
import Skeleton from 'react-loading-skeleton';
|
||||||
|
import { extractModelsAndQueriesFromMetadata } from '../../../hasura-metadata-api/selectors';
|
||||||
|
|
||||||
|
export const LogicalModelPermissionsPage = ({
|
||||||
|
source,
|
||||||
|
name,
|
||||||
|
}: {
|
||||||
|
source: string;
|
||||||
|
name: string;
|
||||||
|
}) => {
|
||||||
|
const { data, isLoading } = useMetadata(m =>
|
||||||
|
extractModelsAndQueriesFromMetadata(m)
|
||||||
|
);
|
||||||
|
const comparators = usePermissionComparators();
|
||||||
|
const logicalModels = data?.models ?? [];
|
||||||
|
const logicalModel = logicalModels.find(
|
||||||
|
model => model.name === name && model.source.name === source
|
||||||
|
);
|
||||||
|
const { create, isLoading: isCreating } = useCreateLogicalModelsPermissions({
|
||||||
|
logicalModels,
|
||||||
|
source: logicalModel?.source,
|
||||||
|
});
|
||||||
|
const { remove, isLoading: isRemoving } = useRemoveLogicalModelsPermissions({
|
||||||
|
logicalModels,
|
||||||
|
source: logicalModel?.source,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tabs
|
||||||
|
// Recreate the key when the logical model permissions change to reset the form
|
||||||
|
key={logicalModel?.select_permissions?.length}
|
||||||
|
data-testid="logical-model-permissions-tab"
|
||||||
|
defaultValue={'logical-model-permissions'}
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
value: 'logical-model-permissions',
|
||||||
|
label: `Permissions`,
|
||||||
|
content: (
|
||||||
|
<div className="mt-md">
|
||||||
|
{isLoading ? (
|
||||||
|
<div
|
||||||
|
className="flex items-center justify-center h-64"
|
||||||
|
data-testid="loading-logical-model-permissions"
|
||||||
|
>
|
||||||
|
<Skeleton />
|
||||||
|
</div>
|
||||||
|
) : !logicalModel ? (
|
||||||
|
<div className="flex items-center justify-center h-64">
|
||||||
|
<span className="text-gray-500">
|
||||||
|
Logical model with name {name} and driver {source} not found
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<LogicalModelPermissions
|
||||||
|
onSave={async permission => {
|
||||||
|
create({
|
||||||
|
logicalModelName: logicalModel?.name,
|
||||||
|
permission,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
onDelete={async permission => {
|
||||||
|
remove({
|
||||||
|
logicalModelName: logicalModel?.name,
|
||||||
|
permission,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
isCreating={isCreating}
|
||||||
|
isRemoving={isRemoving}
|
||||||
|
comparators={comparators}
|
||||||
|
logicalModelName={logicalModel?.name}
|
||||||
|
logicalModels={logicalModels}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const LogicalModelPermissionsRoute = withRouter<{
|
||||||
|
location: Location;
|
||||||
|
router: InjectedRouter;
|
||||||
|
params: {
|
||||||
|
source: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
}>(({ params }) => {
|
||||||
|
return (
|
||||||
|
<RouteWrapper
|
||||||
|
route={
|
||||||
|
'/data/native-queries/logical-models/{{source}}/{{name}}/permissions'
|
||||||
|
}
|
||||||
|
itemSourceName={params.source}
|
||||||
|
itemName={params.name}
|
||||||
|
>
|
||||||
|
<LogicalModelPermissionsPage source={params.source} name={params.name} />
|
||||||
|
</RouteWrapper>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,15 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { LogicalModelWithPermissions } from './types';
|
||||||
|
import { useLogicalModelPermissionsForm } from '../hooks/usePermissionForm';
|
||||||
|
import { FormProvider } from 'react-hook-form';
|
||||||
|
|
||||||
|
export function LogicalModelPermissionsFormProvider({
|
||||||
|
children,
|
||||||
|
logicalModel,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
logicalModel: LogicalModelWithPermissions | undefined;
|
||||||
|
}) {
|
||||||
|
const methods = useLogicalModelPermissionsForm(logicalModel);
|
||||||
|
return <FormProvider {...methods}>{children}</FormProvider>;
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
import { PermissionsIcon } from '../../../../Permissions/PermissionsTable/components/PermissionsIcons';
|
||||||
|
|
||||||
|
interface PermissionAccessCellProps extends React.ComponentProps<'button'> {
|
||||||
|
access: 'fullAccess' | 'partialAccess' | 'noAccess';
|
||||||
|
isEditable: boolean;
|
||||||
|
isCurrentEdit: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PermissionAccessCell: React.FC<PermissionAccessCellProps> = ({
|
||||||
|
access,
|
||||||
|
isEditable,
|
||||||
|
isCurrentEdit,
|
||||||
|
...rest
|
||||||
|
}) => {
|
||||||
|
if (!isEditable) {
|
||||||
|
return (
|
||||||
|
<td className="p-md whitespace-nowrap text-center cursor-not-allowed opacity-30">
|
||||||
|
<PermissionsIcon type={access} selected={isCurrentEdit} />
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<td>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={`cursor-pointer h-20 border-none w-full whitespace-nowrap text-center ${
|
||||||
|
isCurrentEdit ? 'bg-amber-300' : 'hover:bg-indigo-50'
|
||||||
|
}`}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<PermissionsIcon type={access} selected={isCurrentEdit} />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,191 @@
|
|||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { Button } from '../../../../../new-components/Button';
|
||||||
|
import { IconTooltip } from '../../../../../new-components/Tooltip';
|
||||||
|
import { usePermissionsFormContext } from '../hooks/usePermissionForm';
|
||||||
|
import { Permission } from './types';
|
||||||
|
import { Collapse } from '../../../../../new-components/deprecated';
|
||||||
|
import { getEdForm } from '../../../../../components/Services/Data/utils';
|
||||||
|
import { Badge } from '../../../../../new-components/Badge';
|
||||||
|
|
||||||
|
type PermissionsFormProps = {
|
||||||
|
permission: Permission;
|
||||||
|
onSave: () => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
PermissionsInput: ReactNode;
|
||||||
|
isCreating?: boolean;
|
||||||
|
isRemoving?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PermissionsForm = ({
|
||||||
|
permission,
|
||||||
|
onDelete,
|
||||||
|
onSave,
|
||||||
|
PermissionsInput,
|
||||||
|
isCreating,
|
||||||
|
isRemoving,
|
||||||
|
}: PermissionsFormProps) => {
|
||||||
|
const {
|
||||||
|
unsetActivePermission,
|
||||||
|
rowSelectPermissions,
|
||||||
|
setRowSelectPermissions,
|
||||||
|
columns,
|
||||||
|
toggleColumn,
|
||||||
|
toggleAllColumns,
|
||||||
|
columnPermissionsStatus,
|
||||||
|
setPermission,
|
||||||
|
} = usePermissionsFormContext();
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
onSave();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="bg-white rounded p-md border border-gray-300"
|
||||||
|
data-testid="permissions-form"
|
||||||
|
>
|
||||||
|
<div className="pb-4 flex items-center gap-4">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
unsetActivePermission();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
<h3 data-testid="form-title">
|
||||||
|
<strong>Role:</strong>
|
||||||
|
<Badge className="mx-2" data-testid="role-pill">
|
||||||
|
{permission.roleName}
|
||||||
|
</Badge>
|
||||||
|
<strong>Action:</strong>
|
||||||
|
<Badge className="mx-2" data-testid="action-pill">
|
||||||
|
{permission.action}
|
||||||
|
</Badge>
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<fieldset className="grid gap-2">
|
||||||
|
<div>
|
||||||
|
<label className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
id={'without_filter'}
|
||||||
|
type="radio"
|
||||||
|
value={'without_filter'}
|
||||||
|
checked={rowSelectPermissions === 'without_filter'}
|
||||||
|
data-testid="without-filter"
|
||||||
|
onClick={() => {
|
||||||
|
setRowSelectPermissions('without_filter');
|
||||||
|
setPermission(permission.roleName, {});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<NoChecksLabel />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
id={'with_custom_filter'}
|
||||||
|
type="radio"
|
||||||
|
value={rowSelectPermissions}
|
||||||
|
checked={rowSelectPermissions === 'with_custom_filter'}
|
||||||
|
onClick={() => {
|
||||||
|
setRowSelectPermissions('with_custom_filter');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<CustomLabel />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{rowSelectPermissions === 'with_custom_filter' && (
|
||||||
|
<div className="pt-4">
|
||||||
|
<div>{PermissionsInput}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
<Collapse defaultOpen>
|
||||||
|
<Collapse.Header
|
||||||
|
title={`Column ${permission.action} permissions`}
|
||||||
|
tooltip={`Choose columns allowed to be ${getEdForm(
|
||||||
|
permission.action
|
||||||
|
)}`}
|
||||||
|
status={columnPermissionsStatus(permission)}
|
||||||
|
disabledMessage="Set row permissions first"
|
||||||
|
/>
|
||||||
|
<Collapse.Content>
|
||||||
|
<div className="grid gap-2">
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<p>
|
||||||
|
Allow role <strong>{permission.roleName}</strong> to access{' '}
|
||||||
|
<strong>columns</strong>:
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<fieldset className="flex gap-4 flex-wrap">
|
||||||
|
{columns?.map(column => (
|
||||||
|
<label key={column} className="flex gap-2 items-center">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
data-testid={`column-${column}-checkbox`}
|
||||||
|
style={{ marginTop: '0px !important' }}
|
||||||
|
className="rounded shadow-sm border border-gray-300 hover:border-gray-400 focus:ring-yellow-400"
|
||||||
|
checked={permission.columns.includes(column)}
|
||||||
|
onChange={() => {
|
||||||
|
toggleColumn(permission, column);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<i>{column}</i>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => toggleAllColumns(permission)}
|
||||||
|
data-testid="toggle-all-columns"
|
||||||
|
>
|
||||||
|
Toggle All
|
||||||
|
</Button>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</Collapse.Content>
|
||||||
|
</Collapse>
|
||||||
|
|
||||||
|
<div className="pt-2 flex gap-2 mt-4" id="form-buttons-container">
|
||||||
|
<Button
|
||||||
|
isLoading={isCreating}
|
||||||
|
disabled={isRemoving || isCreating}
|
||||||
|
type="submit"
|
||||||
|
mode="primary"
|
||||||
|
title={'Submit'}
|
||||||
|
data-testid="save-permissions-button"
|
||||||
|
>
|
||||||
|
Save Permissions
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
isLoading={isRemoving}
|
||||||
|
disabled={isRemoving || isCreating}
|
||||||
|
type="button"
|
||||||
|
mode="destructive"
|
||||||
|
onClick={onDelete}
|
||||||
|
data-testid="delete-permissions-button"
|
||||||
|
>
|
||||||
|
Delete Permissions
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const NoChecksLabel = () => (
|
||||||
|
<span data-test="without-checks">Without any checks </span>
|
||||||
|
);
|
||||||
|
|
||||||
|
const CustomLabel = () => (
|
||||||
|
<span data-test="custom-check" className="flex items-center">
|
||||||
|
With custom check:
|
||||||
|
<IconTooltip message="Create custom check using permissions builder" />
|
||||||
|
</span>
|
||||||
|
);
|
@ -0,0 +1,66 @@
|
|||||||
|
import { forwardRef } from 'react';
|
||||||
|
import { Action, Permission } from './types';
|
||||||
|
import { PermissionsRowName } from './PermissionsRowName';
|
||||||
|
import { PermissionAccessCell } from './PermissionAccessCell';
|
||||||
|
import { usePermissionsFormContext } from '../hooks/usePermissionForm';
|
||||||
|
|
||||||
|
type PermissionsRowProps = {
|
||||||
|
permission: Permission;
|
||||||
|
index: number;
|
||||||
|
actions: string[];
|
||||||
|
allowedActions: Action[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PermissionsRow = forwardRef<HTMLInputElement, PermissionsRowProps>(
|
||||||
|
({ permission, actions, allowedActions, index }, ref) => {
|
||||||
|
const {
|
||||||
|
setActivePermission,
|
||||||
|
activePermission,
|
||||||
|
unsetActivePermission,
|
||||||
|
permissionAccess,
|
||||||
|
} = usePermissionsFormContext();
|
||||||
|
return (
|
||||||
|
<tr className="group divide-x divide-gray-300">
|
||||||
|
<PermissionsRowName
|
||||||
|
ref={ref}
|
||||||
|
roleName={permission.roleName}
|
||||||
|
isNew={permission.isNew}
|
||||||
|
/>
|
||||||
|
{actions.map(actionName => {
|
||||||
|
const action = allowedActions.find(
|
||||||
|
allowedAction => allowedAction === actionName
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<PermissionAccessCell
|
||||||
|
key={actionName}
|
||||||
|
isEditable={Boolean(action)}
|
||||||
|
access={permissionAccess(action, permission)}
|
||||||
|
isCurrentEdit={
|
||||||
|
activePermission === index && permission.action === action
|
||||||
|
}
|
||||||
|
data-testid={`${permission.roleName}-${actionName}-permissions-cell`}
|
||||||
|
onClick={() => {
|
||||||
|
// Close form if user clicks cell for empty permission
|
||||||
|
if (
|
||||||
|
activePermission !== index &&
|
||||||
|
permission.isNew &&
|
||||||
|
permission.roleName === ''
|
||||||
|
) {
|
||||||
|
unsetActivePermission();
|
||||||
|
}
|
||||||
|
// Focus on input so that user can enter new role name
|
||||||
|
if (permission.isNew && permission.roleName === '') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
ref.current?.focus();
|
||||||
|
} else {
|
||||||
|
setActivePermission(index);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
@ -0,0 +1,40 @@
|
|||||||
|
import { forwardRef } from 'react';
|
||||||
|
import { Role } from './types';
|
||||||
|
import { usePermissionsFormContext } from '../hooks/usePermissionForm';
|
||||||
|
|
||||||
|
interface PermissionsRowNameProps {
|
||||||
|
roleName: Role['name'];
|
||||||
|
isNew: Role['isNew'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PermissionsRowName = forwardRef<
|
||||||
|
HTMLInputElement,
|
||||||
|
PermissionsRowNameProps
|
||||||
|
>(({ roleName, isNew }, inputRef) => {
|
||||||
|
const { setNewRoleName } = usePermissionsFormContext();
|
||||||
|
if (isNew) {
|
||||||
|
return (
|
||||||
|
<td className="w-0 bg-gray-50 p-sm font-semibold text-muted">
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
className="block w-64 h-input px-md shadow-sm rounded border border-gray-300 hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-yellow-200 focus:border-yellow-400"
|
||||||
|
value={roleName}
|
||||||
|
aria-label="create-new-role"
|
||||||
|
data-testid="new-role-input"
|
||||||
|
placeholder="Create new role..."
|
||||||
|
onChange={e => {
|
||||||
|
setNewRoleName(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<td className="w-0 bg-gray-50 p-md font-semibold text-muted">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<label className="flex items-center ml-sm">{roleName}</label>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,68 @@
|
|||||||
|
import { createRef } from 'react';
|
||||||
|
import { PermissionsIcon } from '../../../../Permissions/PermissionsTable/components/PermissionsIcons';
|
||||||
|
import { Action, Permission } from './types';
|
||||||
|
import { PermissionsRow } from './PermissionsRow';
|
||||||
|
|
||||||
|
export type PermissionsTableProps = {
|
||||||
|
allowedActions: Action[];
|
||||||
|
permissions: Permission[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PermissionsTable = ({
|
||||||
|
allowedActions,
|
||||||
|
permissions,
|
||||||
|
}: PermissionsTableProps) => {
|
||||||
|
const actions = ['insert', 'select', 'update', 'delete'];
|
||||||
|
const inputRef = createRef<HTMLInputElement>();
|
||||||
|
return (
|
||||||
|
<div className="grid gap-2" data-testid="permissions-table">
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<span>
|
||||||
|
<PermissionsIcon type="fullAccess" />
|
||||||
|
- full access
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<PermissionsIcon type="noAccess" />
|
||||||
|
- no access
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<PermissionsIcon type="partialAccess" />
|
||||||
|
- partial access
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="overflow-x-auto border border-gray-300 rounded">
|
||||||
|
<table className="min-w-full divide-y divide-gray-200 text-left">
|
||||||
|
<thead>
|
||||||
|
<tr className="divide-x divide-gray-300">
|
||||||
|
<th className="w-0 bg-gray-50 border-r border-gray-200 px-md py-sm text-sm font-semibold text-muted uppercase tracking-wider">
|
||||||
|
ROLE
|
||||||
|
</th>
|
||||||
|
{actions.map(action => (
|
||||||
|
<th
|
||||||
|
className="bg-gray-50 px-md py-sm text-sm font-semibold text-muted text-center uppercase tracking-wider"
|
||||||
|
key={action}
|
||||||
|
>
|
||||||
|
{action.toUpperCase()}
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody className="bg-white divide-y divide-gray-300">
|
||||||
|
{permissions.map((permission, index) => (
|
||||||
|
// Using index as key instead of role.name because role.name is editable
|
||||||
|
<PermissionsRow
|
||||||
|
key={index}
|
||||||
|
index={index}
|
||||||
|
ref={inputRef}
|
||||||
|
permission={permission}
|
||||||
|
actions={actions}
|
||||||
|
allowedActions={allowedActions}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,44 @@
|
|||||||
|
import { LogicalModel, Source } from '../../../../hasura-metadata-types';
|
||||||
|
|
||||||
|
export type LogicalModelWithSourceName = LogicalModel & {
|
||||||
|
source: Source;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Permission = {
|
||||||
|
roleName: string;
|
||||||
|
source: string;
|
||||||
|
action: Action;
|
||||||
|
filter: Record<string, any>;
|
||||||
|
columns: string[];
|
||||||
|
isNew: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PermissionId = {
|
||||||
|
source: Permission['source'];
|
||||||
|
name: Permission['roleName'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Action = 'select' | 'insert' | 'update' | 'delete';
|
||||||
|
|
||||||
|
export type LogicalModelWithPermissions = LogicalModelWithSourceName & {
|
||||||
|
select_permissions?: {
|
||||||
|
role: string;
|
||||||
|
permission: {
|
||||||
|
columns: string[];
|
||||||
|
filter: Record<string, any>;
|
||||||
|
};
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Role = {
|
||||||
|
name: string;
|
||||||
|
isNew?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AccessType = 'fullAccess' | 'noAccess' | 'partialAccess';
|
||||||
|
|
||||||
|
export type OnSave = (permission: Permission) => Promise<void>;
|
||||||
|
|
||||||
|
export type OnDelete = (permission: Permission) => Promise<void>;
|
||||||
|
|
||||||
|
export type RowSelectPermissionsType = 'with_custom_filter' | 'without_filter';
|
@ -0,0 +1,86 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useMetadataMigration } from '../../../../MetadataAPI';
|
||||||
|
import { exportMetadata } from '../../../../DataSource';
|
||||||
|
import { useHttpClient } from '../../../../Network';
|
||||||
|
import { getCreateLogicalModelBody } from './utils/getCreateLogicalModelBody';
|
||||||
|
import { LogicalModel, Source } from '../../../../hasura-metadata-types';
|
||||||
|
import { useQueryClient } from 'react-query';
|
||||||
|
import { useFireNotification } from '../../../../../new-components/Notifications/index';
|
||||||
|
import { METADATA_QUERY_KEY } from '../../../../hasura-metadata-api/useMetadata';
|
||||||
|
import { errorTransform } from './utils/errorTransform';
|
||||||
|
|
||||||
|
const useCreateLogicalModelsPermissions = ({
|
||||||
|
logicalModels,
|
||||||
|
source,
|
||||||
|
}: {
|
||||||
|
logicalModels: LogicalModel[];
|
||||||
|
source: Source | undefined;
|
||||||
|
}) => {
|
||||||
|
const mutate = useMetadataMigration({
|
||||||
|
errorTransform,
|
||||||
|
});
|
||||||
|
const { fireNotification } = useFireNotification();
|
||||||
|
const httpClient = useHttpClient();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const create = useCallback(
|
||||||
|
async ({ permission, logicalModelName, onSuccess }) => {
|
||||||
|
const { resource_version } = await exportMetadata({
|
||||||
|
httpClient,
|
||||||
|
});
|
||||||
|
if (!source) return;
|
||||||
|
|
||||||
|
const body = getCreateLogicalModelBody({
|
||||||
|
permission,
|
||||||
|
logicalModelName,
|
||||||
|
logicalModels,
|
||||||
|
source,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mutate.mutateAsync(
|
||||||
|
{
|
||||||
|
query: { type: 'bulk', args: body, resource_version },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: async () => {
|
||||||
|
fireNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Success!',
|
||||||
|
message: 'Permissions saved successfully!',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: err => {
|
||||||
|
fireNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error!',
|
||||||
|
message:
|
||||||
|
err?.message ??
|
||||||
|
'Something went wrong while saving permissions',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSettled: async () => {
|
||||||
|
await queryClient.invalidateQueries([METADATA_QUERY_KEY]);
|
||||||
|
onSuccess?.();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
fireNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error!',
|
||||||
|
message:
|
||||||
|
error?.message ?? 'Something went wrong while saving permissions',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[logicalModels, source]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
create,
|
||||||
|
...mutate,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useCreateLogicalModelsPermissions };
|
@ -0,0 +1,173 @@
|
|||||||
|
import { useForm, useFormContext } from 'react-hook-form';
|
||||||
|
import { LogicalModelPermissionsState } from '../LogicalModelPermissions';
|
||||||
|
import {
|
||||||
|
AccessType,
|
||||||
|
Action,
|
||||||
|
LogicalModelWithPermissions,
|
||||||
|
Permission,
|
||||||
|
RowSelectPermissionsType,
|
||||||
|
} from '../components/types';
|
||||||
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
import { permissionColumnAccess, permissionRowAccess } from '../utils';
|
||||||
|
|
||||||
|
export function useLogicalModelPermissionsForm(
|
||||||
|
logicalModel: LogicalModelWithPermissions | undefined
|
||||||
|
) {
|
||||||
|
const defaultPermissions: Permission[] = [
|
||||||
|
...(logicalModel?.select_permissions?.map(permission => ({
|
||||||
|
roleName: permission.role,
|
||||||
|
filter: permission.permission.filter,
|
||||||
|
columns: permission.permission.columns,
|
||||||
|
action: 'select' as const,
|
||||||
|
isNew: false,
|
||||||
|
source: logicalModel?.source.name,
|
||||||
|
})) ?? []),
|
||||||
|
{
|
||||||
|
roleName: '',
|
||||||
|
filter: {},
|
||||||
|
columns: [],
|
||||||
|
action: 'select' as const,
|
||||||
|
isNew: true,
|
||||||
|
source: logicalModel?.source.name || '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const methods = useForm<LogicalModelPermissionsState>({
|
||||||
|
defaultValues: {
|
||||||
|
activePermission: null,
|
||||||
|
rowSelectPermissions: 'without_filter',
|
||||||
|
permissions: defaultPermissions,
|
||||||
|
columns: logicalModel?.fields?.map(field => field.name) ?? [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return methods;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePermissionsFormContext() {
|
||||||
|
const { setValue, watch } = useFormContext<LogicalModelPermissionsState>();
|
||||||
|
return {
|
||||||
|
permissions: watch('permissions'),
|
||||||
|
setPermission: (roleName: string, filter: Record<string, any>) => {
|
||||||
|
const permissions = watch('permissions');
|
||||||
|
setValue(
|
||||||
|
'permissions',
|
||||||
|
permissions.map(permission => {
|
||||||
|
if (permission.roleName === roleName) {
|
||||||
|
return {
|
||||||
|
...permission,
|
||||||
|
filter,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return permission;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
rowSelectPermissions: watch('rowSelectPermissions'),
|
||||||
|
setRowSelectPermissions: (rowSelectPermissions: RowSelectPermissionsType) =>
|
||||||
|
setValue('rowSelectPermissions', rowSelectPermissions),
|
||||||
|
columns: watch('columns'),
|
||||||
|
toggleColumn: (permission: Permission, column: string) => {
|
||||||
|
const permissions = watch('permissions');
|
||||||
|
setValue(
|
||||||
|
'permissions',
|
||||||
|
permissions.map(p => {
|
||||||
|
if (
|
||||||
|
p.roleName === permission.roleName &&
|
||||||
|
p.source === permission.source
|
||||||
|
) {
|
||||||
|
const columns = p.columns.includes(column)
|
||||||
|
? p.columns.filter(c => c !== column)
|
||||||
|
: [...p.columns, column];
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
columns,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
toggleAllColumns: (permission: Permission) => {
|
||||||
|
const permissions = watch('permissions');
|
||||||
|
setValue(
|
||||||
|
'permissions',
|
||||||
|
permissions.map(p => {
|
||||||
|
if (
|
||||||
|
p.roleName === permission.roleName &&
|
||||||
|
p.source === permission.source
|
||||||
|
) {
|
||||||
|
const columns =
|
||||||
|
p.columns.length === watch('columns').length
|
||||||
|
? []
|
||||||
|
: watch('columns');
|
||||||
|
return {
|
||||||
|
...p,
|
||||||
|
columns,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
columnPermissionsStatus: (
|
||||||
|
permission: Permission
|
||||||
|
): '' | 'No columns' | 'All columns' | 'Partial columns' => {
|
||||||
|
if (!permission) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (permission.columns.length === 0) {
|
||||||
|
return 'No columns';
|
||||||
|
}
|
||||||
|
if (permission.columns.length === watch('columns').length) {
|
||||||
|
return 'All columns';
|
||||||
|
}
|
||||||
|
return 'Partial columns';
|
||||||
|
},
|
||||||
|
setActivePermission: (index: number) => {
|
||||||
|
const permission = watch('permissions')[index];
|
||||||
|
const rowSelectPermissions: RowSelectPermissionsType =
|
||||||
|
permission.isNew || isEmpty(permission.filter)
|
||||||
|
? 'without_filter'
|
||||||
|
: 'with_custom_filter';
|
||||||
|
|
||||||
|
setValue('activePermission', index);
|
||||||
|
setValue('rowSelectPermissions', rowSelectPermissions);
|
||||||
|
},
|
||||||
|
unsetActivePermission: () => setValue('activePermission', null),
|
||||||
|
activePermission: watch('activePermission'),
|
||||||
|
setNewRoleName: (roleName: string) => {
|
||||||
|
const permissions = watch('permissions');
|
||||||
|
setValue(
|
||||||
|
'permissions',
|
||||||
|
permissions.map(permission => {
|
||||||
|
if (permission.isNew) {
|
||||||
|
return {
|
||||||
|
...permission,
|
||||||
|
roleName,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return permission;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Returns the access type of the permission for the given action
|
||||||
|
*/
|
||||||
|
permissionAccess: (
|
||||||
|
action: Action | undefined,
|
||||||
|
permission: Permission
|
||||||
|
): AccessType => {
|
||||||
|
if (action !== 'select') {
|
||||||
|
return 'noAccess';
|
||||||
|
}
|
||||||
|
const rowAccess = permissionRowAccess(permission);
|
||||||
|
const columnAccess = permissionColumnAccess(permission, watch('columns'));
|
||||||
|
if (rowAccess === 'noAccess' && columnAccess === 'noAccess') {
|
||||||
|
return 'noAccess';
|
||||||
|
}
|
||||||
|
if (rowAccess === 'fullAccess' && columnAccess === 'fullAccess') {
|
||||||
|
return 'fullAccess';
|
||||||
|
}
|
||||||
|
return 'partialAccess';
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,94 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useMetadataMigration } from '../../../../MetadataAPI';
|
||||||
|
import { exportMetadata } from '../../../../DataSource';
|
||||||
|
import { useHttpClient } from '../../../../Network';
|
||||||
|
import { LogicalModel, Source } from '../../../../hasura-metadata-types';
|
||||||
|
import { useQueryClient } from 'react-query';
|
||||||
|
import { useFireNotification } from '../../../../../new-components/Notifications/index';
|
||||||
|
import { METADATA_QUERY_KEY } from '../../../../hasura-metadata-api/useMetadata';
|
||||||
|
import { errorTransform } from './utils/errorTransform';
|
||||||
|
import { Permission } from '../components/types';
|
||||||
|
import { getDeleteLogicalModelBody } from './utils/getDeleteLogicalModelBody';
|
||||||
|
|
||||||
|
const useRemoveLogicalModelsPermissions = ({
|
||||||
|
logicalModels,
|
||||||
|
source,
|
||||||
|
}: {
|
||||||
|
logicalModels: LogicalModel[];
|
||||||
|
source: Source | undefined;
|
||||||
|
}) => {
|
||||||
|
const mutate = useMetadataMigration({
|
||||||
|
errorTransform,
|
||||||
|
});
|
||||||
|
const { fireNotification } = useFireNotification();
|
||||||
|
const httpClient = useHttpClient();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const remove = useCallback(
|
||||||
|
async ({
|
||||||
|
permission,
|
||||||
|
logicalModelName,
|
||||||
|
onSuccess,
|
||||||
|
}: {
|
||||||
|
permission: Permission;
|
||||||
|
logicalModelName: string;
|
||||||
|
onSuccess?: () => void;
|
||||||
|
}) => {
|
||||||
|
const { resource_version } = await exportMetadata({
|
||||||
|
httpClient,
|
||||||
|
});
|
||||||
|
if (!source) return;
|
||||||
|
|
||||||
|
const body = getDeleteLogicalModelBody({
|
||||||
|
permission,
|
||||||
|
logicalModelName,
|
||||||
|
source,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mutate.mutateAsync(
|
||||||
|
{
|
||||||
|
query: { type: 'bulk', args: body, resource_version },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess: async () => {
|
||||||
|
fireNotification({
|
||||||
|
type: 'success',
|
||||||
|
title: 'Success!',
|
||||||
|
message: 'Permissions successfully deleted!',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onError: err => {
|
||||||
|
fireNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error!',
|
||||||
|
message:
|
||||||
|
err?.message ??
|
||||||
|
'Something went wrong while deleting permissions',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSettled: async () => {
|
||||||
|
await queryClient.invalidateQueries([METADATA_QUERY_KEY]);
|
||||||
|
onSuccess?.();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
fireNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Error!',
|
||||||
|
message:
|
||||||
|
error?.message ?? 'Something went wrong while saving permissions',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[source]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
remove,
|
||||||
|
...mutate,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export { useRemoveLogicalModelsPermissions };
|
@ -0,0 +1,16 @@
|
|||||||
|
export function errorTransform(error: any) {
|
||||||
|
const err = error as Record<string, any>;
|
||||||
|
|
||||||
|
let message = '';
|
||||||
|
let name = `Error code: ${err.code}`;
|
||||||
|
|
||||||
|
if ('internal' in err) {
|
||||||
|
name = err?.internal?.[0]?.name ?? name;
|
||||||
|
message = err?.internal?.[0]?.reason ?? 'Internal error';
|
||||||
|
} else message = err.error;
|
||||||
|
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
message,
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,73 @@
|
|||||||
|
import { getPermissionValues } from './getPermissionValues';
|
||||||
|
import { LogicalModel, Source } from '../../../../../hasura-metadata-types';
|
||||||
|
import { Permission } from '../../components/types';
|
||||||
|
import { mapPostgresToPg } from '.';
|
||||||
|
|
||||||
|
export interface CreateLogicalModalBodyArgs {
|
||||||
|
logicalModels: LogicalModel[];
|
||||||
|
logicalModelName: string;
|
||||||
|
permission: Permission;
|
||||||
|
source: Source;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PermissionArgsType = {
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
permission?: Record<string, unknown>;
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
type PermissionBodyType = {
|
||||||
|
type: string;
|
||||||
|
args: PermissionArgsType;
|
||||||
|
};
|
||||||
|
|
||||||
|
const doesRoleExist = (logicalModel: LogicalModel, roleName: string) => {
|
||||||
|
const permissionKeys = ['select_permissions'] as ['select_permissions'];
|
||||||
|
return permissionKeys.some(
|
||||||
|
key =>
|
||||||
|
Array.isArray(logicalModel[key]) &&
|
||||||
|
logicalModel[key]?.some(permission => permission.role === roleName)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getCreateLogicalModelBody = ({
|
||||||
|
logicalModelName,
|
||||||
|
permission,
|
||||||
|
logicalModels,
|
||||||
|
source,
|
||||||
|
}: CreateLogicalModalBodyArgs): PermissionBodyType[] => {
|
||||||
|
const permissionValues = getPermissionValues(permission);
|
||||||
|
const args = [
|
||||||
|
{
|
||||||
|
type: `${mapPostgresToPg(source.kind)}_create_logical_model_${
|
||||||
|
permission.action
|
||||||
|
}_permission`,
|
||||||
|
args: {
|
||||||
|
name: logicalModelName,
|
||||||
|
role: permission.roleName,
|
||||||
|
permission: permissionValues,
|
||||||
|
source: source.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const permissionAlreadyExists = logicalModels.find((model: LogicalModel) =>
|
||||||
|
doesRoleExist(model, permission.roleName)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (permissionAlreadyExists) {
|
||||||
|
args.unshift({
|
||||||
|
type: `${mapPostgresToPg(source.kind)}_drop_logical_model_${
|
||||||
|
permission.action
|
||||||
|
}_permission`,
|
||||||
|
args: {
|
||||||
|
permission: {},
|
||||||
|
name: logicalModelName,
|
||||||
|
role: permission.roleName,
|
||||||
|
source: source.name,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
};
|
@ -0,0 +1,40 @@
|
|||||||
|
import { mapPostgresToPg } from '.';
|
||||||
|
import { Source } from '../../../../../hasura-metadata-types';
|
||||||
|
import { Permission } from '../../components/types';
|
||||||
|
export interface DeleteLogicalModalBodyArgs {
|
||||||
|
logicalModelName: string;
|
||||||
|
permission: Permission;
|
||||||
|
source: Source;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PermissionArgsType = {
|
||||||
|
name: string;
|
||||||
|
role: string;
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PermissionBodyType = {
|
||||||
|
type: string;
|
||||||
|
args: PermissionArgsType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getDeleteLogicalModelBody = ({
|
||||||
|
logicalModelName,
|
||||||
|
permission,
|
||||||
|
source,
|
||||||
|
}: DeleteLogicalModalBodyArgs): PermissionBodyType[] => {
|
||||||
|
const args = [
|
||||||
|
{
|
||||||
|
type: `${mapPostgresToPg(source.kind)}_drop_logical_model_${
|
||||||
|
permission.action
|
||||||
|
}_permission`,
|
||||||
|
args: {
|
||||||
|
name: logicalModelName,
|
||||||
|
role: permission.roleName,
|
||||||
|
source: source.name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return args;
|
||||||
|
};
|
@ -0,0 +1,36 @@
|
|||||||
|
type GetPermissionValuesInputType = {
|
||||||
|
roleName: string;
|
||||||
|
filter?: Record<string, unknown>;
|
||||||
|
check?: Record<string, unknown>;
|
||||||
|
columns?: string[];
|
||||||
|
action: string;
|
||||||
|
isNew: boolean;
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GetPermissionValuesOutputType = {
|
||||||
|
filter?: Record<string, unknown>;
|
||||||
|
check?: Record<string, unknown>;
|
||||||
|
columns?: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPermissionValues = (
|
||||||
|
obj: GetPermissionValuesInputType
|
||||||
|
): GetPermissionValuesOutputType => {
|
||||||
|
const { filter, check, columns } = obj;
|
||||||
|
const result: GetPermissionValuesOutputType = {};
|
||||||
|
|
||||||
|
if (filter !== undefined) {
|
||||||
|
result.filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (check !== undefined) {
|
||||||
|
result.check = check;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (columns !== undefined) {
|
||||||
|
result.columns = columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
@ -0,0 +1,3 @@
|
|||||||
|
export function mapPostgresToPg(kind: string) {
|
||||||
|
return kind === 'postgres' ? 'pg' : kind;
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
export default {
|
||||||
|
version: '12345',
|
||||||
|
is_function_permissions_inferred: true,
|
||||||
|
is_remote_schema_permissions_enabled: false,
|
||||||
|
is_admin_secret_set: true,
|
||||||
|
is_auth_hook_set: false,
|
||||||
|
is_jwt_set: false,
|
||||||
|
jwt: [],
|
||||||
|
is_allow_list_enabled: false,
|
||||||
|
live_queries: { batch_size: 100, refetch_delay: 1 },
|
||||||
|
streaming_queries: { batch_size: 100, refetch_delay: 1 },
|
||||||
|
console_assets_dir: null,
|
||||||
|
experimental_features: ['naming_convention'],
|
||||||
|
is_prometheus_metrics_enabled: false,
|
||||||
|
default_naming_convention: 'hasura-default',
|
||||||
|
feature_flags: [
|
||||||
|
{
|
||||||
|
name: 'stored-procedures',
|
||||||
|
description: 'Expose stored procedures support',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'native-query-interface',
|
||||||
|
description:
|
||||||
|
'Expose custom views, permissions and advanced SQL functionality via custom queries',
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'test-flag',
|
||||||
|
description: 'Testing feature flag integration',
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
@ -0,0 +1,23 @@
|
|||||||
|
const payload = {
|
||||||
|
type: 'bulk',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
type: 'pg_drop_logical_model_select_permission',
|
||||||
|
args: {
|
||||||
|
name: 'LogicalModel',
|
||||||
|
role: 'editor',
|
||||||
|
source: 'Postgres',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resource_version: 222,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
message: 'success',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
payload,
|
||||||
|
response,
|
||||||
|
};
|
@ -0,0 +1,125 @@
|
|||||||
|
import { rest } from 'msw';
|
||||||
|
import config from './config';
|
||||||
|
import metadata from './metadata';
|
||||||
|
import save from './save';
|
||||||
|
import deleteMocks from './delete';
|
||||||
|
|
||||||
|
export const handlers = () => [
|
||||||
|
rest.get('http://localhost:8080/v1alpha1/config', async (req, res, ctx) => {
|
||||||
|
return res(ctx.status(200), ctx.json(config));
|
||||||
|
}),
|
||||||
|
rest.post('http://localhost:8080/v1/metadata', async (req, res, ctx) => {
|
||||||
|
const reqBody = await req.json<{
|
||||||
|
type: string;
|
||||||
|
args: any;
|
||||||
|
}>();
|
||||||
|
if (reqBody.type === 'export_metadata') {
|
||||||
|
return res(ctx.status(200), ctx.json({ metadata }));
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
reqBody.type === 'bulk' &&
|
||||||
|
reqBody.args.length === 2 &&
|
||||||
|
reqBody.args[0].type === 'pg_drop_logical_model_select_permission' &&
|
||||||
|
reqBody.args[1].type === 'pg_create_logical_model_select_permission'
|
||||||
|
) {
|
||||||
|
return res(ctx.status(200), ctx.json(save.response));
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
reqBody.type === 'bulk' &&
|
||||||
|
reqBody.args.length === 1 &&
|
||||||
|
reqBody.args[0].type === 'pg_drop_logical_model_select_permission'
|
||||||
|
) {
|
||||||
|
return res(ctx.status(200), ctx.json(deleteMocks.response));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
rest.get('http://localhost:8080/v1/entitlement', async (req, res, ctx) => {
|
||||||
|
return res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({
|
||||||
|
metadata_db_id: '58a9e616-5fe9-4277-95fa-e27f9d45e177',
|
||||||
|
status: 'none',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const deleteHandlers = () => {
|
||||||
|
let hasDeleted = false;
|
||||||
|
return [
|
||||||
|
rest.get('http://localhost:8080/v1alpha1/config', async (req, res, ctx) => {
|
||||||
|
return res(ctx.status(200), ctx.json(config));
|
||||||
|
}),
|
||||||
|
rest.post('http://localhost:8080/v1/metadata', async (req, res, ctx) => {
|
||||||
|
const reqBody = await req.json<{
|
||||||
|
type: string;
|
||||||
|
args: any;
|
||||||
|
}>();
|
||||||
|
if (reqBody.type === 'export_metadata' && hasDeleted) {
|
||||||
|
return res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({
|
||||||
|
metadata: {
|
||||||
|
...metadata,
|
||||||
|
sources: [
|
||||||
|
...metadata.sources.map(source => {
|
||||||
|
if (source.name !== 'Postgres') {
|
||||||
|
return source;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...source,
|
||||||
|
logical_models: source.logical_models.map(logical_model => {
|
||||||
|
if (logical_model.name !== 'LogicalModel') {
|
||||||
|
return logical_model;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
fields: logical_model.fields,
|
||||||
|
name: logical_model.name,
|
||||||
|
// Omit select_permissions to simulate deletion
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (reqBody.type === 'export_metadata') {
|
||||||
|
return res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({
|
||||||
|
metadata,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (reqBody.type === 'export_metadata') {
|
||||||
|
return res(ctx.status(200), ctx.json({ metadata }));
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
reqBody.type === 'bulk' &&
|
||||||
|
reqBody.args.length === 2 &&
|
||||||
|
reqBody.args[0].type === 'pg_drop_logical_model_select_permission' &&
|
||||||
|
reqBody.args[1].type === 'pg_create_logical_model_select_permission'
|
||||||
|
) {
|
||||||
|
return res(ctx.status(200), ctx.json(save.response));
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
reqBody.type === 'bulk' &&
|
||||||
|
reqBody.args.length === 1 &&
|
||||||
|
reqBody.args[0].type === 'pg_drop_logical_model_select_permission'
|
||||||
|
) {
|
||||||
|
hasDeleted = true;
|
||||||
|
return res(ctx.status(200), ctx.json(deleteMocks.response));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
rest.get('http://localhost:8080/v1/entitlement', async (req, res, ctx) => {
|
||||||
|
return res(
|
||||||
|
ctx.status(200),
|
||||||
|
ctx.json({
|
||||||
|
metadata_db_id: '58a9e616-5fe9-4277-95fa-e27f9d45e177',
|
||||||
|
status: 'none',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
};
|
@ -0,0 +1,175 @@
|
|||||||
|
export default {
|
||||||
|
version: 3,
|
||||||
|
sources: [
|
||||||
|
{
|
||||||
|
name: 'MS',
|
||||||
|
kind: 'mssql',
|
||||||
|
tables: [],
|
||||||
|
logical_models: [
|
||||||
|
{
|
||||||
|
fields: [],
|
||||||
|
name: 'Model',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
configuration: {
|
||||||
|
connection_info: {
|
||||||
|
connection_string:
|
||||||
|
'Driver={ODBC Driver 18 for SQL Server};Server=tcp:host.docker.internal,1433;Database=tempdb;Uid=sa;Pwd=Password!;Encrypt=optional',
|
||||||
|
pool_settings: {
|
||||||
|
idle_timeout: 5,
|
||||||
|
max_connections: null,
|
||||||
|
total_max_connections: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Postgres',
|
||||||
|
kind: 'postgres',
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
name: 'Album',
|
||||||
|
schema: 'public',
|
||||||
|
},
|
||||||
|
object_relationships: [
|
||||||
|
{
|
||||||
|
name: 'Album_Artist',
|
||||||
|
using: {
|
||||||
|
foreign_key_constraint_on: 'ArtistId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'D',
|
||||||
|
using: {
|
||||||
|
manual_configuration: {
|
||||||
|
column_mapping: {
|
||||||
|
ArtistId: 'ArtistId',
|
||||||
|
},
|
||||||
|
insertion_order: null,
|
||||||
|
remote_table: {
|
||||||
|
name: 'Artist',
|
||||||
|
schema: 'public',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
remote_relationships: [
|
||||||
|
{
|
||||||
|
definition: {
|
||||||
|
to_source: {
|
||||||
|
field_mapping: {
|
||||||
|
ArtistId: 'ArtistId',
|
||||||
|
},
|
||||||
|
relationship_type: 'object',
|
||||||
|
source: 'Postgres',
|
||||||
|
table: {
|
||||||
|
name: 'Artist',
|
||||||
|
schema: 'public',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: 'A',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
definition: {
|
||||||
|
to_source: {
|
||||||
|
field_mapping: {
|
||||||
|
ArtistId: 'ArtistId',
|
||||||
|
},
|
||||||
|
relationship_type: 'object',
|
||||||
|
source: 'Postgres',
|
||||||
|
table: {
|
||||||
|
name: 'Artist',
|
||||||
|
schema: 'public',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: 'B',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
definition: {
|
||||||
|
to_source: {
|
||||||
|
field_mapping: {
|
||||||
|
ArtistId: 'ArtistId',
|
||||||
|
},
|
||||||
|
relationship_type: 'object',
|
||||||
|
source: 'Postgres',
|
||||||
|
table: {
|
||||||
|
name: 'Artist',
|
||||||
|
schema: 'public',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
name: 'C',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
table: {
|
||||||
|
name: 'Artist',
|
||||||
|
schema: 'public',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
logical_models: [
|
||||||
|
{
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
name: 'id',
|
||||||
|
nullable: true,
|
||||||
|
type: 'integer',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
nullable: true,
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
name: 'LogicalModel',
|
||||||
|
select_permissions: [
|
||||||
|
{
|
||||||
|
permission: {
|
||||||
|
columns: ['id'],
|
||||||
|
filter: {},
|
||||||
|
},
|
||||||
|
role: 'editor',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: [],
|
||||||
|
name: 'M',
|
||||||
|
select_permissions: [
|
||||||
|
{
|
||||||
|
permission: {
|
||||||
|
columns: [],
|
||||||
|
filter: {
|
||||||
|
_or: [{}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
role: 'M',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
permission: {
|
||||||
|
columns: [],
|
||||||
|
filter: {
|
||||||
|
_and: [{}],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
role: 'reader',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
configuration: {
|
||||||
|
connection_info: {
|
||||||
|
database_url: 'postgres://postgres:pass@postgres:5432/chinook',
|
||||||
|
isolation_level: 'read-committed',
|
||||||
|
use_prepared_statements: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
@ -0,0 +1,38 @@
|
|||||||
|
const payload = {
|
||||||
|
type: 'bulk',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
type: 'pg_drop_logical_model_select_permission',
|
||||||
|
args: {
|
||||||
|
permission: {},
|
||||||
|
name: 'LogicalModel',
|
||||||
|
role: 'editor',
|
||||||
|
source: 'Postgres',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'pg_create_logical_model_select_permission',
|
||||||
|
args: {
|
||||||
|
name: 'LogicalModel',
|
||||||
|
role: 'editor',
|
||||||
|
permission: {
|
||||||
|
filter: {},
|
||||||
|
columns: [],
|
||||||
|
},
|
||||||
|
source: 'Postgres',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
resource_version: 221,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = [
|
||||||
|
{
|
||||||
|
message: 'success',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
message: 'success',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default { payload, response };
|
@ -0,0 +1,23 @@
|
|||||||
|
import isEmpty from 'lodash/isEmpty';
|
||||||
|
import { AccessType, Permission } from './components/types';
|
||||||
|
|
||||||
|
export function permissionRowAccess(permission: Permission): AccessType {
|
||||||
|
if (!permission.filter || isEmpty(permission.filter)) {
|
||||||
|
return 'noAccess';
|
||||||
|
} else {
|
||||||
|
return 'fullAccess';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function permissionColumnAccess(
|
||||||
|
permission: Permission,
|
||||||
|
selectedColumns: string[]
|
||||||
|
): AccessType {
|
||||||
|
if (permission.columns.length === 0) {
|
||||||
|
return 'noAccess';
|
||||||
|
} else if (permission.columns.length > selectedColumns.length) {
|
||||||
|
return 'partialAccess';
|
||||||
|
} else {
|
||||||
|
return 'fullAccess';
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,5 @@
|
|||||||
import startCase from 'lodash/startCase';
|
import startCase from 'lodash/startCase';
|
||||||
import { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
import Skeleton from 'react-loading-skeleton';
|
import Skeleton from 'react-loading-skeleton';
|
||||||
import { useServerConfig } from '../../../../hooks';
|
import { useServerConfig } from '../../../../hooks';
|
||||||
import { Breadcrumbs } from '../../../../new-components/Breadcrumbs';
|
import { Breadcrumbs } from '../../../../new-components/Breadcrumbs';
|
||||||
|
@ -49,4 +49,9 @@ export const NATIVE_QUERY_ROUTES = {
|
|||||||
title: 'Track Stored Procedure',
|
title: 'Track Stored Procedure',
|
||||||
subtitle: 'Expose your stored SQL procedures via the GraphQL API',
|
subtitle: 'Expose your stored SQL procedures via the GraphQL API',
|
||||||
},
|
},
|
||||||
|
'/data/native-queries/logical-models/{{source}}/{{name}}/permissions': {
|
||||||
|
title: 'Logical Models Permissions',
|
||||||
|
subtitle:
|
||||||
|
'Add permissions to your Logical Models to control access to your data',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,28 +0,0 @@
|
|||||||
import { Metadata } from '../../hasura-metadata-types';
|
|
||||||
import { LogicalModelWithSource, NativeQueryWithSource } from './types';
|
|
||||||
|
|
||||||
export const extractModelsAndQueriesFromMetadata = (
|
|
||||||
m: Metadata
|
|
||||||
): { queries: NativeQueryWithSource[]; models: LogicalModelWithSource[] } => {
|
|
||||||
const sources = m.metadata.sources;
|
|
||||||
let models: LogicalModelWithSource[] = [];
|
|
||||||
let queries: NativeQueryWithSource[] = [];
|
|
||||||
|
|
||||||
sources.forEach(s => {
|
|
||||||
if (s.logical_models && s.logical_models.length > 0) {
|
|
||||||
models = [...models, ...s.logical_models.map(m => ({ ...m, source: s }))];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (s.native_queries && s.native_queries.length > 0) {
|
|
||||||
queries = [
|
|
||||||
...queries,
|
|
||||||
...s.native_queries.map(q => ({ ...q, source: s })),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
models,
|
|
||||||
queries,
|
|
||||||
};
|
|
||||||
};
|
|
@ -41,6 +41,8 @@ export const RowPermissionBuilder = ({
|
|||||||
}}
|
}}
|
||||||
table={table}
|
table={table}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={value}
|
permissions={value}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
/>
|
/>
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { rowPermissionsContext } from './RowPermissionsProvider';
|
import { rowPermissionsContext } from './RowPermissionsProvider';
|
||||||
import { useOperators } from './utils/comparatorsFromSchema';
|
import { useOperators } from './utils/comparatorsFromSchema';
|
||||||
import Select from 'react-select';
|
import Select, { components } from 'react-select';
|
||||||
|
import { FiChevronDown } from 'react-icons/fi';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
export const Comparator = ({
|
export const Comparator = ({
|
||||||
comparator,
|
comparator,
|
||||||
@ -21,7 +23,20 @@ export const Comparator = ({
|
|||||||
inputId={`${comparatorLevelId}-select-value`}
|
inputId={`${comparatorLevelId}-select-value`}
|
||||||
isSearchable
|
isSearchable
|
||||||
aria-label={comparatorLevelId}
|
aria-label={comparatorLevelId}
|
||||||
components={{ DropdownIndicator: null }}
|
components={{
|
||||||
|
DropdownIndicator: props => {
|
||||||
|
const { className } = props;
|
||||||
|
return (
|
||||||
|
<components.DropdownIndicator
|
||||||
|
{...props}
|
||||||
|
className={clsx(className, '!text-gray-500 hover:!text-gray-500')}
|
||||||
|
>
|
||||||
|
<FiChevronDown className="w-5 h-5" />
|
||||||
|
</components.DropdownIndicator>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
IndicatorSeparator: () => null,
|
||||||
|
}}
|
||||||
options={operators.map(o => ({
|
options={operators.map(o => ({
|
||||||
value: o.name,
|
value: o.name,
|
||||||
label: o.name,
|
label: o.name,
|
||||||
|
@ -5,6 +5,7 @@ import { isColumnComparator } from './utils';
|
|||||||
import { ColumnComparatorEntry } from './EntryTypes/ColumnComparatorEntry';
|
import { ColumnComparatorEntry } from './EntryTypes/ColumnComparatorEntry';
|
||||||
import { useOperators } from './utils/comparatorsFromSchema';
|
import { useOperators } from './utils/comparatorsFromSchema';
|
||||||
import { ValueInput } from './ValueInput';
|
import { ValueInput } from './ValueInput';
|
||||||
|
import { useForbiddenFeatures } from './ForbiddenFeaturesProvider';
|
||||||
|
|
||||||
export const EntryType = ({
|
export const EntryType = ({
|
||||||
k,
|
k,
|
||||||
@ -17,6 +18,7 @@ export const EntryType = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const operators = useOperators({ path });
|
const operators = useOperators({ path });
|
||||||
const operator = operators.find(o => o.name === k);
|
const operator = operators.find(o => o.name === k);
|
||||||
|
const { hasFeature } = useForbiddenFeatures();
|
||||||
if (isColumnComparator(k)) {
|
if (isColumnComparator(k)) {
|
||||||
return <ColumnComparatorEntry k={k} path={path} v={v} />;
|
return <ColumnComparatorEntry k={k} path={path} v={v} />;
|
||||||
}
|
}
|
||||||
@ -27,6 +29,9 @@ export const EntryType = ({
|
|||||||
return <ArrayEntry k={k} v={v} path={path} />;
|
return <ArrayEntry k={k} v={v} path={path} />;
|
||||||
}
|
}
|
||||||
if (k === '_exists') {
|
if (k === '_exists') {
|
||||||
|
if (!hasFeature('exists')) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
return <ExistsEntry k={k} v={v} path={path} />;
|
return <ExistsEntry k={k} v={v} path={path} />;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
@ -3,8 +3,8 @@ import { isComparator } from '../utils/helpers';
|
|||||||
import { tableContext } from '../TableProvider';
|
import { tableContext } from '../TableProvider';
|
||||||
import { typesContext } from '../TypesProvider';
|
import { typesContext } from '../TypesProvider';
|
||||||
import { rowPermissionsContext } from '../RowPermissionsProvider';
|
import { rowPermissionsContext } from '../RowPermissionsProvider';
|
||||||
import { areTablesEqual } from '../../../../../../hasura-metadata-api';
|
|
||||||
import { createWrapper } from './utils';
|
import { createWrapper } from './utils';
|
||||||
|
import { rootTableContext } from '../RootTableProvider';
|
||||||
|
|
||||||
export function ColumnComparatorEntry({
|
export function ColumnComparatorEntry({
|
||||||
k,
|
k,
|
||||||
@ -107,9 +107,9 @@ function RootColumnsSelect({
|
|||||||
v: any;
|
v: any;
|
||||||
path: string[];
|
path: string[];
|
||||||
}) {
|
}) {
|
||||||
const { table, tables, setValue } = useContext(rowPermissionsContext);
|
const { setValue } = useContext(rowPermissionsContext);
|
||||||
const value = v.find((v: any) => v !== '$');
|
const value = v.find((v: any) => v !== '$');
|
||||||
const rootTable = tables.find(t => areTablesEqual(t.table, table));
|
const { rootTable } = useContext(rootTableContext);
|
||||||
const testId = `${path.join('.')}-root-column-comparator-entry`;
|
const testId = `${path.join('.')}-root-column-comparator-entry`;
|
||||||
return (
|
return (
|
||||||
<select
|
<select
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
import { ReactNode, createContext, useCallback, useContext } from 'react';
|
||||||
|
|
||||||
|
export type Feature = 'exists';
|
||||||
|
|
||||||
|
const forbiddenFeaturesContext = createContext<{
|
||||||
|
forbidden: Feature[];
|
||||||
|
hasFeature: (feature: Feature) => boolean;
|
||||||
|
}>({
|
||||||
|
forbidden: [],
|
||||||
|
hasFeature: () => {
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useForbiddenFeatures() {
|
||||||
|
return useContext(forbiddenFeaturesContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ForbiddenFeaturesProvider({
|
||||||
|
forbidden = [],
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
forbidden: Feature[] | undefined;
|
||||||
|
children: ReactNode;
|
||||||
|
}) {
|
||||||
|
const hasFeature = useCallback(
|
||||||
|
function hasFeature(feature: Feature) {
|
||||||
|
return !forbidden.includes(feature);
|
||||||
|
},
|
||||||
|
[forbidden]
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<forbiddenFeaturesContext.Provider
|
||||||
|
value={{ forbidden: forbidden, hasFeature }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</forbiddenFeaturesContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
@ -3,11 +3,11 @@ import { useContext } from 'react';
|
|||||||
|
|
||||||
import { getTableDisplayName } from '../../../../../DatabaseRelationships';
|
import { getTableDisplayName } from '../../../../../DatabaseRelationships';
|
||||||
import { rowPermissionsContext } from './RowPermissionsProvider';
|
import { rowPermissionsContext } from './RowPermissionsProvider';
|
||||||
|
import { rootTableContext } from './RootTableProvider';
|
||||||
|
|
||||||
export const JsonEditor = () => {
|
export const JsonEditor = () => {
|
||||||
const { permissions, table, setPermissions } = useContext(
|
const { permissions, setPermissions } = useContext(rowPermissionsContext);
|
||||||
rowPermissionsContext
|
const { table } = useContext(rootTableContext);
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 rounded-lg bg-white border border-gray-200 min-h-32 w-full">
|
<div className="p-6 rounded-lg bg-white border border-gray-200 min-h-32 w-full">
|
||||||
<AceEditor
|
<AceEditor
|
||||||
|
@ -0,0 +1,57 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
|
import { RowPermissionsInput } from './RowPermissionsInput';
|
||||||
|
import { comparators } from './__tests__/fixtures/comparators';
|
||||||
|
import { handlers } from './__tests__/fixtures/jsonb/handlers';
|
||||||
|
import { ReactQueryDecorator } from '../../../../../../storybook/decorators/react-query';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Features/Permissions/Form/Logical Model Permissions Input',
|
||||||
|
component: RowPermissionsInput,
|
||||||
|
parameters: {
|
||||||
|
msw: handlers(),
|
||||||
|
},
|
||||||
|
decorators: [ReactQueryDecorator()],
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof RowPermissionsInput>;
|
||||||
|
|
||||||
|
export const Basic: Story = {
|
||||||
|
args: {
|
||||||
|
table: undefined,
|
||||||
|
tables: [],
|
||||||
|
onPermissionsChange: action('onPermissionsChange'),
|
||||||
|
logicalModels: [
|
||||||
|
{
|
||||||
|
fields: [
|
||||||
|
{ name: 'one', nullable: false, type: 'text' },
|
||||||
|
{ name: 'two', nullable: false, type: 'text' },
|
||||||
|
],
|
||||||
|
name: 'hello_world',
|
||||||
|
source: {
|
||||||
|
name: 'default',
|
||||||
|
kind: 'postgres',
|
||||||
|
configuration: {},
|
||||||
|
tables: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: [
|
||||||
|
{ name: 'a', nullable: false, type: 'text' },
|
||||||
|
{ name: 'b', nullable: false, type: 'text' },
|
||||||
|
],
|
||||||
|
name: 'logical_model',
|
||||||
|
source: {
|
||||||
|
name: 'default',
|
||||||
|
kind: 'postgres',
|
||||||
|
configuration: {},
|
||||||
|
tables: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
logicalModel: 'hello_world',
|
||||||
|
comparators,
|
||||||
|
permissions: { one: { _eq: 'eqone' } },
|
||||||
|
},
|
||||||
|
};
|
@ -3,6 +3,8 @@ import { useContext } from 'react';
|
|||||||
import { rowPermissionsContext } from './RowPermissionsProvider';
|
import { rowPermissionsContext } from './RowPermissionsProvider';
|
||||||
import { tableContext } from './TableProvider';
|
import { tableContext } from './TableProvider';
|
||||||
import { PermissionType } from './types';
|
import { PermissionType } from './types';
|
||||||
|
import { logicalModelContext } from './RootLogicalModelProvider';
|
||||||
|
import { useForbiddenFeatures } from './ForbiddenFeaturesProvider';
|
||||||
|
|
||||||
export const Operator = ({
|
export const Operator = ({
|
||||||
operator,
|
operator,
|
||||||
@ -15,12 +17,14 @@ export const Operator = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { operators, setKey } = useContext(rowPermissionsContext);
|
const { operators, setKey } = useContext(rowPermissionsContext);
|
||||||
const { columns, table, relationships } = useContext(tableContext);
|
const { columns, table, relationships } = useContext(tableContext);
|
||||||
|
const { rootLogicalModel } = useContext(logicalModelContext);
|
||||||
const parent = path[path.length - 1];
|
const parent = path[path.length - 1];
|
||||||
const operatorLevelId = `${path?.join('.')}-operator`;
|
const operatorLevelId = `${path?.join('.')}-operator`;
|
||||||
|
const { hasFeature } = useForbiddenFeatures();
|
||||||
return (
|
return (
|
||||||
<select
|
<select
|
||||||
data-testid={operatorLevelId || 'root-operator-picker'}
|
data-testid={operatorLevelId || 'root-operator-picker'}
|
||||||
className="border border-gray-200 rounded-md p-2"
|
className="border border-gray-200 rounded-md p-2 pr-4"
|
||||||
value={operator}
|
value={operator}
|
||||||
disabled={parent === '_where' && isEmpty(table)}
|
disabled={parent === '_where' && isEmpty(table)}
|
||||||
onChange={e => {
|
onChange={e => {
|
||||||
@ -55,7 +59,20 @@ export const Operator = ({
|
|||||||
))}
|
))}
|
||||||
</optgroup>
|
</optgroup>
|
||||||
) : null}
|
) : null}
|
||||||
{operators.exist?.items.length ? (
|
{rootLogicalModel?.fields.length ? (
|
||||||
|
<optgroup label="Columns">
|
||||||
|
{rootLogicalModel?.fields.map((field, index) => (
|
||||||
|
<option
|
||||||
|
data-type="column"
|
||||||
|
key={'column' + index}
|
||||||
|
value={field.name}
|
||||||
|
>
|
||||||
|
{field.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</optgroup>
|
||||||
|
) : null}
|
||||||
|
{hasFeature('exists') && operators.exist?.items.length ? (
|
||||||
<optgroup label="Exist operators">
|
<optgroup label="Exist operators">
|
||||||
{operators.exist.items.map((item, index) => (
|
{operators.exist.items.map((item, index) => (
|
||||||
<option data-type="exist" key={'exist' + index} value={item.value}>
|
<option data-type="exist" key={'exist' + index} value={item.value}>
|
||||||
|
@ -0,0 +1,35 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
import { LogicalModel } from '../../../../../hasura-metadata-types';
|
||||||
|
|
||||||
|
type LogicalModelState = {
|
||||||
|
rootLogicalModel: LogicalModel | undefined;
|
||||||
|
logicalModels: LogicalModel[];
|
||||||
|
logicalModel: LogicalModel['name'] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const logicalModelContext = createContext<LogicalModelState>({
|
||||||
|
logicalModel: '',
|
||||||
|
logicalModels: [],
|
||||||
|
rootLogicalModel: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const RootLogicalModelProvider = ({
|
||||||
|
children,
|
||||||
|
logicalModel,
|
||||||
|
logicalModels,
|
||||||
|
}: Omit<LogicalModelState, 'rootLogicalModel'> & {
|
||||||
|
children?: React.ReactNode | undefined;
|
||||||
|
}) => {
|
||||||
|
const rootLogicalModel = logicalModels.find(t => t.name === logicalModel);
|
||||||
|
return (
|
||||||
|
<logicalModelContext.Provider
|
||||||
|
value={{
|
||||||
|
logicalModel,
|
||||||
|
logicalModels,
|
||||||
|
rootLogicalModel,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</logicalModelContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,45 @@
|
|||||||
|
import { createContext } from 'react';
|
||||||
|
import { Table } from '../../../../../hasura-metadata-types';
|
||||||
|
import { areTablesEqual } from '../../../../../hasura-metadata-api';
|
||||||
|
import { Tables } from './types';
|
||||||
|
|
||||||
|
type RootTableState = {
|
||||||
|
tables: Tables;
|
||||||
|
table: Table;
|
||||||
|
rootTable: Tables[number] | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rootTableContext = createContext<RootTableState>({
|
||||||
|
table: {},
|
||||||
|
tables: [],
|
||||||
|
rootTable: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RootTableProvider
|
||||||
|
*
|
||||||
|
* Provides tables array, table definition (just name) and root table (full table) in context so it can be accessed in any of its children
|
||||||
|
* Not to be confused with TableProvider, which provides the current table
|
||||||
|
* RootTableProvider is meant to be used just once, at the top of the tree,
|
||||||
|
* whereas there are many TableProvider in the tree, used mainly for selecting relationships
|
||||||
|
*/
|
||||||
|
export const RootTableProvider = ({
|
||||||
|
children,
|
||||||
|
table,
|
||||||
|
tables,
|
||||||
|
}: Omit<RootTableState, 'rootTable'> & {
|
||||||
|
children?: React.ReactNode | undefined;
|
||||||
|
}) => {
|
||||||
|
const rootTable = tables.find(t => areTablesEqual(t.table, table));
|
||||||
|
return (
|
||||||
|
<rootTableContext.Provider
|
||||||
|
value={{
|
||||||
|
table,
|
||||||
|
tables,
|
||||||
|
rootTable,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</rootTableContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@ -38,6 +38,8 @@ export const SetRootLevelPermission: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{}}
|
permissions={{}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -56,6 +58,8 @@ export const SetExistsPermission: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{}}
|
permissions={{}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -95,6 +99,8 @@ export const SetMultilevelExistsPermission: StoryObj<
|
|||||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{}}
|
permissions={{}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -142,6 +148,8 @@ export const SetAndPermission: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{}}
|
permissions={{}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -176,6 +184,8 @@ export const SetMultilevelAndPermission: StoryObj<typeof RowPermissionsInput> =
|
|||||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{}}
|
permissions={{}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -218,6 +228,8 @@ export const SetNotPermission: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{}}
|
permissions={{}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -244,6 +256,8 @@ export const SetOrPermission: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{}}
|
permissions={{}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -276,6 +290,8 @@ export const SetMultilevelOrPermission: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{}}
|
permissions={{}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -317,6 +333,8 @@ export const Empty: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{}}
|
permissions={{}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -329,6 +347,8 @@ export const Exists: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{
|
permissions={{
|
||||||
_exists: {
|
_exists: {
|
||||||
_table: {},
|
_table: {},
|
||||||
@ -347,6 +367,8 @@ export const SetDisabledExistsPermission: StoryObj<typeof RowPermissionsInput> =
|
|||||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{
|
permissions={{
|
||||||
_exists: {
|
_exists: {
|
||||||
_table: {},
|
_table: {},
|
||||||
@ -378,6 +400,8 @@ export const ExistsWhere: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{
|
permissions={{
|
||||||
_exists: {
|
_exists: {
|
||||||
_table: { dataset: 'bigquery_sample', name: 'sample_table' },
|
_table: { dataset: 'bigquery_sample', name: 'sample_table' },
|
||||||
@ -400,6 +424,8 @@ export const EmptyExists: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{
|
permissions={{
|
||||||
_exists: {
|
_exists: {
|
||||||
_table: {},
|
_table: {},
|
||||||
@ -417,6 +443,8 @@ export const And: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{
|
permissions={{
|
||||||
_and: [
|
_and: [
|
||||||
{ STATUS: { _eq: 'X-Hasura-User-Id' } },
|
{ STATUS: { _eq: 'X-Hasura-User-Id' } },
|
||||||
@ -434,6 +462,8 @@ export const EmptyAnd: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{
|
permissions={{
|
||||||
_and: [{}],
|
_and: [{}],
|
||||||
}}
|
}}
|
||||||
@ -448,6 +478,8 @@ export const Not: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{
|
permissions={{
|
||||||
_not: { STATUS: { _eq: 'X-Hasura-User-Id' } },
|
_not: { STATUS: { _eq: 'X-Hasura-User-Id' } },
|
||||||
}}
|
}}
|
||||||
@ -462,6 +494,8 @@ export const EmptyNot: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{
|
permissions={{
|
||||||
_not: {},
|
_not: {},
|
||||||
}}
|
}}
|
||||||
@ -476,6 +510,8 @@ export const Relationships: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={['Album']}
|
table={['Album']}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{ Author: { name: { _eq: '' } } }}
|
permissions={{ Author: { name: { _eq: '' } } }}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -488,6 +524,8 @@ export const RelationshipsColumns: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={['Album']}
|
table={['Album']}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{ Label: { id: { _eq: '' } } }}
|
permissions={{ Label: { id: { _eq: '' } } }}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -500,6 +538,8 @@ export const ColumnTypes: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{ Series_reference: { _eq: '' } }}
|
permissions={{ Series_reference: { _eq: '' } }}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -512,6 +552,8 @@ export const BooleanArrayType: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={['Album']}
|
table={['Album']}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{ Author: { _ceq: ['name'] } }}
|
permissions={{ Author: { _ceq: ['name'] } }}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -536,6 +578,8 @@ export const BooleanArrayTypeRoot: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={['Album']}
|
table={['Album']}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{
|
permissions={{
|
||||||
Author: {
|
Author: {
|
||||||
_ceq: [
|
_ceq: [
|
||||||
@ -589,6 +633,8 @@ export const StringObjectType: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ name: 'user_location', schema: 'public' }}
|
table={{ name: 'user_location', schema: 'public' }}
|
||||||
tables={tableWithGeolocationSupport}
|
tables={tableWithGeolocationSupport}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{
|
permissions={{
|
||||||
location: {
|
location: {
|
||||||
_st_d_within: {
|
_st_d_within: {
|
||||||
@ -619,6 +665,8 @@ export const NumericValue: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={['Album']}
|
table={['Album']}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{ id: { _eq: '' } }}
|
permissions={{ id: { _eq: '' } }}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -638,6 +686,8 @@ export const NumericIntValue: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={['Album']}
|
table={['Album']}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{ id: { _eq: 0 } }}
|
permissions={{ id: { _eq: 0 } }}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -650,6 +700,8 @@ export const NumericFloatValue: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={['Album']}
|
table={['Album']}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{ id: { _eq: 0.9 } }}
|
permissions={{ id: { _eq: 0.9 } }}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@ -670,6 +722,8 @@ export const JsonbColumns: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ schema: 'public', name: 'Stuff' }}
|
table={{ schema: 'public', name: 'Stuff' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{ jason: { _contained_in: { a: 'b' } } }}
|
permissions={{ jason: { _contained_in: { a: 'b' } } }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -710,6 +764,8 @@ export const JsonbColumnsHasKeys: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ schema: 'public', name: 'Stuff' }}
|
table={{ schema: 'public', name: 'Stuff' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{ jason: { _has_keys_all: [''] } }}
|
permissions={{ jason: { _has_keys_all: [''] } }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -737,6 +793,8 @@ export const StringColumns: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ schema: 'public', name: 'Stuff' }}
|
table={{ schema: 'public', name: 'Stuff' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -752,13 +810,7 @@ export const StringColumns: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
// Write a number in the input
|
// Write a number in the input
|
||||||
await userEvent.type(canvas.getByTestId('name._eq-value-input'), '1337');
|
await userEvent.type(canvas.getByTestId('name._eq-value-input'), '1337');
|
||||||
|
|
||||||
const onPermissionsChangeMock = args.onPermissionsChange as jest.Mock;
|
expect(args.onPermissionsChange).toHaveBeenCalledWith({
|
||||||
|
|
||||||
const latestPermissions =
|
|
||||||
onPermissionsChangeMock?.mock.calls[
|
|
||||||
onPermissionsChangeMock?.mock.calls.length - 1
|
|
||||||
][0];
|
|
||||||
expect(latestPermissions).toEqual({
|
|
||||||
name: {
|
name: {
|
||||||
_eq: 1337,
|
_eq: 1337,
|
||||||
},
|
},
|
||||||
@ -787,6 +839,8 @@ export const NumberColumns: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ schema: 'public', name: 'Stuff' }}
|
table={{ schema: 'public', name: 'Stuff' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -802,13 +856,7 @@ export const NumberColumns: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
// // Write a number in the input
|
// // Write a number in the input
|
||||||
await userEvent.type(canvas.getByTestId('id._eq-value-input'), '1337');
|
await userEvent.type(canvas.getByTestId('id._eq-value-input'), '1337');
|
||||||
|
|
||||||
const onPermissionsChangeMock = args.onPermissionsChange as jest.Mock;
|
expect(args.onPermissionsChange).toHaveBeenCalledWith({
|
||||||
|
|
||||||
const latestPermissions =
|
|
||||||
onPermissionsChangeMock?.mock.calls[
|
|
||||||
onPermissionsChangeMock?.mock.calls.length - 1
|
|
||||||
][0];
|
|
||||||
expect(latestPermissions).toEqual({
|
|
||||||
id: {
|
id: {
|
||||||
_eq: 12341337,
|
_eq: 12341337,
|
||||||
},
|
},
|
||||||
@ -823,6 +871,8 @@ export const OperatorDropdownHandling: StoryObj<typeof RowPermissionsInput> = {
|
|||||||
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
table={{ dataset: 'bigquery_sample', name: 'sample_table' }}
|
||||||
tables={tables}
|
tables={tables}
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
|
logicalModel={undefined}
|
||||||
|
logicalModels={[]}
|
||||||
permissions={{
|
permissions={{
|
||||||
_not: { STATUS: { _eq: 'X-Hasura-User-Id' } },
|
_not: { STATUS: { _eq: 'X-Hasura-User-Id' } },
|
||||||
}}
|
}}
|
||||||
|
@ -1,23 +1,36 @@
|
|||||||
import { Table } from '../../../../../hasura-metadata-types';
|
import { LogicalModel, Table } from '../../../../../hasura-metadata-types';
|
||||||
import { Tables, Operators, Permissions, Comparators } from './types';
|
import { Tables, Operators, Permissions, Comparators } from './types';
|
||||||
import { RowPermissionsProvider } from './RowPermissionsProvider';
|
import { RowPermissionsProvider } from './RowPermissionsProvider';
|
||||||
import { TypesProvider } from './TypesProvider';
|
import { TypesProvider } from './TypesProvider';
|
||||||
import { TableProvider } from './TableProvider';
|
import { TableProvider } from './TableProvider';
|
||||||
import { RootInput } from './RootInput';
|
import { RootInput } from './RootInput';
|
||||||
import { JsonEditor } from './JsonEditor';
|
import { JsonEditor } from './JsonEditor';
|
||||||
|
import { RootTableProvider } from './RootTableProvider';
|
||||||
|
import { RootLogicalModelProvider } from './RootLogicalModelProvider';
|
||||||
|
import { LogicalModelWithSourceName } from '../../../../../Data/LogicalModels/LogicalModelPermissions/components/types';
|
||||||
|
import {
|
||||||
|
ForbiddenFeaturesProvider,
|
||||||
|
Feature,
|
||||||
|
} from './ForbiddenFeaturesProvider';
|
||||||
|
|
||||||
export const RowPermissionsInput = ({
|
export const RowPermissionsInput = ({
|
||||||
permissions,
|
permissions,
|
||||||
tables,
|
tables,
|
||||||
table,
|
table,
|
||||||
|
logicalModel,
|
||||||
|
logicalModels,
|
||||||
onPermissionsChange,
|
onPermissionsChange,
|
||||||
comparators,
|
comparators,
|
||||||
|
forbidden,
|
||||||
}: {
|
}: {
|
||||||
permissions: Permissions;
|
permissions: Permissions;
|
||||||
tables: Tables;
|
tables: Tables;
|
||||||
table: Table;
|
table: Table | undefined;
|
||||||
|
logicalModels: LogicalModelWithSourceName[];
|
||||||
|
logicalModel: LogicalModel['name'] | undefined;
|
||||||
onPermissionsChange?: (permissions: Permissions) => void;
|
onPermissionsChange?: (permissions: Permissions) => void;
|
||||||
comparators: Comparators;
|
comparators: Comparators;
|
||||||
|
forbidden?: Feature[];
|
||||||
}) => {
|
}) => {
|
||||||
const operators: Operators = {
|
const operators: Operators = {
|
||||||
boolean: {
|
boolean: {
|
||||||
@ -34,11 +47,15 @@ export const RowPermissionsInput = ({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
<ForbiddenFeaturesProvider forbidden={forbidden}>
|
||||||
|
<RootLogicalModelProvider
|
||||||
|
logicalModel={logicalModel}
|
||||||
|
logicalModels={logicalModels}
|
||||||
|
>
|
||||||
|
<RootTableProvider table={table} tables={tables}>
|
||||||
<RowPermissionsProvider
|
<RowPermissionsProvider
|
||||||
operators={operators}
|
operators={operators}
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
table={table}
|
|
||||||
tables={tables}
|
|
||||||
comparators={comparators}
|
comparators={comparators}
|
||||||
onPermissionsChange={onPermissionsChange}
|
onPermissionsChange={onPermissionsChange}
|
||||||
>
|
>
|
||||||
@ -51,5 +68,8 @@ export const RowPermissionsInput = ({
|
|||||||
</TableProvider>
|
</TableProvider>
|
||||||
</TypesProvider>
|
</TypesProvider>
|
||||||
</RowPermissionsProvider>
|
</RowPermissionsProvider>
|
||||||
|
</RootTableProvider>
|
||||||
|
</RootLogicalModelProvider>
|
||||||
|
</ForbiddenFeaturesProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -4,8 +4,6 @@ import { Permissions, RowPermissionsState } from './types';
|
|||||||
import { updateKey } from './utils/helpers';
|
import { updateKey } from './utils/helpers';
|
||||||
|
|
||||||
export const rowPermissionsContext = createContext<RowPermissionsState>({
|
export const rowPermissionsContext = createContext<RowPermissionsState>({
|
||||||
table: {},
|
|
||||||
tables: [],
|
|
||||||
comparators: {},
|
comparators: {},
|
||||||
operators: {},
|
operators: {},
|
||||||
permissions: {},
|
permissions: {},
|
||||||
@ -18,14 +16,9 @@ export const RowPermissionsProvider = ({
|
|||||||
children,
|
children,
|
||||||
operators,
|
operators,
|
||||||
permissions,
|
permissions,
|
||||||
table,
|
|
||||||
tables,
|
|
||||||
comparators,
|
comparators,
|
||||||
onPermissionsChange,
|
onPermissionsChange,
|
||||||
}: Pick<
|
}: Pick<RowPermissionsState, 'permissions' | 'operators' | 'comparators'> & {
|
||||||
RowPermissionsState,
|
|
||||||
'permissions' | 'operators' | 'table' | 'tables' | 'comparators'
|
|
||||||
> & {
|
|
||||||
children?: React.ReactNode | undefined;
|
children?: React.ReactNode | undefined;
|
||||||
onPermissionsChange?: (permissions: Permissions) => void;
|
onPermissionsChange?: (permissions: Permissions) => void;
|
||||||
}) => {
|
}) => {
|
||||||
@ -79,9 +72,7 @@ export const RowPermissionsProvider = ({
|
|||||||
...permissionsState,
|
...permissionsState,
|
||||||
setValue,
|
setValue,
|
||||||
setKey,
|
setKey,
|
||||||
table,
|
|
||||||
setPermissions,
|
setPermissions,
|
||||||
tables,
|
|
||||||
comparators,
|
comparators,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -4,6 +4,7 @@ import { Table } from '../../../../../hasura-metadata-types';
|
|||||||
import { getTableDisplayName } from '../../../../../DatabaseRelationships';
|
import { getTableDisplayName } from '../../../../../DatabaseRelationships';
|
||||||
import { tableContext } from './TableProvider';
|
import { tableContext } from './TableProvider';
|
||||||
import { rowPermissionsContext } from './RowPermissionsProvider';
|
import { rowPermissionsContext } from './RowPermissionsProvider';
|
||||||
|
import { rootTableContext } from './RootTableProvider';
|
||||||
|
|
||||||
export function SelectTable({
|
export function SelectTable({
|
||||||
componentLevelId,
|
componentLevelId,
|
||||||
@ -16,7 +17,8 @@ export function SelectTable({
|
|||||||
}) {
|
}) {
|
||||||
const comparatorName = path[path.length - 1];
|
const comparatorName = path[path.length - 1];
|
||||||
const { table, setTable, setComparator } = useContext(tableContext);
|
const { table, setTable, setComparator } = useContext(tableContext);
|
||||||
const { setValue, tables } = useContext(rowPermissionsContext);
|
const { setValue } = useContext(rowPermissionsContext);
|
||||||
|
const { tables } = useContext(rootTableContext);
|
||||||
const stringifiedTable = JSON.stringify(table);
|
const stringifiedTable = JSON.stringify(table);
|
||||||
// Sync table name with ColumnsContext table value
|
// Sync table name with ColumnsContext table value
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { useState, useContext, useEffect, createContext } from 'react';
|
import { useState, useContext, useEffect, createContext } from 'react';
|
||||||
import { areTablesEqual } from '../../../../../hasura-metadata-api';
|
|
||||||
import { Table } from '../../../../../hasura-metadata-types';
|
import { Table } from '../../../../../hasura-metadata-types';
|
||||||
import { rowPermissionsContext } from './RowPermissionsProvider';
|
|
||||||
import { Columns, Relationships, TableContext } from './types';
|
import { Columns, Relationships, TableContext } from './types';
|
||||||
|
import { rootTableContext } from './RootTableProvider';
|
||||||
|
import { areTablesEqual } from '../../../../../hasura-metadata-api';
|
||||||
|
|
||||||
export const tableContext = createContext<TableContext>({
|
export const tableContext = createContext<TableContext>({
|
||||||
table: {},
|
table: {},
|
||||||
@ -26,7 +26,7 @@ export const TableProvider = ({
|
|||||||
const [comparator, setComparator] = useState<string | undefined>();
|
const [comparator, setComparator] = useState<string | undefined>();
|
||||||
const [columns, setColumns] = useState<Columns>([]);
|
const [columns, setColumns] = useState<Columns>([]);
|
||||||
const [relationships, setRelationships] = useState<Relationships>([]);
|
const [relationships, setRelationships] = useState<Relationships>([]);
|
||||||
const { tables } = useContext(rowPermissionsContext);
|
const { tables } = useContext(rootTableContext);
|
||||||
// Stringify values to get a stable value for useEffect
|
// Stringify values to get a stable value for useEffect
|
||||||
const stringifiedTable = JSON.stringify(table);
|
const stringifiedTable = JSON.stringify(table);
|
||||||
const stringifiedTables = JSON.stringify(tables);
|
const stringifiedTables = JSON.stringify(tables);
|
||||||
|
@ -10,6 +10,7 @@ import { rowPermissionsContext } from './RowPermissionsProvider';
|
|||||||
import set from 'lodash/set';
|
import set from 'lodash/set';
|
||||||
import unset from 'lodash/unset';
|
import unset from 'lodash/unset';
|
||||||
import { getPermissionTypes } from './utils/typeProviderHelpers';
|
import { getPermissionTypes } from './utils/typeProviderHelpers';
|
||||||
|
import { rootTableContext } from './RootTableProvider';
|
||||||
|
|
||||||
export const typesContext = createContext<TypesContext>({
|
export const typesContext = createContext<TypesContext>({
|
||||||
types: {},
|
types: {},
|
||||||
@ -22,7 +23,8 @@ export const TypesProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
const [types, setTypes] = useState<Record<string, { type: PermissionType }>>(
|
const [types, setTypes] = useState<Record<string, { type: PermissionType }>>(
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
const { permissions, tables, table } = useContext(rowPermissionsContext);
|
const { permissions } = useContext(rowPermissionsContext);
|
||||||
|
const { table, tables } = useContext(rootTableContext);
|
||||||
const setType = useCallback(
|
const setType = useCallback(
|
||||||
({
|
({
|
||||||
type,
|
type,
|
||||||
|
@ -46,8 +46,6 @@ export type RowPermissionsState = {
|
|||||||
operators: Operators;
|
operators: Operators;
|
||||||
permissions: Permissions;
|
permissions: Permissions;
|
||||||
comparators: Comparators;
|
comparators: Comparators;
|
||||||
table: Table;
|
|
||||||
tables: Tables;
|
|
||||||
setValue: (path: string[], value: any) => void;
|
setValue: (path: string[], value: any) => void;
|
||||||
setKey: (props: { path: string[]; key: any; type: PermissionType }) => void;
|
setKey: (props: { path: string[]; key: any; type: PermissionType }) => void;
|
||||||
setPermissions: (permissions: Permissions) => void;
|
setPermissions: (permissions: Permissions) => void;
|
||||||
|
@ -8,6 +8,7 @@ import { Table } from '../../../../../../hasura-metadata-types';
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { rowPermissionsContext } from '../RowPermissionsProvider';
|
import { rowPermissionsContext } from '../RowPermissionsProvider';
|
||||||
import { sourceDataTypes, SourceDataTypes } from './sourceDataTypes';
|
import { sourceDataTypes, SourceDataTypes } from './sourceDataTypes';
|
||||||
|
import { rootTableContext } from '../RootTableProvider';
|
||||||
|
|
||||||
function columnOperators(): Array<Operator> {
|
function columnOperators(): Array<Operator> {
|
||||||
return Object.keys(columnOperatorsInfo).reduce((acc, key) => {
|
return Object.keys(columnOperatorsInfo).reduce((acc, key) => {
|
||||||
@ -136,7 +137,8 @@ export const mapScalarDataType = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function useOperators({ path }: { path: string[] }) {
|
export function useOperators({ path }: { path: string[] }) {
|
||||||
const { comparators, tables } = useContext(rowPermissionsContext);
|
const { comparators } = useContext(rowPermissionsContext);
|
||||||
|
const { tables } = useContext(rootTableContext);
|
||||||
const { columns, table } = useContext(tableContext);
|
const { columns, table } = useContext(tableContext);
|
||||||
|
|
||||||
const columnName = path[path.length - 2];
|
const columnName = path[path.length - 2];
|
||||||
|
@ -69,7 +69,6 @@ export const createDefaultValues = ({
|
|||||||
queryType: 'select',
|
queryType: 'select',
|
||||||
filterType: 'none',
|
filterType: 'none',
|
||||||
columns: {},
|
columns: {},
|
||||||
|
|
||||||
supportedOperators,
|
supportedOperators,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -67,7 +67,8 @@ export const useFormData = ({
|
|||||||
dataSourceName,
|
dataSourceName,
|
||||||
})) as Operator[];
|
})) as Operator[];
|
||||||
|
|
||||||
const defaultValues = createDefaultValues({
|
const defaultValues = {
|
||||||
|
...createDefaultValues({
|
||||||
queryType,
|
queryType,
|
||||||
roleName,
|
roleName,
|
||||||
dataSourceName,
|
dataSourceName,
|
||||||
@ -77,7 +78,8 @@ export const useFormData = ({
|
|||||||
defaultQueryRoot,
|
defaultQueryRoot,
|
||||||
metadataSource,
|
metadataSource,
|
||||||
supportedOperators: supportedOperators ?? [],
|
supportedOperators: supportedOperators ?? [],
|
||||||
});
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
const formData = createFormData({
|
const formData = createFormData({
|
||||||
dataSourceName,
|
dataSourceName,
|
||||||
|
@ -1,3 +1,7 @@
|
|||||||
|
import {
|
||||||
|
LogicalModelWithSource,
|
||||||
|
NativeQueryWithSource,
|
||||||
|
} from '../Data/LogicalModels/types';
|
||||||
import { Metadata, Table } from '../hasura-metadata-types';
|
import { Metadata, Table } from '../hasura-metadata-types';
|
||||||
import * as utils from './utils';
|
import * as utils from './utils';
|
||||||
|
|
||||||
@ -31,3 +35,29 @@ export const findTable =
|
|||||||
utils.findMetadataTable(dataSourceName, table, m);
|
utils.findMetadataTable(dataSourceName, table, m);
|
||||||
|
|
||||||
export const resourceVersion = () => (m: Metadata) => m?.resource_version;
|
export const resourceVersion = () => (m: Metadata) => m?.resource_version;
|
||||||
|
|
||||||
|
export const extractModelsAndQueriesFromMetadata = (
|
||||||
|
m: Metadata
|
||||||
|
): { queries: NativeQueryWithSource[]; models: LogicalModelWithSource[] } => {
|
||||||
|
const sources = m.metadata.sources;
|
||||||
|
let models: LogicalModelWithSource[] = [];
|
||||||
|
let queries: NativeQueryWithSource[] = [];
|
||||||
|
|
||||||
|
sources.forEach(s => {
|
||||||
|
if (s.logical_models && s.logical_models.length > 0) {
|
||||||
|
models = [...models, ...s.logical_models.map(m => ({ ...m, source: s }))];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.native_queries && s.native_queries.length > 0) {
|
||||||
|
queries = [
|
||||||
|
...queries,
|
||||||
|
...s.native_queries.map(q => ({ ...q, source: s })),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
models,
|
||||||
|
queries,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
@ -7,4 +7,11 @@ export type LogicalModelField = {
|
|||||||
export type LogicalModel = {
|
export type LogicalModel = {
|
||||||
fields: LogicalModelField[];
|
fields: LogicalModelField[];
|
||||||
name: string;
|
name: string;
|
||||||
|
select_permissions?: {
|
||||||
|
permission: {
|
||||||
|
columns: string[];
|
||||||
|
filter: Record<string, any>;
|
||||||
|
};
|
||||||
|
role: string;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user