AdminJSで管理画面をサクッと開発　カスタムコンポーネント・Tailwind CSSの適用も

本記事はQmonus Value Streamの投稿キャンペーン記事です。

## はじめに

個人開発でアプリのコードを書くのはまあ良いとしても、管理のために必要なCRUD処理のコードを書くのは結構億劫だし、価値も生み出せているとは言えないのでやりたくない作業の１つだと思う。

そこで今回は、[AdminJS](https://docs.adminjs.co/)を利用してサクッと管理画面の開発をしたいと思う。また、カスタムのコンポーネントを作成してそれを適用するという事もやってみようと思う。

## AdminJSで管理画面をサクッと開発

今回はMySQLをデータベースとして利用しているアプリの管理画面を開発する。アプリの方ではORMを利用していないが、管理画面の開発では[Prisma](https://www.prisma.io/docs)を利用する（AdminJSのアダプターにPrisnaがあること、DDLから簡単にPrisma schemaを作成できること、の2点でそうすることにした）。

### prisma schemaの作成

まずはprisma schemaを作成する。

`npx prisma init`で`prisma/schema.prisma`と`.env`が作成される。`.env`を以下のように書き換えてMySQLの接続情報を設定する。

```:.env
DATABASE_URL="mysql://root:@localhost:3306/sample_db"
```

続いて現在のDDLに基づいて、prisma schemaを以下のコマンドで作成し、prisma clientを作成する。

```console
$ npx prisma db pull
$ npx prisma generate
```

これでPrismaの事前準備は完了。

- 参考
  - [Introspection](https://www.prisma.io/docs/getting-started/setup-prisma/add-to-existing-project/relational-databases/introspection-node-mysql )

### AdminJSをExpressサーバーに導入する

今回はNode.jsのExpressサーバーを管理画面のサーバーとすることにしたので、AdminJSのExpress用のプラグインを利用してAdminJSの設定をする。

```js:src/app.js
import AdminJS from 'adminjs';
import AdminJSExpress from '@adminjs/express';
import express from 'express';
import { Database, Resource, getModelByName } from '@adminjs/prisma';
import { PrismaClient } from '@prisma/client';

const PORT = 3080;
const prisma = new PrismaClient();
AdminJS.registerAdapter({ Database, Resource });

const start = async () => {
	const app = express();

	const adminOptions = {
		resources: [
			{
				resource: { model: getModelByName('users'), client: prisma },
				options: {}
			},
			{
				resource: { model: getModelByName('todos'), client: prisma },
				options: {}
			},
			...
		]
	};
	const admin = new AdminJS(adminOptions);

	const adminRouter = AdminJSExpress.buildRouter(admin);
	app.use(admin.options.rootPath, adminRouter);

	app.listen(PORT, () => {
		console.log(`AdminJS started on http://localhost:${PORT}${admin.options.rootPath}`);
	});
};

await start();
```

上記のコードの内、`adminOptions`の部分などはprismaのアダプター用の設定で、それ以外はexpressのプラグインの設定になる。詳細は以下の参考に書かれている通り。

ここまで設定すると、以下の画像のようにCRUDを行る管理画面が立ち上がるようになる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/ee461898-54c1-2dd3-b86e-5845fc546f9e.png)

あとは画面にある各種ボタンからCRUDを行って管理する感じになる。

ここまでAdminJSでの管理画面の開発は完了になる。続けて、カスタムコンポーネントとCSSスタイルの上書きをやってみたいと思う。

- 参考
  - [@adminjs/express](https://docs.adminjs.co/installation/plugins/express) 
  - [@adminjs/prisma](https://docs.adminjs.co/installation/adapters/prisma) 

## カスタムコンポーネントを適用する

AdminJSではReactコンポーネントを独自に実装することで、カスタムコンポーネントとして適用できる。

AdminJSのデフォルトの入力項目の実装は、HTMLの`input`タグで改行ができない。今回はこれをテキストアリアに変えることで改行も入力できるようにしてみる。

まずはReactのコンポーネントを実装する必要があるが、ReactがわからなくてもChatGPT等の生成AIに頼めば以下のようなコードは簡単に生成してくれるので、それを利用すればReactわからん！という状態でもなんとかなるだろう（管理画面の作りこみをしたいわけではないので）。

```tsx:src/adminjs/components/TextareaEdit.tsx
import React from 'react';
import { BasePropertyProps } from 'adminjs';

const TextareaEdit: React.FC<BasePropertyProps> = (props) => {
	const { record, property, onChange } = props;

	return (
		<div className="mb-4">
			<label className="block text-sm mb-2">{property.label}</label>
			<textarea
				className="mt-1 block w-full p-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
				rows={5}
				value={record.params[property.path] || ''}
				onChange={(e) => onChange(property.path, e.target.value)}
			/>
		</div>
	);
};

export default TextareaEdit;
```

続いてこのコンポーネントをAdminJSに適用するための設定を実装するが、以下のような`ComponentLoader`で読み込んだコンポーネントのオブジェクトを作成し、それをexportしておき、それを適用したい`resource`のオプションで設定する。

```js:src/adminjs/components.js
import { ComponentLoader } from 'adminjs';

const componentLoader = new ComponentLoader();

const Components = {
	TextareaEdit: componentLoader.add('TextareaEdit', './components/TextareaEdit')
};

export { componentLoader, Components };
```
```js:src/app.js
import AdminJS from 'adminjs';
import AdminJSExpress from '@adminjs/express';
import express from 'express';
import { Database, Resource, getModelByName } from '@adminjs/prisma';
import { PrismaClient } from '@prisma/client';
import { Components, componentLoader } from './adminjs/components.js';

const PORT = 3080;
const prisma = new PrismaClient();
AdminJS.registerAdapter({ Database, Resource });

const start = async () => {
	const app = express();

	const adminOptions = {
		resources: [
			{
				resource: { model: getModelByName('users'), client: prisma },
				options: {
					properties: {
						name: {
							type: 'string',
							components: {
								edit: Components.TextareaEdit,  // <- ここに設定する							
                            }
						}
					}
				}
			},
            ...
		],
		componentLoader,
	};
	const admin = new AdminJS(adminOptions);

	const adminRouter = AdminJSExpress.buildRouter(admin);
	app.use(admin.options.rootPath, adminRouter);

	app.listen(PORT, () => {
		console.log(`AdminJS started on http://localhost:${PORT}${admin.options.rootPath}`);
	});
};

await start();
```

上記のような設定をすると、以下のようにカスタムコンポーネントが適用されて、`input`ではなく`textarea`になることが確認できる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1372684/962172bd-9b4a-5f1b-975a-a18597782bc2.png)

上記はTailwind CSSが適用されてそのスタイルに装飾されているが、それについては次の項で見ていく。

- 参考
  - [Writing your own Components](https://docs.adminjs.co/ui-customization/writing-your-own-components)

## Tailwind CSSを適用する

[カスタムコンポーネントを適用する](#カスタムコンポーネントを適用する)で見たように、適用したコンポーネントにはTailwind CSSのスタイルが適用されていた。これはAdminJSの`assets`でスタイルを指定することで実現できる。

```js:src/app.js
...
import * as url from 'url';
import path from 'path';

const PORT = 3000;
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));

const prisma = new PrismaClient();
AdminJS.registerAdapter({ Database, Resource });

const start = async () => {
	const app = express();

	const adminOptions = {
		resources: [
			{
				...
			},
            ...
		],
		componentLoader,
		assets: {
			styles: ['/tailwind.css']
		}
	};
	const admin = new AdminJS(adminOptions);

	const adminRouter = AdminJSExpress.buildRouter(admin);
	app.use(admin.options.rootPath, adminRouter);
	app.use(express.static(path.join(__dirname, '../public')));

	app.listen(PORT, () => {
		console.log(`AdminJS started on http://localhost:${PORT}${admin.options.rootPath}`);
	});
};

await start();
```

実装としては、上記のように`adminOptions`の`assets`の設定を行い、`assets.styles`で指定したCSSファイルがExpressのstaticの機能で配信されるようにすればよい。今回は`./public`ディレクトリをstaticの対象にしているので、`./public/tailwind.css`というCSSがAdminJSのスタイルとして追加で設定されるようになる。

ところで`./public/tailwind.css`を作成する方法だが、[Get started with Tailwind CSS](https://tailwindcss.com/docs/installation)に書かれている方法を利用する。
つまり、`content`にTailwind CSSを利用しているファイルのパスを指定して、
```console
$ npx tailwindcss -i ./src/styles/tailwind.css -o ./public/tailwind.css --watch
```
というコマンドを実行すれば作成できる。

```js:tailwind.config.js
export default {
	content: [
		'./src/adminjs/components/**/*.tsx'
	],
	theme: {
		extend: {}
	},
	plugins: []
};
```
```css:src/styles/tailwind.css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

- 参考
  - [Overwriting CSS styles](https://docs.adminjs.co/ui-customization/overwriting-css-styles)

## まとめとして

今回はAdminJSを利用してサクッと管理画面を開発する方法を見てきた。また、カスタムコンポーネントを適用し、カスタムコンポーネントを実装する際にTailwind CSSを利用できるようにする方法もみてきた。
ほんの30分程度で管理画面を作成してやりたいことができるようになったので、管理画面が必要になった時には今後もAdminJSを利用していきたいと思った。