relationAuth迁到component里

This commit is contained in:
wenjiarui 2023-10-07 15:13:43 +08:00
parent b4276d5103
commit 58e815cdeb
29 changed files with 2094 additions and 0 deletions

View File

@ -0,0 +1,3 @@
{
}

View File

@ -0,0 +1,441 @@
import assert from "assert";
import { uniq, pull, union, difference } from 'oak-domain/lib/utils/lodash';
import { ED } from "../../../types/AbstractComponent";
import { groupBy } from "oak-domain/lib/utils/lodash";
import { AuthCascadePath } from "oak-domain/lib/types";
import { RowWithActions } from "../../../types/Page";
import { resolvePath } from "../../../utils/usefulFn";
import { StorageSchema } from "oak-domain/lib/types";
export default OakComponent({
entity: 'actionAuth',
projection: {
id: 1,
relationId: 1,
paths: 1,
deActions: 1,
destEntity: 1,
relation: {
id: 1,
entity: 1,
},
},
isList: true,
properties: {
entity: '' as keyof ED,
actions: [] as string[],
},
filters: [
{
filter() {
const { entity, actions } = this.props!;
assert(entity);
return {
destEntity: entity as string,
relation: {
entityId: {
$exists: false,
}
}
};
/* if (!actions || actions.length === 0) {
return {
destEntity: entity as string,
};
}
else {
return {
destEntity: entity as string,
deActions: {
$overlaps: actions,
},
};
} */
}
}
],
pagination: {
pageSize: 1000,
currentPage: 0,
},
formData({ data }) {
const { entity } = this.props;
const schema = this.features.cache.getSchema();
const cascadeEntities = this.features.relationAuth.getCascadeActionAuths(entity!, true);
const actionAuthGroup = groupBy(data, (ele) => ele.paths?.join(','));
const actionAuthList = Object.keys(actionAuthGroup).map(
(key) => {
let result = {};
const row = actionAuthGroup[key][0];
const { paths } = row;
const path = paths![0];
if (path.includes('$')) {
const relationEntity = this.resolveP(schema, path, row.destEntity);
const relations = this.features.cache.get('relation', {
data: {
id: 1,
entity: 1,
entityId: 1,
name: 1,
display: 1,
},
filter: {
entity: relationEntity,
entityId: {
$exists: false,
},
},
});
Object.assign(result, {
sourceEntity: relationEntity,
relationSelections: relations,
})
} else {
const cascadeEntity = cascadeEntities.find((ele) =>
ele[1] === path
)
const relations = this.features.cache.get('relation', {
data: {
id: 1,
entity: 1,
entityId: 1,
name: 1,
display: 1,
},
filter: {
entity: cascadeEntity![2] as string,
entityId: {
$exists: false,
},
},
});
Object.assign(result, {
sourceEntity: cascadeEntity![2],
relationSelections: relations,
})
}
Object.assign(result, {
paths,
relations: actionAuthGroup[key],
})
return result;
}
)
console.log(actionAuthList);
// const cascadeEntityActions = cascadeEntities.map(
// (ele) => {
// const [de, p, se] = ele;
// const actionAuths = data?.filter(
// ele => ele.destEntity === de
// );
// const relations = this.features.cache.get('relation', {
// data: {
// id: 1,
// entity: 1,
// entityId: 1,
// name: 1,
// display: 1,
// },
// filter: {
// entity: se as string,
// entityId: {
// $exists: false,
// },
// },
// });
// return {
// actionAuths,
// relations,
// path: ele,
// };
// }
// );
// // path里含有$
// const $pathActionAuths: (RowWithActions<ED, 'actionAuth'> & { path: string })[] = [];
// data.forEach((ele) => {
// if (ele.paths?.join('').includes('$')) {
// ele.paths.forEach((path) => {
// if (path.includes('$')) {
// $pathActionAuths.push({
// ...ele,
// path,
// })
// }
// })
// }
// })
// // groupBy
// // 分解groupBy 的key
// const $actionAuthsObject = groupBy($pathActionAuths, 'path');
// // 含有反向指针的路径其所对应实体的请求放在了onChange方法
// Object.keys($actionAuthsObject).forEach((ele) => {
// const entities = ele.split('.');
// const slicePath = entities[entities.length - 1];
// const se = entities[entities.length - 1].split('$')[0];
// const relationEntity = this.resolveP(schema, ele, entity);
// const p = ele;
// const de = entity!;
// // 初始时 relation先用{name: relationName}表示
// let relations = [];
// if (relationEntity === 'user') {
// relations = [{ id: '', name: '当前用户' }];
// }
// else {
// relations = this.features.cache.get('relation', {
// data: {
// id: 1,
// entity: 1,
// entityId: 1,
// name: 1,
// display: 1,
// },
// filter: {
// entity: relationEntity as string,
// entityId: {
// $exists: false,
// },
// },
// });
// }
// cascadeEntityActions.push({
// path: [de, p, se, true],
// relations: relations,
// actionAuths: $actionAuthsObject[ele],
// })
// })
// // relationId为空字符串 表示为user的actionAuth 也要特殊处理
// const hasUserActionAuths: (RowWithActions<ED, 'actionAuth'> & { path: string })[] = [];
// data.forEach((ele) => {
// if (ele.relationId === '') {
// ele.paths?.forEach((path) => {
// hasUserActionAuths.push({
// ...ele,
// path
// })
// })
// }
// })
// // const hasUserActionAuths = data.filter((ele) => ele.relationId === '');
// const $actionAuthsObject2 = groupBy(hasUserActionAuths, 'path');
// Object.keys($actionAuthsObject2).forEach((ele) => {
// const entities = ele.split('.');
// const se = entities[entities.length - 1].split('$')[0];
// const p = ele;
// const de = entity!;
// cascadeEntityActions.push({
// path: [de, p, se, true],
// relations: [{ id: '', name: '当前用户' }],
// actionAuths: $actionAuthsObject2[ele],
// })
// })
return {
actionAuthList,
actionAuths: data,
// cascadeEntityActions,
};
},
lifetimes: {
ready() {
const { entity } = this.props;
const cascadeEntities = this.features.relationAuth.getCascadeActionAuths(entity!, true);
const destEntities = uniq(cascadeEntities.map(ele => ele[2])) as string[];
this.features.cache.refresh('relation', {
data: {
id: 1,
entity: 1,
entityId: 1,
name: 1,
display: 1,
},
filter: {
// entity: {
// $in: destEntities,
// },
entityId: {
$exists: false,
},
},
});
}
},
data: {
relations2: [] as Partial<ED['relation']['Schema']>[],
},
listeners: {
actions(prev, next) {
const actionAuths = this.features.runningTree.getFreshValue(this.state.oakFullpath);
if (actionAuths) {
(actionAuths as ED['actionAuth']['OpSchema'][]).forEach(
(actionAuth) => {
if (actionAuth.$$createAt$$ === 1) {
const { id, deActions } = actionAuth;
this.updateItem({
deActions: next.actions,
}, id);
}
}
);
}
this.reRender();
},
},
methods: {
async onChange(checked: boolean, relationId: string, path: string, actionAuths?: ED['actionAuth']['Schema'][], relationName?: string) {
const { actions } = this.props;
assert(actions && actions.length > 0);
// 排除user这种情况
if (!relationId && (relationName && relationName !== '当前用户')) {
const se = path.split('$')[0];
const { data: relations } = await this.features.cache.refresh('relation', {
data: {
id: 1,
entity: 1,
entityId: 1,
name: 1,
display: 1,
},
filter: {
entity: se,
name: relationName,
entityId: {
$exists: false,
},
},
});
if (!relations || !relations.length) {
this.setMessage({
content: '数据缺失!',
type: 'error',
})
return;
}
relationId = relations[0].id!;
}
if (actionAuths && actionAuths.length) {
// const { deActions } = actionAuth;
if (checked) {
const dASameActionAuth = actionAuths.find((ele) => difference(actions, ele.deActions).length === 0);
// 存在deActions相同paths push并做去重处理
if (dASameActionAuth) {
if (dASameActionAuth.$$deleteAt$$ && dASameActionAuth.$$deleteAt$$ === 1) {
this.recoverItem(dASameActionAuth.id);
}
this.updateItem({
paths: dASameActionAuth.paths.concat(path),
}, dASameActionAuth.id)
}
else {
this.addItem({
paths: [path],
relationId,
destEntity: this.props.entity as string,
deActions: actions,
});
}
}
else {
// 将path从paths中删除
actionAuths.forEach((ele) => {
const pathIndex = ele.paths.findIndex((pathE) => pathE === path);
if (pathIndex !== -1) {
const newPaths = [...ele.paths];
newPaths.splice(pathIndex, 1);
if (!newPaths.length) {
this.removeItem(ele.id);
}
else {
this.updateItem({
paths: newPaths
}, ele.id);
}
}
})
}
// if (checked) {
// const deActions2 = union(deActions, actions);
// this.updateItem({
// deActions: deActions2,
// }, actionAuth.id);
// }
// else {
// actions.forEach(
// action => pull(deActions, action)
// );
// this.updateItem({
// deActions,
// }, actionAuth.id);
// }
}
else {
// 新增actionAuth
assert(checked);
this.addItem({
paths: [path],
relationId,
destEntity: this.props.entity as string,
deActions: actions,
});
}
},
async onChange2(checked: boolean, relationId: string, paths: string[], actionAuths: ED['actionAuth']['Schema'][]) {
console.log(checked);
const { actions } = this.props;
if (checked) {
const dASameActionAuth = (actionAuths as ED['actionAuth']['Schema'][]).find((ele) => ele.relationId === relationId);
if (dASameActionAuth) {
if (dASameActionAuth.$$deleteAt$$ && dASameActionAuth.$$deleteAt$$ === 1) {
this.recoverItem(dASameActionAuth.id);
this.updateItem({
deActions: actions,
}, dASameActionAuth.id)
}
else {
this.updateItem({
deActions: uniq((actions || []).concat(dASameActionAuth.deActions)),
}, dASameActionAuth.id)
}
} else {
this.addItem({
paths,
relationId,
destEntity: this.props.entity as string,
deActions: actions,
})
}
} else {
const dASameActionAuth = (actionAuths as ED['actionAuth']['Schema'][]).find((ele) => ele.relationId === relationId && !ele.$$deleteAt$$);
assert(dASameActionAuth);
const newActions = difference(dASameActionAuth.deActions, actions!);
if (newActions.length === 0) {
this.removeItem(dASameActionAuth.id);
} else {
this.updateItem({
deActions: newActions,
}, dASameActionAuth.id)
}
}
},
confirm() {
this.execute();
},
resolveP(schema: StorageSchema<ED>, path: string, destEntity: string) {
if (path === '') {
return destEntity;
}
const splitArr = path.split('.');
splitArr.unshift(destEntity);
for (let i = 1; i < splitArr.length; i++) {
if (splitArr[i].includes('$')) {
splitArr[i] = splitArr[i].split('$')[0];
continue;
}
// 用已解析的前项来解析后项
const { attributes } = schema[splitArr[i - 1]];
const { ref } = attributes[`${splitArr[i]}Id`];
splitArr[i] = ref as string;
}
return splitArr[splitArr.length - 1];
}
}
})

