Skip to content

Commit 25b8652

Browse files
authored
Merge pull request #10973 from marmelab/core-doc-enterprise
[Doc] Add documentation for headless enterprise features in ra-core documentation
2 parents 7b6846f + a95edd0 commit 25b8652

24 files changed

+2017
-15
lines changed

docs/useSubscribeToRecordList.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,25 +26,25 @@ import { useNotify, useListContext } from 'react-admin';
2626
import { useSubscribeToRecordList } from '@react-admin/ra-realtime';
2727

2828
const ListWatcher = () => {
29-
const notity = useNotify();
29+
const notify = useNotify();
3030
const { refetch, data } = useListContext();
3131
useSubscribeToRecordList(event => {
3232
switch (event.type) {
3333
case 'created': {
34-
notity('New movie created');
34+
notify('New movie created');
3535
refetch();
3636
break;
3737
}
3838
case 'updated': {
3939
if (data.find(record => record.id === event.payload.ids[0])) {
40-
notity(`Movie #${event.payload.ids[0]} updated`);
40+
notify(`Movie #${event.payload.ids[0]} updated`);
4141
refetch();
4242
}
4343
break;
4444
}
4545
case 'deleted': {
4646
if (data.find(record => record.id === event.payload.ids[0])) {
47-
notity(`Movie #${event.payload.ids[0]} deleted`);
47+
notify(`Movie #${event.payload.ids[0]} deleted`);
4848
refetch();
4949
}
5050
break;
@@ -80,25 +80,25 @@ const MovieList = () => (
8080
Whenever an event is published on the `resource/[resource]` topic, the function passed as the first argument will be called with the event as a parameter.
8181

8282
```jsx
83-
const notity = useNotify();
83+
const notify = useNotify();
8484
const { refetch, data } = useListContext();
8585
useSubscribeToRecordList(event => {
8686
switch (event.type) {
8787
case 'created': {
88-
notity('New movie created');
88+
notify('New movie created');
8989
refetch();
9090
break;
9191
}
9292
case 'updated': {
9393
if (data.find(record => record.id === event.payload.ids[0])) {
94-
notity(`Movie #${event.payload.ids[0]} updated`);
94+
notify(`Movie #${event.payload.ids[0]} updated`);
9595
refetch();
9696
}
9797
break;
9898
}
9999
case 'deleted': {
100100
if (data.find(record => record.id === event.payload.ids[0])) {
101-
notity(`Movie #${event.payload.ids[0]} deleted`);
101+
notify(`Movie #${event.payload.ids[0]} deleted`);
102102
refetch();
103103
}
104104
break;
@@ -110,33 +110,33 @@ useSubscribeToRecordList(event => {
110110
**Tip**: Memoize the callback using `useCallback` to avoid unnecessary subscriptions/unsubscriptions.
111111

112112
```jsx
113-
const notity = useNotify();
113+
const notify = useNotify();
114114
const { refetch, data } = useListContext();
115115
const callback = useCallback(
116116
event => {
117117
switch (event.type) {
118118
case 'created': {
119-
notity('New movie created');
119+
notify('New movie created');
120120
refetch();
121121
break;
122122
}
123123
case 'updated': {
124124
if (data.find(record => record.id === event.payload.ids[0])) {
125-
notity(`Movie #${event.payload.ids[0]} updated`);
125+
notify(`Movie #${event.payload.ids[0]} updated`);
126126
refetch();
127127
}
128128
break;
129129
}
130130
case 'deleted': {
131131
if (data.find(record => record.id === event.payload.ids[0])) {
132-
notity(`Movie #${event.payload.ids[0]} deleted`);
132+
notify(`Movie #${event.payload.ids[0]} deleted`);
133133
refetch();
134134
}
135135
break;
136136
}
137137
}
138138
},
139-
[data, refetch, notity]
139+
[data, refetch, notify]
140140
);
141141
useSubscribeToRecordList(callback);
142142
```

docs_headless/astro.config.mjs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@ export default defineConfig({
106106
'usepermissions',
107107
'addrefreshauthtoauthprovider',
108108
'addrefreshauthtodataprovider',
109+
enterpriseEntry('canAccessWithPermissions'),
110+
enterpriseEntry('getPermissionsFromRoles'),
109111
],
110112
},
111113
{
@@ -213,6 +215,28 @@ export default defineConfig({
213215
'usegetrecordrepresentation',
214216
],
215217
},
218+
{
219+
label: 'Realtime',
220+
items: [
221+
enterpriseEntry('usePublish'),
222+
enterpriseEntry('useSubscribe'),
223+
enterpriseEntry('useSubscribeCallback'),
224+
enterpriseEntry('useSubscribeToRecord'),
225+
enterpriseEntry('useSubscribeToRecordList'),
226+
enterpriseEntry('useLock'),
227+
enterpriseEntry('useUnlock'),
228+
enterpriseEntry('useGetLock'),
229+
enterpriseEntry('useGetLockLive'),
230+
enterpriseEntry('useGetLocks'),
231+
enterpriseEntry('useGetLocksLive'),
232+
enterpriseEntry('useLockCallbacks'),
233+
enterpriseEntry('useLockOnMount'),
234+
enterpriseEntry('useLockOnCall'),
235+
enterpriseEntry('useGetListLive'),
236+
enterpriseEntry('useGetOneLive'),
237+
enterpriseEntry('<LockStatusBase>'),
238+
],
239+
},
216240
{
217241
label: 'Recipes',
218242
items: ['caching', 'unittesting'],
@@ -250,3 +274,19 @@ export default defineConfig({
250274
assets: 'assets',
251275
},
252276
});
277+
278+
/**
279+
* @param {string} name
280+
* @returns {any}
281+
*/
282+
function enterpriseEntry(name) {
283+
return {
284+
link: `${name.toLowerCase().replace(/</g, '').replace(/>/g, '')}/`,
285+
label: name,
286+
attrs: { class: 'enterprise' },
287+
badge: {
288+
text: 'React Admin Enterprise',
289+
variant: 'default',
290+
},
291+
};
292+
}
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 1 addition & 0 deletions
Loading
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
---
2+
title: "<LockStatusBase>"
3+
---
4+
5+
`<LockStatusBase>` displays the lock status of the current record. It allows to visually indicate whether the record is locked or not, by the current user or not, and provides an easy way to lock or unlock the record.
6+
7+
This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription.
8+
9+
## Installation
10+
11+
```bash
12+
npm install --save @react-admin/ra-core-ee
13+
# or
14+
yarn add @react-admin/ra-core-ee
15+
```
16+
17+
## Usage
18+
19+
```tsx
20+
import React from 'react';
21+
import { Lock, LockOpen, LoaderCircle } from 'lucide-react';
22+
import { LockStatusBase } from '@react-admin/ra-core-ee';
23+
24+
export const LockStatus = () => {
25+
return (
26+
<LockStatusBase
27+
{...props}
28+
render={({
29+
doLock,
30+
doUnlock,
31+
isLocking,
32+
isPending,
33+
isUnlocking,
34+
lockStatus,
35+
message,
36+
}) => {
37+
if (isPending) {
38+
return null;
39+
}
40+
41+
if (lockStatus === 'lockedByUser') {
42+
return (
43+
<button
44+
title={message}
45+
disabled={isUnlocking}
46+
onClick={(
47+
e: React.MouseEvent<HTMLButtonElement>
48+
) => {
49+
e.stopPropagation();
50+
doUnlock();
51+
}}
52+
>
53+
{isUnlocking ? (
54+
<LoaderCircle className="h-4 w-4 animate-spin" />
55+
) : (
56+
<Lock className="h-4 w-4" />
57+
)}
58+
</button>
59+
);
60+
}
61+
if (lockStatus === 'lockedByAnotherUser') {
62+
return (
63+
<Lock className="h-4 w-4 text-error" />
64+
);
65+
}
66+
if (lockStatus === 'unlocked') {
67+
return (
68+
<button
69+
title={message}
70+
disabled={isLocking}
71+
onClick={(
72+
e: React.MouseEvent<HTMLButtonElement>
73+
) => {
74+
e.stopPropagation();
75+
doLock();
76+
}}
77+
color="warning"
78+
>
79+
{isLocking ? (
80+
<LoaderCircle className="h-4 w-4 animate-spin" />
81+
) : (
82+
<LockOpen className="h-4 w-4" />
83+
)}
84+
</button>
85+
);
86+
}
87+
return null;
88+
}}
89+
/>
90+
);
91+
};
92+
```
93+
94+
In addition to the [`useLockCallbacks`](./useLockCallbacks.md) parameters, `<LockStatusBase>` accepts a `render` prop. The function passed to the `render` prop will be called with the result of the `useLockCallbacks` hook.
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
---
2+
title: "canAccessWithPermissions"
3+
---
4+
5+
`canAccessWithPermissions` is a helper function that facilitates the implementation of [Access Control](./Permissions.md#access-control) policies based on an underlying list of user roles and permissions.
6+
7+
It is a builder block to implement the `authProvider.canAccess()` method, which is called by ra-core to check whether the current user has the right to perform a given action on a given resource or record.
8+
9+
This feature requires a valid [Enterprise Edition](https://marmelab.com/ra-enterprise/) subscription.
10+
11+
## Installation
12+
13+
```bash
14+
npm install --save @react-admin/ra-core-ee
15+
# or
16+
yarn add @react-admin/ra-core-ee
17+
```
18+
19+
## Usage
20+
21+
`canAccessWithPermissions` is a pure function that you can call from your `authProvider.canAccess()` implementation.
22+
23+
```tsx
24+
import { canAccessWithPermissions } from '@react-admin/ra-core-ee';
25+
26+
const authProvider = {
27+
// ...
28+
canAccess: async ({ action, resource, record }) => {
29+
const permissions = myGetPermissionsFunction();
30+
return canAccessWithPermissions({
31+
permissions,
32+
action,
33+
resource,
34+
record,
35+
});
36+
}
37+
// ...
38+
};
39+
```
40+
41+
The `permissions` parameter must be an array of permissions. A *permission* is an object that represents access to a subset of the application. It is defined by a `resource` (usually a noun) and an `action` (usually a verb), with sometimes an additional `record`.
42+
43+
Here are a few examples of permissions:
44+
45+
- `{ action: "*", resource: "*" }`: allow everything
46+
- `{ action: "read", resource: "*" }`: allow read actions on all resources
47+
- `{ action: "read", resource: ["companies", "people"] }`: allow read actions on a subset of resources
48+
- `{ action: ["read", "create", "edit", "export"], resource: "companies" }`: allow all actions except delete on companies
49+
- `{ action: ["write"], resource: "game.score", record: { "id": "123" } }`: allow write action on the score of the game with id 123
50+
51+
:::tip
52+
When the `record` field is omitted, the permission is valid for all records.
53+
:::
54+
55+
In most cases, the permissions are derived from user roles, which are fetched at login and stored in memory or in localStorage. Check the [`getPermissionsFromRoles`](./getPermissionsFromRoles.md) function to merge the permissions from multiple roles into a single flat array of permissions.
56+
57+
## Parameters
58+
59+
This function takes an object as argument with the following fields:
60+
61+
| Name | Optional | Type | Description
62+
| - | - | - | - |
63+
| `permissions` | Required | `Array<Permission>` | An array of permissions for the current user
64+
| `action` | Required | `string` | The action for which to check users has the execution right
65+
| `resource` | Required | `string` | The resource for which to check users has the execution right
66+
| `record` | Required | `string` | The record for which to check users has the execution right
67+
68+
`canAccessWithPermissions` expects the `permissions` to be a flat array of permissions. It is your responsibility to fetch these permissions (usually during login). If the permissions are spread into several role definitions, you can merge them into a single array using the [`getPermissionsFromRoles`](./getPermissionsFromRoles.md) function.
69+
70+
## Building RBAC
71+
72+
The following example shows how to implement Role-based Access Control (RBAC) in `authProvider.canAccess()` using `canAccessWithPermissions` and `getPermissionsFromRoles`. The role permissions are defined in the code, and the user roles are returned by the authentication endpoint. Additional user permissions can also be returned by the authentication endpoint.
73+
74+
The `authProvider` stores the permissions in `localStorage`, so that returning users can access their permissions without having to log in again.
75+
76+
```tsx
77+
// in roleDefinitions.ts
78+
export const roleDefinitions = {
79+
admin: [
80+
{ action: '*', resource: '*' }
81+
],
82+
reader: [
83+
{ action: ['list', 'show', 'export'], resource: '*' },
84+
{ action: 'read', resource: 'posts.*' },
85+
{ action: 'read', resource: 'comments.*' },
86+
],
87+
accounting: [
88+
{ action: '*', resource: 'sales' },
89+
],
90+
};
91+
92+
// in authProvider.ts
93+
import { canAccessWithPermissions, getPermissionsFromRoles } from '@react-admin/ra-core-ee';
94+
import { roleDefinitions } from './roleDefinitions';
95+
96+
const authProvider = {
97+
login: async ({ username, password }) => {
98+
const request = new Request('https://mydomain.com/authenticate', {
99+
method: 'POST',
100+
body: JSON.stringify({ username, password }),
101+
headers: new Headers({ 'Content-Type': 'application/json' }),
102+
});
103+
const response = await fetch(request);
104+
if (response.status < 200 || response.status >= 300) {
105+
throw new Error(response.statusText);
106+
}
107+
const { user: { roles, permissions }} = await response.json();
108+
// merge the permissions from the roles with the extra permissions
109+
const permissions = getPermissionsFromRoles({
110+
roleDefinitions,
111+
userPermissions,
112+
userRoles
113+
});
114+
localStorage.setItem('permissions', JSON.stringify(permissions));
115+
},
116+
canAccess: async ({ action, resource, record }) => {
117+
const permissions = JSON.parse(localStorage.getItem('permissions'));
118+
return canAccessWithPermissions({
119+
permissions,
120+
action,
121+
resource,
122+
record,
123+
});
124+
}
125+
// ...
126+
};
127+
```

0 commit comments

Comments
 (0)