Создание полнофункциональной панели администратора с помощью React и Ant Design

refine — это внутренний инструментальный фреймворк React без головы. Он помогает быстро развиваться при разработке B2B и B2C приложений. Ускоряя вашу работу, он никогда не ограничивает вас и имеет полностью настраиваемую структуру.

Ключевые особенности.

🔥 Безголовый: вы можете принести свой собственный UI и заправить его с помощью Refine для максимальной скорости разработки.

⚙️ Нулевая конфигурация: Однолинейная настройка с помощью суперплатформы. Запуск проекта занимает меньше минуты.

📦 Из коробки: Маршрутизация, сеть, аутентификация, управление состоянием, i18n и UI.

🔌 Независимость от бэкенда: подключается к любому пользовательскому бэкенду. Встроенная поддержка REST API, GraphQL, NestJs CRUD, Airtable, Strapi, Strapi v4, Strapi GraphQL, Supabase, Hasura, Appwrite, Firebase и Altogic.

📝 Native Typescript Core : Вы всегда можете отказаться от использования обычного JavaScript.

🐜 Enterprise UI : Работает без проблем с Ant Design System. (Поддержка нескольких фреймворков UI находится в дорожной карте).

📝 Код без кодовых пластин: Сохраняет вашу кодовую базу чистой и читабельной.

Вы можете использовать его с любой библиотекой пользовательского интерфейса без проблем. Также поддерживается Ant Design как «из коробки».

refine напрямую предоставляет компоненты Ant Design и некоторые хуки для работы с этими компонентами. Эти хуки предоставляют вам необходимые реквизиты для этих компонентов Ant Design.

Расширенный учебник по Refine

В этой статье мы рассмотрим базовый учебник Refine. Поэтому я предлагаю вам прочитать базовый учебник по Refine.

В этом руководстве мы узнаем, как включить функции (i18n, Realtime, Access Control), предоставляемые refine, в наш проект и как мы можем их использовать.

Создание проекта Refine

Давайте начнем с создания нашего проекта refine. Вы можете использовать супершаблон для создания проекта refine.

npx superplate-cli -p refine-react refine-advanced-tutorial
Вход в полноэкранный режим Выход из полноэкранного режима
✔ What will be the name of your app › refine-advanced-tutorial

✔ Package manager: · npm

✔ Do you want to using UI Framework?: · antd

✔ Do you want to customize theme?: · css

✔ Data Provider: · custom-json-rest-data-provider

✔ Auth Provider: · none

✔ Do you want to add an example page? · example-resource

✔ Do you want to customize layout? · no
Войдите в полноэкранный режим Выход из полноэкранного режима

✔ Вы хотите добавить страницу с примером? -example-resource Выбрав этот пункт, вы сможете просматривать учебник в локальном режиме.

cd refine-advanced-tutorial

npm run dev
Вход в полноэкранный режим Выход из полноэкранного режима

Как вы видите, наш пример проекта готов. Теперь давайте рассмотрим, как функции, предлагаемые refine, включаются в проект и как они используются.

Добавление i18n Provider в ваш проект

Создание экземпляра i18n

Сначала мы создадим экземпляр i18n с помощью react-i18next.

src/i18n.ts:

import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import Backend from "i18next-xhr-backend";
import detector from "i18next-browser-languagedetector";

i18n
  .use(Backend)
  .use(detector)
  .use(initReactI18next)
  .init({
    supportedLngs: ["en", "de"],
    backend: {
      loadPath: "/locales/{{lng}}/{{ns}}.json",
    },
    defaultNS: "common",
    fallbackLng: ["en", "de"],
  });

export default i18n;

Войдите в полноэкранный режим Выход из полноэкранного режима

Импортируем экземпляр i18n, который мы создали в файле index.tsx. Затем обернем приложение в React.Suspense.

src/index.tsx:

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

import "./i18n";

ReactDOM.render(
    <React.StrictMode>
        <React.Suspense fallback="loading">
            <App />
        </React.Suspense>
    </React.StrictMode>,
    document.getElementById("root"),
);
Вход в полноэкранный режим Выход из полноэкранного режима

Давайте определим наш i18n-провайдер и доработаем его.

src/App.tsx:

import { Refine } from "@pankod/refine-core";
import { notificationProvider, Layout } from "@pankod/refine-antd";
import routerProvider from "@pankod/refine-react-router";
import "@pankod/refine-antd/dist/styles.min.css";
import dataProvider from "@pankod/refine-simple-rest";
import { PostList, PostCreate, PostEdit, PostShow } from "pages/posts";