View File

@ -0,0 +1 @@
{"confirm": "确认", "reset": "重置"}

View File

@ -0,0 +1,184 @@
import { Table, Checkbox, Button, Row, Radio, Col, Typography, Space, Modal, Badge, Tag } from 'antd';
const { Title, Text } = Typography;
import { RowWithActions, WebComponentProps } from '../../../types/Page';
import { AuthCascadePath, EntityDict } from 'oak-domain/lib/types/Entity';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { difference, intersection, isEqual } from 'oak-domain/lib/utils/lodash';
import { useState, useEffect } from 'react';
import ActionAuthListSingle from '../../relation/single';
type ED = EntityDict & BaseEntityDict;
type CascadeEntityActions = Array<{
path: AuthCascadePath<ED>;
relations: ED['relation']['Schema'][];
actionAuths?: ED['actionAuth']['OpSchema'][];
}>;
export default function render(
props: WebComponentProps<
ED,
'actionAuth',
true,
{
cascadeEntityActions: Array<{
path: AuthCascadePath<ED>;
relations: ED['relation']['Schema'][];
actionAuths?: ED['actionAuth']['Schema'][];
}>;
actionAuthList: Array<{
paths: string[];
sourceEntity: string;
relations: ED['actionAuth']['Schema'][],
relationSelections: Array<{ id: string, name: string }>
}>;
actions: string[];
entity: keyof EntityDict;
},
{
onChange: (checked: boolean, relationId: string, path: string, actionAuth?: ED['actionAuth']['Schema'][]) => void;
onChange2: (checked: boolean, relationId: string, paths: string[], actionAuths: ED['actionAuth']['Schema'][], actionAuth?: ED['actionAuth']['Schema']) => void;
confirm: () => void;
}
>
) {
const { cascadeEntityActions, oakDirty, actions, entity, actionAuthList } = props.data;
const { onChange, t, clean, confirm, onChange2 } = props.methods;
return (
<Space direction="vertical" style={{ width: '100%' }}>
<ActionAuthListSingle entity={entity} />
<Table
columns={[
{
key: '1',
title: '源对象',
width: 100,
render: (value, record) => {
const { sourceEntity } = record;
return sourceEntity;
},
},
{
key: '1',
title: '路径',
width: 200,
render: (value, record) => {
const { paths } = record;
return paths.map((ele, index) => {
if (index === 0) {
return ele;
} else {
return <><br />{ele}</>
}
})
},
},
{
fixed: 'right',
title: '相关角色',
key: 'operation',
width: 300,
render: (value, record) => {
// const { relations, actionAuths, path } = record;
const { relations, relationSelections } = record;
return (
<div style={{
width: '100%',
display: 'flex',
flexWrap: 'wrap',
}}>
{/* {
relations?.map(
(r) => {
let checked = false, indeterminate = false;
// filter出对应path的actionAuth来决定relation的check
// sort deActions长的在后不然会影响checked
const actionAuthsByPath = actionAuths?.filter((ele) => ele.paths.includes(path[1]))
.sort((a, b) => b.deActions.length - a.deActions.length);
if (actionAuthsByPath && actions.length > 0) {
for (const aa of actionAuthsByPath) {
// 1.relationId相同deActions也要相同
// 如果path中存在多对一的情况要使用name进行判断
if (!aa.$$deleteAt$$ && (aa.relationId === r.id
|| (record.path.includes('$') && aa.relation?.name === r.name))
) {
const { deActions } = aa;
checked = difference(actions, deActions).length === 0;
indeterminate = !checked && intersection(actions, deActions).length > 0;
break;
}
}
}
return (
<Checkbox
disabled={actions.length === 0}
checked={checked}
indeterminate={indeterminate}
onChange={({ target }) => {
const { checked } = target;
const actionAuths2 = actionAuths?.filter(
ele => ele.relationId === r.id || (record.path.includes('$') && ele.relation?.name === r.name)
);
onChange(checked, r.id, path[1], actionAuths2)
}}
>
{r.name}
</Checkbox>
)
}
)
} */}
{
relationSelections.map(
(ele) => {
let checked = false, indeterminate = false;
if (actions && actions.length > 0) {
const relation = relations.find((ele2) => ele2.relationId === ele.id && !ele2.$$deleteAt$$);
if (relation) {
const { deActions } = relation;
checked = difference(actions, deActions).length === 0;
indeterminate = !checked && intersection(actions, deActions).length > 0;
}
}
return <Checkbox
disabled={actions.length === 0}
checked={checked}
indeterminate={indeterminate}
onChange={({ target }) => {
onChange2(target.checked, ele.id, record.paths, relations);
}}
>
{ele.name}
</Checkbox>
}
)
}
</div>
)
}
}
]}
dataSource={actionAuthList}
pagination={false}
/>
<Row justify="end" style={{ marginTop: 20, padding: 5 }}>
<Button
style={{ marginRight: 10 }}
type='primary'
disabled={!oakDirty}
onClick={() => confirm()}
>
{t("confirm")}
</Button>
<Button
disabled={!oakDirty}
onClick={() => clean()}
>
{t("reset")}
</Button>
</Row>
</Space>
);
}

