209 lines
6.0 KiB
JavaScript
209 lines
6.0 KiB
JavaScript
import { useCallback, useEffect, useState } from 'react';
|
||
import { Card } from '@consta/uikit/Card';
|
||
import { Text } from '@consta/uikit/Text';
|
||
import { Badge } from '@consta/uikit/Badge';
|
||
import { Button } from '@consta/uikit/Button';
|
||
|
||
const API_URL = '/api/user-info';
|
||
|
||
const formatError = (error) => {
|
||
if (error instanceof Error && error.message) {
|
||
return error.message;
|
||
}
|
||
return 'Не удалось получить данные профиля.';
|
||
};
|
||
|
||
const EmptyState = ({ title, description }) => (
|
||
<div className="empty-state">
|
||
<Text weight="bold">{title}</Text>
|
||
{description ? (
|
||
<Text size="s" view="secondary">
|
||
{description}
|
||
</Text>
|
||
) : null}
|
||
</div>
|
||
);
|
||
|
||
function App() {
|
||
const [data, setData] = useState(null);
|
||
const [status, setStatus] = useState('loading');
|
||
const [error, setError] = useState('');
|
||
const [reloadKey, setReloadKey] = useState(0);
|
||
|
||
const reload = useCallback(() => {
|
||
setReloadKey((value) => value + 1);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
let mounted = true;
|
||
const controller = new AbortController();
|
||
|
||
const load = async () => {
|
||
setStatus('loading');
|
||
setError('');
|
||
|
||
try {
|
||
const response = await fetch(API_URL, { signal: controller.signal });
|
||
if (!response.ok) {
|
||
throw new Error(`Ошибка загрузки: ${response.status}`);
|
||
}
|
||
const payload = await response.json();
|
||
if (!mounted) {
|
||
return;
|
||
}
|
||
setData(payload);
|
||
setStatus('success');
|
||
} catch (err) {
|
||
if (!mounted || err?.name === 'AbortError') {
|
||
return;
|
||
}
|
||
setError(formatError(err));
|
||
setStatus('error');
|
||
}
|
||
};
|
||
|
||
load();
|
||
|
||
return () => {
|
||
mounted = false;
|
||
controller.abort();
|
||
};
|
||
}, [reloadKey]);
|
||
|
||
if (status === 'loading') {
|
||
return (
|
||
<div className="app">
|
||
<Card className="section status">
|
||
<Text size="l" weight="bold">
|
||
Загрузка профиля
|
||
</Text>
|
||
<Text size="s" view="secondary">
|
||
Получаем данные о пользователе и доступных ресурсах.
|
||
</Text>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
if (status === 'error') {
|
||
return (
|
||
<div className="app">
|
||
<Card className="section status">
|
||
<Text size="l" weight="bold">
|
||
Не удалось загрузить данные
|
||
</Text>
|
||
<Text size="s" view="secondary">
|
||
{error}
|
||
</Text>
|
||
<div className="status-actions">
|
||
<Button label="Повторить" onClick={reload} />
|
||
</div>
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const roles = data?.roles ?? [];
|
||
const links = data?.available_links ?? [];
|
||
const organization = data?.organization;
|
||
|
||
return (
|
||
<div className="app">
|
||
<Card className="section">
|
||
<div className="profile-summary">
|
||
<div className="profile-info">
|
||
<Text size="l" weight="bold">
|
||
{data?.full_name || 'Пользователь'}
|
||
</Text>
|
||
<Text size="s" view="secondary">
|
||
{data?.email || 'Email не указан'}
|
||
</Text>
|
||
</div>
|
||
<div className="profile-meta">
|
||
<Text size="s" view="secondary">
|
||
Организация
|
||
</Text>
|
||
<Text weight="bold">
|
||
{organization?.name || 'Не указана'}
|
||
</Text>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
|
||
<Card className="section">
|
||
<div className="section-header">
|
||
<Text size="m" weight="bold">
|
||
Роли и доступ
|
||
</Text>
|
||
<Text size="s" view="secondary">
|
||
Список доступов пользователя внутри организации.
|
||
</Text>
|
||
</div>
|
||
{roles.length ? (
|
||
<div className="roles">
|
||
{roles.map((role) => (
|
||
<div className="role-item" key={role.id || role.name}>
|
||
<Badge label={role.name} />
|
||
<div className="role-content">
|
||
<Text size="s" view="secondary">
|
||
{role.description || 'Описание роли отсутствует.'}
|
||
</Text>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<EmptyState
|
||
title="Роли не назначены"
|
||
description="Свяжитесь с администратором для выдачи доступа."
|
||
/>
|
||
)}
|
||
</Card>
|
||
|
||
<Card className="section">
|
||
<div className="section-header">
|
||
<Text size="m" weight="bold">
|
||
Доступные сервисы
|
||
</Text>
|
||
<Text size="s" view="secondary">
|
||
Быстрые ссылки на инструменты и ресурсы.
|
||
</Text>
|
||
</div>
|
||
{links.length ? (
|
||
<div className="links-grid">
|
||
{links.map((link) => (
|
||
<Card className="link-card" key={link.id || link.url}>
|
||
<div className="link-header">
|
||
<Text weight="bold">{link.title}</Text>
|
||
<Text size="s" view="secondary">
|
||
{link.description || 'Описание отсутствует'}
|
||
</Text>
|
||
</div>
|
||
<div className="link-action">
|
||
<Text
|
||
as="a"
|
||
href={link.url}
|
||
view="link"
|
||
size="s"
|
||
target="_blank"
|
||
rel="noreferrer"
|
||
>
|
||
Перейти
|
||
</Text>
|
||
</div>
|
||
</Card>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<EmptyState
|
||
title="Пока нет доступных ссылок"
|
||
description="Ресурсы появятся после подключения сервисов."
|
||
/>
|
||
)}
|
||
</Card>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export default App;
|