import { useTranslation } from "react-i18next";

function App() {
  const { t, i18n } = useTranslation();

  const i18nProvider = {
    translate: (key: string, params: object) => t(key, params),
    changeLocale: (lang: string) => i18n.changeLanguage(lang),
    getLocale: () => i18n.language,
  };

  return (
    <Refine
      routerProvider={routerProvider}
      notificationProvider={notificationProvider}
      Layout={Layout}
      dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
      resources={[
        {
          name: "posts",
          list: PostList,
          create: PostCreate,
          edit: PostEdit,
          show: PostShow,
        },
      ]}
      i18nProvider={i18nProvider}
    />
  );
}

export default App;
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь давайте добавим наш собственный перевод. Создадим два отдельных json-файла на английском и немецком языках.

/public/locales/en/common.json

{
  "posts": {
    "posts": "Posts",
    "fields": {
      "id": "Id",
      "title": "Title",
      "category": "Category",
      "status": {
        "title": "Status",
        "published": "Published",
        "draft": "Draft",
        "rejected": "Rejected"
      },
      "content": "Content",
      "createdAt": "Created At"
    },
    "titles": {
      "create": "Create Post",
      "edit": "Edit Post",
      "list": "Posts",
      "show": "Show Post"
    }
  },
  "table": {
    "actions": "Actions"
  }
}
Войти в полноэкранный режим Выйти из полноэкранного режима

/public/locales/de/common.json

{
  "posts": {
    "posts": "Einträge",
    "fields": {
      "id": "Id",
      "title": "Titel",
      "category": "Kategorie",
      "status": {
        "title": "Status",
        "published": "Veröffentlicht",
        "draft": "Draft",
        "rejected": "Abgelehnt"
      },
      "content": "Inhalh",
      "createdAt": "Erstellt am"
    },
    "titles": {
      "create": "Erstellen",
      "edit": "Bearbeiten",
      "list": "Einträge",
      "show": "Eintrag zeigen"
    }
  },
  "table": {
    "actions": "Aktionen"
  }
}
Войти в полноэкранный режим Выход из полноэкранного режима

В этой статье в качестве примера мы включили перевод только небольшой части.

Теперь давайте создадим компонент select в заголовке и рассмотрим наши посты в соответствии с выбранным языком.

src/components/header.tsx:

import { useGetLocale, useSetLocale } from "@pankod/refine-core";
import {
  AntdLayout,
  Space,
  Menu,
  Button,
  Icons,
  Dropdown,
} from "@pankod/refine-antd";
import { useTranslation } from "react-i18next";

const { DownOutlined } = Icons;

export const Header: React.FC = () => {
  const { i18n } = useTranslation();
  const locale = useGetLocale();
  const changeLanguage = useSetLocale();

  const currentLocale = locale();

  const menu = (
    <Menu selectedKeys={[currentLocale]}>
      {[...(i18n.languages || [])].sort().map((lang: string) => (
        <Menu.Item key={lang} onClick={() => changeLanguage(lang)}>
          {lang === "en" ? "English" : "German"}
        </Menu.Item>
      ))}
    </Menu>
  );

  return (
    <AntdLayout.Header
      style={{
        display: "flex",
        justifyContent: "flex-end",
        alignItems: "center",
        padding: "0px 24px",
        height: "48px",
        backgroundColor: "#FFF",
      }}
    >
      <Dropdown overlay={menu}>
        <Button type="link">
          <Space>
            {currentLocale === "en" ? "English" : "German"}
            <DownOutlined />
          </Space>
        </Button>
      </Dropdown>
    </AntdLayout.Header>
  );
};
Вход в полноэкранный режим Выход из полноэкранного режима

Давайте определим заголовок, который мы создали в рамках уточнения.

  return (
    <Refine
      routerProvider={routerProvider}
      notificationProvider={notificationProvider}
      Layout={Layout}
      i18nProvider={i18nProvider}
      dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
      Header={Header}
      resources={[
        {
          name: "posts",
          list: PostList,
          create: PostCreate,
          edit: PostEdit,
          show: PostShow,
        },
      ]}
    />
  );
Войти в полноэкранный режим Выход из полноэкранного режима

Теперь наш i18n Provider готов к использованию, давайте протестируем его вместе.

Использование перевода с содержимым таблицы

import { 
  useTranslate,
  useMany,
} from "@pankod/refine-core";
import {
  List,
  Table,
  TextField,
  useTable,
  Space,
  EditButton,
  ShowButton,
} from "@pankod/refine-antd";

