Refineで管理画面作成

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

インストール
npm create refine-app@latest
インストールされたのはver.4.47.1
- テンプレートはNext.js
- data-providerは一旦REST API
- UIフレームワークはMUIを使ってみる
- サンプルページあり
- パッケージマネージャーはnpm
あと、DBはなんとなくPostgreSQLを使ってみようかなーという想定。
npm run dev
でローカル起動してサンプルページは確認OK
さわってみる
ドキュメントを読みながらコードを編集して、画面の変化を確認する。
ドキュメントに関しはRefineがまだ歴史も浅く発展途上ということもあり、ちょいちょい最新のコードに追従できてない部分がありそう。ドキュメントを斜め読みし、チュートリアルを途中まで(基本のCRUDあたりまで)実施。
色々噛み砕くと、
- resouce: 管理する対象の項目を定義
-
provider: システム必要な機能(データ取得とか認証とか)を定義
みたいなコンセプトらしい。
もっといろんな概念があるけど、最初からしっかり認識する必要があるのは↑の2つくらい?
サンプルではblog-postsリソースとcategoriesとリソースがある。
加えてstocksリソースをCLIから作成。
npm run refine add resource
必要なファイルが作成されて、/src/app/layout.tsxに定義が追記される。
画面を開くと左ペインのメニューが自動的に追加されている。
ただし、実際にメニューを選択し一覧表示しようとすると、REST APIのdata-providerはデフォルトでfakeAPIを参照するため、存在しないリソースは表示されない。

Material UI
Material UI is an open-source React component library that implements Google's Material Design.
翻訳
Material UI は、Google のMaterial Designを実装するオープンソースの React コンポーネント ライブラリです。
なるほど!
(Materialなんちゃら系をちゃんと識別できてなかった)

バックエンド
Refineが担うのは基本的にフロントエンドなので、バックエンドは用意する必要がある(RefineはREST APIやGraphQL、各BaaS等との連携をサポート)
Supabase
テンプレートにNext.jsを選んでいるのでAPIも同一プロジェクト内で構築できそうだが、DBをどこに用意するかとか、Next.jsでのAPI整備とかどうしよう(Next.jsの理解浅い)、、、とかあったので、BaaSを使うのが手っ取り早そう→人気そうだった、くらいの理由で一旦開発段階ではSupabaseを使ってみることに。
ちなみに認証基盤やストレージも使えるっぽい。
Supabaseセットアップ
- 無料プランで登録し
- Refineプロジェクト用のSupabaseプロジェクト(=DBみたいな)を作成
- テーブル作成
- 適当なレコード登録
一旦ここまで
Refine側の準備
インストールの時点でdata-providerとしてSupabaseを選んでなかったので、後から必要なパッケージなどをインストールする必要がある。
ただ色々試してみた感じだと、導入のパターンによってそれぞれ成果物(ファイル構成、コード)に差異がある模様。。。
- プロジェクト作成時にdata-providerにSupabase選んだ場合
- auth-providerもSupabaseように作られてしまう。
- ドキュメントに沿ってセットアップする場合
- CLIのswizzleという機能を使う場合
結局、「プロジェクト作成時にdata-providerにSupabase選んだ」別プロジェクトと現プロジェクトとの差分をみて、必要なファイルのコピーとパッケージインストールを実施。
データ取得
Supabase用のdata-providerは内部的にはどうやらREST API経由でデータ取得しようとしてる模様。
しかしテスト用に登録したはずのレコードが取得できず。
なにか設定が足りないのかも。。。と思って調べてみるとこんな記事が。
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環境で完結できそう。

実装
タイトル変更
アプリケーションのタイトルはRefineコンポーネントのオプションで設定可能
<Refine
...
options={{
title: {
text: "タイトル"
},
...
}}
>
ロゴ差し替え
Nextjsのアイコンまわり設定
Next.jsで独自svgファイルをインライン SVGとして埋め込む方法 ↓のパッケージを使って、importしたsvgをそのままReactコンポーネントとして扱えるらしい。画面上に表示するアプリケーションロゴはタイトル変更と同じくRefineコンポーネントのオプションで設定する。
src/app/app_logo.svgを配置し、import AppLogo from "./app_logo.svg"
した上で
<Refine
...
options={{
title: {
text: "タイトル",
icon: <AppLogo />
},
...
}}
>

