Как создать панель администратора в React JS — часть 2

В предыдущей статье мы рассмотрели, как создать панель администратора 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

Оцените статью
Procodings.ru
Добавить комментарий