import { IPost, ICategory } from "interfaces";

export const PostList: React.FC = () => {
  const translate = useTranslate();
  const { tableProps } = useTable<IPost>();

  const categoryIds =
      tableProps?.dataSource?.map((item) => item.category.id) ?? [];
  const { data, isLoading } = useMany<ICategory>({
      resource: "categories",
      ids: categoryIds,
      queryOptions: {
          enabled: categoryIds.length > 0,
      },
  });

  return (
      <List>
          <Table {...tableProps} rowKey="id">
              <Table.Column dataIndex="id" title="ID" />
              <Table.Column
                  dataIndex="title"
                  title={translate("posts.fields.title")}
              />
              <Table.Column
                  dataIndex={["category", "id"]}
                  title={translate("posts.fields.category")}
                  render={(value) => {
                      if (isLoading) {
                          return <TextField value="Loading..." />;
                      }

                      return (
                          <TextField
                              value={
                                  data?.data.find((item) => item.id === value)
                                      ?.title
                              }
                          />
                      );
                  }}
              />
              <Table.Column<IPost>
                  title={translate("table.actions")}
                  dataIndex="actions"
                  key="actions"
                  render={(_value, record) => (
                      <Space>
                          <EditButton size="small" recordItemId={record.id} />
                          <ShowButton size="small" recordItemId={record.id} />
                      </Space>
                  )}
              />
          </Table>
      </List>
  );
};
Войти в полноэкранный режим Выйти из полноэкранного режима

С помощью refine i18n Provider вы можете добавить нужный вам перевод и организовать содержимое на разных языках.

Ознакомьтесь с refine i18n Provider для получения более подробной информации и пошагового руководства.

Добавьте Live Provider (Realtime) в ваш проект с помощью Refine

refine позволяет добавить поддержку реального времени в ваше приложение с помощью реквизита liveProvider для . Он может быть использован для обновления и отображения данных в режиме реального времени в вашем приложении.

Теперь давайте сделаем наше приложение работающим в реальном времени с помощью refine Live Provider.

В этой статье мы будем использовать Ably для обеспечения функций реального времени.

Установка

Нам нужно установить пакет Ably live provider из refine.

 npm install @pankod/refine-ably
Войдите в полноэкранный режим Выход из полноэкранного режима

Сначала создадим ably-client и определим наш API-ключ Ably.

src/utility/client.ts:

import { Ably } from "@pankod/refine-ably";

export const ablyClient = new Ably.Realtime("YOUR_ABLY_API_KEY");
Войти в полноэкранный режим Выход из полноэкранного режима

Затем передадим liveProvider из @pankod/refine-ably в .

src/App.tsx:

import { Refine } from "@pankod/refine-core";
import { notificationProvider, Layout } from "@pankod/refine-antd";
import routerProvider from "@pankod/refine-react-router";
import "@pankod/refine-antd/dist/styles.min.css";
import dataProvider from "@pankod/refine-simple-rest";
import { liveProvider } from "@pankod/refine-ably";
import { ablyClient } from "utility";

import { PostList, PostCreate, PostEdit, PostShow } from "pages/posts";

import { Header } from "./components/header";

import { useTranslation } from "react-i18next";

function App() {
  const { t, i18n } = useTranslation();

  const i18nProvider = {
    translate: (key: string, params: object) => t(key, params),
    changeLocale: (lang: string) => i18n.changeLanguage(lang),
    getLocale: () => i18n.language,
  };

  return (
    <Refine
      routerProvider={routerProvider}
      notificationProvider={notificationProvider}
      Layout={Layout}
      i18nProvider={i18nProvider}
      dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
      Header={Header}
      liveProvider={liveProvider(ablyClient)}
      liveMode="auto"
      resources={[
        {
          name: "posts",
          list: PostList,
          create: PostCreate,
          edit: PostEdit,
          show: PostShow,
        },
      ]}
    />
  );
}

export default App;
Войти в полноэкранный режим Выйти из полноэкранного режима

Вы можете настроить liveMode, в данном примере используется режим «auto».

Ознакомьтесь с Refine Live Provider для получения более подробной информации и пошагового руководства.

Наш проект теперь работает в реальном времени! Благодаря Refine Live Provider мы сделали наш проект Realtime, добавив всего 2 строки.

Давайте посмотрим, как работает наш проект RealTime.

Добавление контроля доступа к проекту с помощью Refine