View File

@ -0,0 +1,3 @@
{
}

View File

@ -0,0 +1,91 @@
import assert from "assert";
import { AuthCascadePath } from "oak-domain/lib/types";
import { ED } from "../../../types/AbstractComponent";
export default OakComponent({
entity: 'actionAuth',
projection: {
id: 1,
relationId: 1,
paths: 1,
deActions: 1,
destEntity: 1,
relation: {
id: 1,
entity: 1,
},
},
isList: true,
properties: {
relationId: '',
entity: '' as keyof ED,
},
filters: [
{
filter() {
const { relationId } = this.props;
assert(relationId);
return {
relationId,
};
}
}
],
pagination: {
pageSize: 1000,
currentPage: 0,
},
formData({ data }) {
const { entity, relationId } = this.props;
const [relation] = this.features.cache.get('relation', {
data: {
id: 1,
name: 1,
display: 1,
},
filter: {
id: relationId,
},
});
const { name } = relation || {};
const cascadeEntities = this.features.relationAuth.getCascadeActionEntitiesBySource(entity!);
const cascadeEntityActions = cascadeEntities.map(
(ele) => {
const { path } = ele;
const cascadePath = path[1];
const actionAuth = data?.find(
ele => ele.paths?.includes(cascadePath) && ele.destEntity === path[0]
);
return {
actionAuth,
...ele,
};
}
);
return {
relationName: name,
cascadeEntityActions,
};
},
methods: {
onChange(actions: string[], path: AuthCascadePath<ED>, actionAuth?: ED['actionAuth']['OpSchema']) {
if (actionAuth) {
this.updateItem({
deActions: actions,
}, actionAuth.id);
}
else {
this.addItem({
relationId: this.props.relationId,
paths: [path[1]],
destEntity: path[0] as string,
deActions: actions,
});
}
},
confirm() {
this.execute();
}
}
})

View File

@ -0,0 +1 @@
{"confirm": "确认", "reset": "重置"}

View File

