console: add root field permissions to GDC permissions tab

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/7536
Co-authored-by: Matt Hardman <28978422+mattshardman@users.noreply.github.com>
GitOrigin-RevId: eaa788e45e12900ac5237c4fb1c98d19f64778ed
This commit is contained in:
Erik Magnusson 2023-01-23 13:27:37 +02:00 committed by hasura-bot
parent a6f81a7208
commit e86d24b1fb
34 changed files with 1461 additions and 117 deletions

View File

@ -183,7 +183,7 @@ const DatabaseListItem: React.FC<DatabaseListItemProps> = ({
</span>
)}
</td>
<td className="px-sm py-xs max-w-xs align-top">
<td className="px-sm py-xs max-w-xs align-center">
<CollapsibleToggle
dataSource={dataSource}
dbVersion={dbVersion}
@ -202,7 +202,7 @@ const DatabaseListItem: React.FC<DatabaseListItemProps> = ({
</ToolTip>
)}
</td>
<td className="px-sm py-xs max-w-xs align-top break-all">
<td className="px-sm py-xs max-w-xs align-center break-all">
{showUrl ? (
typeof dataSource.url === 'string' ? (
dataSource.url

View File

@ -85,7 +85,7 @@ export const GDCDatabaseListItem: React.FC<GDCDatabaseListItemItemProps> = ({
Remove
</Button>
</td>
<td className="px-sm py-xs max-w-xs align-top break-all">
<td className="px-sm py-xs max-w-xs align-center break-all">
<div className="font-bold">
{dataSource.name}{' '}
<span className="font-normal">({dataSource.kind})</span>

View File

@ -6,7 +6,7 @@ import {
} from './PermissionsConfirmationModal';
export default {
title: 'Features/Permissions Form/Permissions Confirmation Modal',
title: 'Features/Table Permissions/Permissions Confirmation Modal',
component: PermissionsConfirmationModal,
argTypes: {
onSubmit: { action: true },

View File

@ -1,7 +1,8 @@
import { IndicatorCard } from '@/new-components/IndicatorCard';
import React, { useState } from 'react';
import Skeleton from 'react-loading-skeleton';
import { useListAllTableColumns } from '../hooks';
import { useListAllTableColumns } from '@/features/Data';
import { TableColumn } from '@/features/DataSource';
import { ModifyTableColumn } from '../types';
import { EditTableColumnDialog } from './EditTableColumnDialog/EditTableColumnDialog';
import { TableColumnDescription } from './TableColumnDescription';
@ -34,7 +35,7 @@ export const TableColumns: React.VFC<TableColumnProps> = props => {
return (
<>
{(columns ?? []).map(c => (
{(columns ?? []).map((c: TableColumn) => (
<TableColumnDescription
column={c}
key={c.name}

View File

@ -1,2 +1 @@
export { useListAllTableColumns } from './useListAllTableColumns';
export { useUpdateTableConfiguration } from './useUpdateTableConfiguration';

View File

@ -1,2 +1,3 @@
export { useTableDefinition } from './useTableDefinition';
export { useDatabaseHierarchy } from './useDatabaseHierarchy';
export { useListAllTableColumns } from './useListAllTableColumns';

View File

@ -19,6 +19,7 @@ const roleName = 'user';
export const GDCSelect: Story<PermissionsFormProps> = args => (
<PermissionsForm {...args} />
);
GDCSelect.args = {
dataSourceName: 'sqlite',
queryType: 'select',

View File

@ -1,11 +1,8 @@
import React from 'react';
import { useConsoleForm } from '@/new-components/Form';
import { Button } from '@/new-components/Button';
import { IndicatorCard } from '@/new-components/IndicatorCard';
import { PermissionsSchema, schema } from './../schema';
import { AccessType, QueryType } from '../types';
import {
AggregationSection,
@ -17,6 +14,7 @@ import {
} from './components';
import { useFormData, useUpdatePermissions } from './hooks';
import ColumnRootFieldPermissions from './components/RootFieldPermissions/RootFieldPermissions';
export interface ComponentProps {
dataSourceName: string;
@ -60,7 +58,7 @@ const Component = (props: ComponentProps) => {
const isSubmittingError =
updatePermissions.isError || deletePermissions.isError;
//
// for update it is possible to set pre update and post update row checks
const rowPermissions = queryType === 'update' ? ['pre', 'post'] : [queryType];
@ -101,7 +99,6 @@ const Component = (props: ComponentProps) => {
{queryType}
</h3>
</div>
<RowPermissionsSectionWrapper
roleName={roleName}
queryType={queryType}
@ -131,32 +128,37 @@ const Component = (props: ComponentProps) => {
</React.Fragment>
))}
</RowPermissionsSectionWrapper>
{queryType !== 'delete' && (
<ColumnPermissionsSection
roleName={roleName}
queryType={queryType}
columns={formData?.columns}
table={table}
dataSourceName={dataSourceName}
/>
)}
{['insert', 'update'].includes(queryType) && (
<ColumnPresetsSection
queryType={queryType}
columns={formData?.columns}
/>
)}
{queryType === 'select' && (
<AggregationSection queryType={queryType} roleName={roleName} />
)}
{['insert', 'update', 'delete'].includes(queryType) && (
<BackendOnlySection queryType={queryType} />
)}
<hr className="my-4" />
{queryType === 'select' && (
<ColumnRootFieldPermissions
filterType={filterType}
dataSourceName={dataSourceName}
table={table}
/>
)}
<hr className="my-4" />
{/* {!!tableNames?.length && (
<ClonePermissionsSection
queryType={queryType}
@ -165,7 +167,6 @@ const Component = (props: ComponentProps) => {
roles={allRoles}
/>
)} */}
<div className="pt-2 flex gap-2">
<Button
type="submit"

View File

@ -9,9 +9,9 @@ const selectArgs: CreateInsertArgs = {
role: 'user',
formData: {
queryType: 'select',
filterType: 'none',
query_root_fields: null,
subscription_root_fields: null,
filter: {},
rowCount: '0',
columns: {
@ -59,7 +59,7 @@ test('create select args object from form data', () => {
{
args: {
permission: {
aggregation_enabled: false,
allow_aggregations: false,
columns: ['email', 'type'],
filter: {},
},

View File

@ -8,8 +8,10 @@ import { PermissionsSchema } from '../../schema';
type SelectPermissionMetadata = {
columns: string[];
filter: Record<string, any>;
aggregation_enabled?: boolean;
allow_aggregations?: boolean;
limit?: number;
query_root_fields?: any[];
subscription_root_fields?: any[];
};
const createSelectObject = (input: PermissionsSchema) => {
@ -37,9 +39,17 @@ const createSelectObject = (input: PermissionsSchema) => {
const permissionObject: SelectPermissionMetadata = {
columns,
filter,
aggregation_enabled: input.aggregationEnabled,
allow_aggregations: input.aggregationEnabled,
};
if (input.query_root_fields) {
permissionObject.query_root_fields = input.query_root_fields;
}
if (input.subscription_root_fields) {
permissionObject.subscription_root_fields =
input.subscription_root_fields;
}
if (input.rowCount && input.rowCount !== '0') {
permissionObject.limit = parseInt(input.rowCount, 10);
}
@ -71,7 +81,7 @@ const createPermission = (formData: PermissionsSchema) => {
export interface CreateInsertArgs {
dataSourceName: string;
table: unknown;
queryType: string;
queryType: any;
role: string;
accessType: AccessType;
formData: PermissionsSchema;
@ -82,7 +92,7 @@ export interface CreateInsertArgs {
interface ExistingPermission {
table: unknown;
role: string;
queryType: string;
queryType: any;
}
/**
* creates the insert arguments to update permissions

View File

@ -1,11 +1,16 @@
import React from 'react';
import React, { useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { Collapse } from '@/new-components/deprecated';
import { isFeatureSupported } from '@/dataSources';
import { useIsDisabled } from '../hooks/useIsDisabled';
import { QueryType } from '../../types';
import { PermissionsConfirmationModal } from './RootFieldPermissions/PermissionsConfirmationModal';
import {
getPermissionsModalTitle,
getPermissionsModalDescription,
} from './RootFieldPermissions/PermissionsConfirmationModal.utils';
import { isPermissionModalDisabled } from '../utils/getPermissionModalStatus';
export interface AggregationProps {
queryType: QueryType;
@ -18,44 +23,99 @@ export const AggregationSection: React.FC<AggregationProps> = ({
roleName,
defaultOpen,
}) => {
const { register, watch } = useFormContext();
const { watch, setValue } = useFormContext();
const [showConfirmationModal, setShowConfirmationModal] = useState(false);
// if no row permissions are selected selection should be disabled
const disabled = useIsDisabled(queryType);
const enabled = watch('aggregationEnabled');
const [enabled, queryRootFields, subscriptionRootFields] = watch([
'aggregationEnabled',
'query_root_fields',
'subscription_root_fields',
]);
if (!isFeatureSupported('tables.permissions.aggregation')) {
return null;
}
const handleUpdate = () => {
setValue('aggregationEnabled', !enabled);
setValue(
'query_root_fields',
queryRootFields.filter((field: string) => field !== 'select_aggregate')
);
setValue(
'subscription_root_fields',
subscriptionRootFields.filter(
(field: string) => field !== 'select_aggregate'
)
);
};
const permissionsModalTitle = getPermissionsModalTitle({
scenario: 'aggregate',
role: roleName,
});
const permissionsModalDescription =
getPermissionsModalDescription('aggregate');
return (
<Collapse
title="Aggregation queries permissions"
tooltip="Allow queries with aggregate functions like sum, count, avg,
<>
<Collapse
title="Aggregation queries permissions"
tooltip="Allow queries with aggregate functions like sum, count, avg,
max, min, etc"
status={enabled ? 'Enabled' : 'Disabled'}
data-test="toggle-agg-permission"
disabled={disabled}
defaultOpen={defaultOpen || enabled}
>
<Collapse.Content>
<div title={disabled ? 'Set row permissions first' : ''}>
<label className="flex items-center gap-4">
<input
type="checkbox"
title={disabled ? 'Set row permissions first' : ''}
disabled={disabled}
className="m-0 mt-0 rounded shadow-sm border border-gray-300 hover:border-gray-400 focus:ring-yellow-400"
{...register('aggregationEnabled')}
/>
<span>
Allow role <strong>{roleName}</strong> to make aggregation queries
</span>
</label>
</div>
</Collapse.Content>
</Collapse>
status={enabled ? 'Enabled' : 'Disabled'}
data-test="toggle-agg-permission"
disabled={disabled}
defaultOpen={defaultOpen || enabled}
>
<Collapse.Content>
<div title={disabled ? 'Set row permissions first' : ''}>
<label className="flex items-center gap-4">
<input
type="checkbox"
title={disabled ? 'Set row permissions first' : ''}
disabled={disabled}
className="m-0 mt-0 rounded shadow-sm border border-gray-300 hover:border-gray-400 focus:ring-yellow-400"
checked={enabled}
onChange={() => {
const pkRootFieldsAreSelected =
queryRootFields?.includes('select_aggregate') ||
subscriptionRootFields?.includes('select_aggregate');
const hideModal = isPermissionModalDisabled();
if (
!showConfirmationModal &&
pkRootFieldsAreSelected &&
!hideModal
) {
setShowConfirmationModal(true);
return;
}
handleUpdate();
}}
/>
<span>
Allow role <strong>{roleName}</strong> to make aggregation
queries
</span>
</label>
</div>
</Collapse.Content>
</Collapse>
{showConfirmationModal && (
<PermissionsConfirmationModal
title={permissionsModalTitle}
description={permissionsModalDescription}
onClose={() => setShowConfirmationModal(false)}
onSubmit={() => {
handleUpdate();
setShowConfirmationModal(false);
}}
/>
)}
</>
);
};

View File

@ -105,7 +105,7 @@ export const ClonePermissionsRow: React.FC<ClonePermissionsRowProps> = ({
};
export interface ClonePermissionsSectionProps {
queryType: string;
queryType: any;
tables: string[];
supportedQueryTypes: string[];
roles: string[];
@ -115,7 +115,7 @@ export interface ClonePermissionsSectionProps {
export interface ClonePermission {
id: number;
tableName: string;
queryType: string;
queryType: any;
roleName: string;
}

View File

@ -2,6 +2,7 @@ import React from 'react';
import { Meta, Story } from '@storybook/react';
import { z } from 'zod';
import { SimpleForm } from '@/new-components/Form';
import { ReactQueryDecorator } from '@/storybook/decorators/react-query';
import {
ColumnPermissionsSection,
@ -14,6 +15,7 @@ export default {
title: 'Features/Permissions/Form/Column Section',
component: ColumnPermissionsSection,
decorators: [
ReactQueryDecorator(),
(StoryComponent: React.FC) => (
<SimpleForm
schema={schema}

View File

@ -1,14 +1,25 @@
import React from 'react';
import React, { useState } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { Button } from '@/new-components/Button';
import { Collapse } from '@/new-components/deprecated';
import { TableColumn } from '@/features/DataSource';
import { useListAllTableColumns } from '@/features/Data';
import { PermissionsConfirmationModal } from './RootFieldPermissions/PermissionsConfirmationModal';
import { getEdForm } from '../../../../components/Services/Data/utils';
import { useIsDisabled } from '../hooks/useIsDisabled';
import { QueryType } from '../../types';
import { isPermissionModalDisabled } from '../utils/getPermissionModalStatus';
const getAccessText = (queryType: string) => {
import {
getPermissionsModalTitle,
getPermissionsModalDescription,
} from './RootFieldPermissions/PermissionsConfirmationModal.utils';
import {
SubscriptionRootPermissionType,
QueryRootPermissionType,
} from './RootFieldPermissions/types';
const getAccessText = (queryType: any) => {
if (queryType === 'insert') {
return 'to set input for';
}
@ -24,6 +35,8 @@ export interface ColumnPermissionsSectionProps {
queryType: QueryType;
roleName: string;
columns?: string[];
table: unknown;
dataSourceName: string;
}
const useStatus = (disabled: boolean) => {
@ -52,17 +65,50 @@ const useStatus = (disabled: boolean) => {
return { data: 'Partial columns', isError: false };
};
const checkIfConfirmationIsNeeded = (
fieldName: string,
tableColumns: TableColumn[],
selectedColumns: Record<string, boolean>,
queryRootFields: QueryRootPermissionType,
subscriptionRootFields: SubscriptionRootPermissionType
) => {
const primaryKeys = tableColumns
?.filter(column => column.isPrimaryKey)
?.map(column => column.name);
const pkRootFieldsAreSelected =
queryRootFields?.includes('select_by_pk') ||
subscriptionRootFields?.includes('select_by_pk');
return (
selectedColumns[fieldName] &&
pkRootFieldsAreSelected &&
primaryKeys.includes(fieldName)
);
};
// @todo
// this hasn't been fully implemented, it still needs computed columns adding
export const ColumnPermissionsSection: React.FC<ColumnPermissionsSectionProps> =
({ roleName, queryType, columns }) => {
const { register, setValue } = useFormContext();
({ roleName, queryType, columns, table, dataSourceName }) => {
const { setValue, watch } = useFormContext();
const [showConfirmation, setShowConfirmationModal] = useState<
string | null
>(null);
const [selectedColumns, queryRootFields, subscriptionRootFields] = watch([
'columns',
'query_root_fields',
'subscription_root_fields',
]);
// if no row permissions are selected selection should be disabled
const disabled = useIsDisabled(queryType);
const { data: status, isError } = useStatus(disabled);
const { columns: tableColumns } = useListAllTableColumns(
dataSourceName,
table
);
const onClick = () => {
columns?.forEach(column => {
const toggleAllOn = status !== 'All columns';
@ -76,58 +122,117 @@ export const ColumnPermissionsSection: React.FC<ColumnPermissionsSectionProps> =
return <div>Error loading column permission data</div>;
}
return (
<Collapse defaultOpen={!disabled}>
<Collapse.Header
title={`Column ${queryType} permissions`}
tooltip={`Choose columns allowed to be ${getEdForm(queryType)}`}
status={status}
disabled={disabled}
disabledMessage="Set row permissions first"
/>
<Collapse.Content>
<div
title={disabled ? 'Set row permissions first' : ''}
className="grid gap-2"
>
<div className="flex gap-2 items-center">
<p>
Allow role <strong>{roleName}</strong>{' '}
{getAccessText(queryType)}
&nbsp;
<strong>columns</strong>:
</p>
</div>
const handleUpdate = (fieldName: string) => {
setValue(
'query_root_fields',
queryRootFields.filter((field: string) => field !== 'select_by_pk')
);
setValue(
'subscription_root_fields',
subscriptionRootFields.filter(
(field: string) => field !== 'select_by_pk'
)
);
setValue(`columns.${fieldName}`, !selectedColumns[fieldName]);
};
<fieldset className="flex gap-4 flex-wrap">
{columns?.map(fieldName => (
<label key={fieldName} className="flex gap-2 items-center">
<input
type="checkbox"
title={disabled ? 'Set a row permission first' : ''}
disabled={disabled}
style={{ marginTop: '0px !important' }}
className="rounded shadow-sm border border-gray-300 hover:border-gray-400 focus:ring-yellow-400"
{...register(`columns.${fieldName}`)}
/>
<i>{fieldName}</i>
</label>
))}
<Button
type="button"
size="sm"
title={disabled ? 'Set a row permission first' : ''}
disabled={disabled}
onClick={onClick}
data-test="toggle-all-col-btn"
>
Toggle All
</Button>
</fieldset>
</div>
{/* {getExternalTablePermissionsMsg()} */}
</Collapse.Content>
</Collapse>
const permissionsModalTitle = getPermissionsModalTitle({
scenario: 'pks',
role: roleName,
primaryKeyColumns: tableColumns
?.filter(column => column.isPrimaryKey)
?.map(column => column.name)
?.join(','),
});
const permissionsModalDescription = getPermissionsModalDescription('pks');
return (
<>
<Collapse defaultOpen={!disabled}>
<Collapse.Header
title={`Column ${queryType} permissions`}
tooltip={`Choose columns allowed to be ${getEdForm(queryType)}`}
status={status}
disabled={disabled}
disabledMessage="Set row permissions first"
/>
<Collapse.Content>
<div
title={disabled ? 'Set row permissions first' : ''}
className="grid gap-2"
>
<div className="flex gap-2 items-center">
<p>
Allow role <strong>{roleName}</strong>{' '}
{getAccessText(queryType)}
&nbsp;
<strong>columns</strong>:
</p>
</div>
<fieldset className="flex gap-4 flex-wrap">
{columns?.map(fieldName => (
<label key={fieldName} className="flex gap-2 items-center">
<input
type="checkbox"
title={disabled ? 'Set a row permission first' : ''}
disabled={disabled}
style={{ marginTop: '0px !important' }}
className="rounded shadow-sm border border-gray-300 hover:border-gray-400 focus:ring-yellow-400"
checked={selectedColumns[fieldName]}
onChange={() => {
const hideModal = isPermissionModalDisabled();
if (
!hideModal &&
!showConfirmation &&
checkIfConfirmationIsNeeded(
fieldName,
tableColumns,
selectedColumns,
queryRootFields,
subscriptionRootFields
)
) {
setShowConfirmationModal(fieldName);
return;
}
setValue(
`columns.${fieldName}`,
!selectedColumns[fieldName]
);
}}
/>
<i>{fieldName}</i>
</label>
))}
<Button
type="button"
size="sm"
title={disabled ? 'Set a row permission first' : ''}
disabled={disabled}
onClick={onClick}
data-test="toggle-all-col-btn"
>
Toggle All
</Button>
</fieldset>
</div>
{/* {getExternalTablePermissionsMsg()} */}
</Collapse.Content>
</Collapse>
{showConfirmation && (
<PermissionsConfirmationModal
title={permissionsModalTitle}
description={permissionsModalDescription}
onClose={() => setShowConfirmationModal(null)}
onSubmit={() => {
handleUpdate(showConfirmation);
setShowConfirmationModal(null);
}}
/>
)}
</>
);
};

View File

@ -0,0 +1,24 @@
import React from 'react';
import { ComponentMeta, Story } from '@storybook/react';
import {
PermissionsConfirmationModal,
Props,
} from './PermissionsConfirmationModal';
export default {
title: 'Features/Permissions/Confirmation Modal',
component: PermissionsConfirmationModal,
argTypes: {
onSubmit: { action: true },
onClose: { action: true },
},
} as ComponentMeta<typeof PermissionsConfirmationModal>;
export const Base: Story<Props> = args => (
<PermissionsConfirmationModal {...args} />
);
Base.args = {
title: <>title</>,
description: <>description</>,
};

View File

@ -0,0 +1,83 @@
import React from 'react';
import { Analytics, REDACT_EVERYTHING } from '@/features/Analytics';
import { FaExclamationTriangle } from 'react-icons/fa';
import { Button } from '@/new-components/Button';
import { Dialog } from '@/new-components/Dialog';
import { LS_KEYS, setLSItem } from '@/utils/localStorage';
import { Checkbox } from '@/new-components/Form';
type CustomDialogFooterProps = {
onSubmit: () => void;
onClose: () => void;
};
const CustomDialogFooter: React.FC<CustomDialogFooterProps> = ({
onClose,
onSubmit,
}) => {
const storeDoNotShowPermissionsDialogFlag = (enabled: string | boolean) => {
setLSItem(
LS_KEYS.permissionConfirmationModalStatus,
enabled ? 'disabled' : 'enabled'
);
};
return (
<div className="flex items-center border-t border-gray-300 bg-white p-sm">
<div className="flex-grow">
<Checkbox
name="noPermissionsConfirmationDialog"
onCheckedChange={enabled => {
storeDoNotShowPermissionsDialogFlag(enabled);
}}
>
<div>Don&apos;t ask me again</div>
</Checkbox>
</div>
<div className="flex">
<Button onClick={onClose}>Cancel</Button>
<div className="ml-2">
<Button mode="primary" onClick={onSubmit}>
Disable
</Button>
</div>
</div>
</div>
);
};
export type Props = {
onSubmit: () => void;
onClose: () => void;
title: React.ReactElement;
description: React.ReactElement;
};
export const PermissionsConfirmationModal: React.FC<Props> = ({
onSubmit,
onClose,
title,
description,
}) => {
return (
<Dialog
title=""
hasBackdrop
footer={<CustomDialogFooter onSubmit={onSubmit} onClose={onClose} />}
>
<Analytics name="PermissionsConfirmationModal" {...REDACT_EVERYTHING}>
<div className="flex items-top p-md">
<div className="text-yellow-500">
<FaExclamationTriangle className="w-9 h-9 mr-md fill-current" />
</div>
<div>
<p className="font-semibold">{title}</p>
<div className="overflow-y-auto max-h-[calc(100vh-14rem)]">
<p className="m-0">{description}</p>
</div>
</div>
</div>
</Analytics>
</Dialog>
);
};

View File

@ -0,0 +1,44 @@
import React from 'react';
import { LS_KEYS, getLSItem } from '@/utils/localStorage';
type Scenario = 'aggregate' | 'pk' | 'pks';
type GetPermissionsModalTitleArgs = {
scenario: Scenario;
role: string;
primaryKeyColumns?: string;
};
export const getPermissionsModalTitle = ({
scenario,
role,
primaryKeyColumns,
}: GetPermissionsModalTitleArgs) =>
scenario === 'aggregate' ? (
<>Are you sure you want to remove aggregation queries for role {role}?</>
) : (
<>
Are you sure you want to disable the access to column(s){' '}
<span className="font-mono italic">{primaryKeyColumns}</span> for role{' '}
{role}?
</>
);
export const getPermissionsModalDescription = (scenario: Scenario) =>
scenario === 'aggregate' ? (
<>
<span className="font-mono italic">select_aggregate</span> will be
disabled in GraphQL root field visibility since the permission is removed.
</>
) : (
<>
<span className="font-mono italic">select_by_pk</span> will be disabled in
GraphQL root field visibility since the primary key is disabled.
</>
);
export const getPermissionModalEnabled = () => {
const status = getLSItem(LS_KEYS.permissionConfirmationModalStatus);
const isEnabled = !status || status === 'enabled';
return isEnabled;
};

View File

@ -0,0 +1,201 @@
import React from 'react';
import { useFormContext } from 'react-hook-form';
import clsx from 'clsx';
import { OverlayTrigger } from 'react-bootstrap';
import { FaQuestionCircle } from 'react-icons/fa';
import { Collapse } from '@/new-components/deprecated';
import { Switch } from '@/new-components/Switch';
import { Tooltip } from '@/new-components/Tooltip';
import { Table } from '@/features/hasura-metadata-types';
import { useListAllTableColumns } from '@/features/Data';
import {
getSectionStatusLabel,
hasSelectedPrimaryKey as hasSelectedPrimaryKeyFinder,
} from './utils';
import { SelectPermissionsRow } from './SelectPermissionsRow';
import {
QueryRootPermissionType,
SubscriptionRootPermissionType,
PermissionRootTypes,
} from './types';
import { useRootFieldPermissions } from './hooks/useRootFieldPermissions';
import { useSourceSupportStreaming } from '../../hooks/useSourceSupportStreaming';
export type RootKeyValues = 'query_root_fields' | 'subscription_root_fields';
export const QUERY_ROOT_VALUES = 'query_root_fields';
export const SUBSCRIPTION_ROOT_VALUES = 'subscription_root_fields';
export const queryRootPermissionFields: QueryRootPermissionType[] = [
'select',
'select_by_pk',
'select_aggregate',
];
export const subscriptionRootPermissionFields: SubscriptionRootPermissionType[] =
['select', 'select_by_pk', 'select_aggregate', 'select_stream'];
const QueryRootFieldDescription = () => (
<div>
Allow the following root fields under the <b>Query root field</b>
</div>
);
const SubscriptionRootFieldDescription = () => (
<div>
Allow the following root fields under the <b>Subscription root field</b>
</div>
);
export interface ColumnPermissionsSectionProps {
columns?: string[];
filterType: string;
table: Table;
dataSourceName: string;
}
export const ColumnRootFieldPermissions: React.FC<ColumnPermissionsSectionProps> =
({ dataSourceName, table, filterType }) => {
const { watch, setValue } = useFormContext();
const [
hasEnabledAggregations,
selectedColumns,
queryRootFields,
subscriptionRootFields,
] = watch([
'aggregationEnabled',
'columns',
'query_root_fields',
'subscription_root_fields',
]);
console.log('table', table);
const disabled = filterType === 'none';
const { columns: tableColumns } = useListAllTableColumns(
dataSourceName,
table
);
const hasSelectedPrimaryKeys = hasSelectedPrimaryKeyFinder(
selectedColumns,
tableColumns
);
const updateFormValues = (
key: RootKeyValues,
value: PermissionRootTypes
) => {
setValue(key, value);
};
const rootFieldPermissions = useRootFieldPermissions({
queryRootFields,
subscriptionRootFields,
hasEnabledAggregations,
hasSelectedPrimaryKeys,
updateFormValues,
});
const {
isSubscriptionStreamingEnabled,
onEnableSectionSwitchChange,
onToggleAll,
isRootPermissionsSwitchedOn,
onUpdatePermission,
} = rootFieldPermissions;
const supportsStreaming = useSourceSupportStreaming(dataSourceName);
const getFilteredSubscriptionRootPermissionFields = (
fields: SubscriptionRootPermissionType[]
) => {
if (!supportsStreaming)
return fields.filter(field => field !== 'select_stream');
return fields;
};
const bodyTitle = disabled ? 'Set row permissions first' : '';
return (
<Collapse defaultOpen={!disabled}>
<Collapse.Header
title="Root field permissions"
tooltip="Choose root fields to be added under the query and subscription root fields."
status={getSectionStatusLabel({
queryRootPermissions: queryRootFields,
subscriptionRootPermissions: subscriptionRootFields,
hasEnabledAggregations,
hasSelectedPrimaryKeys,
isSubscriptionStreamingEnabled,
})}
disabledMessage="Set row permissions first"
/>
<Collapse.Content>
<div title={bodyTitle}>
<div
className={`px-md mb-xs flex items-center ${clsx(
disabled && `opacity-70 pointer-events-none`
)}`}
>
<Switch
checked={isRootPermissionsSwitchedOn}
onCheckedChange={onEnableSectionSwitchChange}
/>
<div className="mx-xs">
Enable GraphQL root field visibility customization.
</div>
<OverlayTrigger
placement="right"
overlay={
<Tooltip tooltipContentChildren>
By enabling this you can customize the root field
permissions. When this switch is turned off, all values are
enabled by default.
</Tooltip>
}
>
<FaQuestionCircle aria-hidden="true" />
</OverlayTrigger>
</div>
<div
className={`px-md ${clsx(
!isRootPermissionsSwitchedOn && 'hidden'
)}`}
>
<SelectPermissionsRow
currentPermissions={queryRootFields}
description={<QueryRootFieldDescription />}
hasEnabledAggregations={hasEnabledAggregations}
hasSelectedPrimaryKeys={hasSelectedPrimaryKeys}
isSubscriptionStreamingEnabled={isSubscriptionStreamingEnabled}
permissionFields={queryRootPermissionFields}
permissionType={QUERY_ROOT_VALUES}
onToggleAll={() =>
onToggleAll(QUERY_ROOT_VALUES, queryRootFields)
}
onUpdate={onUpdatePermission}
/>
<SelectPermissionsRow
currentPermissions={subscriptionRootFields}
description={<SubscriptionRootFieldDescription />}
hasEnabledAggregations={hasEnabledAggregations}
hasSelectedPrimaryKeys={hasSelectedPrimaryKeys}
isSubscriptionStreamingEnabled={isSubscriptionStreamingEnabled}
permissionFields={getFilteredSubscriptionRootPermissionFields(
subscriptionRootPermissionFields
)}
permissionType={SUBSCRIPTION_ROOT_VALUES}
onToggleAll={() =>
onToggleAll(SUBSCRIPTION_ROOT_VALUES, subscriptionRootFields)
}
onUpdate={onUpdatePermission}
/>
</div>
</div>
</Collapse.Content>
</Collapse>
);
};
export default ColumnRootFieldPermissions;

View File

@ -0,0 +1,34 @@
import React from 'react';
import { PermissionRootType } from './types';
type Props = {
permission: PermissionRootType;
onPermissionChange: (value: PermissionRootType) => void;
disabled?: boolean;
checked: boolean;
title?: string;
};
export const SelectPermissionFields: React.FC<Props> = ({
permission,
onPermissionChange,
disabled = false,
checked = true,
title,
}) => (
<div className="mr-sm">
<div className="checkbox">
<label title={title}>
<input
type="checkbox"
className="legacy-input-fix disabled:bg-gray-100 disabled:cursor-not-allowed"
checked={checked}
value={permission}
onChange={() => onPermissionChange(permission)}
disabled={disabled}
/>
<i>{permission}</i>
</label>
</div>
</div>
);

View File

@ -0,0 +1,19 @@
import React from 'react';
import { Button } from '@/new-components/Button';
type Props = {
onToggle: () => void;
text: React.ReactElement;
};
export const SelectPermissionSectionHeader: React.FC<Props> = ({
text,
onToggle,
}) => (
<div className="flex items-center">
<span className="mr-sm ">{text}</span>
<Button size="sm" onClick={onToggle} data-test="toggle-all-col-btn">
<div className="font-semibold">Toggle All</div>
</Button>
</div>
);

View File

@ -0,0 +1,75 @@
import React from 'react';
import { SelectPermissionFields } from './SelectPermissionFields';
import { SelectPermissionSectionHeader } from './SelectPermissionSectionHeader';
import { getPermissionCheckboxState } from './utils';
import { RootKeyValues } from './RootFieldPermissions';
import {
PermissionRootType,
PermissionRootTypes,
CombinedPermissionRootTypes,
} from './types';
type Props = {
onToggleAll: () => void;
permissionFields: PermissionRootTypes;
currentPermissions: CombinedPermissionRootTypes;
onUpdate: (
key: RootKeyValues,
value: PermissionRootType,
permissions: CombinedPermissionRootTypes
) => void;
description: React.ReactElement;
hasEnabledAggregations: boolean;
hasSelectedPrimaryKeys: boolean;
permissionType: RootKeyValues;
isSubscriptionStreamingEnabled: boolean;
};
export const SelectPermissionsRow: React.FC<Props> = ({
onToggleAll,
permissionFields,
currentPermissions,
onUpdate,
description,
hasEnabledAggregations,
hasSelectedPrimaryKeys,
permissionType,
isSubscriptionStreamingEnabled,
}) => {
return (
<div>
<SelectPermissionSectionHeader
onToggle={onToggleAll}
text={description}
/>
<div className="px-md flex">
{permissionFields?.map(permission => {
const permissionCheckboxState = getPermissionCheckboxState({
permission,
hasEnabledAggregations,
hasSelectedPrimaryKeys,
isSubscriptionStreamingEnabled,
rootPermissions: currentPermissions,
});
return (
<SelectPermissionFields
key={permission}
title={
currentPermissions === null
? 'Set row permissions first'
: permissionCheckboxState.title
}
disabled={permissionCheckboxState.disabled}
checked={permissionCheckboxState.checked}
onPermissionChange={(value: PermissionRootType) => {
onUpdate(permissionType, value, currentPermissions);
}}
permission={permission}
/>
);
})}
</div>
</div>
);
};

View File

@ -0,0 +1,117 @@
import { useServerConfig } from '@/hooks';
import {
RootKeyValues,
SUBSCRIPTION_ROOT_VALUES,
QUERY_ROOT_VALUES,
subscriptionRootPermissionFields,
queryRootPermissionFields,
} from '../RootFieldPermissions';
import {
PermissionRootType,
RootFieldPermissionsType,
SubscriptionRootPermissionTypes,
PermissionRootTypes,
CombinedPermissionRootTypes,
} from '../types';
type Props = RootFieldPermissionsType;
export const useRootFieldPermissions = ({
queryRootFields,
subscriptionRootFields,
hasEnabledAggregations,
hasSelectedPrimaryKeys,
updateFormValues,
}: Props) => {
const { data: configData } = useServerConfig();
const isSubscriptionStreamingEnabled =
!!configData?.experimental_features.includes('streaming_subscriptions');
const isRootPermissionsSwitchedOn =
queryRootFields !== null && subscriptionRootFields !== null;
const onUpdatePermission = (
key: RootKeyValues,
permission: PermissionRootType,
currentPermissionArray: CombinedPermissionRootTypes
) => {
const containsString = currentPermissionArray?.includes(permission);
if (containsString || !currentPermissionArray) {
const newPermissionArray = currentPermissionArray?.filter(
(queryPermission: string) => queryPermission !== permission
);
if (newPermissionArray) updateFormValues(key, newPermissionArray);
return;
}
updateFormValues(
key,
[...currentPermissionArray, permission].filter(Boolean)
);
};
const onEnableSection = (
key: RootKeyValues,
permissionTypeFields: PermissionRootTypes
) => {
if (permissionTypeFields === null) return;
let newState = permissionTypeFields;
if (!hasEnabledAggregations) {
newState = newState.filter(
(permission: string) => permission !== 'select_aggregate'
);
}
if (!hasSelectedPrimaryKeys) {
newState = newState.filter(
(permission: string) => permission !== 'select_by_pk'
);
}
if (key === SUBSCRIPTION_ROOT_VALUES && !isSubscriptionStreamingEnabled) {
newState = newState.filter(
(permission: string) => permission !== 'select_stream'
);
}
updateFormValues(key, newState);
};
const onToggleAll = (
key: RootKeyValues,
currentPermissionArray: PermissionRootTypes
) => {
if (currentPermissionArray && currentPermissionArray?.length > 0) {
return updateFormValues(key, []);
}
const toToggle: SubscriptionRootPermissionTypes = ['select'];
if (key === SUBSCRIPTION_ROOT_VALUES && isSubscriptionStreamingEnabled) {
toToggle.push('select_stream');
}
if (hasEnabledAggregations) toToggle.push('select_aggregate');
if (hasSelectedPrimaryKeys) toToggle.push('select_by_pk');
updateFormValues(key, toToggle);
};
const onEnableSectionSwitchChange = () => {
if (isRootPermissionsSwitchedOn) {
updateFormValues(SUBSCRIPTION_ROOT_VALUES, null);
updateFormValues(QUERY_ROOT_VALUES, null);
return;
}
onEnableSection(SUBSCRIPTION_ROOT_VALUES, subscriptionRootPermissionFields);
onEnableSection(QUERY_ROOT_VALUES, queryRootPermissionFields);
};
return {
isSubscriptionStreamingEnabled,
onEnableSectionSwitchChange,
onToggleAll,
onUpdatePermission,
isRootPermissionsSwitchedOn,
};
};

View File

@ -0,0 +1,34 @@
import { RootKeyValues } from './RootFieldPermissions';
export type QueryRootPermissionType =
| 'select'
| 'select_by_pk'
| 'select_aggregate';
export type SubscriptionRootPermissionType =
| 'select'
| 'select_by_pk'
| 'select_aggregate'
| 'select_stream';
export type QueryRootPermissionTypes = QueryRootPermissionType[] | null;
export type SubscriptionRootPermissionTypes =
| SubscriptionRootPermissionType[]
| null;
export type PermissionRootType =
| QueryRootPermissionType
| SubscriptionRootPermissionType;
export type PermissionRootTypes =
| QueryRootPermissionTypes
| SubscriptionRootPermissionTypes;
export type CombinedPermissionRootTypes = QueryRootPermissionTypes &
SubscriptionRootPermissionTypes;
export type RootFieldPermissionsType = {
hasEnabledAggregations: boolean;
hasSelectedPrimaryKeys: boolean;
queryRootFields: QueryRootPermissionTypes;
subscriptionRootFields: SubscriptionRootPermissionTypes;
updateFormValues: (key: RootKeyValues, value: PermissionRootTypes) => void;
};

View File

@ -0,0 +1,307 @@
import { TableColumn } from '@/features/DataSource';
import {
getSectionStatusLabel,
SectionLabelProps,
getPermissionCheckboxState,
PermissionCheckboxStateArg,
getSelectByPkCheckboxState,
getSelectStreamCheckboxState,
getSelectAggregateCheckboxState,
hasSelectedPrimaryKey,
} from './utils';
describe('hasSelectedPrimaryKey', () => {
describe('when pk is not selected', () => {
it('it returns false', () => {
const tableColumns = [
{ name: 'AlbumId', isPrimaryKey: true },
] as TableColumn[];
expect(hasSelectedPrimaryKey({ AlbumId: false }, tableColumns)).toEqual(
false
);
});
});
describe('when pk is selected', () => {
it('it returns true', () => {
const tableColumns = [
{ name: 'AlbumId', isPrimaryKey: true },
] as TableColumn[];
expect(hasSelectedPrimaryKey({ AlbumId: true }, tableColumns)).toEqual(
true
);
});
});
});
describe('getSectionStatusLabel', () => {
describe('when permissions are null', () => {
it('returns "all enabled"', () => {
const args: SectionLabelProps = {
subscriptionRootPermissions: null,
queryRootPermissions: null,
hasEnabledAggregations: false,
hasSelectedPrimaryKeys: false,
isSubscriptionStreamingEnabled: false,
};
expect(getSectionStatusLabel(args)).toEqual(' - all enabled');
});
});
describe('when permissions are empty', () => {
it('returns "all disabled"', () => {
const args: SectionLabelProps = {
subscriptionRootPermissions: [],
queryRootPermissions: [],
hasEnabledAggregations: false,
hasSelectedPrimaryKeys: false,
isSubscriptionStreamingEnabled: false,
};
expect(getSectionStatusLabel(args)).toEqual(' - all disabled');
});
});
describe('when permissions are non empty', () => {
it('returns "partially disabled"', () => {
const args: SectionLabelProps = {
subscriptionRootPermissions: [
'select',
'select_by_pk',
'select_aggregate',
'select_stream',
],
queryRootPermissions: [],
hasEnabledAggregations: false,
hasSelectedPrimaryKeys: false,
isSubscriptionStreamingEnabled: false,
};
expect(getSectionStatusLabel(args)).toEqual(' - partially enabled');
});
});
describe('when permissions are all selected', () => {
it('returns "all enabled"', () => {
const args: SectionLabelProps = {
subscriptionRootPermissions: [
'select',
'select_by_pk',
'select_aggregate',
'select_stream',
],
queryRootPermissions: ['select', 'select_by_pk', 'select_aggregate'],
hasEnabledAggregations: true,
hasSelectedPrimaryKeys: true,
isSubscriptionStreamingEnabled: true,
};
expect(getSectionStatusLabel(args)).toEqual(' - all enabled');
});
});
describe('when aggregations are not selected', () => {
describe('when permissions are all selected', () => {
it('returns "all enabled"', () => {
const args: SectionLabelProps = {
subscriptionRootPermissions: [
'select',
'select_by_pk',
'select_stream',
],
queryRootPermissions: ['select', 'select_by_pk'],
hasEnabledAggregations: false,
hasSelectedPrimaryKeys: true,
isSubscriptionStreamingEnabled: true,
};
expect(getSectionStatusLabel(args)).toEqual(' - all enabled');
});
});
describe('when some permissions are selected', () => {
it('returns "partially enabled"', () => {
const args: SectionLabelProps = {
subscriptionRootPermissions: ['select'],
queryRootPermissions: ['select'],
hasEnabledAggregations: false,
hasSelectedPrimaryKeys: true,
isSubscriptionStreamingEnabled: true,
};
expect(getSectionStatusLabel(args)).toEqual(' - partially enabled');
});
});
});
describe('when primary keys are not selected', () => {
describe('when permissions are all selected', () => {
it('returns "all enabled"', () => {
const args: SectionLabelProps = {
subscriptionRootPermissions: [
'select',
'select_aggregate',
'select_stream',
],
queryRootPermissions: ['select', 'select_aggregate'],
hasEnabledAggregations: true,
hasSelectedPrimaryKeys: false,
isSubscriptionStreamingEnabled: true,
};
expect(getSectionStatusLabel(args)).toEqual(' - all enabled');
});
});
describe('when some permissions are selected', () => {
it('returns "partially enabled"', () => {
const args: SectionLabelProps = {
subscriptionRootPermissions: ['select'],
queryRootPermissions: ['select'],
hasEnabledAggregations: false,
hasSelectedPrimaryKeys: false,
isSubscriptionStreamingEnabled: true,
};
expect(getSectionStatusLabel(args)).toEqual(' - partially enabled');
});
});
});
describe('when subscription streaming is not selected', () => {
describe('when permissions are all selected', () => {
it('returns "all enabled"', () => {
const args: SectionLabelProps = {
subscriptionRootPermissions: [
'select',
'select_by_pk',
'select_aggregate',
],
queryRootPermissions: ['select', 'select_by_pk', 'select_aggregate'],
hasEnabledAggregations: true,
hasSelectedPrimaryKeys: true,
isSubscriptionStreamingEnabled: false,
};
expect(getSectionStatusLabel(args)).toEqual(' - all enabled');
});
});
describe('when some permissions are selected', () => {
it('returns "partially enabled"', () => {
const args: SectionLabelProps = {
subscriptionRootPermissions: ['select', 'select_by_pk'],
queryRootPermissions: ['select', 'select_by_pk'],
hasEnabledAggregations: true,
hasSelectedPrimaryKeys: true,
isSubscriptionStreamingEnabled: false,
};
expect(getSectionStatusLabel(args)).toEqual(' - partially enabled');
});
});
});
});
describe('getPermissionCheckboxState', () => {
describe('root permissions is null', () => {
it('returns disabled and checked', () => {
const args: PermissionCheckboxStateArg = {
permission: '',
hasEnabledAggregations: false,
hasSelectedPrimaryKeys: false,
isSubscriptionStreamingEnabled: false,
rootPermissions: null,
};
expect(getPermissionCheckboxState(args)).toEqual({
disabled: true,
checked: true,
});
});
});
describe('when permission is select', () => {
describe('when permission is in root permissions', () => {
it('returns default state', () => {
const args: PermissionCheckboxStateArg = {
rootPermissions: ['select'],
permission: 'select',
hasEnabledAggregations: false,
hasSelectedPrimaryKeys: false,
isSubscriptionStreamingEnabled: false,
};
expect(getPermissionCheckboxState(args)).toEqual({
disabled: false,
checked: true,
});
});
});
});
describe('when permission is NOT in root permissions', () => {
it('returns default state', () => {
const args: PermissionCheckboxStateArg = {
rootPermissions: ['select_by_pk'],
permission: 'select',
hasEnabledAggregations: false,
hasSelectedPrimaryKeys: false,
isSubscriptionStreamingEnabled: false,
};
expect(getPermissionCheckboxState(args)).toEqual({
disabled: false,
checked: false,
});
});
});
});
describe('getSelectByPkCheckboxState', () => {
it.each`
rootPermissions | permission | hasSelectedPrimaryKeys | expected
${['select_by_pk']} | ${'select_by_pk'} | ${false} | ${{ checked: false, disabled: true, title: 'Allow access to the table primary key column(s) first' }}
${['select_by_pk']} | ${'select_by_pk'} | ${true} | ${{ checked: true, disabled: false, title: '' }}
${['select']} | ${'select_by_pk'} | ${true} | ${{ checked: false, disabled: false, title: '' }}
`(
'returns the select_by_pk checkbox state for rootPermissions $rootPermissions, permission $permission, hasSelectedPrimaryKeys $hasSelectedPrimaryKeys',
({ rootPermissions, permission, hasSelectedPrimaryKeys, expected }) => {
expect(
getSelectByPkCheckboxState({
hasSelectedPrimaryKeys,
rootPermissions,
permission,
})
).toEqual(expected);
}
);
});
describe('getSelectStreamCheckboxState', () => {
it.each`
rootPermissions | permission | isSubscriptionStreamingEnabled | expected
${['select_stream']} | ${'select_stream'} | ${false} | ${{ checked: true, disabled: true, title: 'Enable the streaming subscriptions experimental feature first' }}
${['select_stream']} | ${'select_by_pk'} | ${false} | ${{ checked: false, disabled: true, title: 'Enable the streaming subscriptions experimental feature first' }}
${['select_stream']} | ${'select_by_pk'} | ${true} | ${{ checked: false, disabled: false, title: '' }}
`(
'returns the select_stream checkbox state for rootPermissions $rootPermissions, permission $permission, isSubscriptionStreamingEnabled $isSubscriptionStreamingEnabled',
({
rootPermissions,
permission,
isSubscriptionStreamingEnabled,
expected,
}) => {
expect(
getSelectStreamCheckboxState({
isSubscriptionStreamingEnabled,
rootPermissions,
permission,
})
).toEqual(expected);
}
);
});
describe('getSelectAggregateCheckboxState', () => {
it.each`
rootPermissions | permission | hasEnabledAggregations | expected
${['select_stream']} | ${'select_stream'} | ${false} | ${{ checked: false, disabled: true, title: 'Enable aggregation queries permissions first' }}
${['select_stream']} | ${'select_stream'} | ${true} | ${{ checked: true, disabled: false, title: '' }}
${['select_stream']} | ${'select'} | ${true} | ${{ checked: false, disabled: false, title: '' }}
`(
'returns the select_stream checkbox state for rootPermissions $rootPermissions, permission $permission, hasEnabledAggregations $hasEnabledAggregations',
({ rootPermissions, permission, hasEnabledAggregations, expected }) => {
expect(
getSelectAggregateCheckboxState({
hasEnabledAggregations,
rootPermissions,
permission,
})
).toEqual(expected);
}
);
});

View File

@ -0,0 +1,198 @@
import { TableColumn } from '@/features/DataSource';
import {
queryRootPermissionFields,
subscriptionRootPermissionFields,
} from './RootFieldPermissions';
import {
QueryRootPermissionTypes,
SubscriptionRootPermissionTypes,
} from './types';
export type SectionLabelProps = {
subscriptionRootPermissions: SubscriptionRootPermissionTypes;
queryRootPermissions: QueryRootPermissionTypes;
hasEnabledAggregations: boolean;
hasSelectedPrimaryKeys: boolean;
isSubscriptionStreamingEnabled: boolean | undefined;
};
export const getSectionStatusLabel = ({
subscriptionRootPermissions,
queryRootPermissions,
hasEnabledAggregations,
hasSelectedPrimaryKeys,
isSubscriptionStreamingEnabled,
}: SectionLabelProps) => {
if (subscriptionRootPermissions === null && queryRootPermissions === null)
return ' - all enabled';
if (
subscriptionRootPermissions?.length === 0 &&
queryRootPermissions?.length === 0
)
return ' - all disabled';
let currentAmountOfAvailablePermission =
queryRootPermissionFields.length + subscriptionRootPermissionFields.length;
if (!hasEnabledAggregations) {
// exists on both query and subscription
currentAmountOfAvailablePermission -= 2;
}
if (!hasSelectedPrimaryKeys) {
// exists on both query and subscription
currentAmountOfAvailablePermission -= 2;
}
if (!isSubscriptionStreamingEnabled) {
// exists only on subscription
currentAmountOfAvailablePermission -= 1;
}
const amountOfSelectedPermissions =
(queryRootPermissions?.length || 0) +
(subscriptionRootPermissions?.length || 0);
if (currentAmountOfAvailablePermission === amountOfSelectedPermissions) {
return ' - all enabled';
}
return ' - partially enabled';
};
type CheckboxPermissionStateProps = {
checked: boolean;
disabled: boolean;
title?: string;
};
export type PermissionCheckboxStateArg = {
permission: string;
hasEnabledAggregations: boolean;
hasSelectedPrimaryKeys: boolean;
isSubscriptionStreamingEnabled: boolean | undefined;
rootPermissions: string[] | null;
};
type SelectByPkCheckboxStateArgs = {
hasSelectedPrimaryKeys: boolean;
rootPermissions: string[];
permission: string;
};
export const getSelectByPkCheckboxState = ({
hasSelectedPrimaryKeys,
rootPermissions,
permission,
}: SelectByPkCheckboxStateArgs): CheckboxPermissionStateProps => {
const getPkCheckedState = () => {
if (!hasSelectedPrimaryKeys) return false;
if (rootPermissions?.includes(permission)) return true;
return false;
};
return {
checked: getPkCheckedState(),
disabled: !hasSelectedPrimaryKeys,
title: !hasSelectedPrimaryKeys
? 'Allow access to the table primary key column(s) first'
: '',
};
};
type SelectStreamCheckboxStateArg = {
rootPermissions: string[];
permission: string;
isSubscriptionStreamingEnabled: boolean | undefined;
};
export const getSelectStreamCheckboxState = ({
rootPermissions,
permission,
isSubscriptionStreamingEnabled,
}: SelectStreamCheckboxStateArg): CheckboxPermissionStateProps => ({
checked: rootPermissions?.includes(permission),
disabled: !isSubscriptionStreamingEnabled,
title: !isSubscriptionStreamingEnabled
? 'Enable the streaming subscriptions experimental feature first'
: '',
});
type SelectAggregateCheckboxStateArg = {
hasEnabledAggregations: boolean;
rootPermissions: string[];
permission: string;
};
export const getSelectAggregateCheckboxState = ({
hasEnabledAggregations,
rootPermissions,
permission,
}: SelectAggregateCheckboxStateArg) => {
const getAggregationCheckedState = () => {
if (!hasEnabledAggregations) return false;
if (rootPermissions?.includes(permission)) return true;
return false;
};
return {
checked: getAggregationCheckedState(),
disabled: !hasEnabledAggregations,
title: !hasEnabledAggregations
? 'Enable aggregation queries permissions first'
: '',
};
};
export const getPermissionCheckboxState = ({
permission,
hasEnabledAggregations,
hasSelectedPrimaryKeys,
isSubscriptionStreamingEnabled,
rootPermissions,
}: PermissionCheckboxStateArg): CheckboxPermissionStateProps => {
if (rootPermissions === null)
return {
disabled: true,
checked: true,
};
switch (permission) {
case 'select_by_pk':
return getSelectByPkCheckboxState({
hasSelectedPrimaryKeys,
rootPermissions,
permission,
});
case 'select_stream':
return getSelectStreamCheckboxState({
rootPermissions,
permission,
isSubscriptionStreamingEnabled,
});
case 'select_aggregate':
return getSelectAggregateCheckboxState({
hasEnabledAggregations,
rootPermissions,
permission,
});
default:
return {
disabled: false,
checked: rootPermissions?.includes(permission),
};
}
};
export const hasSelectedPrimaryKey = (
selectedColumns: Record<string, boolean | undefined>,
columns: TableColumn[]
) => {
return !!columns.find(column => {
const isPrimaryKey = column.isPrimaryKey;
const colName = column.name;
const hasPickedColumn = selectedColumns[colName];
return hasPickedColumn && isPrimaryKey;
});
};

View File

@ -151,6 +151,7 @@ export const createPermission = {
const backendOnly: boolean = permission?.backend_only || false;
return {
// Needs to be cast to const for the zod schema to accept it as a literal for the discriminated union
queryType: 'insert' as const,
check,
checkType,
@ -182,6 +183,7 @@ export const createPermission = {
const aggregationEnabled: boolean = permission?.allow_aggregations || false;
const selectPermissions = {
// Needs to be cast to const for the zod schema to accept it as a literal for the discriminated union
queryType: 'select' as const,
filter,
filterType,
@ -189,6 +191,8 @@ export const createPermission = {
rowCount,
aggregationEnabled,
operators,
query_root_fields: permission.query_root_fields || null,
subscription_root_fields: permission.subscription_root_fields || null,
};
if (rowCount) {
@ -214,6 +218,7 @@ export const createPermission = {
});
return {
// Needs to be cast to const for the zod schema to accept it as a literal for the discriminated union
queryType: 'update' as const,
check,
checkType,
@ -236,6 +241,7 @@ export const createPermission = {
});
return {
// Needs to be cast to const for the zod schema to accept it as a literal for the discriminated union
queryType: 'delete' as const,
filter,
filterType,

View File

@ -36,6 +36,7 @@ const metadata: Metadata = {
},
allow_aggregations: true,
limit: 3,
subscription_root_fields: ['select', 'select_by_pk'],
},
},
],

View File

@ -35,11 +35,14 @@ const defaultValuesMockResult: ReturnType<typeof createDefaultValues> = {
typeName: 'ArtistId',
},
},
query_root_fields: null,
subscription_root_fields: ['select', 'select_by_pk'],
queryType: 'select',
rowCount: '3',
};
test('use default values returns values correctly', () => {
const result = createDefaultValues(defaultValuesInput);
expect(result).toEqual(defaultValuesMockResult);
});

View File

@ -0,0 +1,12 @@
import { useMetadataSource } from '@/features/MetadataAPI';
export const sourcesSupportingStreaming = ['postgres', 'mssql'];
export const useSourceSupportStreaming = (databaseName: string) => {
const { data: sourceMetadata } = useMetadataSource(databaseName);
const kind = sourceMetadata?.kind;
if (!kind) return false;
return sourcesSupportingStreaming.includes(kind);
};

View File

@ -0,0 +1,4 @@
import { LS_KEYS, getLSItem } from '@/utils/localStorage';
export const isPermissionModalDisabled = () =>
getLSItem(LS_KEYS.permissionConfirmationModalStatus) === 'disabled';

View File

@ -31,6 +31,8 @@ export const schema = z.discriminatedUnion('queryType', [
rowCount: z.string().optional(),
aggregationEnabled: z.boolean().optional(),
clonePermissions: z.array(z.any()).optional(),
query_root_fields: z.array(z.string()).nullable().optional(),
subscription_root_fields: z.array(z.string()).nullable().optional(),
}),
z.object({
queryType: z.literal('update'),

View File

@ -25,8 +25,8 @@ export interface SelectPermissionDefinition {
columns?: string[];
filter?: Record<string, unknown>;
allow_aggregations?: boolean;
query_root_fields?: string[];
subscription_root_fields?: string[];
query_root_fields?: string[] | null;
subscription_root_fields?: string[] | null;
limit?: number;
}

View File

@ -6,7 +6,7 @@ import {
} from './PermissionsConfirmationModal';
export default {
title: 'Features/Permissions Form/Permissions Confirmation Modal',
title: 'Features/Table Permissions/Permissions Confirmation Modal',
component: PermissionsConfirmationModal,
argTypes: {
onSubmit: { action: true },