Zenn
Open12

Refineで管理画面作成

r_matsubar_matsuba

Refine

https://refine.dev/

ファーストインプレッション

  • Reactベースの管理画面を作るのになんか良さげなフレームワークっぽい
  • いろいろ1から実装しなくても各種サービスとの連携が用意されてる
  • Next.jsも使えるっぽい。使ってみよ(React.js、Next.js共に実務経験無し、プライベートで少し学習したレベル)
r_matsubar_matsuba

インストール

npm create refine-app@latest

インストールされたのはver.4.47.1

  • テンプレートはNext.js
  • data-providerは一旦REST API
  • UIフレームワークはMUIを使ってみる
  • サンプルページあり
  • パッケージマネージャーはnpm

あと、DBはなんとなくPostgreSQLを使ってみようかなーという想定。

npm run dev

でローカル起動してサンプルページは確認OK

さわってみる

ドキュメントを読みながらコードを編集して、画面の変化を確認する。
https://refine.dev/docs/
ドキュメントに関しはRefineがまだ歴史も浅く発展途上ということもあり、ちょいちょい最新のコードに追従できてない部分がありそう。

ドキュメントを斜め読みし、チュートリアルを途中まで(基本のCRUDあたりまで)実施。
色々噛み砕くと、

  • resouce: 管理する対象の項目を定義
  • provider: システム必要な機能(データ取得とか認証とか)を定義
    みたいなコンセプトらしい。
    もっといろんな概念があるけど、最初からしっかり認識する必要があるのは↑の2つくらい?

サンプルではblog-postsリソースとcategoriesとリソースがある。
加えてstocksリソースをCLIから作成。

npm run refine add resource

必要なファイルが作成されて、/src/app/layout.tsxに定義が追記される。
画面を開くと左ペインのメニューが自動的に追加されている。
ただし、実際にメニューを選択し一覧表示しようとすると、REST APIのdata-providerはデフォルトでfakeAPIを参照するため、存在しないリソースは表示されない。
https://api.fake-rest.refine.dev/

r_matsubar_matsuba

Material UI

https://mui.com/material-ui/

Material UI is an open-source React component library that implements Google's Material Design.

翻訳

Material UI は、Google のMaterial Designを実装するオープンソースの React コンポーネント ライブラリです。

なるほど!
(Materialなんちゃら系をちゃんと識別できてなかった)

r_matsubar_matsuba

バックエンド

Refineが担うのは基本的にフロントエンドなので、バックエンドは用意する必要がある(RefineはREST APIやGraphQL、各BaaS等との連携をサポート)

Supabase

https://supabase.com/
BaaSと呼ばれる類のものらしい。DB+APIまで提供してくれるっぽい。
テンプレートにNext.jsを選んでいるのでAPIも同一プロジェクト内で構築できそうだが、DBをどこに用意するかとか、Next.jsでのAPI整備とかどうしよう(Next.jsの理解浅い)、、、とかあったので、BaaSを使うのが手っ取り早そう→人気そうだった、くらいの理由で一旦開発段階ではSupabaseを使ってみることに。
ちなみに認証基盤やストレージも使えるっぽい。

Supabaseセットアップ

  • 無料プランで登録し
  • Refineプロジェクト用のSupabaseプロジェクト(=DBみたいな)を作成
  • テーブル作成
  • 適当なレコード登録
    一旦ここまで

Refine側の準備

インストールの時点でdata-providerとしてSupabaseを選んでなかったので、後から必要なパッケージなどをインストールする必要がある。
ただ色々試してみた感じだと、導入のパターンによってそれぞれ成果物(ファイル構成、コード)に差異がある模様。。。

結局、「プロジェクト作成時にdata-providerにSupabase選んだ」別プロジェクトと現プロジェクトとの差分をみて、必要なファイルのコピーとパッケージインストールを実施。

データ取得

Supabase用のdata-providerは内部的にはどうやらREST API経由でデータ取得しようとしてる模様。
しかしテスト用に登録したはずのレコードが取得できず。
なにか設定が足りないのかも。。。と思って調べてみるとこんな記事が。
https://zenn.dev/yoshikawa_fuma/articles/5317fd9bcfdfa8
RLS(Row Level Security)を有効にしてる場合だと、「ポリシー」とやらを作成する必要があったっぽい。
とりあえずSELECTを許可するポリシーを作成したらAPI経由でデータ取得できた。
ちなみにポリシー設定の画面にはこんな文言が。

Row Level Security is enabled for this table, but no policies are set
Select queries may return 0 results.

AWS

現状Supabaseサービス上でDB構築しているが、自社のメインIaaSがAWSなのでゆくゆくはRDSやらへの移行も考える必要が出てくるかも?
もしくはSupabaseがセルフホスティングもできるとのことなので、ECSとかでコンテナ動かせばAWS環境で完結できそう。
https://supabase.com/docs/guides/self-hosting

