π¨ Work in progress. Use in production at your own risk! We are π€ Feel free to contribute though to get it there though. I love contributors.
Easily bootstrap an admin interface for your AWS Amplify Apps that use a GraphQL
API
,Storage
andAuth
.
AWS Amplify is a great tool for generating cross-platform apps that string together AWS resources like AppSync for GraphQL, Cognito for user management and S3 for file storage, among other things.
One of the promises of AWS Amplify is Focus on the features, not the back-end. This stops short of providing an easy-to-use back-end for managing content - unless you're fine digging around DynamoDB, S3 and so on. Enter react-admin
with ra-aws-amplify
.
Screenshot of the example app in this package, using Auth
, GraphQL API
and Storage
.
- Installation
- Usage
useAmplifyDataProvider
- DynamoDB Access Patterns with
react-admin
- Authentication and Sign in with
Auth
- Image Upload with
Storage
- Pagination using
nextToken
- Your knowledge and ideas
- Proper typescript typing
- Jest tests
- handling many to many connections
- Filtering, sorting of get & list
- Search with
@searchable
directive - REST API support
Check out some good first issues
to start!
$ yarn add ra-aws-amplify@alpha react-admin aws-amplify
Example schema:
type Post @model {
id: ID!
title: String!
content: String
}
// App.js
import React, { useEffect, useState } from 'react';
import { Admin, Resource, ListGuesser } from 'react-admin';
import { useAmplifyDataProvider } from 'ra-aws-amplify';
// grab your amplify generated code
import config from './aws-exports';
import schema from './graphql/schema.json';
import * as queries from './graphql/queries';
import * as mutations from './graphql/mutations';
function App() {
const dataProvider = useAmplifyDataProvider({ config, schema, queries, mutations });
return
<Admin dataProvider={dataProvider}>
<Resource name="Post" list={ListGuesser} />
</Admin>
)
}
export default App;
useAmplifyDataProvider
is a hook that generates a dataProvider to pass into the <Admin />
component from react-admin
. It's smart enough to pick up what kind of authentication you're using for your API (based on the config you pass) as well as hook up the generated queries and mutations from running amplify push
.
import { AUTH_TYPE } from 'aws-appsync';
import { useAmplifyDataProvider } from 'react-admin';
import config from './aws-exports';
import schema from './graphql/schema.json';
import * as queries from './graphql/queries';
import * as mutations from './graphql/mutations';
const dataProvider = useAmplifyDataProvider({
// required
config, // generated aws-exports.js
schema, // generated json schema.json
queries, // generated queries.js
mutations // mutations generated.js
// optional
authType: AUTH_TYPE.API_KEY // specify an authType that config doesn't
})
No problem. When you import * as queries from './graphql/queries';
it just returns an object of named queries. You can create your own, ensuring the names and parameters match those of the generated queries, i.e.;
// customQueries.js
export const customQueries = {
listPosts: `
query CustomListPostQuery {
listPosts {
items {
id
title
}
}
}
`,
};
// App.js
import * as queries from './graphql/queries';
import { customQueries } from './customQueries';
const dataProvider = useAmplifyDataProvider({
queries: {
...queries,
...customQueries,
},
});
Coming with DynamoDB's powerful speed and scaling features are (often painful) rigidity problems with access patterns. You need to consider access patterns for your front-end and your back-end when writing your schema. In general, favour flexible relationships over simpler ones, i.e. belongs to
over has one
. This is especially relevant when you want to filter or sort. In some cases, it's easier (though more expen$ive) to use the @searchable
directive.
Here are a few scenarios.
See
example/src/Post/PostCreate.tsx
,example/src/Post/PostEdit.tsx
andexample/src/Media/MediaUploadInput.tsx
for examples.
Given the schema:
type Post @model {
id: ID!
title: String!
content: String
image: Media @connection
}
type Media @model {
id: ID!
name: String
attachment: S3Object!
}
# S3Object minimum type
type S3Object {
key: String!
identityId: String
level: String
}
Some custom work is still required despite the easy setup touted by react-admin
to achieve relationships like this. One limitation of DynamoDB is that you can't create records as part of a parent record. In an ideal world, you could pass image input to the post input and the resolvers would generate both records for you. That's not the case here.
Instead, the step is more like;
- Create image
- Use image ID to create/update the post
react-admin
doesn't support sequential creation through the dataProvider
so you'll have to get creative in your back end. In the example this has been approached with a media popup. The steps look like this instead:
- Fill out new post form
- Click "Add media"
- Open a MUI dialog with a
<CreateMedia />
form in it - Use the
useMutation
hook fromreact-admin
to create it on the fly - On success, close the dialog and set the
postImageId
field (autogenerated, you can name it if you wish) via theuseInput
hook fromreact-admin
- Save the post as you usually would
See
example/src/Post/PostShow.tsx
andexample/src/Comment/CommentCreate.tsx
for an example
Based off the React Admin Advanced Recipes: Creating a Record Related to the Current One tutorial react-admin
's model assumes that when using any component or hook that calls a GET_MANY_REFERENCE
, you can query your model by its connection. DyanmoDB doesn't support this out of the box, so we need to use a has many
connection with the @key
directive, specifying a queryField
. This will generate a query that allows this access pattern.
type Post @model {
id: ID!
title: String!
content: String
comments: [Comment] @connection(keyName: "byPost", fields: ["id"])
}
type Comment
@model
@key(name: "byPost", fields: ["postId"], queryField: "commentsByPost") {
id: ID!
content: String
postId: ID!
}
Your generated GraphQL queries will pump out something like this:
export const commentsByPost = /* GraphQL */ `
query CommentsByPost(
$postId: ID
$sortDirection: ModelSortDirection
$filter: ModelCommentFilterInput
$limit: Int
$nextToken: String
) {
commentsByPost(
postId: $postId
sortDirection: $sortDirection
filter: $filter
limit: $limit
nextToken: $nextToken
) {
items {
id
content
postId
}
nextToken
}
}
`;
Under the hood, ra-aws-amplify
will look to match this query during a GET_MANY_REFERENCE
call.
Here's an example where we show comments on a post. You must set the target
prop to the queryField
value in your schema, like so:
// PostsShow.js
export const PostShow = prop => {
<Show {...props}>
...
<ReferenceManyField
reference="Comment"
// target here should match queryField.
target="commentsByPost"
>
<Datagrid>
<TextField source="content" />
</Datagrid>
</ReferenceManyField>
</Show>;
};
The package will pick up on this and wire everything up as expected.
Coming soon...
Coming soon...
This package exposes a few tools for handling authentication out of the box with @aws-amplify/Auth
:
Wrap your app in this provider so Auth
is available at all contexts, with an abstracted API so it's easier to refactor to another provider if DynamoDB drives you nuts π.
// index.tsx
import ReactDOM from 'react-dom';
import { AmplifyAuthProvider } from 'ra-aws-amplify';
import { App } from './App';
ReactDOM.render(
<AmplifyAuthProvider>
<App />
</AmplifyAuthProvider>,
document.getElementById('root')
);
This context provider is used by the following hooks.
Just provides direct access to the aws amplify Auth
class via a hook.
import { useAuth } from 'ra-aws-amplify';
...
const auth = useAuth();
// https://aws-amplify.github.io/docs/js/authentication#sign-up
auth.signUp({username, password}).then(...);
This has been included to encourage flexibility. In the future, should you switch to say, Azure, you can build a hook called useAuth
that exposes the methods you use (i.e. signUp, signOut) and do a relatively small refactor on your front end.
react-admin
has some login functionality built in that we can tap into. This hook does just that and integrates Auth
with react-admin
out of the box.
import React from 'react';
import { Admin, Resource, ListGuesser } from 'react-admin';
import { useAuthProvider } from 'ra-aws-amplify';
export const App = () => {
const authProvider = useAuthProvider();
return (
<Admin authProvider={authProvider} dataProvider={...}>
<Resource name="Post" list={ListGuesser} />
</Admin>
);
};
Listening for Amplify Hub events is a pain in the ass, so, at least for login, this package does that for you. Internally, it listens to the Hub and 'hookifies' the user object, so you don't have to worry about promises.
This returns undefined
when not signed in, and the result of Auth.currentAuthenticatedUser
when successfully authenticated.
import { useUser } from 'ra-aws-amplify';
...
const user = useUser();
const auth = useAuth();
auth.changePassword(user, ...);
You'll have to create a custom LoginPage
for federated sign in to work. You can use the useLogin
hook exposed by react-admin
to access the login method inside the authProvider
from this package. It'll automatically popup sign-in windows when you pass in the a provider
property, i.e.
const login = useLogin();
login({ provider: 'google' });
For a complete example, your Login components might look like this:
import React from 'react';
import { Login, useLogin } from 'react-admin';
import { Button } from '@material-ui/core';
// <LoginForm />
const LoginForm = () => {
const login = useLogin();
const handleLogin = () => login({ federated: true, provider: 'google' });
return <Button onClick={handleLogin}>Login with Google</Button>;
};
// <LoginPage />
const LoginPage = props => <Login {...props} loginForm={<LoginForm />} />;
// <App />
const App = () => {
const authProvider = useAuthProvider();
return <Admin authProvider={authProvider} loginPage={LoginPage} />;
};
react-admin
has tools for dealing with permissions. By using the authProvider
from this package, you automatically get id token claims passed in for use via the usePermissions
hook:
import { usePermissions } from 'react-admin';
const { permissions } = usePermissions();
console.log(permissions.claims['cognito:groups']); // => ['admin', 'user']
You can then use these in your Resource
, List
, Show
, Create
, Edit
components. See the react-admin
Authorization docs for use cases.
Use this in conjunction with the Pre Token Generation Lambda Trigger for even more fine-grained access control.
This package exposes a handful of input and field components to help you deal with image & file upload via aws-amplify's Storage
package.
- Required schema
<S3ImageInput />
and<S3FileInput />
<S3ImageField />
and<S3FileField />
<S3Input />
and<S3Field />
protected
,private
files
Your schema must include an S3Object
type where you want your file upload to be:
type Post @model {
id: ID!
title: String!
content: String
featureImage: S3Object
files: [S3Object!]
}
type S3Object {
key: String!
identityId: String
level: String
type: String!
}
You can then use <S3ImageInput />
and <S3FileInput />
to upload files. Your <CreatePost />
might look something like the following:
// CreatePost.js
import React from 'react';
import { Create, SimpleForm, TextInput } from 'react-admin';
import { S3ImageInput, S3FileInput } from 'ra-aws-amplify';
export const CreateApp: React.FC = props => {
return (
<Create {...props}>
<SimpleForm>
<TextInput source="title" />
<TextInput source="content" multiline />
<S3ImageInput source="featureImage" accept="image/*" multiple={false} />
<S3FileInput source="files" multiple={true} />
</SimpleForm>
</Create>
);
};
If you want to use the image and files in your <List />
component, you can use the <S3ImageField />
and <S3FileField />
to display them:
import React from 'react';
import { List, Datagrid, TextField, SingleFieldList } from 'react-admin';
import { S3ImageField, S3FileField } from 'ra-aws-amplify';
export const ListPosts = props => {
return (
<List {...props}>
<Datagrid rowClick="edit">
<S3ImageField source="featureImage" />
<ArrayField source="files">
<SingleFieldList>
<S3FileField />
</SingleFieldList>
</ArrayField>
<TextField source="title" />
<TextField source="content" />
</Datagrid>
</List>
);
};
The logic for handling Storage
upload & retrieval is actually abstracted into these two components. They both use React.cloneElement
to pass the props down to either S3ImageField
or S3FileField
. The responsibility of, for example, S3ImageField is just to display whatever URL is passed down to it.
You can pass in the level
option as a prop to <S3Input level={...} />
(one of public
, protected
, and private
) and that will get passed on to Storage
. If you do this, it's important to either use the authProvider
from this package, or in your custom authProvider
pass the identityId
into getPermissions
:
export const authProvider = {
...
getPermissions: () =>
Promise.all([
Auth.currentCredentials(),
Auth.currentAuthenticatedUser(),
]).then(([{ identityId }, use]) => ({
identityId,
// this allows you to check which groups the user is in too! Booya, bonus.
groups: user.signInUserSession.accessToken.payload['cognito:groups'],
})),
}
If you set the level to either private
or protected
the <S3Input />
component will automatically attach both level and identityId to the record under the hood, required for access later.
Pagination doesn't work just yet. See this issue, please help out!
This package also exports some reducers and components for handling pagination specific to dynamodb - which has no concept of totals or pages. This library utilises custom reducers to catch the nextToken
and use it in subsequent GET_LIST
calls.
- Pass in the custom reducers from this package
- Pass in the
<AmplifyPagination />
component to your<List />
- Pass in the
nextToken
as a filter to your<List />
// App.js
import { reducers } from 'ra-aws-amplify';
import { PostsList } from './PostsList';
...
export const App = () => (
<Admin ... customReducers={reducers}>
<Resource name="Post" list={PostsList}>
</Admin>
)
// PostsList.js
import { List, ... } from 'react-admin';
import { AmplifyPagination } from 'ra-aws-amplify';
import { useSelector } from 'react-redux';
export const PostsList = props => {
const nextToken = useSelector(state => state.nextToken);
return (
<List {...props} pagination={<AmplifyPagination />} filter={{nextToken}}>
...
</List>
)
}
The dataProvider
handles the rest for you.
Have you learnt something interesting about integrating react-admin
with AWS Amplify on a private project? Open source only works because people like you help people like me create awesome things and share our knowledge. Any help with this package is much appreciated, whether it's knowledge, tests, improving types, additional components, optimisations, solutions, etc. Just create an issue and let's get started!
Read contribution guidelines.