ModelAuth.tsx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442
  1. import React from 'react'
  2. import classnames from 'classnames'
  3. import memoizeOne from 'memoize-one'
  4. import { Table, Tabs, Radio, Checkbox, Select, Row, Col, Button, Tag, Tooltip, Icon } from 'antd'
  5. const { Column } = Table
  6. const { TabPane } = Tabs
  7. const RadioGroup = Radio.Group
  8. const { Option } = Select
  9. import { RadioChangeEvent } from 'antd/lib/radio'
  10. import { CheckboxChangeEvent } from 'antd/lib/checkbox'
  11. import { TableProps, ColumnProps } from 'antd/lib/table'
  12. import { IViewVariable, IViewModelProps, IViewModel, IExecuteSqlResponse, IViewRole, IViewRoleRowAuth } from '../types'
  13. import {
  14. ViewModelTypesLocale,
  15. ViewVariableValueTypes,
  16. ViewModelVisualTypesLocale,
  17. ViewVariableTypes
  18. } from '../constants'
  19. import OperatorTypes from 'utils/operatorTypes'
  20. import ConditionValuesControl, { ConditionValueTypes } from 'components/ConditionValuesControl'
  21. import ModelAuthModal from './ModelAuthModal'
  22. import Styles from '../View.less'
  23. interface IViewRoleRowAuthConverted {
  24. name: string
  25. values: Array<string | number | boolean>
  26. enable: boolean
  27. variable: IViewVariable
  28. }
  29. interface IViewRoleConverted {
  30. roleId: number
  31. roleName: string
  32. roleDesc: string
  33. columnAuth: string[]
  34. rowAuthConverted: {
  35. [variableName: string]: IViewRoleRowAuthConverted
  36. }
  37. }
  38. interface IModelAuthProps {
  39. visible: boolean
  40. model: IViewModel
  41. variable: IViewVariable[]
  42. sqlColumns: IExecuteSqlResponse['columns']
  43. roles: any[] // @FIXME role typing
  44. viewRoles: IViewRole[]
  45. onModelChange: (partialModel: IViewModel) => void
  46. onViewRoleChange: (viewRole: IViewRole[]) => void
  47. onStepChange: (stepChange: number) => void
  48. }
  49. enum EAllCheckedCheckboxStatus {
  50. empty = 1,
  51. indeterminate,
  52. allChecked
  53. }
  54. interface IAllCheckedCheckboxStatus { [variableName: string]: EAllCheckedCheckboxStatus }
  55. interface IModelAuthStates {
  56. modalVisible: boolean
  57. selectedRoleId: number
  58. selectedColumnAuth: string[]
  59. allCheckedCheckboxStatus: IAllCheckedCheckboxStatus
  60. }
  61. export class ModelAuth extends React.PureComponent<IModelAuthProps, IModelAuthStates> {
  62. private authDatasourceMap = new Map<number, IViewRoleConverted>()
  63. public state: Readonly<IModelAuthStates> = {
  64. modalVisible: false,
  65. selectedRoleId: 0,
  66. selectedColumnAuth: [],
  67. allCheckedCheckboxStatus: {}
  68. }
  69. public componentDidUpdate () {
  70. this.checkDataToChangeAllCheckedCheckboxStatus()
  71. }
  72. private modelTypeOptions = Object.entries(ViewModelTypesLocale).map(([value, label]) => ({
  73. label,
  74. value
  75. }))
  76. private visualTypeOptions = Object.entries(ViewModelVisualTypesLocale).map(([visualType, text]) => (
  77. <Option key={visualType} value={visualType}>{text}</Option>
  78. ))
  79. private modelChange = (record: IViewModelProps, propName: keyof IViewModelProps) => (e: RadioChangeEvent | string) => {
  80. const value: string = (e as RadioChangeEvent).target ? (e as RadioChangeEvent).target.value : e
  81. const { name, ...rest } = record
  82. const partialModel: IViewModel = {
  83. [name]: {
  84. ...rest,
  85. [propName]: value
  86. }
  87. }
  88. this.props.onModelChange(partialModel)
  89. }
  90. private stepChange = (step: number) => () => {
  91. this.props.onStepChange(step)
  92. }
  93. private setColumnAuth = (viewRole: IViewRoleConverted) => () => {
  94. const { roleId, columnAuth } = viewRole
  95. const { model } = this.props
  96. this.setState({
  97. modalVisible: true,
  98. selectedRoleId: roleId,
  99. selectedColumnAuth: columnAuth.filter((column) => !!model[column])
  100. })
  101. }
  102. private allCheckedCheckboxStatusChange = (roleId: number, rowAuthConverted: IViewRoleRowAuthConverted) => {
  103. const { name, enable: checked } = rowAuthConverted
  104. this.setState((prevState, props) => {
  105. const { allCheckedCheckboxStatus } = prevState
  106. let status = allCheckedCheckboxStatus[name]
  107. const localItem = this.authDatasourceMap.get(roleId)
  108. localItem.rowAuthConverted[name] = rowAuthConverted
  109. this.authDatasourceMap.set(roleId, localItem)
  110. if (checked) {
  111. const isAllChecked = [...this.authDatasourceMap.values()].every((viewRoleConverted) => viewRoleConverted.rowAuthConverted[name]?.enable)
  112. status = isAllChecked ? EAllCheckedCheckboxStatus.allChecked : EAllCheckedCheckboxStatus.indeterminate
  113. } else {
  114. const isEmpty = [...this.authDatasourceMap.values()].every((viewRoleConverted) => !viewRoleConverted.rowAuthConverted[name]?.enable)
  115. status = isEmpty ? EAllCheckedCheckboxStatus.empty : EAllCheckedCheckboxStatus.indeterminate
  116. }
  117. if (status !== allCheckedCheckboxStatus[name]) {
  118. return { allCheckedCheckboxStatus: { ...allCheckedCheckboxStatus, [name]: status } }
  119. }
  120. })
  121. }
  122. private checkDataToChangeAllCheckedCheckboxStatus = () => {
  123. this.authDatasourceMap.forEach((viewRoleConverted, roleId) => {
  124. const { rowAuthConverted } = viewRoleConverted
  125. Object.values(rowAuthConverted).forEach((viewRoleRowAuthConverted) => {
  126. this.allCheckedCheckboxStatusChange(roleId, viewRoleRowAuthConverted)
  127. })
  128. })
  129. }
  130. private rowAuthCheckedChange = (roleId: number, rowAuthConverted: IViewRoleRowAuthConverted) => (e: CheckboxChangeEvent) => {
  131. const checked = e.target.checked
  132. const { name, values } = rowAuthConverted
  133. const updatedRoleAuth: IViewRoleRowAuth = {
  134. name,
  135. values,
  136. enable: checked
  137. }
  138. this.viewRoleChange(roleId, updatedRoleAuth)
  139. this.allCheckedCheckboxStatusChange(roleId, { ...rowAuthConverted, enable: checked })
  140. }
  141. private rowAuthCheckedChangeAll = (variableName: string) => (e: CheckboxChangeEvent) => {
  142. const checked = e.target.checked
  143. this.viewRoleChangeAll(variableName, checked)
  144. }
  145. private viewRoleChangeAll = (checkedVariableName: string, checked: boolean) => {
  146. const { onViewRoleChange } = this.props
  147. const viewRoles = [...this.authDatasourceMap].map(([roleId, viewRoleConverted]) => {
  148. const { columnAuth, rowAuthConverted } = viewRoleConverted
  149. const rowAuth = Object.entries(rowAuthConverted).map(([variableName, viewRoleAuthConverted]) => {
  150. const { name, values, enable } = viewRoleAuthConverted
  151. return { name, values, enable: checkedVariableName === name ? checked : enable }
  152. })
  153. return { roleId, columnAuth, rowAuth }
  154. })
  155. onViewRoleChange(viewRoles)
  156. }
  157. private rowAuthValuesChange = (roleId: number, rowAuthConverted: IViewRoleRowAuthConverted) => (values: Array<string | number | boolean>) => {
  158. const { name, enable } = rowAuthConverted
  159. const updatedRoleAuth: IViewRoleRowAuth = {
  160. name,
  161. values,
  162. enable
  163. }
  164. this.viewRoleChange(roleId, updatedRoleAuth)
  165. }
  166. private viewRoleChange = (roleId: number, updatedRoleAuth: IViewRoleRowAuth) => {
  167. const { onViewRoleChange, viewRoles } = this.props
  168. let viewRole = viewRoles.find((v) => v.roleId === roleId)
  169. if (!viewRole) {
  170. viewRole = {
  171. roleId,
  172. columnAuth: [],
  173. rowAuth: [updatedRoleAuth]
  174. }
  175. } else {
  176. const variableIdx = viewRole.rowAuth.findIndex((auth) => auth.name === updatedRoleAuth.name)
  177. if (variableIdx < 0) {
  178. viewRole.rowAuth.push(updatedRoleAuth)
  179. } else {
  180. viewRole.rowAuth[variableIdx].values = updatedRoleAuth.values
  181. viewRole.rowAuth[variableIdx].enable = updatedRoleAuth.enable
  182. }
  183. }
  184. onViewRoleChange([{ ...viewRole }])
  185. }
  186. private getAuthTableColumns = memoizeOne((model: IViewModel, variables: IViewVariable[], allCheckedCheckboxStatus: IAllCheckedCheckboxStatus) => {
  187. const columnsChildren = variables
  188. .filter((v) => (v.type === ViewVariableTypes.Authorization && !v.fromService))
  189. .map<ColumnProps<IViewRoleConverted>>((variable) => ({
  190. title: (
  191. <>
  192. <Checkbox
  193. checked={allCheckedCheckboxStatus?.[variable.name] === EAllCheckedCheckboxStatus.allChecked}
  194. indeterminate={allCheckedCheckboxStatus?.[variable.name] === EAllCheckedCheckboxStatus.indeterminate}
  195. className={Styles.cellVarCheckbox}
  196. onChange={this.rowAuthCheckedChangeAll(variable.name)}
  197. />
  198. {`${variable.alias || variable.name}`}
  199. </>
  200. ),
  201. dataIndex: 'rowAuthConverted' + variable.key,
  202. width: 250,
  203. render: (_, record: IViewRoleConverted) => {
  204. const { name: variableName, valueType } = variable
  205. const { roleId, rowAuthConverted } = record
  206. const { values: rowAuthValues, enable } = rowAuthConverted[variableName]
  207. const operatorType = (valueType === ViewVariableValueTypes.Boolean ? OperatorTypes.Equal : OperatorTypes.In)
  208. return (
  209. <div className={Styles.cellVarValue}>
  210. <Tooltip title={enable ? '禁用' : '启用'}>
  211. <Checkbox
  212. checked={enable}
  213. className={Styles.cellVarCheckbox}
  214. onChange={this.rowAuthCheckedChange(roleId, rowAuthConverted[variableName])}
  215. />
  216. </Tooltip>
  217. {enable && (
  218. <ConditionValuesControl
  219. className={Styles.cellVarInput}
  220. size="default"
  221. visualType={valueType}
  222. operatorType={operatorType}
  223. conditionValues={rowAuthValues}
  224. onChange={this.rowAuthValuesChange(roleId, rowAuthConverted[variableName])}
  225. />
  226. )}
  227. </div>
  228. )
  229. }
  230. }))
  231. const columns: Array<ColumnProps<IViewRoleConverted>> = [{
  232. title: '角色',
  233. dataIndex: 'roleName',
  234. width: 300,
  235. render: (roleName: string, record: IViewRoleConverted) => (
  236. <span>
  237. {roleName}
  238. {record.roleDesc && (
  239. <Tooltip title={record.roleDesc}>
  240. <Icon className={Styles.cellIcon} type="info-circle" />
  241. </Tooltip>
  242. )}
  243. </span>
  244. )
  245. }]
  246. if (columnsChildren.length > 0) {
  247. columns.push({
  248. title: '权限变量值设置',
  249. children: columnsChildren
  250. })
  251. }
  252. columns.push({
  253. title: '可见字段',
  254. dataIndex: 'columnAuth',
  255. width: 120,
  256. render: (columnAuth: string[], record) => {
  257. if (columnAuth.length === 0) {
  258. return (<Tag onClick={this.setColumnAuth(record)}>全部可见</Tag>)
  259. }
  260. if (columnAuth.length === Object.keys(model).length) {
  261. return (<Tag onClick={this.setColumnAuth(record)} color="#f50">不可见</Tag>)
  262. }
  263. return (<Tag color="green" onClick={this.setColumnAuth(record)}>部分可见</Tag>)
  264. }
  265. })
  266. return columns
  267. })
  268. private getAuthTableScroll = memoizeOne((columns: Array<ColumnProps<any>>) => {
  269. const scroll: TableProps<any>['scroll'] = {}
  270. const columnsTotalWidth = columns.reduce((acc, c) => acc + (c.width as number), 0)
  271. scroll.x = columnsTotalWidth
  272. return scroll
  273. })
  274. private getAuthDatasource = (roles: any[], varibles: IViewVariable[], viewRoles: IViewRole[]) => {
  275. if (!Array.isArray(roles)) { return [] }
  276. const authDatasourceMap = new Map<number, IViewRoleConverted>()
  277. const authDatasource = roles.map<IViewRoleConverted>((role) => {
  278. const { id: roleId, name: roleName, description: roleDesc } = role
  279. const viewRole = viewRoles.find((v) => v.roleId === roleId)
  280. const columnAuth = viewRole ? viewRole.columnAuth : []
  281. const rowAuthConverted = varibles.reduce<IViewRoleConverted['rowAuthConverted']>((obj, variable) => {
  282. const { name: variableName, type, fromService } = variable
  283. if (type === ViewVariableTypes.Query) { return obj }
  284. if (type === ViewVariableTypes.Authorization && fromService) { return obj }
  285. const authIdx = viewRole ? viewRole.rowAuth.findIndex((auth) => auth.name === variableName) : -1
  286. obj[variableName] = {
  287. name: variableName,
  288. values: [],
  289. enable: false,
  290. variable
  291. }
  292. if (authIdx >= 0) {
  293. const { enable, values } = viewRole.rowAuth[authIdx]
  294. obj[variableName] = {
  295. ...obj[variableName],
  296. enable,
  297. values
  298. }
  299. }
  300. return obj
  301. }, {})
  302. const authDatasourceItem = {
  303. roleId,
  304. roleName,
  305. roleDesc,
  306. columnAuth,
  307. rowAuthConverted
  308. }
  309. authDatasourceMap.set(roleId, authDatasourceItem)
  310. return authDatasourceItem
  311. })
  312. this.authDatasourceMap = authDatasourceMap
  313. return authDatasource
  314. }
  315. private renderColumnModelType = (text: string, record) => (
  316. <RadioGroup
  317. options={this.modelTypeOptions}
  318. value={text}
  319. onChange={this.modelChange(record, 'modelType')}
  320. />
  321. )
  322. private renderColumnVisualType = (text: string, record) => (
  323. <Select
  324. className={Styles.tableControl}
  325. value={text}
  326. onChange={this.modelChange(record, 'visualType')}
  327. >
  328. {this.visualTypeOptions}
  329. </Select>
  330. )
  331. private saveModelAuth = (columnAuth: string[]) => {
  332. const { onViewRoleChange, viewRoles } = this.props
  333. const { selectedRoleId } = this.state
  334. let viewRole = viewRoles.find((v) => v.roleId === selectedRoleId)
  335. if (!viewRole) {
  336. viewRole = {
  337. roleId: selectedRoleId,
  338. columnAuth,
  339. rowAuth: []
  340. }
  341. } else {
  342. viewRole = {
  343. ...viewRole,
  344. columnAuth
  345. }
  346. }
  347. onViewRoleChange([viewRole])
  348. this.closeModelAuth()
  349. }
  350. private closeModelAuth = () => {
  351. this.setState({ modalVisible: false })
  352. }
  353. public render () {
  354. const { visible, model, variable, viewRoles, sqlColumns, roles, onModelChange } = this.props
  355. const { modalVisible, selectedColumnAuth, selectedRoleId, allCheckedCheckboxStatus } = this.state
  356. const modelDatasource = Object.entries(model).map(([name, value]) => ({ name, ...value }))
  357. const authColumns = this.getAuthTableColumns(model, variable, allCheckedCheckboxStatus)
  358. const authScroll = this.getAuthTableScroll(authColumns)
  359. const authDatasource = this.getAuthDatasource(roles, variable, viewRoles)
  360. const styleCls = classnames({
  361. [Styles.containerHorizontal]: true,
  362. [Styles.modelAuth]: true
  363. })
  364. const style = visible ? {} : { display: 'none' }
  365. return (
  366. <div className={styleCls} style={style}>
  367. <Tabs defaultActiveKey="model" className={Styles.authTab}>
  368. <TabPane tab="模型" key="model">
  369. <div className={Styles.authTable}>
  370. <Table bordered pagination={false} rowKey="name" dataSource={modelDatasource}>
  371. <Column title="字段名称" dataIndex="name" />
  372. <Column title="数据类型" dataIndex="modelType" render={this.renderColumnModelType} />
  373. <Column title="可视化类型" dataIndex="visualType" render={this.renderColumnVisualType} />
  374. </Table>
  375. </div>
  376. </TabPane>
  377. <TabPane tab="权限" key="auth">
  378. <div className={Styles.authTable}>
  379. <Table
  380. bordered
  381. rowKey="roleId"
  382. pagination={false}
  383. columns={authColumns}
  384. scroll={authScroll}
  385. dataSource={authDatasource}
  386. />
  387. </div>
  388. <ModelAuthModal
  389. visible={modalVisible}
  390. model={model}
  391. roleId={selectedRoleId}
  392. auth={selectedColumnAuth}
  393. onSave={this.saveModelAuth}
  394. onCancel={this.closeModelAuth}
  395. />
  396. </TabPane>
  397. </Tabs>
  398. <Row className={Styles.bottom} type="flex" align="middle" justify="end">
  399. <Col span={12} className={Styles.toolBtns}>
  400. <Button type="primary" onClick={this.stepChange(-1)}>上一步</Button>
  401. <Button onClick={this.stepChange(-2)}>取消</Button>
  402. <Button onClick={this.stepChange(1)}>保存</Button>
  403. </Col>
  404. </Row>
  405. </div>
  406. )
  407. }
  408. }
  409. export default ModelAuth