ReactAdminを試してみた

ReactAdminを少し触ってみました。 marmelab.com

そもそもReactAdminとはReactでダッシュボードを作成するためのフレームワークのようで、公式のページでは"A Web Framework for B2B applications"とあるので凝ったページというよりは一般的なダッシュページとかを簡単に作れるのかなと思います。

公式のドキュメントは以下になりまして、"Data Provider"と"Auth Provider"がAdminコンポーネントプロパティになっていまして、それぞれ一覧データの取得と認証の設定になります。それからResourceコンポーネントが一覧に出す項目の内容を設定するためのコンポーネントになっています。 marmelab.com

とりあえずAuth Providerの設定を行い認証が出来るところまで確認できました。

github.com

Auth Provider

まず"Auth Provider"の実装の全体は以下のようになります。

import { AUTH_CHECK, AUTH_LOGIN, AUTH_LOGOUT, AUTH_ERROR } from "react-admin";

export default (type, params) => {
  if (type === AUTH_CHECK) {
    return localStorage.getItem("name") ? Promise.resolve() : Promise.reject();
  } else if (type === AUTH_LOGIN) {
    const { company, group, name, password } = params;
    const request = new Request("http://localhost:8080/login", {
      method: "POST",
      body: JSON.stringify({ company, group, name, password }),
      headers: new Headers({ "Content-Type": "application/json" }),
    });
    return fetch(request)
      .then((response) => {
        if (response.status < 200 || response.status >= 300) {
          throw new Error(response.statusText);
        }
        return response.json();
      })
      .then((result) => {
        if (result.hasOwnProperty("name")) {
          localStorage.setItem("name", result.name);
        }
      });
  } else if (type === AUTH_LOGOUT) {
  } else if (type === AUTH_ERROR) {
  }
  return Promise.resolve();
};

AUTH_CHECKは認証済みかどうかを判定するときに呼び出され、認証済みであればPromise.resolve()を返すことで一覧に進み、Promise.reject()を返すとログイン画面に進みます。今回はローカルストレージにnameが保存されていないと未ログインとしてログイン画面に進むようにしています。

 if (type === AUTH_CHECK) {
    return localStorage.getItem("name") ? Promise.resolve() : Promise.reject();
 }

次にAUTH_LOGINはログイン実行時に呼び出され、今回は認証先のURLがhttp://localhost:8080/login(Spring Bootでプロセスを立ち上げています)で、それからリクエストを投げた後に、レスポンスで帰ってきたnameをローカルストレージに保存してログイン完了としています。

 } else if (type === AUTH_LOGIN) {
    const { company, group, name, password } = params;
    const request = new Request("http://localhost:8080/login", {
      method: "POST",
      body: JSON.stringify({ company, group, name, password }),
      headers: new Headers({ "Content-Type": "application/json" }),
    });
    return fetch(request)
      .then((response) => {
        if (response.status < 200 || response.status >= 300) {
          throw new Error(response.statusText);
        }
        return response.json();
      })
      .then((result) => {
        if (result.hasOwnProperty("name")) {
          localStorage.setItem("name", result.name);
        }
      });
 }

SpringBoot側はSpringSecurityを使っているのですが、以下のようにAuthenticationSuccessHandler で認証成功後にJsonにnameをセットして返すようにしています。

@Component
public class AuthSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    MappingJackson2HttpMessageConverter httpMessageConverter;

    public void onAuthenticationSuccess(HttpServletRequest request,   HttpServletResponse response, Authentication authentication) throws IOException  {
        HttpOutputMessage outputMessage = new ServletServerHttpResponse(response);
        Map<String, String> resMap = new HashMap<>();
        resMap.put("name", authentication.getName());
        httpMessageConverter.write(resMap, MediaType.APPLICATION_JSON, outputMessage);
        response.setStatus(HttpStatus.OK.value()); // 200 OK.
    }
}

それからAUTH_LOGOUTとAUTH_ERRORでログアウトおよび認証エラー時ですが認証情報を消すなど必要だと思いますが、今回は何もしていないです。

  } else if (type === AUTH_LOGOUT) {
  } else if (type === AUTH_ERROR) {
  }

Auth Providerの実装はこのようになっています。

これをAdminコンポーネントに設定するので、以下のようにプロパティとして渡しています。また、今回はデータ取得先が"https://jsonplaceholder.typicode.com"なのでconst dataProvider = jsonServerProvider("https://jsonplaceholder.typicode.com");をDataProvider としてAdminコンポーネントに渡しています。

import * as React from "react";
import { Admin, Resource, ListGuesser } from "react-admin";
import jsonServerProvider from "ra-data-json-server";
import authProvider from "./authProvider";
import MyLoginPage from "./MyLoginPage";
import MyLogoutButton from "./MyLogoutButton";
import { PostList } from "./posts";

const dataProvider = jsonServerProvider("https://jsonplaceholder.typicode.com");
const App = () => (
  <Admin
    loginPage={MyLoginPage}
    logoutButton={MyLogoutButton}
    dataProvider={dataProvider}
    authProvider={authProvider}
  >
    <Resource name="posts" list={PostList} />
    <Resource name="users" list={ListGuesser} />
  </Admin>
);

export default App;

カスタムログインページ

次にログイン用のページについて、デフォルトではユーザー名とパスワードのみになるのでパラメータを追加する場合はログインページを自作する必要する必要があります。自作したログインページはloginPage={MyLoginPage}のようにAdminコンポーネントのプロパティとして渡します。 ログインページは以下のようにThemeProviderを返せばよいようです。今回はcompanyとgroupをログインの項目に追加しています。

import * as React from "react";
import { useState } from "react";
import { useLogin, useNotify, Notification, defaultTheme } from "react-admin";
import { ThemeProvider } from "@material-ui/styles";
import { createMuiTheme } from "@material-ui/core/styles";

const MyLoginPage = ({ theme }) => {
  const [company, setCompany] = useState("");
  const [group, setGroup] = useState("");
  const [name, setName] = useState("");
  const [password, setPassword] = useState("");
  const login = useLogin();
  const notify = useNotify();
  const submit = (e) => {
    e.preventDefault();
    // will call authProvider.login({ email, password })
    login({ company, group, name, password }).catch(() =>
      notify("Invalid email or password")
    );
  };

  return (
    <ThemeProvider theme={createMuiTheme(defaultTheme)}>
      <input
        name="name"
        type="text"
        value={company}
        onChange={(e) => setCompany(e.target.value)}
      />
      <br />
      <input
        name="name"
        type="text"
        value={group}
        onChange={(e) => setGroup(e.target.value)}
      />
      <br />
      <form onSubmit={submit}>
        <input
          name="name"
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
        />
        <br />
        <input
          name="password"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
        <br />
        <input type="submit" value="ログイン" />
      </form>
      <Notification />
    </ThemeProvider>
  );
};

export default MyLoginPage;

一覧表示

認証後はResource コンポーネントを表示するのですが、PostListとListGuesserを表示するようになっていまして、

import * as React from "react";
import { Admin, Resource, ListGuesser } from "react-admin";
import jsonServerProvider from "ra-data-json-server";
import authProvider from "./authProvider";
import MyLoginPage from "./MyLoginPage";
import MyLogoutButton from "./MyLogoutButton";
import { PostList } from "./posts";

const dataProvider = jsonServerProvider("https://jsonplaceholder.typicode.com");
const App = () => (
  <Admin
    loginPage={MyLoginPage}
    logoutButton={MyLogoutButton}
    dataProvider={dataProvider}
    authProvider={authProvider}
  >
    <Resource name="posts" list={PostList} />
    <Resource name="users" list={ListGuesser} />
  </Admin>
);

export default App;

"Data Providerで指定したURL" + "Resourceのname"に対してREST APIを実行するようです。 ログイン直後のpostsを表示する場合、以下のようにリクエストを飛ばしており、start, end, order, sortで表示対象を指定していることが分かります、また全件数の情報についてはレスポンスヘッダーに付与してるようで100件の場合は"x-total-count: 100"を返しています。
"https://jsonplaceholder.typicode.com/posts?end=10&order=ASC&sort=id&start=0" f:id:steavevaivai:20210711234325p:plain

また、2ページ目を表示する場合、ページ上のURLのパラメータ部分のがRESTのパラメータとして渡されるようで、ページのURLが以下の場合
http://localhost:3000/#/posts?filter=%7B%7D&order=ASC&page=2&perPage=10&sort=id
RESTでは以下のリクエストを投げて一覧を更新しています。
https://jsonplaceholder.typicode.com/posts?end=20&order=ASC&sort=id&start=10
f:id:steavevaivai:20210711235022p:plain