リレーション先のデータ取得
const { dataGridProps } = useDataGrid({
resouce: "orgs"
meta: {
select: "*, users(name)"
}
});
みたいな感じで取ってこれる

Datepickerまわり
アダプタ設定
Refineで(Next.jsで?)MUIのDatepickerを使うには一工夫必要そう
DatePickerで使う日付ライブラリのアダプタ設定
↑のサンプル通りに設定しようとすると↓のエラーが
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」コンポーネント
Controllerコンポーネントのrenderプロパティに、DatePickerを出力するコールバック関数を設定するとき、
コールバック関数に渡される引数「filed」をそのままDatePickerコンポーネントのプロパティとして展開すると
「TypeError: value.isValid is not a function at AdapterDayjs.isValid」のエラーが発生。
fieldの中身は { onChange, onBlur, value, name, ref }
valueはdayjsオブジェクトに変換してDatePickerコンポーネントに渡す必要がある
DatePickerのスタイリング
MUI + React Hook FormrenderInput
プロパティは廃止されてるっぽい
おそらく、@mui/labに含まれていた安定化前のバージョンでは使えてた
自分が今使ってるのは安定化バージョンimport { DatePicker } from '@mui/x-date-pickers/DatePicker';
テキストフォームをカスタマイズするにはslot
やslotProps
プロパティを使えば良さそう
slotProps
を使って、内部のTextFieldのmargin設定できた
<DatePicker
...
slotProps={{
textField: {
margin: "normal"
},
}}
/>

データ表示
日付・日時表示
RefineのDateField
コンポーネントを使う
locale="ja"を指定すれば日本語フォーマットで表示してくれる
format="LL"で日付
format="LLL"で日時
<DateField value={record?.created_at} format="LLL" locales="ja"/>
※MUI XのDateField
とは別物
https://mui.com/x/react-date-pickers/date-field/

データベース
updated_atの自動更新
updated_atを現在時刻にする関数を作成し、レコード更新時にその関数が呼び出されるよう各テーブルにトリガーを設定する。
参考
タイムゾーンについて
created_at, updated_at以外にも日付フィールドがあるが、なんかおかしい。
DatePickerで日付設定、DateFieldで表示しているが、日本時間で表示されないので調査。
併せて、タイムゾーンをどこで扱うべきなのかまとめてみる。
DBのタイムゾーン設定
タイムゾーンを設定するクエリがあるっぽい
ただ、DBの設定はUTCにしておくことが推奨されてる模様
created_atやupdated_atのデータタイプ、デフォルト値
現状、データタイプは timestamp
デフォルト値は CURRENT_TIMESTAMP
データタイプはtimestamptzを使った方が良さそう
デフォルト値もnow()とかの方が良いのかも。もうちょい調べる
追記)
CURRENT_TIMESTAMPでもnow()でも同じ結果が得られそう。
ただ、Supabase上のサジェストだとnow()が出てくるので、now()を採用する
アプリケーション側
DateFieldコンポーネントは内部ではDay.jsを使用してる
(おそらくブラウザのシステムタイムゾーンを参照してる)
データ登録側では、MUIのDatePickerを使うときにタイムゾーンによる変換が上手くいってなさそう
(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オブジェクトのまま扱うべし

UI
Tab
MUI - 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、ドキュメント読みながら結構サクッと作れた
Modal
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は、おそらく一覧画面から個別の編集画面に遷移することなく複数のレコードを編集するようなケースを想定しているため、それを実現するためにモーダルを表示する都度異なるレコードの編集ができるよう設計されているのだと思う。

supabaseメモ
supabaseはfirebase互換?もしそうであればfirebase運用でも良いかも、、?
→互換、というよりfirebase代替として生まれたプラットフォームらしい
firebase→supabaseへの移行とかはあり得るっぽい
↑の記事でも「supabaseはFlutterのDBとして最近注目を集めている」とあるように、Flutterアプリのバックエンドとして人気そう
AWS上で運用するならECSでコンテナ実行+RDSの形でいける?
→supabaseをコンテナで動かす場合にバックエンドとDBをセパレートできるのか?は要確認
→Dockerの設定次第でできるんじゃなかろうか?
→CloudFormationのテンプレートとかもあるっぽい

Autocomplete
MUI - Autocomplete
Refine - useAutocomplete