Вы можете контролировать свой проект как угодно с помощью refine react admin framework. Теперь давайте добавим провайдер контроля доступа в наш проект refine.

Контроль доступа — это обширная тема, в которой существует множество продвинутых решений, предоставляющих различные возможности. refine намеренно агностичен для своего API, чтобы иметь возможность интегрировать различные методы (RBAC, ABAC, ACL и т.д.) и различные библиотеки (Casbin, CASL, Cerbos, AccessControl.js). Метод can будет точкой входа для этих решений.

За подробной информацией обращайтесь к документации Access Control Provider. →

Давайте создадим две роли, Admin и Editor. Admin имеет полные CRUD полномочия на посты. Роль Editor, с другой стороны, имеет полномочия только на создание и редактирование новых постов. Другими словами, человек в роли редактора не может удалять посты и не может просматривать все строки таблицы.

Давайте начнем с создания двух кнопок для ролей Admin и Editor в созданном нами компоненте заголовка.

/src/componets/header.tsx:

import { useGetLocale, useSetLocale } from "@pankod/refine-core";
import {
  AntdLayout,
  Space,
  Menu,
  Button,
  Icons,
  Dropdown,
  Radio,
} from "@pankod/refine-antd";
import { useTranslation } from "react-i18next";

const { DownOutlined } = Icons;

interface HeaderProps {
  role: string;
}

export const Header: React.FC<HeaderProps> = ({ role }) => {
  const { i18n } = useTranslation();
  const locale = useGetLocale();
  const changeLanguage = useSetLocale();

  const currentLocale = locale();

  const menu = (
    <Menu selectedKeys={[currentLocale]}>
      {[...(i18n.languages || [])].sort().map((lang: string) => (
        <Menu.Item key={lang} onClick={() => changeLanguage(lang)}>
          {lang === "en" ? "English" : "German"}
        </Menu.Item>
      ))}
    </Menu>
  );

  return (
    <AntdLayout.Header
      style={{
        display: "flex",
        justifyContent: "space-between",
        alignItems: "center",
        padding: "0px 24px",
        height: "48px",
        backgroundColor: "#FFF",
      }}
    >
      <Radio.Group
      value={role}
      onChange={(event) => {
        localStorage.setItem("role", event.target.value);
        location.reload();
      }}
      >
        <Radio.Button value="admin">Admin</Radio.Button>
        <Radio.Button value="editor">Editor</Radio.Button>
      </Radio.Group>

      <Dropdown overlay={menu}>
        <Button type="link">
          <Space>
            {currentLocale === "en" ? "English" : "German"}
            <DownOutlined />
          </Space>
        </Button>
      </Dropdown>
    </AntdLayout.Header>
  );
};
Вход в полноэкранный режим Выйти из полноэкранного режима

В этой статье мы будем использовать Cerbos для усовершенствования контроля доступа.

npm install cerbos
Вход в полноэкранный режим Выход из полноэкранного режима

После завершения установки создадим объект Cerbos в файле App.tsx и определим его в .

import { Cerbos } from "cerbos";

const cerbos = new Cerbos({
  hostname: "https://demo-pdp.cerbos.cloud", // The Cerbos PDP instance
  playgroundInstance: "WS961950bd85QNYlAvTmJYubP0bqF7e3", // The playground instance ID to test
});
Войти в полноэкранный режим Выход из полноэкранного режима
    <Refine
      routerProvider={routerProvider}
      notificationProvider={notificationProvider}
      Layout={Layout}
      i18nProvider={i18nProvider}
      dataProvider={dataProvider("https://api.fake-rest.refine.dev")}
      Header={() => <Header role={role} />}
      liveProvider={liveProvider(ablyClient)}
      liveMode="auto"
      accessControlProvider={{
        can: async ({ action, params, resource }) => {
          const cerbosPayload = {
            principal: {
              id: "demoUser", // Fake a user ID
              roles: [role],
              // this is where user attributes can be passed
              attr: {},
            },
            // the resouces being access - can be multiple
            resource: {
              kind: resource,
              instances: {
                [params?.id || "new"]: {
                  attr: params,
                },
              },
            },
            // the list of actions on the resource to check authorization for
            actions: [action],
          };
          const result = await cerbos.check(cerbosPayload);
          return Promise.resolve({
            can: result.isAuthorized(params?.id || "new", action),
          });
        },
      }}
      resources={[
        {
          name: "posts",
          list: PostList,
          create: PostCreate,
          edit: PostEdit,
          show: PostShow,
          canDelete: true,
        },
      ]}
    />
