feat(client): improve acl module

This commit is contained in:
chenos 2022-02-26 23:03:58 +08:00
parent 6f2069e918
commit d1ac62ddaf
10 changed files with 310 additions and 47 deletions

View File

@ -3,6 +3,8 @@ export interface AvailableActionOptions {
type: 'new-data' | 'old-data'; type: 'new-data' | 'old-data';
displayName?: string; displayName?: string;
resource?: string; resource?: string;
onNewRecord?: boolean;
allowConfigureFields?: boolean;
} }
export class AclAvailableAction { export class AclAvailableAction {

View File

@ -47,7 +47,7 @@ export const RoleConfigure = () => {
'x-component': 'Checkbox', 'x-component': 'Checkbox',
'x-content': '允许配置系统,包括界面配置、数据表配置、权限配置、系统配置等全部配置项', 'x-content': '允许配置系统,包括界面配置、数据表配置、权限配置、系统配置等全部配置项',
}, },
strategy: { 'strategy.actions': {
title: '通用数据操作权限', title: '通用数据操作权限',
description: '所有数据表都默认使用通用数据操作权限;同时,可以针对每个数据表单独配置权限。', description: '所有数据表都默认使用通用数据操作权限;同时,可以针对每个数据表单独配置权限。',
'x-component': 'StrategyActions', 'x-component': 'StrategyActions',

View File

@ -1,15 +1,62 @@
import { FormItem, FormLayout } from '@formily/antd'; import { FormItem, FormLayout } from '@formily/antd';
import { Checkbox, Select, Table } from 'antd'; import { ArrayField } from '@formily/core';
import { connect, useField, useForm } from '@formily/react';
import { Checkbox, Select, Table, Tag } from 'antd';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { useAvailableActions } from '.'; import { useAvailableActions } from '.';
import { useCollectionManager, useCompile, useRecord } from '../..'; import { useCollectionManager, useCompile, useRecord } from '../..';
export const RolesResourcesActions = () => { const toActionMap = (arr: any[]) => {
const obj = {};
arr?.forEach((action) => {
obj[action.name] = action;
});
return obj;
};
export const RolesResourcesActions = connect((props) => {
const { onChange } = props;
const form = useForm();
const roleCollection = useRecord(); const roleCollection = useRecord();
const availableActions = useAvailableActions(); const availableActions = useAvailableActions();
const { getCollection } = useCollectionManager(); const { getCollection } = useCollectionManager();
const collection = getCollection(roleCollection.name); const collection = getCollection(roleCollection.name);
const compile = useCompile(); const compile = useCompile();
const { t } = useTranslation();
const field = useField<ArrayField>();
const actionMap: any = toActionMap(field.value || []);
const inAction = (actionName, fieldName) => {
const action = actionMap?.[actionName];
if (!action) {
return false;
}
return action?.fields?.includes(fieldName);
};
const availableActionsWithFields = availableActions.filter((action) => action.allowConfigureFields);
const fieldPermissions = collection?.fields?.map((field) => {
const permission = { ...field };
for (const action of availableActionsWithFields) {
permission[action.name] = inAction(action.name, field.name);
}
return permission;
});
const toggleAction = (actionName: string) => {
if (actionMap[actionName]) {
delete actionMap[actionName];
} else {
actionMap[actionName] = {
name: actionName,
fields: collection?.fields?.map?.((item) => item.name),
};
}
onChange(Object.values(actionMap));
};
const allChecked = {};
for (const action of availableActionsWithFields) {
allChecked[action.name] = collection?.fields?.length === actionMap?.[action.name]?.fields?.length;
}
return ( return (
<div> <div>
<FormLayout layout={'vertical'}> <FormLayout layout={'vertical'}>
@ -24,13 +71,26 @@ export const RolesResourcesActions = () => {
render: (value) => compile(value), render: (value) => compile(value),
}, },
{ {
dataIndex: 'type', dataIndex: 'onNewRecord',
title: '类型', title: '类型',
render: (onNewRecord) =>
onNewRecord ? (
<Tag color={'green'}>{t('Operate on new record')}</Tag>
) : (
<Tag color={'geekblue'}>{t('Operate on existing record')}</Tag>
),
}, },
{ {
dataIndex: 'enable', dataIndex: 'enabled',
title: '允许操作', title: '允许操作',
render: () => <Checkbox />, render: (enabled, action) => (
<Checkbox
checked={enabled}
onChange={() => {
toggleAction(action.name);
}}
/>
),
}, },
{ {
dataIndex: 'scope', dataIndex: 'scope',
@ -38,36 +98,81 @@ export const RolesResourcesActions = () => {
render: () => <Select size={'small'} />, render: () => <Select size={'small'} />,
}, },
]} ]}
dataSource={availableActions} dataSource={availableActions?.map((item) => {
let enabled = false;
let scope = null;
if (actionMap[item.name]) {
enabled = true;
scope = actionMap[item.name]['scope'];
}
return {
...item,
enabled,
scope,
};
})}
/> />
</FormItem> </FormItem>
<FormItem label={'字段权限'}> <FormItem label={'字段权限'}>
<Table <Table
dataSource={collection?.fields} pagination={false}
dataSource={fieldPermissions}
columns={[ columns={[
{ {
dataIndex: ['uiSchema', 'title'], dataIndex: ['uiSchema', 'title'],
title: '字段名称', title: '字段名称',
render: (value) => compile(value),
}, },
{ ...availableActionsWithFields.map((action) => {
dataIndex: 'view', const checked = allChecked?.[action.name];
title: '查看', return {
render: () => <Checkbox />, dataIndex: action.name,
}, title: (
{ <>
dataIndex: 'update', <Checkbox
title: '编辑', checked={checked}
render: () => <Checkbox />, onChange={() => {
}, const item = actionMap[action.name] || {
{ name: action.name,
dataIndex: 'create', };
title: '添加', if (checked) {
render: () => <Checkbox />, item.fields = [];
}, } else {
item.fields = collection?.fields?.map?.((item) => item.name);
}
actionMap[action.name] = item;
onChange(Object.values(actionMap));
}}
/>{' '}
{compile(action.displayName)}
</>
),
render: (checked, field) => (
<Checkbox
checked={checked}
onChange={() => {
const item = actionMap[action.name] || {
name: action.name,
};
const fields: string[] = item.fields || [];
if (checked) {
const index = fields.indexOf(field.name);
fields.splice(index, 1);
} else {
fields.push(field.name);
}
item.fields = fields;
actionMap[action.name] = item;
onChange(Object.values(actionMap));
}}
/>
),
};
}),
]} ]}
/> />
</FormItem> </FormItem>
</FormLayout> </FormLayout>
</div> </div>
); );
}; });