@ -0,0 +1,109 @@
import { Table, Checkbox, Button, Row } from 'antd';
import { Typography } from 'antd';
const { Title, Text } = Typography;
import { RowWithActions, WebComponentProps } from '../../../types/Page';
import { AuthCascadePath, EntityDict } from 'oak-domain/lib/types/Entity';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { assert } from 'oak-domain/lib/utils/assert';
type ED = EntityDict & BaseEntityDict;
export default function render(
props: WebComponentProps<
ED,
'actionAuth',
true,
{
entity: string;
relationName: string;
cascadeEntityActions: Array<{
path: AuthCascadePath<ED>;
actions: string[];
actionAuth?: ED['actionAuth']['OpSchema'];
}>;
},
{
onChange: (actions: string[], path: AuthCascadePath<ED>, actionAuth?: ED['actionAuth']['OpSchema']) => void;
confirm: () => void;
}
>
) {
const { cascadeEntityActions, oakDirty, entity, relationName } = props.data;
const { onChange, t, clean, confirm } = props.methods;
return (
<>
<Row justify="center" style={{ margin: 20, padding: 10 }}>
<Row style={{ marginRight: 10 }}>
<Title level={4}></Title>
<Text code>{entity}</Text>
</Row>
<Row>
<Title level={4}></Title>
<Text code>{relationName}</Text>
</Row>
</Row>
<Table
columns={[
{
key: '1',
title: '目标对象',
width: 100,
render: (value, record) => {
const { path } = record;
return path[0];
},
},
{
key: '1',
title: '路径',
width: 200,
render: (value, record) => {
const { path } = record;
return path[1];
},
},
{
fixed: 'right',
title: '操作',
key: 'operation',
width: 300,
render: (value, record) => {
const { actions, actionAuth, path } = record;
return (
<Checkbox.Group
style={{
width: '100%',
display: 'flex',
flexWrap: 'wrap',
}}
options={actions}
value={actionAuth?.deActions}
onChange={(value) => onChange(value as string[], path, actionAuth)}
/>
)
}
}
]}
dataSource={cascadeEntityActions}
pagination={false}
/>
<Row justify="end" style={{ marginTop: 20, padding: 5 }}>
<Button
style={{ marginRight: 10 }}
type='primary'
disabled={!oakDirty}
onClick={() => confirm()}
>
{t("confirm")}
</Button>
<Button
disabled={!oakDirty}
onClick={() => clean()}
>
{t("reset")}
</Button>
</Row>
</>
);
}

View File

@ -0,0 +1,3 @@
{
}

View File

@ -0,0 +1,80 @@
import { AuthCascadePath, EntityDict } from 'oak-domain/lib/types/Entity';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
type ED = EntityDict & BaseEntityDict;
export default OakComponent({
isList: false,
properties: {
entity: '' as keyof ED,
},
data: {
checkedActions: [] as string[],
relationIds: [] as string[],
},
formData() {
const { entity } = this.props;
const actions = this.features.relationAuth.getActions(entity!);
const daas = this.features.relationAuth.getCascadeActionAuths(entity!, false);
const relations = this.features.cache.get('relation', {
data: {
id: 1,
entity: 1,
entityId: 1,
name: 1,
display: 1,
},
filter: {
entity: entity as string,
entityId: {
$exists: false,
},
}
});
const dras = this.features.relationAuth.getCascadeRelationAuths(entity!, false);
const deduceRelationAttr = this.features.relationAuth.getDeduceRelationAttribute(entity!);
return {
relations,
actions,
daas,
dras,
hasDirectActionAuth: daas.length > 0,
hasDirectRelationAuth: dras.length > 0,
deduceRelationAttr,
};
},
lifetimes: {
async ready() {
const { entity } = this.props;
// 现在的数据结构可以在任意对象上增加relation这个if以后并不成立。 by Xc
if (this.features.relationAuth.hasRelation(entity!)) {
await this.features.cache.refresh('relation', {
data: {
id: 1,
entity: 1,
entityId: 1,
name: 1,
display: 1,
},
filter: {
entity: entity as string,
entityId: {
$exists: false,
},
}
});
// 没定义entity显式的reRender
this.reRender();
}
}
},
methods: {
onActionsSelected(checkedActions: string[]) {
this.setState({ checkedActions });
},
onRelationsSelected(relationIds: string[]) {
this.setState({ relationIds });
}
}
});

View File

@ -0,0 +1 @@
{"action":"操作", "relation":"关系", "sourceEntity": "源对象", "path": "路径"}

View File

@ -0,0 +1,3 @@
.container {
background-color: var(--oak-bg-color-container);
}

View File

@ -0,0 +1,173 @@
import { AuthCascadePath, EntityDict } from 'oak-domain/lib/types/Entity';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { Row, Radio, Col, Tabs, Checkbox, Table, Space, Button } from 'antd';
import { Typography } from 'antd';
const { Title, Text } = Typography;
import ActionAuth from '../actionAuth';
import RelationAuth from '../relationAuth';
import { WebComponentProps } from '../../../types/Page';
import { useState } from 'react';
import Styles from './web.pc.module.less';
type ED = EntityDict & BaseEntityDict;
export default function render(props: WebComponentProps<ED, keyof ED, false, {
entity: keyof ED;
actions: string[];
daas: AuthCascadePath<ED>[];
dras: AuthCascadePath<ED>[];
checkedActions: string[];
relations: ED['relation']['OpSchema'][];
relationIds: string[];
hasDirectActionAuth: boolean;
hasDirectRelationAuth: boolean;
deduceRelationAttr?: string;
}, {
onActionsSelected: (actions: string[]) => void;
onRelationsSelected: (relationIds: string[]) => void;
}>) {
const { oakFullpath, entity, actions, checkedActions, hasDirectActionAuth, hasDirectRelationAuth,
dras, daas, relationIds, relations, deduceRelationAttr } = props.data;
const { onActionsSelected, onRelationsSelected, t } = props.methods;
const [tab, setTab] = useState('actionAuth');
const items = deduceRelationAttr ? [
{
label: 'deduceRelation',
key: 'deduceRelation',
children: (
<div
style={{
width: '100%',
height: '100%',
minHeight: 600,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
actionAuth已被deduce到[<b>{deduceRelationAttr}</b>]
</div>
)
},
] : [
{
label: 'actionAuth',
key: 'actionAuth',
children: (
<ActionAuth
entity={entity}
oakPath={oakFullpath && `${oakFullpath}.actionAuths`}
actions={checkedActions}
/>
)
}
];
/* if (hasDirectActionAuth) {
items.push(
{
label: 'directActionAuth',
key: 'directActionAuth',
children: (
<Table
columns={[
{
key: '2',
title: t('sourceEntity'),
width: 100,
render: (value, record) => {
return record[2];
},
},
{
key: '1',
title: t('path'),
width: 200,
render: (value, record) => {
return record[1];
},
},
]}
dataSource={daas}
pagination={false}
/>
)
}
);
} */
// if (relations?.length > 0) {
// items.push(
// {
// label: 'relationAuth',
// key: 'relationAuth',
// children: (
// <RelationAuth
// entity={entity}
// oakPath={oakFullpath && `${oakFullpath}.relationAuths`}
// relationIds={relationIds}
// />
// )
// }
// );
// }
const ActionSelector = actions && (
<Row style={{ width: '100%' }} justify="center" align="middle">
<Text strong>{t('action')}:</Text>
<Row style={{ flex: 1, marginLeft: 10 }} justify="start" align="middle" wrap>
<Checkbox.Group
options={actions}
value={checkedActions}
onChange={(value) => onActionsSelected(value as string[])}
style={{
display: 'flex',
flexWrap: 'wrap',
}}
/>
</Row>
</Row>
);
const RelationSelector = relations && (
<Row style={{ width: '100%' }} justify="center" align="middle">
<Text strong>{t('relation')}:</Text>
<Row style={{ flex: 1, marginLeft: 10 }} justify="start" align="middle" wrap>
<Checkbox.Group
options={relations.map(ele => ({ label: ele.name!, value: ele.id! }))}
value={relationIds}
onChange={(value) => onRelationsSelected(value as string[])}
style={{
display: 'flex',
flexWrap: 'wrap',
}}
/>
</Row>
</Row>
);
const showActionSelector = ['actionAuth', 'directActionAuth'].includes(tab);
const showRelationSelector = ['relationAuth', 'directRelationAuth'].includes(tab);
return (
<div className={Styles.container}>
<Row justify="center" style={{ margin: 20, padding: 10, minHeight: 100 }} align="middle">
<Col span={8}>
<Row style={{ width: '100%' }} justify="center" align="middle">
<Text strong>{t('actionAuth:attr.destEntity')}:</Text>
<Text code style={{ marginLeft: 10 }}>{entity}</Text>
</Row>
</Col>
<Col span={12}>
{showActionSelector ? ActionSelector : (showRelationSelector && RelationSelector)}
</Col>
</Row>
<Tabs
defaultActiveKey="1"
type="card"
size="large"
items={items}
onChange={(key) => setTab(key)}
/>
</div>
);
}

View File

@ -0,0 +1,3 @@
{
"component": true
}

View File

@ -0,0 +1,41 @@
import { assert } from 'oak-domain/lib/utils/assert';
import {
CardDef,
ED,
OakAbsAttrDef,
onActionFnDef,
} from '../../../types/AbstractComponent';
export default OakComponent({
isList: true,
formData() {
const { data, links } = this.features.relationAuth.getEntityGraph();
return {
data,
links,
};
},
properties: {
onEntityClicked: (entity: string) => undefined as void,
},
methods: {
onEntityClicked(entity: string) {
if (this.props.onEntityClicked) {
this.props.onEntityClicked(entity);
} else {
if (process.env.NODE_ENV === 'development') {
console.warn(
'直接使用relation/entityList作为page用法即将废除请使用自定义页面包裹'
);
}
this.features.navigator.navigateTo(
{
url: '/relation/entity',
},
{
entity,
}
);
}
},
},
});

View File

@ -0,0 +1,3 @@
.container {
background-color: var(--oak-bg-color-container);
}

View File

@ -0,0 +1,153 @@
import { RowWithActions, WebComponentProps } from '../../../types/Page';
import { EntityDict } from 'oak-domain/lib/types/Entity';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { Row, Switch, Col, Input, Form } from 'antd';
import ReactEcharts from 'echarts-for-react';
import { useState } from 'react';
import { uniq } from 'oak-domain/lib/utils/lodash';
import Styles from './web.pc.module.less';
type ED = EntityDict & BaseEntityDict;
export default function render(
props: WebComponentProps<
ED,
keyof ED,
true,
{
data: Array<{ name: string, x?: number, y?: number }>;
links: Array<{ source: string, target: string }>;
},
{
onEntityClicked: (entity: string) => void;
}
>
) {
const { data, links } = props.data;
const { onEntityClicked } = props.methods;
const [search, setSearch] = useState('');
const [strict, setStrict] = useState(false);
const keywords = search && search.split(',');
let data2 = data;
let links2 = links;
if (keywords) {
if (!strict) {
links2 = links.filter(
ele => {
if (keywords.find(k => ele.source.includes(k) || ele.target.includes(k))) {
return true;
}
return false;
}
);
data2 = uniq(links2.map(ele => ele.source).concat(links2.map(ele => ele.target))).map(
ele => ({ name: ele })
);
}
else {
links2 = links.filter(
ele => {
if (keywords.find(k => ele.source.includes(k) && ele.target.includes(k))) {
return true;
}
return false;
}
);
data2 = data.filter(
ele => {
if (keywords.find(k => ele.name.includes(k))) {
return true;
}
return false;
}
);
}
}
return (
<div className={Styles.container}>
<Form
style={{
margin: 20,
}}
>
<Form.Item
label="filter"
>
<>
<Input
onChange={({ currentTarget }) => setSearch(currentTarget.value)}
allowClear
/>
</>
</Form.Item>
<Form.Item
label="strict mode"
>
<>
<Switch
checked={strict}
onChange={(checked) => setStrict(checked)}
/>
</>
</Form.Item>
</Form>
<ReactEcharts
style={{ width: '100%', height: '100%', minHeight: 750 }}
option={{
tooltip: {},
series: [
{
type: 'graph',
layout: 'force',
force: {
initLayout: 'circular',
gravity: 0,
repulsion: [10, 80],
edgeLength: [10, 50]
},
data: data2,
links: links2,
lineStyle: {
opacity: 0.9,
width: 2,
curveness: 0
},
label: {
show: true
},
autoCurveness: true,
roam: true,
draggable: true,
edgeSymbol: ['none', 'arrow'],
edgeSymbolSize: 7,
emphasis: {
scale: true,
label: {
show: true,
},
focus: 'adjacency',
lineStyle: {
width: 10
}
}
},
],
}}
notMerge={true}
lazyUpdate={false}
onEvents={{
click: (info: any) => {
const { data, dataType } = info;
if (dataType === 'node') {
const { name } = data;
onEntityClicked(name);
}
},
}}
/>
</div>
);
}

View File

@ -0,0 +1,3 @@
{
"component": true
}

View File

@ -0,0 +1,103 @@
import { assert } from 'oak-domain/lib/utils/assert';
import {
CardDef,
ED,
OakAbsAttrDef,
onActionFnDef,
} from '../../../types/AbstractComponent';
export default OakComponent({
entity: 'relation',
isList: true,
projection: {
id: 1,
name: 1,
display: 1,
entity: 1,
entityId: 1,
},
formData({ data = [] }) {
// 根据设计这里如果同一个entity上同时存在有entityId和没有entityId的则隐藏掉没有entityId的行
const relations = data.filter((ele) => !!ele.entityId);
data.forEach((ele) => {
if (!ele.entityId) {
if (
!relations.find(
(ele2) => ele2.entity === ele.entity && ele2.entityId
)
) {
relations.push(ele);
}
} else {
assert(ele.entityId === this.props.entityId);
}
});
const hasRelationEntites =
this.features.relationAuth.getHasRelationEntities();
return {
relations,
hasRelationEntites,
};
},
filters: [
{
filter() {
const { entity, entityId } = this.props;
const filter = {};
if (entity) {
Object.assign(filter, { entity });
}
if (entityId) {
Object.assign(filter, {
$or: [
{
entityId: {
$exists: false,
},
},
{
entityId,
},
],
});
} else {
Object.assign(filter, {
entityId: {
$exists: false,
},
});
}
return filter;
},
},
],
properties: {
entity: '' as keyof ED,
entityId: '',
},
features: ['relationAuth'],
methods: {
onActionClicked(id: string, entity: string) {
this.features.navigator.navigateTo(
{
url: '/relation/actionAuthBySource',
},
{
relationId: id,
entity,
}
);
},
onRelationClicked(id: string, entity: string) {
this.features.navigator.navigateTo(
{
url: '/relation/relationAuthBySource',
},
{
relationId: id,
entity,
}
);
},
},
});

View File

@ -0,0 +1,69 @@
import PureList from '../../../components/list';
import FilterPanel from '../../../components/filterPanel';
import { RowWithActions, WebComponentProps } from '../../../types/Page';
import { EntityDict } from 'oak-domain/lib/types/Entity';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { assert } from 'oak-domain/lib/utils/assert';
type ED = EntityDict & BaseEntityDict;
export default function render(
props: WebComponentProps<
ED,
'relation',
true,
{
relations: RowWithActions<ED, 'relation'>[];
hasRelationEntites: string[];
},
{
onActionClicked: (id: string, entity: keyof ED) => void;
onRelationClicked: (id: string, entity: keyof ED) => void;
}
>
) {
const { relations, oakLoading, oakFullpath, hasRelationEntites } = props.data;
const { onActionClicked, onRelationClicked } = props.methods;
return (
<>
<FilterPanel
entity="relation"
oakPath={oakFullpath}
columns={[
{
attr: 'entity',
selectProps: {
options: hasRelationEntites ? hasRelationEntites.map(
ele => ({
label: ele,
value: ele,
})
) : [],
}
},
{
attr: 'entityId',
},
]}
/>
<PureList
entity="relation"
loading={oakLoading}
data={relations || []}
attributes={['entity', 'entityId', 'name', 'display']}
extraActions={[{ action: 'action', label: '动作', show: true }, { action: 'relation', label: '关系', show: true }]}
onAction={(row: ED['relation']['OpSchema'], action: string) => {
const { id, entity } = row;
if (action === 'action') {
onActionClicked(id!, entity!);
}
else {
assert(action === 'relation');
onRelationClicked(id!, entity!);
}
}}
// disabledOp={true}
/>
</>
);
}

View File

@ -0,0 +1,3 @@
{
}

View File

@ -0,0 +1,176 @@
import assert from "assert";
import { AuthCascadePath } from "oak-domain/lib/types";
import { uniq, pull, difference } from 'oak-domain/lib/utils/lodash';
import { ED } from "../../../types/AbstractComponent";
export default OakComponent({
entity: 'relationAuth',
projection: {
id: 1,
sourceRelationId: 1,
destRelationId: 1,
},
isList: true,
properties: {
entity: '' as keyof ED,
relationIds: [] as string[],
},
filters: [
{
filter() {
const { entity, relationIds } = this.props;
// 这里不能用relationIds过滤否则没法处理relationId的反选
return {
destRelation: {
entity: entity as string,
entityId: {
$exists: false,
},
},
};
/* if (relationIds && relationIds.length > 0) {
return {
destRelationId: {
$in: relationIds,
},
};
}
else {
return {
destRelation: {
entity: entity as string,
entityId: {
$exists: false,
},
},
};
} */
},
}
],
pagination: {
pageSize: 1000,
currentPage: 0,
},
formData({ data }) {
const { entity, relationIds } = this.props;
const auths = this.features.relationAuth.getCascadeRelationAuths(entity!, true);
const sourceEntities = auths.map(
ele => ele[2]
) as string[];
const sourceRelations = this.features.cache.get('relation', {
data: {
id: 1,
entity: 1,
entityId: 1,
name: 1,
display: 1,
},
filter: {
entity: {
$in: sourceEntities,
},
entityId: {
$exists: false,
},
},
});
return {
relationAuths: data,
auths,
sourceRelations,
};
},
listeners: {
relationIds(prev, next) {
const relationAuths = this.features.runningTree.getFreshValue(this.state.oakFullpath);
if (relationAuths) {
const { relationIds } = next;
(relationAuths as ED['relationAuth']['OpSchema'][]).forEach(
(relationAuth) => {
if (relationAuth.$$createAt$$ === 1 && !relationIds.includes(relationAuth.destRelationId)) {
const { id } = relationAuth;
this.removeItem(id);
}
}
);
}
this.reRender();
},
},
lifetimes: {
ready() {
const { entity } = this.props;
const auths = this.features.relationAuth.getCascadeRelationAuths(entity!, true);
const sourceEntities = auths.map(
ele => ele[2]
) as string[];
this.features.cache.refresh('relation', {
data: {
id: 1,
entity: 1,
entityId: 1,
name: 1,
display: 1,
},
filter: {
entity: {
$in: sourceEntities,
},
entityId: {
$exists: false,
},
},
});
},
},
methods: {
onChange(checked: boolean, sourceRelationId: string, path: string, relationAuths?: ED['relationAuth']['OpSchema'][]) {
const { relationIds } = this.props;
assert(relationIds);
if (checked) {
if (relationAuths) {
// 这些relationAuths可能是已经带有relationIds的也有可能是被删除掉的比较复杂
const includedRelationIds = [] as string[];
for (const auth of relationAuths) {
if (auth.$$deleteAt$$) {
this.recoverItem(auth.id);
}
includedRelationIds.push(auth.destRelationId);
}
const restRelationIds = difference(relationIds, includedRelationIds);
restRelationIds.forEach(
(relationId) => this.addItem({
sourceRelationId,
path,
destRelationId: relationId,
})
);
}
else {
relationIds.forEach(
relationId => this.addItem({
sourceRelationId,
path,
destRelationId: relationId,
})
);
}
}
else {
assert(relationAuths);
relationAuths.forEach(
relationAuth => {
if (!relationAuth.$$deleteAt$$) {
this.removeItem(relationAuth.id);
}
}
);
}
},
confirm() {
this.execute();
},
}
});

View File

@ -0,0 +1 @@
{"confirm": "确认", "reset": "重置", "grantedRoles": "授权角色"}

View File

@ -0,0 +1,126 @@
import { Table, Checkbox, Button, Row, Radio, Col } from 'antd';
import { Typography } from 'antd';
const { Title, Text } = Typography;
import { RowWithActions, WebComponentProps } from '../../../types/Page';
import { AuthCascadePath, EntityDict } from 'oak-domain/lib/types/Entity';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { intersection, difference } from 'oak-domain/lib/utils/lodash';
type ED = EntityDict & BaseEntityDict;
export default function render(
props: WebComponentProps<
ED,
'relationAuth',
true,
{
relationIds: string[];
relationAuths: ED['relationAuth']['OpSchema'][];
auths: AuthCascadePath<ED>[];
sourceRelations: ED['relation']['OpSchema'][];
},
{
onChange: (checked: boolean, relationId: string, path: string, relationAuths?: ED['relationAuth']['OpSchema'][]) => void;
confirm: () => void;
}
>
) {
const { relationIds, relationAuths, oakDirty, auths, sourceRelations } = props.data;
const { onChange, t, clean, confirm } = props.methods;
return (
<>
<Table
columns={[
{
key: '1',
title: t('relationAuth:attr.sourceEntity'),
width: 100,
render: (value, record) => record[2],
},
{
key: '1',
title: t('relationAuth:attr.path'),
width: 200,
render: (value, record) => record[1],
},
{
fixed: 'right',
title: t('grantedRoles'),
key: 'roles',
width: 300,
render: (value, record) => {
const sourceEntity = record[2];
const relations = sourceRelations.filter(
ele => ele.entity === sourceEntity && !relationIds.includes(ele.id)
);
return (
<div style={{
width: '100%',
display: 'flex',
flexWrap: 'wrap',
}}>
{
relations?.map(
(r) => {
const disabled = relationIds.length === 0;
let checked = false, indeterminate = false;
if (!disabled && relationAuths) {
const includedRelationIds = [] as string[];
for(const auth of relationAuths) {
if (!auth.$$deleteAt$$ && auth.sourceRelationId === r.id && auth.path === record[1]) {
includedRelationIds.push(auth.destRelationId);
}
}
checked = difference(relationIds, includedRelationIds).length === 0;
indeterminate = !checked && intersection(relationIds, includedRelationIds).length > 0;
}
return (
<Checkbox
disabled={disabled}
checked={checked}
indeterminate={indeterminate}
onChange={({ target }) => {
const { checked } = target;
const refRelationAuths = relationAuths?.filter(
ele => ele.sourceRelationId === r.id && ele.path === record[1]
&& relationIds!.includes(ele.destRelationId)
);
onChange(checked, r.id, record[1], refRelationAuths)
}}
>
{r.name}
</Checkbox>
);
}
)
}
</div>
)
}
}
]}
dataSource={auths}
pagination={false}
/>
<Row justify="end" style={{ marginTop: 20, padding: 5 }}>
<Button
style={{ marginRight: 10 }}
type='primary'
disabled={!oakDirty}
onClick={() => confirm()}
>
{t("confirm")}
</Button>
<Button
disabled={!oakDirty}
onClick={() => clean()}
>
{t("reset")}
</Button>
</Row>
</>
);
}

View File

@ -0,0 +1,3 @@
{
}

View File

@ -0,0 +1,154 @@
import assert from "assert";
import { uniq } from 'oak-domain/lib/utils/lodash';
import { AuthCascadePath } from "oak-domain/lib/types";
import { ED } from "../../../types/AbstractComponent";
export default OakComponent({
entity: 'relationAuth',
projection: {
id: 1,
sourceRelationId: 1,
sourceRelation: {
id: 1,
name: 1,
display: 1,
entity: 1,
entityId: 1,
},
paths: 1,
destRelationId: 1,
destRelation: {
id: 1,
entity: 1,
name: 1,
display: 1,
entityId: 1,
},
},
isList: true,
properties: {
relationId: '',
entity: '' as keyof ED,
},
filters: [
{
filter() {
const { relationId } = this.props;
assert(relationId);
return {
sourceRelationId: relationId,
};
}
}
],
pagination: {
pageSize: 1000,
currentPage: 0,
},
formData({ data }) {
const { entity, relationId } = this.props;
const [relation] = this.features.cache.get('relation', {
data: {
id: 1,
name: 1,
display: 1,
},
filter: {
id: relationId,
},
});
const { name } = relation || {};
const cascadeEntities = this.features.relationAuth.getCascadeRelationAuthsBySource(entity!);
const cascadeEntityRelations = cascadeEntities.map(
(ele) => {
const [de, p, se] = ele;
// 可能的relations只查询系统规定的relation不考虑定制的relation
const relations = this.features.cache.get('relation', {
data: {
id: 1,
entity: 1,
name: 1,
entityId: 1,
display: 1,
},
filter: {
entity: de as string,
entityId: {
$exists: false,
},
$not: {
entity: entity as string,
id: relationId,
}, // 自己不能管自己的权限
},
});
//已授权的relation中可能有定制的relationId
const authedRelations = data ? data.filter(
(ele) => ele.path === p
) : [];
return {
entity: de,
path: p,
relations,
authedRelations,
};
}
).filter(
(ele) => ele.relations.length > 0 || ele.authedRelations.length > 0
);
return {
relationName: name,
cascadeEntityRelations,
};
},
lifetimes: {
ready() {
const { entity } = this.props;
const cascadeRelationEntities = this.features.relationAuth.getCascadeRelationAuthsBySource(entity!);
const cascadeEntities = uniq(cascadeRelationEntities.map(ele => ele[0])) as string[];
if (cascadeEntities.length > 0) {
this.features.cache.refresh('relation', {
data: {
id: 1,
entity: 1,
name: 1,
entityId: 1,
},
filter: {
entity: {
$in: cascadeEntities,
},
entityId: {
$exists: false,
},
},
});
}
},
},
methods: {
onChange(relationId: string, checked: boolean, relationAuthId?: string, path?: string) {
if (checked) {
if (relationAuthId) {
this.recoverItem(relationAuthId);
}
else {
this.addItem({
sourceRelationId: this.props.relationId!,
path,
destRelationId: relationId,
});
}
}
else {
assert(relationAuthId);
this.removeItem(relationAuthId);
}
},
confirm() {
this.execute();
}
}
})

View File

@ -0,0 +1 @@
{"confirm": "确认", "reset": "重置"}

View File

@ -0,0 +1,159 @@
import { Table, Checkbox, Button, Row } from 'antd';
import { Typography } from 'antd';
const { Title, Text } = Typography;
import { RowWithActions, WebComponentProps } from '../../../types/Page';
import { AuthCascadePath, EntityDict } from 'oak-domain/lib/types/Entity';
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
import { assert } from 'oak-domain/lib/utils/assert';
type ED = EntityDict & BaseEntityDict;
export default function render(
props: WebComponentProps<
ED,
'relationAuth',
true,
{
entity: string;
relationName: string;
cascadeEntityRelations: Array<{
entity: keyof ED;
path: string;
relations: ED['relation']['Schema'][];
authedRelations: ED['relationAuth']['Schema'][];
}>;
},
{
onChange: (relationId: string, checked: boolean, relationAuthId?: string, path?: string) => void;
confirm: () => void;
}
>
) {
const { cascadeEntityRelations, oakDirty, entity, relationName } = props.data;
const { onChange, t, clean, confirm } = props.methods;
return (
<div style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'stretch',
width: '100%'
}}>
<Row justify="center" style={{ margin: 20, padding: 10 }}>
<Row style={{ marginRight: 10 }}>
<Title level={4}></Title>
<Text code>{entity}</Text>
</Row>
<Row>
<Title level={4}></Title>
<Text code>{relationName}</Text>
</Row>
</Row>
<Table
columns={[
{
key: '1',
title: '对象',
width: 100,
render: (value, record) => {
const { entity } = record;
return entity;
},
},
{
key: '1',
title: '路径',
width: 200,
render: (value, record) => {
const { path } = record;
return path;
},
},
{
fixed: 'right',
title: '角色',
key: 'operation',
width: 300,
render: (value, record) => {
const { relations, authedRelations, path } = record;
// 可能存在定制的relation
const customizedRelationsAuthed: ED['relation']['Schema'][] = [];
const relationIdAuthed: string[] = [];
authedRelations.forEach(
(ele) => {
if (!ele.$$deleteAt$$) {
if (relations.find(ele2 => ele2.id === ele.destRelationId)) {
relationIdAuthed.push(ele.destRelationId);
}
else {
customizedRelationsAuthed.push(ele.destRelation!);
}
}
}
);
return (
<div style={{
width: '100%',
display: 'flex',
flexWrap: 'wrap',
}}>
{relations.map(
(ele) => {
const authed = relationIdAuthed.includes(ele.id);
return (
<Checkbox
key={ele.id!}
checked={authed}
onChange={({ target }) => {
const { checked } = target;
const relationAuth = authedRelations.find(ele2 => ele2.destRelationId === ele.id);
if (relationAuth) {
onChange(ele.id, checked, relationAuth.id);
}
else {
onChange(ele.id, checked, undefined, path);
}
}}
>
{ele.display || ele.name}
</Checkbox>
);
}
)}
{customizedRelationsAuthed.map(
(ele) => <Checkbox
key={ele.id!}
checked={true}
disabled={true}
>
{ele.display || ele.name}
</Checkbox>
)}
</div>
);
}
}
]}
dataSource={cascadeEntityRelations}
pagination={false}
/>
<Row justify="end" style={{ marginTop: 20, padding: 5 }}>
<Button
style={{ marginRight: 10 }}
type='primary'
disabled={!oakDirty}
onClick={() => confirm()}
>
{t("confirm")}
</Button>
<Button
disabled={!oakDirty}
onClick={() => clean()}
>
{t("reset")}
</Button>
</Row>
</div>
);
}

View File

@ -252,6 +252,9 @@ export default OakComponent({
const dASameActionAuth = actionAuths.find((ele) => difference(actions, ele.deActions).length === 0);
// 存在deActions相同paths push并做去重处理
if (dASameActionAuth) {
if (dASameActionAuth.$$deleteAt$$ && dASameActionAuth.$$deleteAt$$ === 1) {
this.recoverItem(dASameActionAuth.id);
}
this.updateItem({
paths: dASameActionAuth.paths.concat(path),
}, dASameActionAuth.id)