В предыдущей статье мы рассмотрели, как создать панель администратора react с помощью пакета react-admin, где мы смогли создать модуль пользователя и поста, с возможностью
- просматривать все существующие посты
- Создать новый пост
- Редактировать пост
- Удалить пост
- Фильтровать сообщения по пользователю
- Также мы можем экспортировать наши данные в CSV файл.
Единственным недостатком нашей предыдущей сборки является то, что мы использовали предоставленный dataProvider, предоставленный нам react-admin, в реальных проектах мы определенно будем работать с нашими собственными API и бэкендом, поэтому важно знать, как интегрировать наш собственный бэкенд. К концу статьи вы сможете
- Интегрировать свой собственный API/бэкенд в react-admin
- Добавить пагинацию
- Добавить фильтрацию и поиск
- Добавлять страницы аутентификации
Без лишних слов, давайте приступим.
Сначала мы клонируем наш предыдущий репозиторий и сделаем чек-аут в новую ветку.
1.
https://github.com/okeken/react-admin-tutorial.git
2.
cd react-admin-tutorial
3.
git checkout -b v2
4.
yarn or npm install
Шаг 4 установит все зависимости, если все работает нормально, ваш экран должен выглядеть как показано ниже.
Прежде чем мы продолжим, мы быстро настроим наш бэкенд для этого демо, мы будем использовать json-server, json-server-auth и fakerjs.
Создайте новую папку и запустите в ней новый проект nodejs,
откройте новый терминал и выполните следующие команды по порядку
mkdir admin-backend
cd admin-backend
npm init --y
Эти команды настроят наш проект nodejs, а затем мы установим необходимые пакеты,
yarn add json-server @faker-js/faker
Есть два способа настроить нашу базу данных, мы можем создать db.json или index.js.
Но мы будем использовать смесь обоих способов из-за некоторой гибкости, которая нам нужна либо после развертывания, либо во время разработки Итак, сначала мы создадим index.js
Добавьте код ниже,
const { faker } = require("@faker-js/faker");
// sample brand list
const brandList = [
{
id: 1,
name: "Unbranded",
},
{
id: 2,
name: "Handmade",
},
{
id: 3,
name: "Recycled",
},
{
id: 4,
name: "Bespoke",
},
{
id: 5,
name: "Small",
},
{
id: 6,
name: "Generic",
},
{
id: 7,
name: "Intelligent",
},
{
id: 8,
name: "Licensed",
},
{
id: 9,
name: "Oriental",
},
{
id: 10,
name: "Sleek",
},
{
id: 11,
name: "Luxurious",
},
{
id: 12,
name: "Gorgeous",
},
{
id: 13,
name: "Refined",
},
{
id: 14,
name: "Awesome",
},
{
id: 15,
name: "Practical",
},
{
id: 16,
name: "Electronic",
},
{
id: 17,
name: "Fantastic",
},
{
id: 18,
name: "Modern",
},
{
id: 19,
name: "Handcrafted",
},
{
id: 20,
name: "Tasty",
},
];
module.exports = () => {
const data = { products: [], customers: [], orders: [], brands: brandList };
// Create 2000 products
for (let i = 0; i < 2000; i++) {
const title = faker.commerce.product();
const price = faker.commerce.price();
const description = faker.commerce.productDescription();
const image = faker.image.image();
const chosenBrand = Math.floor(
Math.random() * (brandList?.length ?? 10 + 1)
);
const brand = brandList[chosenBrand]; // pick a random brand from the brands array with ranging from 0 to the length of the brands array
const brandName = (id) => brandList.find((brand) => brand.id === id)?.name;
data.products.push({
id: i + 1,
title,
price,
description,
image,
brandId: brand.id,
brandName: brandName(brand.id),
});
}
// Create 50 users
for (let i = 0; i < 50; i++) {
const name = faker.name.firstName();
const email = faker.internet.email();
const address = faker.address.streetAddress();
const city = faker.address.city();
const state = faker.address.state();
const zip = faker.address.zipCode();
const phone = faker.phone.phoneNumber();
const country = faker.address.country();
data.customers.push({
id: i + 1,
name,
email,
phone,
address: `${address} ${city}, ${state} ${zip} ${country}`,
});
}
// create 300 orders
for (let i = 0; i < 500; i++) {
const customerId = faker.datatype.number({ min: 1, max: 50 });
const productId = faker.datatype.number({ min: 1, max: 2000 });
const quantity = faker.datatype.number({ min: 1, max: 10 });
const price = faker.commerce.price();
data.orders.push({
id: i + 1,
customerId,
productId,
quantity,
price,
total: price * quantity,
});
}
return data;
};
Перейдите в package.json, в разделе scripts, удалите, по умолчанию
"test": "echo "Error: no test specified" && exit 1"
и замените его на
"dev": "json-server --watch index.js --port 5000 --no-cors",
"start": "json-server index.js --port 5000 --no-cors"
—watch -> следит за изменениями файлов.
—port -> для установки порта, через который мы запускаем наш сервер
-no-cors -> чтобы предотвратить любые проблемы с cors во фронтенде.
Сохраните изменения и запустите сервер в терминале с помощью команды
yarn dev
Если все работает как ожидалось, вы должны увидеть следующие экраны как в терминале, так и в браузере.
Мы закончили с бэкендом, давайте вернемся к фронтенду.
Давайте подключимся к реальному API.
Мы попытаемся смоделировать структуру нашего API так, чтобы она выглядела как в таблице ниже, на основе этого мы попытаемся настроить react-admin для использования нашего API.
Действия | Конечные точки API |
---|---|
получить все продукты | GET baseUrl/products |
получить продукт по идентификатору | GET baseUrl/products/id |
обновить продукт | PUT baseUrl/products/id |
удалить продукт | DELETE baseUrl/products/id |
создать продукт | POST baseUrl/products/id |
получить постраничные продукты | GET baseUrl/products?_page=1&_limit=10 |
поиск продуктов | GET baseUrl/products?q=search terms |
фильтр продуктов | GET baseUrl/products?brandsId=2 |
Создайте файл dataProvider.js и поместите в него приведенный ниже код. Этот файл отвечает за отображение наших запросов API в react-admin, считайте его переводчиком, который нужен react-admin, чтобы общаться с нашим API и эффективно предоставлять необходимые манипуляции для создания нашей приборной панели.
import { fetchUtils } from 'react-admin';
import { stringify } from 'query-string';
const apiUrl = 'localhost:5000';
const httpClient = fetchUtils.fetchJson;
export default {
getList: (resource, params) => {
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
const query = {
sort: JSON.stringify([field, order]),
range: JSON.stringify([(page - 1) * perPage, page * perPage - 1]),
filter: JSON.stringify(params.filter),
};
const url = `${apiUrl}/${resource}?${stringify(query)}`;
return httpClient(url).then(({ headers, json }) => ({
data: json,
total: parseInt(headers.get('content-range').split('/').pop(), 10),
}));
},
getOne: (resource, params) =>
httpClient(`${apiUrl}/${resource}/${params.id}`).then(({ json }) => ({
data: json,
})),
getMany: (resource, params) => {
const query = {
filter: JSON.stringify({ id: params.ids }),
};
const url = `${apiUrl}/${resource}?${stringify(query)}`;
return httpClient(url).then(({ json }) => ({ data: json }));
},
getManyReference: (resource, params) => {
const { page, perPage } = params.pagination;
const { field, order } = params.sort;
const query = {
sort: JSON.stringify([field, order]),
range: JSON.stringify([(page - 1) * perPage, page * perPage - 1]),
filter: JSON.stringify({
...params.filter,
[params.target]: params.id,
}),
};
const url = `${apiUrl}/${resource}?${stringify(query)}`;
return httpClient(url).then(({ headers, json }) => ({
data: json,
total: parseInt(headers.get('content-range').split('/').pop(), 10),
}));
},
update: (resource, params) =>
httpClient(`${apiUrl}/${resource}/${params.id}`, {
method: 'PUT',
body: JSON.stringify(params.data),
}).then(({ json }) => ({ data: json })),
updateMany: (resource, params) => {
const query = {
filter: JSON.stringify({ id: params.ids}),
};
return httpClient(`${apiUrl}/${resource}?${stringify(query)}`, {
method: 'PUT',
body: JSON.stringify(params.data),
}).then(({ json }) => ({ data: json }));
},
create: (resource, params) =>
httpClient(`${apiUrl}/${resource}`, {
method: 'POST',
body: JSON.stringify(params.data),
}).then(({ json }) => ({
data: { ...params.data, id: json.id },
})),
delete: (resource, params) =>
httpClient(`${apiUrl}/${resource}/${params.id}`, {
method: 'DELETE',
}).then(({ json }) => ({ data: json })),
deleteMany: (resource, params) => {
const query = {
filter: JSON.stringify({ id: params.ids}),
};
return httpClient(`${apiUrl}/${resource}?${stringify(query)}`, {
method: 'DELETE',
}).then(({ json }) => ({ data: json }));
}
};
Теперь давайте начнем изменять этот файл, основываясь на структуре нашего API.
- GetList: Возвращает все элементы в ресурсе, из нашего api он возвращает массив продуктов, заказов, пользователей и брендов. чтобы использовать его, мы должны сначала изменить наш
const query = { sort: JSON.stringify([field, order]), range: JSON.stringify([(page - 1) * perPage, page * perPage - 1]), filter: JSON.stringify(params.filter), };
иreturn httpClient(url).then(({ headers, json }) => ({ data: json, total: parseInt(headers.get('content-range').split('/'). pop(), 10), }));
toconst query = { _page: page, _limit: perPage, ...params.filter, };
return httpClient(url).then((resp) => { return { data: resp.json, total: +resp.headers.get("X-Total-Count"), }; });
_page, _limit приходят из нашего api для целей пагинации, params.filter вернет объект, который мы можем использовать для сортировки, фильтрации, упорядочивания. ключ total в нашем операторе return представляет общее количество элементов в нашем ресурсе, json-сервер выставляет заголовок «X-Total-Count» для использования здесь, обратите внимание на знак + перед resp.headers, он используется для преобразования из строки в целое число.
- DeleteMany: json-сервер не позволяет нам удалять несколько элементов одновременно, однако, я сделал для этого workarond. сначала мы установим заголовок для использования
const headers = { Accept: "application/json", "Content-Type": "application/json", };
мы убралиfetchUtils.fetchJson()
и заменили его наfetch
, в конце концов, это просто обертка вокруг fetch плюс некоторые небольшие дополнения.
params.ids
даст нам массив идентификаторов элементов, которые мы хотим удалить, мы пройдемся по нему и сделаем наш API запрос, затем мы используем promise.all для получения ответа на все наши запросы. блестяще! 😎
const delFetch = params.ids.map((eleid) => {
return fetch(`${apiUrl}/${resource}/${eleid}`, {
method: "DELETE",
headers: headers,
});
});
const response = await Promise.all([delFetch]).then((res) => {
return {
data: params.ids,
};
});
Примечание: формат, который мы возвращаем из нашего ответа
должен быть в этом формате, ключ ‘data’ со значением params.ids в виде массива. Иначе react-admin начнет на нас кричать.
Если вы добрались до этого места, я очень горжусь вашим прогрессом. 👏🏼👏🏼
Теперь, давайте пойдем и интегрируем все наши изменения в наше приложение и начнем что-то делать. 😁
Перейдите в app.js и импортируйте dataProvider.js, замените его на предыдущий dataProvider и закомментируйте наши предыдущие компоненты.
В каталоге компонентов создайте новый компонент Filter.jsx и вставьте в него приведенный ниже код.
//FilterPost.jsx
import React from "react";
import { Filter as FilterAdmin, ReferenceInput, TextInput, SelectInput } from "react-admin";
const Filter = ({searchLabel = 'Search', label='', reference='', source='', ...otherProps}) => (
<FilterAdmin {...otherProps}>
<TextInput
label={searchLabel}
source="q"
alwaysOn />
<ReferenceInput
label={label}
source={source}
reference={reference}
allowEmpty>
<SelectInput optionText="name" />
</ReferenceInput>
</FilterAdmin>
);
export default Filter;
В каталоге src снова создайте новую папку под названием «views».
Создайте в ней подкаталог, как показано на рисунке ниже
(
в views/brands/index.jsx
добавьте в него код, приведенный ниже
import * as React from "react";
import { Datagrid, List, TextField } from "react-admin";
const BrandList = props => (
<List {...props}>
<Datagrid rowClick="edit">
<TextField source="id" />
<TextField source="name" />
</Datagrid>
</List>)
export default BrandList;
в файле views/products/index.jsx
import React from "react";
import { List, Datagrid, TextField, EditButton } from "react-admin";
import Filter from "../../Components/Filter";
const filterProps = {
label: "brands",
reference: "brands",
source: "brandId",
}
const ProductsList = props => (
<List filters={<Filter {...filterProps} />} {...props}>
<Datagrid rowClick="edit">
<TextField source="id" />
<TextField source="title" />
<TextField source="brandName" />
<TextField source="price" />
<TextField source="description" />
<EditButton />
</Datagrid>
</List>
);
export default ProductsList
в views/products/components/CreateProducts.jsx
добавьте код ниже
//CreateProducts.jsx
import React from "react";
import {
Create,
SimpleForm,
ReferenceInput,
TextInput,
SelectInput,
} from "react-admin";
const ProductsCreate = props => (
<Create {...props}>
<SimpleForm>
<ReferenceInput
source="brandId"
reference="brands" label="brands">
<SelectInput optionText="name" />
</ReferenceInput>
<TextInput source="title" />
<TextInput source="price" />
<TextInput multiline source="description" />
</SimpleForm>
</Create>
);
export default ProductsCreate;
в views/components/EditProducts.jsx
добавьте код ниже;
//EditProducts.jsx
import React from "react";
import {
Edit,
SimpleForm,
ReferenceInput,
TextInput,
SelectInput,
} from "react-admin";
//
const EditProducts = props => (
<Edit {...props}>
<SimpleForm>
<ReferenceInput source="brandId" reference="brands" label="brands">
<SelectInput optionText="name" />
</ReferenceInput>
<TextInput source="title" />
<TextInput source="price" />
<TextInput multiline source="description" />
</SimpleForm>
</Edit>
);
export default EditProducts;
Перейдите в app.js и импортируйте только что созданные компоненты, чтобы окончательный код выглядел так, как показано ниже.
import * as React from "react";
import { Admin, Resource } from "react-admin";
import { Dashboard } from "./Components/DashBoard.jsx";
import BrandList from "./views/brands/index.jsx";
import dataProvider from "./dataProvider";
import ProductsCreate from "./views/products/components/CreateProducts.jsx";
import EditProducts from "./views/products/components/EditProducts.jsx";
import ProductList from "./views/products";
const App = () => (
<Admin dashboard={Dashboard} dataProvider={dataProvider}>
<Resource name="brands" list={BrandList} />
<Resource
name="products"
list={ProductList}
edit={EditProducts}
create={ProductsCreate}
/>
</Admin>
);
export default App;
- Откройте свой
admin-backend
и запуститеyarn dev
, чтобы запустить локальный бэкенд. - Вернитесь к проекту фронтенда и запустите
yarn start
в терминале. Если все работает нормально, вы должны увидеть gif-видео ниже.
Давайте внесем некоторые изменения в код нашего бэкенда, чтобы мы могли развернуть его на нашем любимом хостинг-сервере плюс аутентификация и авторизация,
запустите yarn add json-server-auth axios
или npm install json-server-auth axios
в вашем терминале, затем
создайте новую папку src
, переместите в нее наш предыдущий index.js, создайте app.js и поместите в него код ниже
json-server-auth
открывает для нас API для аутентификации, а также некоторые охраняемые маршруты, которые мы сделали для продуктов и брендов.
Регистрация нового пользователя
Любой из следующих маршрутов регистрирует нового пользователя:
- POST /register
- POST /signup
- POST /usersemail и пароль необходимы в теле запроса:
POST /register
{
"email": "user@user.com",
"password": "mypassword"
}
Ваш ответ должен быть примерно таким:
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Im9saXZpZXJAbWFpbDEyLmNvbSIsImlhdCI6MTY1NTkyMzg4NCwiZXhwIjoxNjU1OTI3NDg0LCJzdWIiOiIyIn0.eNVKi0mjOeZl7RpLPWZbpo5ggdAtB2uq1h96cuAp3eQ",
"user": {
"email": "user@user.com",
"id": 1
}
}
Вход в систему пользователя
Любой из следующих маршрутов позволяет войти в систему существующему пользователю :
POST /login
POST /signin
email и пароль являются обязательными полями:
POST /login
{
"email": "user@user.com",
"password": "mypassword"
}
вы должны получить ответ, как показано ниже, содержащий JWT-токен и данные пользователя, за исключением пароля:
{
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Im9saXZpZXJAbWFpbDEyLmNvbSIsImlhdCI6MTY1NTkyNzA3MSwiZXhwIjoxNjU1OTMwNjcxLCJzdWIiOiIyIn0.PixNo_fWZJ2IiCByjtePLDSrf4_Zikup6hQt_qxQbmI",
"user": {
"email": "user@user.com",
"id": 1
}
}
const path = require("path");
const jsonServer = require("json-server");
const auth = require("json-server-auth");
const server = jsonServer.create();
const router = jsonServer.router(path.join(__dirname, "db.json"));
server.db = router.db;
const middlewares = jsonServer.defaults();
server.use(middlewares);
const rules = auth.rewriter({
"/products*": "/660/products$1",
"/orders*": "/440/orders$1",
});
// You must apply the middlewares in the following order
const port = process.env.PORT || 5000;
server.use(rules);
server.use(auth);
server.use(router);
server.listen(port, () => {
console.log("JSON Server is running on port " + port);
});
Создайте файл db.json и поместите туда несколько примеров данных
{
"posts": [
{ "id": 1, "title": "json-server", "author": "typicode" }
],
"comments": [
{ "id": 1, "body": "some comment", "postId": 1 }
],
"profile": { "name": "typicode" },
"users": [],
}
создайте файл routes.json и поместите туда гвардию маршрутов авторизации
{
"/products*": "/660/products$1",
"/orders*": "/440/orders$1"
}
Примечания:
Маршруты | Разрешение |
---|---|
/660/* | Пользователь должен войти в систему для записи или чтения ресурса. |
/440/* | Никто не может писать этот ресурс. Пользователь должен войти в систему, чтобы прочитать ресурс. |
Существует еще много способов реализации route guard с json-server-auth, для детального изучения вы можете ознакомиться с их репозиторием на github здесь
перейдите к src/index.js
и давайте извлечем все товары, заказы и создание клиентов в функцию. Мы будем использовать node js fs (файловую систему) для динамического изменения нашего db.json.
Скопируйте измененные данные в src/index.js.
const { faker } = require("@faker-js/faker");
const fs = require("fs");
// sample brand list
const brandList = [
{
id: 1,
name: "Unbranded",
},
{
id: 2,
name: "Handmade",
},
{
id: 3,
name: "Recycled",
},
{
id: 4,
name: "Bespoke",
},
{
id: 5,
name: "Small",
},
{
id: 6,
name: "Generic",
},
{
id: 7,
name: "Intelligent",
},
{
id: 8,
name: "Licensed",
},
{
id: 9,
name: "Oriental",
},
{
id: 10,
name: "Sleek",
},
{
id: 11,
name: "Luxurious",
},
{
id: 12,
name: "Gorgeous",
},
{
id: 13,
name: "Refined",
},
{
id: 14,
name: "Awesome",
},
{
id: 15,
name: "Practical",
},
{
id: 16,
name: "Electronic",
},
{
id: 17,
name: "Fantastic",
},
{
id: 18,
name: "Modern",
},
{
id: 19,
name: "Handcrafted",
},
{
id: 20,
name: "Tasty",
},
];
// Get content from file
const filePath = process.cwd() + "//src/db.json";
var contents = fs.readFileSync(filePath);
// Define to JSON type
var jsonContent = JSON.parse(contents);
const products = () => {
const product = [];
for (let i = 0; i < 2000; i++) {
const title = faker.commerce.product();
const price = faker.commerce.price();
const description = faker.commerce.productDescription();
const image = faker.image.image();
const chosenBrand = Math.floor(Math.random() * brandList.length);
const brand = brandList[chosenBrand]; // pick a random brand from the brands array with ranging from 0 to the length of the brands array
const brandName = (id) => brandList.find((brand) => brand.id === id).name;
product.push({
id: i + 1,
title,
price,
description,
image,
brandId: brand.id,
brandName: brandName(brand.id),
});
}
return product;
};
const users = () => {
const user = [];
// Create 50 users
for (let i = 0; i < 50; i++) {
const name = faker.name.firstName();
const email = faker.internet.email();
const address = faker.address.streetAddress();
const city = faker.address.city();
const state = faker.address.state();
const zip = faker.address.zipCode();
const phone = faker.phone.number();
const country = faker.address.country();
user.push({
id: i + 1,
name,
email,
phone,
address: `${address} ${city}, ${state} ${zip} ${country}`,
});
}
return user;
};
const orders = () => {
const order = [];
// create 300 orders
for (let i = 0; i < 500; i++) {
const customerId = faker.datatype.number({ min: 1, max: 50 });
const productId = faker.datatype.number({ min: 1, max: 2000 });
const quantity = faker.datatype.number({ min: 1, max: 10 });
const price = faker.commerce.price();
order.push({
id: i + 1,
customerId,
productId,
quantity,
price,
total: price * quantity,
});
}
return order;
};
const modified = {
...jsonContent,
brands: brandList,
customers: users(),
orders: orders(),
products: products(),
};
// write to a new file named 2pac.txt
fs.writeFile(filePath, JSON.stringify(modified, null, 2), (err) => {
// throws an error, you could also catch it here
if (err) throw err;
});
module.exports = () => {
const data = {
products: products(),
customers: users(),
orders: orders(),
brands: brandList,
};
return data;
};
Перейдите в package.json, давайте изменим наш скрипт dev и запустим логику;
"dev": "json-server --watch src/index.js -m ./node_modules/json-server-auth --port 5000 --no-cors -r src/routes.json",
"start2": "node src/index.js && json-server src/db.json -m ./node_modules/json-server-auth --port 5000 --no-cors -r src/routes.json",
"start":"node src/index.js && node src/app.js --no-cors"
Примечание: «dev» — для разработки, а start — для развертывания/производства.
Откройте терминал и сделайте yarn start
или yarn dev
, и все должно работать как и раньше.
За исключением того, что вы не сможете снова просматривать продукты, пока мы не войдем в систему.
Добавьте страницы аутентификации
Измените dataProvider для отправки заголовка авторизации,
Как и dataProvider, мы будем реализовывать логику авторизации в файле под названием authProvider.js. Создайте его и вставьте приведенный ниже код,
// src/components/authProvider.js
import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_ERROR, AUTH_CHECK } from "react-admin";
import axios from "axios";
import { baseUrl } from "./env";
export const authProvider = async (type, params) => {
// when a user tries to log in
if (type === AUTH_LOGIN) {
const { email, password } = params;
return axios
.post(`${baseUrl}login`, {
email,
password,
})
.then(({ data }) => {
localStorage.setItem("authToken", data.accessToken);
return data;
})
.catch((e) => e);
}
// when a user tries to logout
if (type === AUTH_LOGOUT) {
localStorage.removeItem("authToken");
return Promise.resolve();
}
// when the API throws an error
if (type === AUTH_ERROR) {
const { status } = params;
if (status === 401 || status === 403) {
localStorage.removeItem("authToken");
return Promise.reject();
}
return Promise.resolve();
}
// when a user navigates to a new location
if (type === AUTH_CHECK) {
return localStorage.getItem("authToken")
? Promise.resolve()
: Promise.reject();
}
return Promise.reject("Unknown Method");
};
Перейдите в app.js, импортируйте authProvider.js и добавьте prop authProvider ={authProvider}
в компонент Admin.
import * as React from "react";
import { Admin, Resource } from "react-admin";
import { Dashboard } from "./Components/DashBoard.jsx";
import BrandList from "./views/brands/index.jsx";
import dataProvider from "./dataProvider";
import { authProvider } from "./authProvider.js";
import ProductsCreate from "./views/products/components/CreateProducts.jsx";
import EditProducts from "./views/products/components/EditProducts.jsx";
import ProductList from "./views/products";
const App = () => (
<Admin
dashboard={Dashboard}
authProvider={authProvider}
dataProvider={dataProvider}
>
<Resource name="brands" list={BrandList} />
<Resource
name="products"
list={ProductList}
edit={EditProducts}
create={ProductsCreate}
/>
</Admin>
);
export default App;
Перезапустите ваш внешний сервер, у вас должна автоматически появиться страница входа в систему. Но мы хотим создать собственную страницу входа и регистрации. Давайте продолжим и установим пакет Material UI, необходимый нам для этих двух страниц,
yarn add @mui/material @mui/icons-material @emotion/react @emotion/styled react-admin@latest
Мы также хотим обновить react-admin до последней версии, так как в ней много изменений, которые не позволяют перейти на версию 3.x.x. После установки, давайте создадим Login.jsx в папке views и вставим код ниже;
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import CssBaseline from '@mui/material/CssBaseline';
import TextField from '@mui/material/TextField';
import Link from '@mui/material/Link';
import Paper from '@mui/material/Paper';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import Typography from '@mui/material/Typography';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import { useLogin, useNotify } from 'react-admin';
const theme = createTheme();
function Login() {
const login = useLogin()
const notify = useNotify()
const handleSubmit = (event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
const userData = {
email: data.get('email'),
password: data.get('password'),
}
notify('Login successful', {type:'success'})
login(userData);
};
return (
<ThemeProvider theme={theme}>
<Grid container component="main" sx={{ height: '100vh' }}>
<CssBaseline />
<Grid
item
xs={false}
sm={4}
md={7}
sx={{
backgroundImage: 'url(https://source.unsplash.com/random)',
backgroundRepeat: 'no-repeat',
backgroundColor: (t) =>
t.palette.mode === 'light' ? t.palette.grey[50] : t.palette.grey[900],
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
/>
<Grid item xs={12} sm={8} md={5} component={Paper} elevation={6} square>
<Box
sx={{
my: 8,
mx: 4,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Sign in
</Typography>
<Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 1 }}>
<TextField
margin="normal"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
autoFocus
/>
<TextField
margin="normal"
required
fullWidth
name="password"
label="Password"
id="password"
type="password"
autoComplete="current-password"
/>
<Button
type="submit"
fullWidth
variant="contained"
sx={{ mt: 3, mb: 2 }}
>
Sign In
</Button>
<Grid container>
<Grid item xs>
</Grid>
<Grid item>
<Link href="#/register" variant="body2">
{"Don't have an account? Sign Up"}
</Link>
</Grid>
</Grid>
</Box>
</Box>
</Grid>
</Grid>
</ThemeProvider>
);
}
export default Login;
Для регистрации создайте Register.jsx в папке views и вставьте в него приведенный ниже код;
import * as React from 'react';
import Avatar from '@mui/material/Avatar';
import Button from '@mui/material/Button';
import CssBaseline from '@mui/material/CssBaseline';
import TextField from '@mui/material/TextField';
import Link from '@mui/material/Link';
import Grid from '@mui/material/Grid';
import Box from '@mui/material/Box';
import LockOutlinedIcon from '@mui/icons-material/LockOutlined';
import Typography from '@mui/material/Typography';
import Container from '@mui/material/Container';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import axios from 'axios'
import { baseUrl } from '../env';
import { useNotify } from 'react-admin';
import { useNavigate } from "react-router-dom";
const theme = createTheme();
export default function SignUp() {
const notify = useNotify()
const navigate = useNavigate()
const handleSubmit = async (event) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
console.log({
email: data.get('email'),
password: data.get('password'),
});
const userData = {
email: data.get('email'),
password: data.get('password'),
}
try{
const response = await axios.post(`${baseUrl}register`, userData)
localStorage.setItem('authToken', response.data.accessToken)
notify(`Registration successful`, { type: 'success' });
navigate('/#')
}
catch(e){
notify(`Error registering, try again`, { type: 'error' });
}
};
return (
<ThemeProvider theme={theme}>
<Container component="main" maxWidth="xs">
<CssBaseline />
<Box
sx={{
marginTop: 8,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Avatar sx={{ m: 1, bgcolor: 'secondary.main' }}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Sign up
</Typography>
<Box component="form" noValidate onSubmit={handleSubmit} sx={{ mt: 3 }}>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
/>
</Grid>
<Grid item xs={12}>
<TextField
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="new-password"
/>
</Grid>
<Grid item xs={12}>
</Grid>
</Grid>
<Button
type="submit"
fullWidth
variant="contained"
sx={{ mt: 3, mb: 2 }}
>
Sign Up
</Button>
<Grid container justifyContent="flex-end">
<Grid item>
<Link href="#/login" variant="body2">
Already have an account? Sign in
</Link>
</Grid>
</Grid>
</Box>
</Box>
</Container>
</ThemeProvider>
);
}
Давайте продолжим использовать эти страницы в нашем app.js и импортируем их обе. Передадим компоненту admin свойство loginPage так же, как мы это сделали для Dashboard, импортируем CustomRoutes
из react-admin
и используем его, как показано ниже;
<CustomRoutes noLayout>
<Route path="/register" element={<Register />} />
</CustomRoutes>
необходимо импортировать компонент Route из react-router-dom
, окончательная версия должна выглядеть следующим образом
import * as React from "react";
import { Admin, Resource, CustomRoutes } from "react-admin";
import { Dashboard } from "./Components/DashBoard.jsx";
import BrandList from "./views/brands/index.jsx";
import dataProvider from "./dataProvider";
import { authProvider } from "./authProvider.js";
import ProductsCreate from "./views/products/components/CreateProducts.jsx";
import EditProducts from "./views/products/components/EditProducts.jsx";
import ProductList from "./views/products";
import Login from "./views/Login.jsx";
import { Route } from "react-router-dom";
import Register from "./views/Register";
const App = () => (
<Admin
loginPage={Login}
dashboard={Dashboard}
authProvider={authProvider}
dataProvider={dataProvider}
>
<CustomRoutes noLayout>
<Route path="/register" element={<Register />} />
</CustomRoutes>
<Resource name="brands" list={BrandList} />
<Resource
name="products"
list={ProductList}
edit={EditProducts}
create={ProductsCreate}
/>
</Admin>
);
export default App;
Вы можете заметить, что ваши товары и страница бренда больше не отображают свои данные, давайте быстро добавим авторизацию к этим запросам. В файле dataProvider.js изменим getList так, как показано ниже.
.....
getList: (resource, params) => {
const { page, perPage } = params.pagination;
const query = {
_page: page,
_limit: perPage,
...params.filter,
};
const url = `${baseUrl}${resource}?${stringify(query)}`;
const token = localStorage.getItem("authToken");
const options = {
headers: new Headers({ Accept: "application/json" }),
};
if (token) {
options.headers.set("Authorization", `Bearer ${token}`);
return httpClient(url, options).then((resp) => {
return {
data: resp.json,
total: +resp.headers.get("X-Total-Count"),
};
});
}
},
....
Выйдите из системы и создайте нового пользователя, после успешной регистрации вы будете перенаправлены на страницу приборной панели, все должно работать как ожидалось.
Вы можете развернуть свой backend api на предпочитаемом хостинге и указать его на baseUrl в нашем frontend. Я использую heroku для этого урока.
Todo: Поскольку эта статья уже достаточно длинная, есть еще несколько вещей, которые мы можем сделать, чтобы расширить это дальше
- Настроить приборную панель с помощью наших собственных стилей
- Перенести проект на nextjs
- Добавление валидации формы при входе и регистрации на странице.
Вот и все, ребята, если вы дошли до этого, я болею за вас, дайте мне знать ваши мысли, предложения и вопросы в разделе комментариев.
исходные коды: frontend end и backend
xoxo