View File

@ -1,11 +1,45 @@
import { Checkbox, Select, Table } from 'antd'; import { ArrayField } from '@formily/core';
import { connect, useField } from '@formily/react';
import { Checkbox, Select, Table, Tag } from 'antd';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next';
import { useAvailableActions } from '.'; import { useAvailableActions } from '.';
import { useCompile } from '../..'; import { useCompile } from '../..';
export const StrategyActions = () => { const toScopes = (value) => {
if (!value) {
return {};
}
const scopes = {};
value?.forEach?.((item) => {
const [name, scope] = item.split(':');
scopes[name] = scope || 'all';
});
return scopes;
};
const toFieldValue = (scopes) => {
const values = [];
for (const name in scopes) {
if (Object.prototype.hasOwnProperty.call(scopes, name)) {
const scope = scopes[name];
if (scope === 'all') {
values.push(name);
} else {
values.push(`${name}:${scope}`);
}
}
}
return values;
};
export const StrategyActions = connect((props) => {
const { onChange } = props;
const availableActions = useAvailableActions(); const availableActions = useAvailableActions();
const compile = useCompile(); const compile = useCompile();
const { t } = useTranslation();
const field = useField<ArrayField>();
const scopes = toScopes(field.value);
return ( return (
<div> <div>
<Table <Table
@ -18,22 +52,66 @@ export const StrategyActions = () => {
render: (value) => compile(value), render: (value) => compile(value),
}, },
{ {
dataIndex: 'type', dataIndex: 'onNewRecord',
title: '类型', title: '类型',
render: (onNewRecord) =>
onNewRecord ? (
<Tag color={'green'}>{t('Operate on new record')}</Tag>
) : (
<Tag color={'geekblue'}>{t('Operate on existing record')}</Tag>
),
}, },
{ {
dataIndex: 'enable', dataIndex: 'enabled',
title: '允许操作', title: '允许操作',
render: () => <Checkbox />, render: (enabled, action) => (
<Checkbox
checked={enabled}
onChange={(e) => {
if (enabled) {
delete scopes[action.name];
} else {
scopes[action.name] = 'all';
}
onChange(toFieldValue(scopes));
}}
/>
),
}, },
{ {
dataIndex: 'scope', dataIndex: 'scope',
title: '可操作的数据范围', title: '可操作的数据范围',
render: () => <Select size={'small'} />, render: (scope, action) =>
!action.onNewRecord && (
<Select
size={'small'}
value={scope}
options={[
{ label: 'All records', value: 'all' },
{ label: 'Own records', value: 'own' },
]}
onChange={(value) => {
scopes[action.name] = value;
onChange(toFieldValue(scopes));
}}
/>
),
}, },
]} ]}
dataSource={availableActions} dataSource={availableActions?.map((item) => {
let scope = 'all';
let enabled = false;
if (scopes[item.name]) {
enabled = true;
scope = scopes[item.name];
}
return {
...item,
enabled,
scope,
};
})}
/> />
</div> </div>
); );
}; });

View File

@ -29,6 +29,20 @@ const collection = {
description: '使用英文', description: '使用英文',
} as ISchema, } as ISchema,
}, },
{
type: 'string',
name: 'usingConfig',
interface: 'input',
uiSchema: {
title: '权限策略',
type: 'string',
'x-component': 'Select',
enum: [
{ label: '单独配置', value: 'resourceAction', color: 'orange' },
{ label: '通用配置', value: 'strategy', color: 'default' },
],
} as ISchema,
},
{ {
type: 'hasMany', type: 'hasMany',
name: 'fields', name: 'fields',
@ -100,6 +114,18 @@ export const roleCollectionsSchema: ISchema = {
}, },
}, },
column3: { column3: {
type: 'void',
'x-decorator': 'Table.Column.Decorator',
'x-component': 'Table.Column',
properties: {
usingConfig: {
type: 'string',
'x-component': 'CollectionField',
'x-read-pretty': true,
},
},
},
column4: {
type: 'void', type: 'void',
title: 'Actions', title: 'Actions',
'x-component': 'Table.Column', 'x-component': 'Table.Column',
@ -136,6 +162,14 @@ export const roleCollectionsSchema: ISchema = {
{ value: false, label: '使用通用权限' }, { value: false, label: '使用通用权限' },
{ value: true, label: '单独配置权限' }, { value: true, label: '单独配置权限' },
], ],
'x-reactions': {
target: 'actions',
fulfill: {
state: {
hidden: '{{!$self.value}}',
},
},
},
}, },
actions: { actions: {
'x-component': 'RolesResourcesActions', 'x-component': 'RolesResourcesActions',

View File

@ -28,6 +28,16 @@ const collection = {
description: '使用英文', description: '使用英文',
} as ISchema, } as ISchema,
}, },
{
type: 'boolean',
name: 'default',
interface: 'boolean',
uiSchema: {
title: '默认角色',
type: 'boolean',
'x-component': 'Checkbox',
} as ISchema,
},
], ],
}; };
@ -44,7 +54,7 @@ export const roleSchema: ISchema = {
resource: 'roles', resource: 'roles',
action: 'list', action: 'list',
params: { params: {
pageSize: 5, pageSize: 50,
filter: {}, filter: {},
sort: ['createdAt'], sort: ['createdAt'],
appends: [], appends: [],
@ -92,6 +102,10 @@ export const roleSchema: ISchema = {
'x-component': 'CollectionField', 'x-component': 'CollectionField',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
}, },
default: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
footer: { footer: {
type: 'void', type: 'void',
'x-component': 'Action.Drawer.Footer', 'x-component': 'Action.Drawer.Footer',
@ -156,6 +170,18 @@ export const roleSchema: ISchema = {
}, },
}, },
column3: { column3: {
type: 'void',
'x-decorator': 'Table.Column.Decorator',
'x-component': 'Table.Column',
properties: {
default: {
type: 'string',
'x-component': 'CollectionField',
'x-read-pretty': true,
},
},
},
column4: {
type: 'void', type: 'void',
title: 'Actions', title: 'Actions',
'x-component': 'Table.Column', 'x-component': 'Table.Column',
@ -246,6 +272,10 @@ export const roleSchema: ISchema = {
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-disabled': true, 'x-disabled': true,
}, },
default: {
'x-component': 'CollectionField',
'x-decorator': 'FormItem',
},
footer: { footer: {
type: 'void', type: 'void',
'x-component': 'Action.Drawer.Footer', 'x-component': 'Action.Drawer.Footer',

View File

@ -10,6 +10,7 @@ export const useRoleResourceValues = (options) => {
resourceOf: record.roleName, resourceOf: record.roleName,
action: 'get', action: 'get',
params: { params: {
appends: ['actions', 'actions.scope'],
filterByTk: record.name, filterByTk: record.name,
}, },
}, },
@ -17,6 +18,9 @@ export const useRoleResourceValues = (options) => {
); );
useEffect(() => { useEffect(() => {
if (record.usingConfig === 'strategy') { if (record.usingConfig === 'strategy') {
options.onSuccess({
data: {},
});
return; return;
} }
if (visible) { if (visible) {

View File

@ -1,18 +1,23 @@
import { useForm } from '@formily/react'; import { useForm } from '@formily/react';
import { useAPIClient, useRecord } from '../../../'; import { useActionContext, useAPIClient, useRecord, useResourceActionContext } from '../../../';
export const useSaveRoleResourceAction = () => { export const useSaveRoleResourceAction = () => {
const form = useForm(); const form = useForm();
const api = useAPIClient(); const api = useAPIClient();
const record = useRecord(); const record = useRecord();
const ctx = useActionContext();
const { refresh } = useResourceActionContext();
return { return {
async run() { async run() {
await api.resource('roles.resources', record.roleName).create({ await api.resource('roles.resources', record.roleName)[record.exists ? 'update' : 'create']({
filterByTk: record.name,
values: { values: {
...form.values, ...form.values,
name: record.name, name: record.name,
}, },
}); });
ctx.setVisible(false);
refresh();
}, },
}; };
}; };

View File

@ -3,7 +3,6 @@ import { observer, useField } from '@formily/react';
import { Button, Modal } from 'antd'; import { Button, Modal } from 'antd';
import classnames from 'classnames'; import classnames from 'classnames';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { SortableItem } from '../../common'; import { SortableItem } from '../../common';
import { useDesigner } from '../../hooks'; import { useDesigner } from '../../hooks';
@ -114,7 +113,7 @@ Action.Designer = () => {
}; };
Action.Link = observer((props) => { Action.Link = observer((props) => {
return <Action {...props} component={Link} className={'nb-action-link'} />; return <Action {...props} component={'a'} className={'nb-action-link'} />;
}); });
Action.Drawer = ActionDrawer; Action.Drawer = ActionDrawer;

View File

@ -4,28 +4,34 @@ const availableActions: {
[key: string]: AvailableActionOptions; [key: string]: AvailableActionOptions;
} = { } = {
create: { create: {
displayName: '{{ t("Create") }}', displayName: '{{t("Add new")}}',
type: 'new-data',
},
import: {
displayName: '{{ t("Import") }}',
type: 'new-data', type: 'new-data',
onNewRecord: true,
allowConfigureFields: true,
}, },
// import: {
// displayName: '{{t("Import")}}',
// type: 'new-data',
// scope: false,
// },
export: { export: {
displayName: '{{ t("Export") }}', displayName: '{{t("Export")}}',
type: 'new-data', type: 'old-data',
allowConfigureFields: true,
}, },
view: { view: {
displayName: '{{ t("View") }}', displayName: '{{t("View")}}',
type: 'old-data', type: 'old-data',
aliases: ['get', 'list'], aliases: ['get', 'list'],
allowConfigureFields: true,
}, },
update: { update: {
displayName: '{{ t("Edit") }}', displayName: '{{t("Edit")}}',
type: 'old-data', type: 'old-data',
allowConfigureFields: true,
}, },
destroy: { destroy: {
displayName: '{{ t("Delete") }}', displayName: '{{t("Delete")}}',
type: 'old-data', type: 'old-data',
}, },
}; };