Загрузка данных


import { Provider } from 'react-redux';
import { Navigate, Route, Routes } from 'react-router-dom';

import {
  AccessProvider,
  Can,
  Permissions,
  ProtectedRoute,
  useAccess,
} from '@/shared/permissions';

import { store } from '@/shared/store';
import { useGetEquipmentQuery } from '@/shared/api/equipment/client';

type LocationRow = {
  id: string;
  name: string;
};

const locationRows: LocationRow[] = [
  {
    id: '1',
    name: 'Переговорная 1',
  },
  {
    id: '2',
    name: 'Переговорная 2',
  },
];

/**
 * Пример подключения AccessProvider.
 *
 * Важно:
 * AccessProvider должен быть внутри Redux Provider,
 * потому что он берет роли пользователя из store.
 */
export const ExampleAppProviders = () => {
  return (
    <Provider store={store}>
      <AccessProvider>
        <ExampleApp />
      </AccessProvider>
    </Provider>
  );
};

/**
 * Пример защиты страниц через ProtectedRoute.
 */
const ExampleApp = () => {
  return (
    <Routes>
      <Route path="/" element={<ExamplePage />} />

      <Route
        path="/audit"
        element={
          <ProtectedRoute permissions={Permissions.auditRead}>
            <AuditPage />
          </ProtectedRoute>
        }
      />

      <Route
        path="/settings"
        element={
          <ProtectedRoute
            permissions={[
              Permissions.controllerSettingsRead,
              Permissions.deviceSettingsRead,
              Permissions.padletSettingsRead,
            ]}
            checkMode="some"
          >
            <SettingsPage />
          </ProtectedRoute>
        }
      />

      <Route path="/forbidden" element={<ForbiddenPage />} />

      <Route path="*" element={<Navigate to="/" replace />} />
    </Routes>
  );
};

const ExamplePage = () => {
  return (
    <div>
      <h1>Примеры использования permissions</h1>

      <OnePermissionExample />

      <EveryPermissionsExample />

      <SomePermissionsExample />

      <FallbackExample />

      <TableExample />

      <RtkQuerySkipExample />
    </div>
  );
};

/**
 * Пример 1.
 *
 * Проверка одного permission.
 * Кнопка будет отображена только если у пользователя есть право location:create.
 */
const OnePermissionExample = () => {
  return (
    <Can permissions={Permissions.locationCreate}>
      <button onClick={handleCreateLocation}>
        Добавить локацию
      </button>
    </Can>
  );
};

/**
 * Пример 2.
 *
 * Проверка нескольких permissions.
 * По умолчанию checkMode = "every", то есть нужны все permissions.
 */
const EveryPermissionsExample = () => {
  return (
    <Can
      permissions={[
        Permissions.locationRead,
        Permissions.locationUpdate,
      ]}
    >
      <button onClick={handleUpdateLocation}>
        Редактировать локацию
      </button>
    </Can>
  );
};

/**
 * Пример 3.
 *
 * Проверка нескольких permissions.
 * checkMode = "some" означает, что достаточно хотя бы одного permission.
 */
const SomePermissionsExample = () => {
  return (
    <Can
      permissions={[
        Permissions.locationUpdate,
        Permissions.locationDelete,
      ]}
      checkMode="some"
    >
      <div>
        <button onClick={handleUpdateLocation}>
          Редактировать
        </button>

        <button onClick={handleDeleteLocation}>
          Удалить
        </button>
      </div>
    </Can>
  );
};

/**
 * Пример 4.
 *
 * fallback показывается, если доступа нет.
 */
const FallbackExample = () => {
  return (
    <Can
      permissions={Permissions.equipmentTokensCreate}
      fallback={<span>Нет доступа к созданию токена</span>}
    >
      <button onClick={handleCreateEquipmentToken}>
        Создать токен
      </button>
    </Can>
  );
};

/**
 * Пример 5.
 *
 * Для таблиц лучше не оборачивать каждую кнопку в Can.
 * Иначе на больших списках будет много лишних компонентов.
 *
 * Лучше один раз получить canUpdateLocation / canDeleteLocation
 * и использовать обычные условия.
 */
const TableExample = () => {
  const { can } = useAccess();

  const canUpdateLocation = can(Permissions.locationUpdate);
  const canDeleteLocation = can(Permissions.locationDelete);

  const hasActions = canUpdateLocation || canDeleteLocation;

  return (
    <table>
      <thead>
        <tr>
          <th>Название</th>

          {hasActions && <th>Действия</th>}
        </tr>
      </thead>

      <tbody>
        {locationRows.map((location) => {
          return (
            <tr key={location.id}>
              <td>{location.name}</td>

              {hasActions && (
                <td>
                  {canUpdateLocation && (
                    <button onClick={() => handleUpdateLocationById(location.id)}>
                      Редактировать
                    </button>
                  )}

                  {canDeleteLocation && (
                    <button onClick={() => handleDeleteLocationById(location.id)}>
                      Удалить
                    </button>
                  )}
                </td>
              )}
            </tr>
          );
        })}
      </tbody>
    </table>
  );
};

/**
 * Пример 6.
 *
 * RTK Query запрос лучше не дергать, если у пользователя нет READ-доступа.
 */
const RtkQuerySkipExample = () => {
  const { can } = useAccess();

  const canReadEquipment = can(Permissions.equipmentRead);

  const {
    data,
    isLoading,
    isError,
  } = useGetEquipmentQuery(undefined, {
    skip: !canReadEquipment,
  });

  if (!canReadEquipment) {
    return <div>Нет доступа к оборудованию</div>;
  }

  if (isLoading) {
    return <div>Загрузка оборудования...</div>;
  }

  if (isError) {
    return <div>Ошибка загрузки оборудования</div>;
  }

  return (
    <div>
      <h2>Оборудование</h2>

      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
};

const AuditPage = () => {
  return <div>Страница аудита</div>;
};

const SettingsPage = () => {
  return <div>Страница настроек</div>;
};

const ForbiddenPage = () => {
  return <div>Нет доступа</div>;
};

const handleCreateLocation = () => {
  console.log('create location');
};

const handleUpdateLocation = () => {
  console.log('update location');
};

const handleDeleteLocation = () => {
  console.log('delete location');
};

const handleCreateEquipmentToken = () => {
  console.log('create equipment token');
};

const handleUpdateLocationById = (locationId: string) => {
  console.log('update location', locationId);
};

const handleDeleteLocationById = (locationId: string) => {
  console.log('delete location', locationId);
};