r_matsubar_matsuba

実装

タイトル変更

アプリケーションのタイトルはRefineコンポーネントのオプションで設定可能
https://refine.dev/docs/core/refine-component/#title

<Refine
    ...
    options={{
        title: {
            text: "タイトル"
        },
        ...
    }}
>

ロゴ差し替え

Nextjsのアイコンまわり設定
https://nextjs.org/docs/app/api-reference/file-conventions/metadata/app-icons
Next.jsで独自svgファイルをインライン SVGとして埋め込む方法
https://zenn.dev/toono_f/articles/bd50ddd0a7bc76
↓のパッケージを使って、importしたsvgをそのままReactコンポーネントとして扱えるらしい。
https://react-svgr.com/docs/next/

画面上に表示するアプリケーションロゴはタイトル変更と同じくRefineコンポーネントのオプションで設定する。
src/app/app_logo.svgを配置し、import AppLogo from "./app_logo.svg"した上で

<Refine
    ...
    options={{
        title: {
            text: "タイトル",
            icon: <AppLogo />
        },
        ...
    }}
>
r_matsubar_matsuba

Datepickerまわり

アダプタ設定

Refineで(Next.jsで?)MUIのDatepickerを使うには一工夫必要そう

DatePickerで使う日付ライブラリのアダプタ設定
https://mui.com/x/react-date-pickers/getting-started/#date-library-adapter-setup

↑のサンプル通りに設定しようとすると↓のエラーが
https://qiita.com/tkms13/items/0bc3f40422772e3ec17d

LocalizationProviderで直接Wrapせずに、LocalizationProviderでchildrenをWrapするだけのClientコンポーネントを作成。

"use client";

// DatePicker https://mui.com/x/react-date-pickers/getting-started/#installation
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import 'dayjs/locale/ja';
import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider';
import React from "react";

export default function MuiDatePickerWrapper({children}: Readonly<{
  children: React.ReactNode;
}>) {
    return (
        <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ja">
            {children}
        </LocalizationProvider>
    );
};

RootLayoutで設定しておけばアプリケーション全体ですぐDatePickerが使える。

<Refine
    ...
>
    <MuiDatePickerWrapper>
        {children}
    </MuiDatePickerWrapper>
</Refine>

MUI DateTimePicker + Day.js

react-hook-formを使う際に外部UIライブラリとの連携を容易にするための「Controller」コンポーネント
https://react-hook-form.com/get-started#IntegratingControlledInputs
https://www.react-hook-form.com/api/usecontroller/controller/

Controllerコンポーネントのrenderプロパティに、DatePickerを出力するコールバック関数を設定するとき、
コールバック関数に渡される引数「filed」をそのままDatePickerコンポーネントのプロパティとして展開すると
「TypeError: value.isValid is not a function at AdapterDayjs.isValid」のエラーが発生。

fieldの中身は { onChange, onBlur, value, name, ref }

valueはdayjsオブジェクトに変換してDatePickerコンポーネントに渡す必要がある
https://stackoverflow.com/questions/78594765/how-to-use-mui-datepicker-component-in-react-hook-form/78594797#78594797

DatePickerのスタイリング

MUI + React Hook Form
https://zenn.dev/longbridge/articles/640710005e11b1
最新のDatePickerではrenderInputプロパティは廃止されてるっぽい
おそらく、@mui/labに含まれていた安定化前のバージョンでは使えてた

自分が今使ってるのは安定化バージョンimport { DatePicker } from '@mui/x-date-pickers/DatePicker';
テキストフォームをカスタマイズするにはslotslotPropsプロパティを使えば良さそう
https://stackoverflow.com/questions/76055985/renderinput-error-while-using-datepicker-from-mui-x
https://mui.com/x/react-date-pickers/custom-field/
slotPropsを使って、内部のTextFieldのmargin設定できた

<DatePicker
    ...
    slotProps={{
        textField: {
            margin: "normal"
        },
    }}
/>
r_matsubar_matsuba

データベース

updated_atの自動更新

updated_atを現在時刻にする関数を作成し、レコード更新時にその関数が呼び出されるよう各テーブルにトリガーを設定する。

参考
https://qiita.com/ruemura3/items/7bdca11243c8f1b49ae2
https://zenn.dev/buenotheebiten/articles/6527222e649633

タイムゾーンについて

created_at, updated_at以外にも日付フィールドがあるが、なんかおかしい。
DatePickerで日付設定、DateFieldで表示しているが、日本時間で表示されないので調査。
併せて、タイムゾーンをどこで扱うべきなのかまとめてみる。

DBのタイムゾーン設定

タイムゾーンを設定するクエリがあるっぽい
https://qiita.com/7mpy/items/bedd102355a51e93a7df

ただ、DBの設定はUTCにしておくことが推奨されてる模様
https://qiita.com/ryuta_yoshida/items/c57547b1a0d57427c858#db設定