Вход в полноэкранный режим Выход из полноэкранного режима

Мы будем выполнять наши действия в соответствии с ролью, которую мы выберем в заголовке. Как вы можете видеть выше, мы установили это с помощью метода access Control Provider can.

Теперь с помощью уточняющего useCanhook Выполним операции в соответствии с ролями в нашем списке.

src/pages/PostList.tsx:

import {
  IResourceComponentsProps,
  useMany,
  useTranslate,
  useCan,
} from "@pankod/refine-core";

import {
  List,
  Table,
  TextField,
  useTable,
  Space,
  EditButton,
  ShowButton,
  FilterDropdown,
  useSelect,
  Select,
  Radio,
  TagField,
  NumberField,
} from "@pankod/refine-antd";

import { IPost, ICategory } from "interfaces";

export const PostList: React.FC<IResourceComponentsProps> = () => {
  const translate = useTranslate();
  const { tableProps } = useTable<IPost>();

  const categoryIds =
    tableProps?.dataSource?.map((item) => item.category.id) ?? [];
  const { data, isLoading } = useMany<ICategory>({
    resource: "categories",
    ids: categoryIds,
    queryOptions: {
      enabled: categoryIds.length > 0,
    },
  });

  const { selectProps: categorySelectProps } = useSelect<ICategory>({
    resource: "categories",
    optionLabel: "title",
    optionValue: "id",
  });

  const { data: canAccess } = useCan({
    resource: "posts",
    action: "field",
    params: { field: "hit" },
  });

  return (
    <List>
      <Table {...tableProps} rowKey="id">
        <Table.Column dataIndex="id" title="ID" />
        <Table.Column
          dataIndex="title"
          title={translate("posts.fields.title")}
        />
        <Table.Column
          dataIndex={["category", "id"]}
          title={translate("posts.fields.category")}
          render={(value) => {
            if (isLoading) {
              return <TextField value="Loading..." />;
            }

            return (
              <TextField
                value={data?.data.find((item) => item.id === value)?.title}
              />
            );
          }}
          filterDropdown={(props) => (
            <FilterDropdown {...props}>
              <Select
                style={{ minWidth: 200 }}
                mode="multiple"
                placeholder="Select Category"
                {...categorySelectProps}
              />
            </FilterDropdown>
          )}
        />
        {canAccess?.can && (
          <Table.Column
            dataIndex="hit"
            title="Hit"
            render={(value: number) => (
              <NumberField
                value={value}
                options={{
                  notation: "compact",
                }}
              />
            )}
          />
        )}
        <Table.Column
          dataIndex="status"
          title="Status"
          render={(value: string) => <TagField value={value} />}
          filterDropdown={(props: any) => (
            <FilterDropdown {...props}>
              <Radio.Group>
                <Radio value="published">Published</Radio>
                <Radio value="draft">Draft</Radio>
                <Radio value="rejected">Rejected</Radio>
              </Radio.Group>
            </FilterDropdown>
          )}
        />
        <Table.Column<IPost>
          title={translate("table.actions")}
          dataIndex="actions"
          render={(_, record) => (
            <Space>
              <EditButton hideText size="small" recordItemId={record.id} />
              <ShowButton hideText size="small" recordItemId={record.id} />
            </Space>
          )}
        />
      </Table>
    </List>
  );
};
Войти в полноэкранный режим Выйти из полноэкранного режима

Здесь, если выбранная роль — Admin, в нашей таблице появится раздел ‘Hit’. Мы указали, что роль Редактора не может отображать этот раздел.

Просмотрите статью Уточнение поставщика контроля доступа для получения более подробной информации и пошагового руководства

Заключение

В этом руководстве мы показали, насколько полезны и просты функции внутреннего инструмента Refine Framework. Эти функции значительно сократят ваше время разработки. Хотя Refine предлагает вам возможность быстрой разработки, он не ограничивает вас и дает возможность настраивать ваш проект по своему усмотрению.

Мы видели, как просто в Refine включить и использовать функции интернационализации (i18n), Live Provider (Realtime) и контроля доступа. С помощью refine вы можете разрабатывать более сложные приложения простым способом.

С refine react admin вы можете разработать любое веб-приложение, которое захотите, с Admin Panel, базовым Crud App или поддержкой Next.js-SSR.

refine предоставляет возможность разрабатывать B2B и B2C приложения без каких-либо ограничений и в полностью настраиваемом виде.

Смотрите подробную информацию о refine. →

Для получения информации о других возможностях refine →

Пример Live CodeSandbox

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *