muffin-admin 1.28.1 → 1.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.babelrc +5 -0
- package/.eslintrc.json +44 -0
- package/admin.jsx +50 -46
- package/authprovider.js +53 -48
- package/buttons/ActionButton.jsx +76 -45
- package/buttons/LinkButton.jsx +12 -12
- package/dashboard.jsx +46 -29
- package/dataprovider.js +62 -49
- package/fields/AvatarField.jsx +28 -19
- package/fields/FKField.jsx +16 -12
- package/fields/JsonField.jsx +25 -0
- package/hooks/useAction.js +9 -0
- package/index.js +7 -4
- package/inputs/FKInput.jsx +29 -17
- package/inputs/JsonInput.jsx +62 -0
- package/jsconfig.json +9 -0
- package/package.json +15 -12
- package/resources.jsx +25 -15
- package/ui.jsx +89 -33
- package/utils.js +23 -19
- package/views/create.jsx +22 -18
- package/views/edit.jsx +34 -26
- package/views/list.jsx +62 -32
- package/views/show.jsx +35 -26
- package/webpack.config.js +19 -18
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"root": true,
|
|
3
|
+
"extends": [
|
|
4
|
+
"eslint:recommended",
|
|
5
|
+
"plugin:react/recommended",
|
|
6
|
+
"prettier"
|
|
7
|
+
],
|
|
8
|
+
"parser": "@babel/eslint-parser",
|
|
9
|
+
"settings": {
|
|
10
|
+
"react": {
|
|
11
|
+
"version": "detect"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"parserOptions": {
|
|
15
|
+
"ecmaFeatures": {
|
|
16
|
+
"jsx": true,
|
|
17
|
+
"experimentalObjectRestSpread": true
|
|
18
|
+
},
|
|
19
|
+
"ecmaVersion": 2020,
|
|
20
|
+
"sourceType": "module",
|
|
21
|
+
"babelOptions": {
|
|
22
|
+
"presets": [
|
|
23
|
+
"@babel/preset-react"
|
|
24
|
+
],
|
|
25
|
+
"rootMode": "upward"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"plugins": [
|
|
29
|
+
"prettier",
|
|
30
|
+
"react"
|
|
31
|
+
],
|
|
32
|
+
"env": {
|
|
33
|
+
"browser": true,
|
|
34
|
+
"es6": true,
|
|
35
|
+
"jest": true,
|
|
36
|
+
"node": true
|
|
37
|
+
},
|
|
38
|
+
"rules": {
|
|
39
|
+
"no-console": "warn",
|
|
40
|
+
"no-debugger": "warn",
|
|
41
|
+
"prettier/prettier": "warn",
|
|
42
|
+
"react/prop-types": "off"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/admin.jsx
CHANGED
|
@@ -1,56 +1,60 @@
|
|
|
1
|
-
import React from
|
|
1
|
+
import React from 'react'
|
|
2
2
|
|
|
3
|
-
import { Admin, AppBar, Layout, Login } from
|
|
4
|
-
import { IconButton, SvgIcon, Tooltip, Typography } from
|
|
5
|
-
import * as icons from
|
|
3
|
+
import { Admin, AppBar, Layout, Login } from 'react-admin'
|
|
4
|
+
import { IconButton, SvgIcon, Tooltip, Typography } from '@mui/material'
|
|
5
|
+
import * as icons from '@mui/icons-material'
|
|
6
6
|
|
|
7
|
-
import authProvider from
|
|
8
|
-
import dataProvider from
|
|
7
|
+
import authProvider from './authprovider'
|
|
8
|
+
import dataProvider from './dataprovider'
|
|
9
9
|
import { processAdmin, setupAdmin } from './utils'
|
|
10
10
|
|
|
11
|
-
import './dashboard.jsx'
|
|
12
|
-
import './resources.jsx'
|
|
13
|
-
|
|
11
|
+
import './dashboard.jsx'
|
|
12
|
+
import './resources.jsx'
|
|
14
13
|
|
|
15
14
|
// Initialize the admin
|
|
16
|
-
setupAdmin('admin', props => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
15
|
+
setupAdmin('admin', (props) => {
|
|
16
|
+
const { apiUrl, auth, adminProps, appBarLinks, dashboard, resources } = props
|
|
17
|
+
|
|
18
|
+
let appBar = (props) => (
|
|
19
|
+
<AppBar {...props}>
|
|
20
|
+
<Typography
|
|
21
|
+
variant='h6'
|
|
22
|
+
color='inherit'
|
|
23
|
+
id='react-admin-title'
|
|
24
|
+
style={{
|
|
25
|
+
flex: 1,
|
|
26
|
+
textOverflow: 'ellipsis',
|
|
27
|
+
whiteSpace: 'nowrap',
|
|
28
|
+
overflow: 'hidden',
|
|
29
|
+
}}
|
|
30
|
+
/>
|
|
31
|
+
{appBarLinks.map((info) => (
|
|
32
|
+
<Tooltip key={info.url} title={info.title}>
|
|
33
|
+
<IconButton color='inherit' href={info.url}>
|
|
34
|
+
<SvgIcon component={icons[info.icon]} />
|
|
35
|
+
</IconButton>
|
|
36
|
+
</Tooltip>
|
|
37
|
+
))}
|
|
38
|
+
</AppBar>
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<Admin
|
|
43
|
+
authProvider={processAdmin('auth', auth)}
|
|
44
|
+
dataProvider={processAdmin('data', apiUrl)}
|
|
45
|
+
dashboard={processAdmin('dashboard', dashboard)}
|
|
46
|
+
loginPage={processAdmin('login', auth)}
|
|
47
|
+
layout={(props) => <Layout appBar={appBar} {...props} />}
|
|
48
|
+
{...adminProps}
|
|
49
|
+
>
|
|
50
|
+
{processAdmin('resources', resources)}
|
|
51
|
+
</Admin>
|
|
52
|
+
)
|
|
53
|
+
})
|
|
50
54
|
|
|
51
55
|
// Initialize authentication and data providers
|
|
52
|
-
setupAdmin('auth', authProvider)
|
|
53
|
-
setupAdmin('data', dataProvider)
|
|
56
|
+
setupAdmin('auth', authProvider)
|
|
57
|
+
setupAdmin('data', dataProvider)
|
|
54
58
|
|
|
55
59
|
// Initialize login page
|
|
56
|
-
setupAdmin('login', (
|
|
60
|
+
setupAdmin('login', () => Login)
|
package/authprovider.js
CHANGED
|
@@ -1,79 +1,84 @@
|
|
|
1
1
|
import Cookies from 'js-cookie'
|
|
2
|
-
import { makeRequest, requestHeaders } from './utils'
|
|
3
|
-
|
|
2
|
+
import { makeRequest, processAdmin, requestHeaders, setupAdmin } from './utils'
|
|
3
|
+
|
|
4
|
+
setupAdmin(
|
|
5
|
+
'auth-get',
|
|
6
|
+
({ storage }) =>
|
|
7
|
+
(name) =>
|
|
8
|
+
storage == 'localstorage' ? localStorage.getItem(name) : Cookies.get(name)
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
setupAdmin('auth-set', ({ storage }) => (name, value) => {
|
|
12
|
+
if (storage == 'localstorage') {
|
|
13
|
+
localStorage.setItem(name, value)
|
|
14
|
+
requestHeaders['Authorization'] = value
|
|
15
|
+
} else Cookies.set(name, value)
|
|
16
|
+
})
|
|
4
17
|
|
|
5
18
|
export default (props) => {
|
|
19
|
+
const { identityURL, authorizeURL, logoutURL, required, storage_name } = props
|
|
6
20
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
const authorize = (name, value) => {
|
|
10
|
-
if (value === undefined) {
|
|
11
|
-
return storage == 'localstorage' ? localStorage.getItem(name) : Cookies.get(name);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
if (storage == 'localstorage') requestHeaders['Authorization'] = value;
|
|
15
|
-
storage == 'localstorage' ? localStorage.setItem(name, value) : Cookies.set(name, value);
|
|
16
|
-
return value;
|
|
17
|
-
}
|
|
21
|
+
const authGet = processAdmin('auth-get', props)
|
|
22
|
+
const authSet = processAdmin('auth-set', props)
|
|
18
23
|
|
|
19
24
|
// Initialize request headers
|
|
20
|
-
|
|
25
|
+
authSet(storage_name, authGet(storage_name))
|
|
21
26
|
|
|
22
27
|
let getIdentity = async () => {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
if (identityURL) {
|
|
29
|
+
let { json } = await makeRequest(identityURL)
|
|
30
|
+
return json
|
|
31
|
+
}
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
if (required)
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
checkAuth: async (data) => {
|
|
34
|
-
const auth = authorize(storage_name);
|
|
35
|
-
if (!auth) throw {message: 'Authorization required'}
|
|
36
|
-
|
|
37
|
-
if (!identityURL) return auth;
|
|
38
|
-
|
|
39
|
-
let user = await getIdentity();
|
|
40
|
-
if (!user) throw {message: 'Authorization required'};
|
|
34
|
+
if (required)
|
|
35
|
+
return {
|
|
36
|
+
login: async (data) => {
|
|
37
|
+
if (!authorizeURL) throw { message: 'Authorization is not supported' }
|
|
41
38
|
|
|
42
|
-
|
|
39
|
+
let { json } = await makeRequest(authorizeURL, { data, method: 'POST' })
|
|
40
|
+
authSet(storage_name, json)
|
|
43
41
|
},
|
|
44
42
|
|
|
45
43
|
checkError: async (error) => {
|
|
46
|
-
const {
|
|
44
|
+
const { status } = error
|
|
47
45
|
|
|
48
46
|
if (status == 401 || status == 403) {
|
|
49
47
|
// Clean storage
|
|
50
|
-
|
|
48
|
+
authSet(storage_name, '')
|
|
51
49
|
|
|
52
|
-
throw {
|
|
50
|
+
throw {
|
|
51
|
+
message: 'Invalid authorization',
|
|
52
|
+
redirectTo: logoutURL,
|
|
53
|
+
logoutUser: !logoutURL,
|
|
54
|
+
}
|
|
53
55
|
}
|
|
54
|
-
|
|
55
56
|
},
|
|
56
57
|
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
checkAuth: async (data) => {
|
|
59
|
+
const auth = authGet(storage_name)
|
|
60
|
+
if (!auth) throw { message: 'Authorization required' }
|
|
61
|
+
|
|
62
|
+
if (!identityURL) return auth
|
|
59
63
|
|
|
60
|
-
let
|
|
61
|
-
|
|
64
|
+
let user = await getIdentity()
|
|
65
|
+
if (!user) throw { message: 'Authorization required' }
|
|
66
|
+
|
|
67
|
+
return user
|
|
62
68
|
},
|
|
63
69
|
|
|
64
70
|
logout: async () => {
|
|
71
|
+
// Clean storage
|
|
72
|
+
authSet(storage_name, '')
|
|
65
73
|
|
|
66
|
-
|
|
67
|
-
authorize(storage_name, '');
|
|
68
|
-
|
|
69
|
-
if (logoutURL) window.location = logoutURL;
|
|
74
|
+
if (logoutURL) window.location = logoutURL
|
|
70
75
|
},
|
|
71
76
|
|
|
77
|
+
getIdentity,
|
|
78
|
+
|
|
72
79
|
getPermissions: (data) => {
|
|
73
|
-
const role =
|
|
74
|
-
return role ? Promise.resolve(role) : Promise.reject()
|
|
80
|
+
const role = authGet(storage_name + '_role')
|
|
81
|
+
return role ? Promise.resolve(role) : Promise.reject()
|
|
75
82
|
},
|
|
76
|
-
|
|
77
|
-
}
|
|
78
|
-
|
|
83
|
+
}
|
|
79
84
|
}
|
package/buttons/ActionButton.jsx
CHANGED
|
@@ -1,68 +1,99 @@
|
|
|
1
|
-
import React from 'react'
|
|
1
|
+
import React, { useEffect } from 'react'
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import * as icons from "@material-ui/icons"
|
|
3
|
+
import useAction from '../hooks/useAction'
|
|
5
4
|
|
|
5
|
+
import {
|
|
6
|
+
Button,
|
|
7
|
+
useRefresh,
|
|
8
|
+
useUnselectAll,
|
|
9
|
+
useNotify,
|
|
10
|
+
useRecordContext,
|
|
11
|
+
} from 'react-admin'
|
|
12
|
+
import * as icons from '@mui/icons-material'
|
|
6
13
|
|
|
7
|
-
export const BulkActionButton = ({
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
export const BulkActionButton = ({
|
|
15
|
+
label,
|
|
16
|
+
icon,
|
|
17
|
+
title,
|
|
18
|
+
action,
|
|
19
|
+
resource,
|
|
20
|
+
selectedIds,
|
|
21
|
+
}) => {
|
|
22
|
+
const refresh = useRefresh(),
|
|
23
|
+
unselectAll = useUnselectAll(resource),
|
|
24
|
+
notify = useNotify(),
|
|
25
|
+
Icon = icons[icon]
|
|
11
26
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
const mutation = useAction(resource, action)
|
|
28
|
+
|
|
29
|
+
let onClick = () => {
|
|
30
|
+
mutation.mutate(
|
|
31
|
+
{
|
|
32
|
+
action,
|
|
33
|
+
ids: selectedIds,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
onSuccess: ({ data }) => {
|
|
37
|
+
if (data && data.message) notify(data.message, { type: 'success' })
|
|
38
|
+
refresh()
|
|
39
|
+
unselectAll()
|
|
40
|
+
},
|
|
41
|
+
onError: (err) => {
|
|
42
|
+
notify(typeof err === 'string' ? err : err.message, { type: 'error' })
|
|
22
43
|
},
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
}
|
|
26
|
-
})
|
|
44
|
+
}
|
|
45
|
+
)
|
|
27
46
|
}
|
|
28
47
|
|
|
29
48
|
return (
|
|
30
|
-
<Button
|
|
31
|
-
{
|
|
49
|
+
<Button
|
|
50
|
+
label={label}
|
|
51
|
+
title={title}
|
|
52
|
+
onClick={onClick}
|
|
53
|
+
disabled={mutation.isLoading}
|
|
54
|
+
>
|
|
55
|
+
{Icon && <Icon />}
|
|
32
56
|
</Button>
|
|
33
57
|
)
|
|
34
58
|
}
|
|
35
59
|
|
|
36
|
-
export const ActionButton = (
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
let onClick = event => {
|
|
41
|
-
mutate({
|
|
42
|
-
resource,
|
|
43
|
-
type: 'runAction',
|
|
44
|
-
payload: {action, record},
|
|
45
|
-
}, {
|
|
46
|
-
onSuccess: res => {
|
|
47
|
-
let data = res && res.data;
|
|
60
|
+
export const ActionButton = (props) => {
|
|
61
|
+
const { icon, label, title, resource, action } = props
|
|
62
|
+
const record = useRecordContext()
|
|
63
|
+
const mutation = useAction(resource, action)
|
|
48
64
|
|
|
49
|
-
|
|
50
|
-
|
|
65
|
+
const refresh = useRefresh(),
|
|
66
|
+
notify = useNotify(),
|
|
67
|
+
Icon = icons[icon]
|
|
51
68
|
|
|
52
|
-
|
|
69
|
+
let onClick = () => {
|
|
70
|
+
mutation.mutate(
|
|
71
|
+
{ record },
|
|
72
|
+
{
|
|
73
|
+
onSuccess: ({ data }) => {
|
|
74
|
+
refresh()
|
|
75
|
+
if (data && data.redirectTo) window.location = data.redirectTo
|
|
76
|
+
if (data && data.message) notify(data.message, { type: 'success' })
|
|
53
77
|
},
|
|
54
|
-
|
|
55
|
-
notify(typeof
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
onError: (error) => {
|
|
79
|
+
notify(typeof error === 'string' ? error : error.message, {
|
|
80
|
+
type: 'error',
|
|
81
|
+
})
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
)
|
|
58
85
|
}
|
|
59
86
|
|
|
60
87
|
return (
|
|
61
|
-
<Button
|
|
62
|
-
{
|
|
88
|
+
<Button
|
|
89
|
+
label={label}
|
|
90
|
+
title={title}
|
|
91
|
+
onClick={onClick}
|
|
92
|
+
disabled={mutation.isLoading}
|
|
93
|
+
>
|
|
94
|
+
{Icon && <Icon />}
|
|
63
95
|
</Button>
|
|
64
96
|
)
|
|
65
|
-
|
|
66
97
|
}
|
|
67
98
|
|
|
68
99
|
export default ActionButton
|
package/buttons/LinkButton.jsx
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import React from 'react'
|
|
1
|
+
import React from 'react'
|
|
2
2
|
|
|
3
|
-
import { Button } from 'react-admin'
|
|
4
|
-
import { Link } from 'react-router-dom'
|
|
5
|
-
import * as icons from
|
|
3
|
+
import { Button } from 'react-admin'
|
|
4
|
+
import { Link } from 'react-router-dom'
|
|
5
|
+
import * as icons from '@mui/icons-material'
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
const LinkButton = ({ label, icon, title, resource, filters }) => {
|
|
8
|
+
const Icon = icons[icon] || icon
|
|
9
|
+
let linkParams = {
|
|
10
|
+
pathname: `/${resource}`,
|
|
11
|
+
search: filters ? `filter=${JSON.stringify(filters)}` : undefined,
|
|
12
|
+
}
|
|
13
13
|
|
|
14
14
|
return (
|
|
15
|
-
<Button label={
|
|
16
|
-
{
|
|
15
|
+
<Button label={label} title={title} component={Link} to={linkParams}>
|
|
16
|
+
{Icon && <Icon />}
|
|
17
17
|
</Button>
|
|
18
18
|
)
|
|
19
19
|
}
|
package/dashboard.jsx
CHANGED
|
@@ -1,45 +1,61 @@
|
|
|
1
|
-
import React from
|
|
1
|
+
import React from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
Grid,
|
|
5
|
+
Card,
|
|
6
|
+
CardContent,
|
|
7
|
+
Typography,
|
|
8
|
+
Table,
|
|
9
|
+
TableBody,
|
|
10
|
+
TableRow,
|
|
11
|
+
TableCell,
|
|
12
|
+
} from '@mui/material'
|
|
13
|
+
// import ReactJSON from "react-json-view";
|
|
5
14
|
|
|
6
|
-
import { setupAdmin } from './utils'
|
|
15
|
+
import { setupAdmin } from './utils'
|
|
7
16
|
|
|
8
|
-
|
|
9
|
-
const AdminTableView = ({src}) => (
|
|
17
|
+
const AdminTableView = ({ src }) => (
|
|
10
18
|
<Table>
|
|
11
|
-
<TableBody>
|
|
12
|
-
src.map((row, idx) =>
|
|
13
|
-
<TableRow key={
|
|
14
|
-
row.map((cell, idx) =>
|
|
15
|
-
<TableCell key={
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
)
|
|
19
|
-
|
|
19
|
+
<TableBody>
|
|
20
|
+
{src.map((row, idx) => (
|
|
21
|
+
<TableRow key={idx} hover>
|
|
22
|
+
{row.map((cell, idx) => (
|
|
23
|
+
<TableCell key={idx}>{cell}</TableCell>
|
|
24
|
+
))}
|
|
25
|
+
</TableRow>
|
|
26
|
+
))}
|
|
27
|
+
</TableBody>
|
|
20
28
|
</Table>
|
|
21
|
-
|
|
22
29
|
)
|
|
23
30
|
|
|
24
31
|
const AdminCard = ({ src }) => {
|
|
25
32
|
if (Array.isArray(src)) {
|
|
26
33
|
return (
|
|
27
|
-
<Grid container item spacing={
|
|
28
|
-
{
|
|
34
|
+
<Grid container item spacing={2}>
|
|
35
|
+
{src.map((card, idx) => (
|
|
36
|
+
<AdminCard key={idx} src={card} />
|
|
37
|
+
))}
|
|
29
38
|
</Grid>
|
|
30
39
|
)
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
let {title, value} = src
|
|
42
|
+
let { title, value } = src
|
|
34
43
|
|
|
35
44
|
return (
|
|
36
45
|
<Grid item xs>
|
|
37
46
|
<Card>
|
|
38
47
|
<CardContent>
|
|
39
|
-
<Typography
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
<Typography
|
|
49
|
+
variant='h5'
|
|
50
|
+
component='h2'
|
|
51
|
+
m={2}
|
|
52
|
+
style={{ textAlign: 'center' }}
|
|
53
|
+
>
|
|
54
|
+
{title}
|
|
55
|
+
</Typography>
|
|
56
|
+
{(Array.isArray(value) && <AdminTableView src={value} />) || (
|
|
57
|
+
<pre>{JSON.stringify(value, null, 2)}</pre>
|
|
58
|
+
)}
|
|
43
59
|
</CardContent>
|
|
44
60
|
</Card>
|
|
45
61
|
</Grid>
|
|
@@ -47,11 +63,12 @@ const AdminCard = ({ src }) => {
|
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
// Process dashboard
|
|
50
|
-
setupAdmin('dashboard', props => {
|
|
51
|
-
|
|
66
|
+
setupAdmin('dashboard', (props) => {
|
|
67
|
+
if (!props) return
|
|
52
68
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
return () => (
|
|
70
|
+
<Grid container spacing={1}>
|
|
71
|
+
<AdminCard src={props} />
|
|
72
|
+
</Grid>
|
|
73
|
+
)
|
|
57
74
|
})
|