created_atやupdated_atのデータタイプ、デフォルト値

現状、データタイプは timestamp
デフォルト値は CURRENT_TIMESTAMP

データタイプはtimestamptzを使った方が良さそう
https://wiki.postgresql.org/wiki/Don't_Do_This#Don.27t_use_timestamp_.28without_time_zone.29

デフォルト値もnow()とかの方が良いのかも。もうちょい調べる

追記)
CURRENT_TIMESTAMPでもnow()でも同じ結果が得られそう。
ただ、Supabase上のサジェストだとnow()が出てくるので、now()を採用する

アプリケーション側

DateFieldコンポーネントは内部ではDay.jsを使用してる
https://refine.dev/docs/ui-integrations/material-ui/components/fields/date-field/
DateFieldコンポーネントでDBから取得したUTC時刻を表示する際には、自動的に日本のタイムゾーンに変換して表示してくれるっぽい
(おそらくブラウザのシステムタイムゾーンを参照してる)

データ登録側では、MUIのDatePickerを使うときにタイムゾーンによる変換が上手くいってなさそう
https://mui.com/x/react-date-pickers/getting-started/
(JSTの0時(UTCにおける前日15時)にしたいのにUTC0時でDBに格納されてしまってる)

追記)
DatePickerで得られるdayjsオブジェクトを、時刻「00:00:00」に丸めるためにformatメソッドで文字列にしてしまっていたのが問題だった(初歩的。。。)

dayjsオブジェクトをそのままControllerコンポーネントからコールバック関数に渡されるonChangeメソッドの引数に指定すればOK

まとめ

  • DB自体のタイムゾーン設定はしない(UTCのままで)
  • 日時フィールド(created_at, updated_at, etc.)のデータタイプはtimestamp→timestamptzに統一
  • デフォルト値はCURRENT_TIMESTAMP→now()に(updated_atの自動更新もnow()に)
  • DatePickerで得られるdayjsオブジェクトはdayjsオブジェクトのまま扱うべし
r_matsubar_matsuba

UI

Tab

MUI - Tabs
https://mui.com/material-ui/react-tabs/

a11yProps = Accessibility Props
長いとこういうふうに省略表現される文化を知った

a11y = Accessibility
https://developer.mozilla.org/ja/docs/Glossary/Accessibility#:~:text=ウェブアクセシビリティ (%E7%95%A5%E8%AA%9E%3A%20A11Y%E3%80%81,%E3%81%9F%E3%82%81%E3%81%AE%E3%83%99%E3%82%B9%E3%83%88%E3%83%97%E3%83%A9%E3%82%AF%E3%83%86%E3%82%A3%E3%82%B9%E3%81%A7%E3%81%99%E3%80%82

Tab、ドキュメント読みながら結構サクッと作れた

useModalForm
https://refine.dev/docs/packages/react-hook-form/use-modal-form/#usage
https://refine.dev/docs/examples/form/mui/useModalForm/

注意点。

editやcloneで使うとき、

const {
    formState: {isDirty, errors},
    refineCore: {onFinish, formLoading},
    modal: {visible, close, show},
    saveButtonProps,
} = useModalForm({
    refineCoreProps: {
        resource: "orders",
        action: "edit",
        id: orderId,
    },
});

useModalFormにrefineCoreProps: {id: orderId}のように固定のIDを渡しても使ってくれないっぽい。

<button onClick={() => show(orderId)}>Edit</button>

このように、show関数の引数にeditやcloneの対象レコードのIDを渡す必要がある。

useModalFormは、おそらく一覧画面から個別の編集画面に遷移することなく複数のレコードを編集するようなケースを想定しているため、それを実現するためにモーダルを表示する都度異なるレコードの編集ができるよう設計されているのだと思う。

r_matsubar_matsuba

supabaseメモ

supabaseはfirebase互換?もしそうであればfirebase運用でも良いかも、、?
→互換、というよりfirebase代替として生まれたプラットフォームらしい
https://www.issoh.co.jp/column/details/3002/#SupabaseFirebase-3
firebase→supabaseへの移行とかはあり得るっぽい
https://zenn.dev/hassaku_hako/articles/5686adfd6d4a86
↑の記事でも「supabaseはFlutterのDBとして最近注目を集めている」とあるように、Flutterアプリのバックエンドとして人気そう
https://findy-tools.io/products/supabase/32/140

AWS上で運用するならECSでコンテナ実行+RDSの形でいける?
→supabaseをコンテナで動かす場合にバックエンドとDBをセパレートできるのか?は要確認
→Dockerの設定次第でできるんじゃなかろうか?
https://supabase.com/docs/guides/self-hosting/docker
→CloudFormationのテンプレートとかもあるっぽい
https://github.com/supabase-community/supabase-on-aws

作成者以外のコメントは許可されていません