Amplify Admin UIを試してみる（ブログ構築）

re:Invent2020で発表された`Amplify Admin UI`を試してみます。
確認ポイントとしては、以下で、特に`1.`については可能性を探ってみたいポイントです。

1. 今後、管理画面の構築が不要になる！？
2. HeadlessCMSとの違い

# `Amplify Admin UI`とは

以下の2つの機能を提供してくれるツール（管理画面）です。

* （アプリ管理者　※非開発者向け機能）アプリケーションのユーザーおよびコンテンツ管理など
* （システム管理者　※開発者向け機能）データベースのモデリング、認証・認可機能の追加、管理画面のユーザー管理（システム管理者・アプリ管理者）など

# 作成したサンプルアプリ

![画面収録 2020-12-13 0.10.55.mov.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/0705c8d4-6a97-f28a-8a30-1ffa2541ebf5.gif)

Demo Page
https://thirosue.github.io/amplify-admin-ui-sample/

ソースコードの全量は[GitHub](https://github.com/Thirosue/amplify-admin-ui-sample)にあります。
https://github.com/Thirosue/amplify-admin-ui-sample

# 前提

* `node`がインストールされていること
* `yarn`がインストールされていること

# 大まかな流れ

1. 初期アプリセットアップ
2. Amplifyセットアップ
3. `Admin UI`有効化
4. `Admin UI`ユーザ払い出し
5. データモデル定義
6. データ登録
7. アプリ修正

# 初期アプリセットアップ

```bash
$ git clone git@github.com:Thirosue/amplify-admin-ui-sample.git
$ cd amplify-admin-ui-sample
$ yarn
$ yarn start
```

ここまで実行のあと、以下URLにアクセスすると、簡単な「ブログ一覧」、「ブログページ」が表示されるはずです。

http://localhost:3000/amplify-admin-ui-sample

```bash
Compiled successfully!

You can now view amplify-admin-ui-sample in the browser.

  Local:            http://localhost:3000/amplify-admin-ui-sample
  On Your Network:  http://192.168.11.11:3000/amplify-admin-ui-sample

Note that the development build is not optimized.
To create a production build, use yarn build.
```

# Amplify セットアップ

Amplifyは[こちら](https://qiita.com/Junpei_Takagi/items/f2bc567761880471fd54)を参考にセットアップします。
[AWS Amplify CLIの使い方〜インストールから初期セットアップまで〜](https://qiita.com/Junpei_Takagi/items/f2bc567761880471fd54)

# Amplify Admin UI有効化

AWSマネジメントコンソールで以下に移動します。

「AWS Amplify」 - 「作成したアプリ」 - 「Set up Admin UI」 - 「on」 - 「Invite users」でユーザ招待 - メールを確認してログイン（※初期PWセットアップとかとか）

最初は、データモデル定義する（For 開発者）ため、「admin」ユーザを招待します。

* 「AWS Amplify」 遷移のあと

![スクリーンショット 2020-12-12 16.21.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/f8f40e27-f5d0-a846-ebca-f34b640a99e9.png)

* 「作成したアプリ」 クリックのあと

![スクリーンショット 2020-12-12 16.21.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/20db9f15-60cf-5156-ce86-3d6adcf66e37.png)

* 「Set up Admin UI」 クリックのあと

![スクリーンショット 2020-12-12 16.23.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/af2a6d16-6895-bd66-0994-83749a5a0ff0.png)

* 「on」のあと

![スクリーンショット 2020-12-12 16.26.06.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/486a89f3-c579-f820-b650-48d47d6116db.jpeg)

* 「Invite user(s)」でユーザ招待

![スクリーンショット 2020-12-12 16.27.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/d58dc24e-aaef-0d8d-6a07-892cd4cadd1c.png)

* 招待メール

![スクリーンショット 2020-12-12 16.30.14.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/ef1fb36a-ab2b-1273-80d2-9fbc3e067e7e.jpeg)

* ログイン後トップ画面

![スクリーンショット 2020-12-12 16.35.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/6e4554a8-29ce-5b2c-a425-2bed65c78b46.png)

# コンテンツ（Blog）のモデリング

`Admin UI`で以下を行う。

* 「Data」へ移動

![スクリーンショット 2020-12-12 16.39.26.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/0044f161-8ed4-6cc3-204d-e24c56437e87.jpeg)

* 「Add」 - 「Add model」

![スクリーンショット 2020-12-12 16.41.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/d20568e2-e6bf-48d9-3205-f95fa902e8a3.png)

* データモデル定義

ブログに必要な最低限のデータ定義をします。

![スクリーンショット 2020-12-12 16.44.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/0acbfd69-0d3a-e182-2e20-f4a088c642fd.png)

* 作成中...（裏でCfnが走っている）

![スクリーンショット 2020-12-12 16.47.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/3897a148-ef94-6137-9d51-f1c23fe828a8.png)

![スクリーンショット 2020-12-12 16.48.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/57e737e2-e822-1c7e-dac0-45c3f57e8e73.png)

# コンテンツ管理者ユーザ作成

AWSマネジメントコンソールに戻って、次は非開発者向けの管理者用のユーザ（「Manage only」ユーザ）を作成します。

![スクリーンショット 2020-12-12 16.53.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/91e1da82-ab6b-258a-bcb8-d54526a0d90d.png)

ここでブログを投稿していきたいところですが、2020/12/12現在では、以下のようになってしまいます。

![スクリーンショット 2020-12-12 16.58.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/f710f78a-97f4-c668-3517-34c1bebaceac.png)

# コンテンツ投稿

仕方ないので、Adminユーザからブログを投稿していきます。

* 1投目

![スクリーンショット 2020-12-12 17.05.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/2030e3c0-288e-18d5-2013-b51d5fbeb66e.png)

* 2投目

![スクリーンショット 2020-12-12 17.08.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/d22adfb7-adef-2037-40e1-853a948e16d2.png)

* 3投目

![スクリーンショット 2020-12-12 17.10.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/fda044ef-c4a5-e357-165f-5c4d41ec899b.png)

* 最終的な一覧はこんな感じ

![スクリーンショット 2020-12-12 18.06.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/16c5314c-e23e-8183-51de-f58c09cc2aeb.png)

## Dynamoに登録されているはずだから見にいくと...

2020/12/12現在ではDynamoDBは空っぽ...

[公式ブログ](https://aws.amazon.com/jp/blogs/news/aws-amplify-admin-ui-helps-you-develop-app-backends-no-cloud-experience-required-jp/) では、

> この画面から、レストランのオーナーはメニューに項目を追加、削除、編集をいつでも行うことが出来ます。また、ここで変更した項目は保存後すぐにウェブサイトに反映されます。

こう書いてあるので、Betaが外れる（Bugfixされる）と、`DynamoDB`に登録されるはずです。

* DynamoDB

![スクリーンショット 2020-12-12 18.10.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/f7452812-a543-fff8-1c59-7bd5defcaad8.png)

* Cfn（Dynamoの物理名）も念のため、確認

![スクリーンショット 2020-12-12 18.10.28.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/52d0f7c5-8a27-757d-ac8b-1bceb9d56550.jpeg)

* この状態で`amplify pull`してみると、API（AppSync）が作成されている （=`amplify add api`でGraphQLのAPIを作成した状態）

```bash
% amplify pull

Pre-pull status:

Current Environment: dev

| Category | Resource name        | Operation | Provider plugin   |
| -------- | -------------------- | --------- | ----------------- |
| Api      | amplifyadminuisample | No Change | awscloudformation |

✔ Successfully pulled backend environment dev from the cloud.


Post-pull status:

Current Environment: dev

| Category | Resource name        | Operation | Provider plugin   |
| -------- | -------------------- | --------- | ----------------- |
| Api      | amplifyadminuisample | No Change | awscloudformation |
```

* `Admin UI`で作成すると、スタックが作成済みの状態からpullすることになるため、クライアントのコードの自動生成もスキップされてしまう...（`amplify cli`の以下フロー）

```bash
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
```

結論、Beta版の現状（2020/12/12時点）では、`amplify cli`で作成した方が無難...（当たり前か...）

# まとめ

## 1. 管理画面の構築が不要になる！？

* 非開発者向けの管理画面は、`Admin UI`自体をカスタマイズ可能（Lambdaのボイラーテンプレート出力みたいに）になれば、かなり嬉しいかも
* 開発者向け機能は、当分`cli`で (アップデートまち)

## 2. HeadlessCMSとの違い

* ContentFulなどとほぼ同様の使用感
* コンテンツ以外の管理（ユーザ管理など）もできるので、今後アップデートが進めば一元管理できるかも

# 参考リンク

- Amplify関連
  - [新機能 – AWS Amplify Admin UI: アプリケーションのバックエンド開発を支援し、クラウドの経験を必要としない管理ツール](https://aws.amazon.com/jp/blogs/news/aws-amplify-admin-ui-helps-you-develop-app-backends-no-cloud-experience-required-jp/) 
  - [AWS 怠惰なプログラマ向けお手軽アプリ開発手法 2019](https://feature-webpush.dma9ecr5ksxts.amplifyapp.com/)
  - [AWS Amplify CLIの使い方〜インストールから初期セットアップまで〜](https://qiita.com/Junpei_Takagi/items/f2bc567761880471fd54)

# 以下、蛇足 （仕方ないので`amplify cli`で、Amplifyを利用したブログを完成させる）

一旦、`Admin UI`で作成したAppSyncは削除

## AppSync 追加

こんな感じで、`Admin UI`で作成したものと同様のModelを生成し、`amplify push`する。

```bash
% amplify add api
? Please select from one of the below mentioned services: GraphQL
? Provide API name: amplifyadminuisample
? Choose the default authorization type for the API API key
? Enter a description for the API key: 1df1c9bb-1bd8-1fde-b2d6-cea1ee234285
? After how many days from now the API key should expire (1-365): 7
? Do you want to configure advanced settings for the GraphQL API No, I am done.
? Do you have an annotated GraphQL schema? No
? Choose a schema template: Single object with fields (e.g., “Todo” with ID, name, description)
```

```bash
? Do you want to generate code for your newly created GraphQL API Yes
? Choose the code generation language target javascript
? Enter the file name pattern of graphql queries, mutations and subscriptions src/graphql/**/*.js
? Do you want to generate/update all possible GraphQL operations - queries, mutations and subscriptions Yes
? Enter maximum statement depth [increase from default if your schema is deeply nested] 2
```

* Model定義

```graphql:amplify/backend/api/amplifyadminuisample/schema.graphql
type Blog @model {
  id: ID!
  title: String!
  description: String!
  body: String!
  imagePath: String!
}
```

## コンテンツ追加

管理コンソールから手で登録
→bodyはMD形式です

![スクリーンショット 2020-12-12 23.02.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73176/15f721ef-e1a1-d361-690e-e0f015b0bcd9.png)

## ソースコード修正

* GraphQL クエリ

```graphql:/src/graphql/queries.js
export const listBlogs = /* GraphQL */ `
  query ListBlogs(
    $filter: ModelBlogFilterInput
    $limit: Int
    $nextToken: String
  ) {
    listBlogs(filter: $filter, limit: $limit, nextToken: $nextToken) {
      items {
        id
        title
        description
        body
        imagePath
      }
      nextToken
    }
  }
`;

```

* Amplify Import

```javascript:/src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import reportWebVitals from './reportWebVitals';
import Amplify from 'aws-amplify'; // <---- 追加
import awsconfig from './aws-exports'; // <---- 追加
Amplify.configure(awsconfig); // <---- 追加

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);
```

* サービスクラス作成

```javascript:/src/service/blog.js
import { API, graphqlOperation } from 'aws-amplify';
import { listBlogs } from '../graphql/queries';

async function list() {
    const results = await API.graphql(graphqlOperation(listBlogs));
    return results.data.listBlogs.items
}

export default list;
```

* カスタムフック作成

```javascript:/src/hook/useBlog.js
import { useState, useEffect } from 'react';
import getBlogs from '../service/blog';

// eslint-disable-next-line
export default () => {
    const [blogs, setBlogs] = useState([]);

    // このカスタムフックを利用しているコンポーネントがマウントされたら ブログ一覧 を取得する。
    useEffect(() => {
        const fetchAll = async () => {
            const results = await getBlogs();
            setBlogs(results)
        }

        fetchAll()
    }, []);

    return { blogs };
};
```

* ページコンポーネント修正

リストレンダリングするように修正

```javascript:/src/App.js
function App() {
  const classes = useStyles();
  const [detail, setDetail] = React.useState(false);
  const { blogs } = useBlog(); // <------ カスタムフックを利用する
  const [id, setId] = React.useState(1); // <------ 選択したブログID用

  React.useEffect(() => {
    console.log(blogs)
  }, [blogs]);

  const handleClick = async id => {
    console.log('select blog id : %s', id);
    setId(id - 1) // ブログIDをセットする
    setDetail(true)
  }

  return (
    <DialogContent>
      {!detail && (
        <>
          <ThemeProvider theme={theme}>
            <Typography variant="h4">Sample Blog</Typography>
          </ThemeProvider>
          <Box mb="1.5rem" />
          <Grid container spacing={3}>
            {/* リストレンダリング */}
            {0 < blogs.length && blogs.sort((a, b) => a.id - b.id).map(blog =>
              <Grid key={blog.id} item xs={12} sm={4} md={3}>
                {/* ブログ選択時はブログIDを渡す */}
                <Card onClick={() => handleClick(blog.id)}>
                  <CardMedia
                    className={classes.media}
                    image={"/amplify-admin-ui-sample/" + blog.imagePath}
                  />
                  <CardContent>
                    <Typography gutterBottom variant="h5" component="h2">
                      {blog.title}
                    </Typography>
                    <Typography variant="body2" color="textSecondary" component="div">
                      {blog.description}
                    </Typography>
                  </CardContent>
                </Card>
              </Grid>
            )}
          </Grid>
        </>
      )}
      {!!detail && (
        <div className={classes.root}>
          <Grid container spacing={3}>
            <Grid item xs={12} sm={12} md={12}>
              {/* 選択したブログを表示する */}
              <Card>
                <CardMedia
                  className={classes.detail}
                  image={"/amplify-admin-ui-sample/" + blogs[id].imagePath}
                />
                <CardContent>
                  <Typography className={classes.title} gutterBottom variant="h4" component="h2">
                    {blogs[id].title}
                  </Typography>
                  <Typography className={classes.content} variant="subtitle1" color="textSecondary" component="div">
                    <ReactMarkdown>{blogs[id].body}</ReactMarkdown>
                  </Typography>
                </CardContent>
              </Card>
            </Grid>
          </Grid>
          <Box mb="1rem" />
          <Link
            component="button"
            variant="subtitle1"
            onClick={() => setDetail(false)}
          >
            Back
          </Link>
        </div>
      )}
    </DialogContent>
  );
}
```

* 修正差分

https://github.com/Thirosue/amplify-admin-ui-sample/commit/e4abc73bb77347000bc0e7627d295b332a2308f9
※ amplifyフォルダはコミット対象外にしています。

* 対象ブランチ

https://github.com/Thirosue/amplify-admin-ui-sample/tree/feature/amplify