add deploy stage
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
#VITE_API_BASE_URL=https://admin-api.eventhub.local
|
||||||
|
VITE_WS_URL=wss://admin-ws.eventhub.local
|
||||||
|
VITE_APP_TITLE=EventHub Admin
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
FROM nginx:alpine
|
||||||
|
COPY dist /usr/share/nginx/html
|
||||||
|
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
.PHONY: install dev build docker-build docker-run docker-stop docker-restart clean
|
||||||
|
|
||||||
|
IMAGE_NAME = eventhub-admin-ui
|
||||||
|
CONTAINER_NAME = admin-ui
|
||||||
|
NETWORK_NAME = eventhub_network
|
||||||
|
|
||||||
|
# Установить зависимости
|
||||||
|
install:
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# Запустить dev‑сервер
|
||||||
|
dev:
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Собрать production‑бандл
|
||||||
|
build:
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# Собрать Docker‑образ
|
||||||
|
docker-build:
|
||||||
|
docker build -t $(IMAGE_NAME) .
|
||||||
|
|
||||||
|
# Запустить контейнер (внутри сети eventhub_network)
|
||||||
|
docker-run:
|
||||||
|
docker run -d --name $(CONTAINER_NAME) --network $(NETWORK_NAME) --restart unless-stopped $(IMAGE_NAME)
|
||||||
|
|
||||||
|
# Остановить и удалить контейнер
|
||||||
|
docker-stop:
|
||||||
|
docker stop $(CONTAINER_NAME) || true
|
||||||
|
docker rm $(CONTAINER_NAME) || true
|
||||||
|
|
||||||
|
# Перезапустить: стоп → билд → ран
|
||||||
|
docker-restart: docker-stop docker-build docker-run
|
||||||
|
|
||||||
|
# Удалить сгенерированные файлы
|
||||||
|
clean:
|
||||||
|
rm -rf node_modules dist .vite
|
||||||
|
npm cache clean --force
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /v1/ {
|
||||||
|
proxy_pass http://eventhub:8445; # admin-api слушает HTTP на порту 8445
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
|
location /admin/ws {
|
||||||
|
proxy_pass http://eventhub:8446; # admin-ws слушает HTTP на порту 8446
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Upgrade $http_upgrade;
|
||||||
|
proxy_set_header Connection "upgrade";
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,8 +20,8 @@ export const useAdminWebSocket = () => {
|
|||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const accessToken = useAuthStore((s) => s.accessToken);
|
const accessToken = useAuthStore((s) => s.accessToken);
|
||||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||||
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
const pingIntervalRef = useRef<ReturnType<typeof setInterval>>();
|
const pingIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
|
||||||
const lastMessageTimestamp = useRef<number>(0);
|
const lastMessageTimestamp = useRef<number>(0);
|
||||||
const wsRef = useRef<WebSocket | null>(null);
|
const wsRef = useRef<WebSocket | null>(null);
|
||||||
|
|
||||||
@@ -32,8 +32,7 @@ export const useAdminWebSocket = () => {
|
|||||||
const connect = () => {
|
const connect = () => {
|
||||||
const wsUrl = import.meta.env.DEV
|
const wsUrl = import.meta.env.DEV
|
||||||
? `ws://localhost:5173/admin/ws?token=${accessToken}`
|
? `ws://localhost:5173/admin/ws?token=${accessToken}`
|
||||||
: `${import.meta.env.VITE_WS_URL || 'wss://admin-ws.eventhub.local'}/admin/ws?token=${accessToken}`;
|
: `wss://${window.location.host}/admin/ws?token=${accessToken}`;
|
||||||
|
|
||||||
const ws = new WebSocket(wsUrl);
|
const ws = new WebSocket(wsUrl);
|
||||||
wsRef.current = ws;
|
wsRef.current = ws;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { adminsApi } from '../api/adminsApi';
|
import { adminsApi } from '../api/adminsApi';
|
||||||
import { AdminListParams } from '../types/api';
|
import { AdminListParams, Admin } from '../types/api';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import { normalizeData } from '../utils/normalize';
|
import { normalizeData } from '../utils/normalize';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { eventsApi } from '../api/eventsApi';
|
import { eventsApi } from '../api/eventsApi';
|
||||||
import { EventListParams } from '../types/api';
|
import { Event, EventListParams } from '../types/api';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import { normalizeData } from '../utils/normalize';
|
import { normalizeData } from '../utils/normalize';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { reviewsApi } from '../api/reviewsApi';
|
import { reviewsApi } from '../api/reviewsApi';
|
||||||
import { ReviewListParams } from '../types/api';
|
import { ReviewListParams, Review } from '../types/api';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import { normalizeData } from '../utils/normalize';
|
import { normalizeData } from '../utils/normalize';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { subscriptionsApi } from '../api/subscriptionsApi';
|
import { subscriptionsApi } from '../api/subscriptionsApi';
|
||||||
import { SubscriptionListParams } from '../types/api';
|
import { SubscriptionListParams, Subscription as SubscriptionType } from '../types/api';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import { normalizeData } from '../utils/normalize';
|
import { normalizeData } from '../utils/normalize';
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ export const useSubscription = (id: string) => {
|
|||||||
export const useUpdateSubscription = () => {
|
export const useUpdateSubscription = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: ({ id, data }: { id: string; data: Partial<Subscription> }) =>
|
mutationFn: ({ id, data }: { id: string; data: Partial<SubscriptionType> }) =>
|
||||||
subscriptionsApi.updateSubscription(id, data),
|
subscriptionsApi.updateSubscription(id, data),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ['subscriptions'] });
|
queryClient.invalidateQueries({ queryKey: ['subscriptions'] });
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { ticketsApi } from '../api/ticketsApi';
|
import { ticketsApi } from '../api/ticketsApi';
|
||||||
import { TicketListParams } from '../types/api';
|
import { TicketListParams, Ticket } from '../types/api';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import { normalizeData } from '../utils/normalize';
|
import { normalizeData } from '../utils/normalize';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import { usersApi } from '../api/usersApi';
|
import { usersApi } from '../api/usersApi';
|
||||||
import { UserListParams } from '../types/api';
|
import { UserListParams, User } from '../types/api';
|
||||||
import { message } from 'antd';
|
import { message } from 'antd';
|
||||||
import { normalizeData } from '../utils/normalize';
|
import { normalizeData } from '../utils/normalize';
|
||||||
|
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ const AdminLayout: React.FC = () => {
|
|||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
height: '100vh',
|
height: '100vh',
|
||||||
position: 'sticky',
|
// position: 'sticky',
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { Card, Descriptions, Spin, Button, Tag, Space, Modal, Form, Input, Select, message } from 'antd';
|
import { Card, Descriptions, Spin, Button, Tag, Modal, Form, Input, Select, message } from 'antd';
|
||||||
import { EditOutlined } from '@ant-design/icons';
|
import { EditOutlined } from '@ant-design/icons';
|
||||||
import { useAdmin, useUpdateAdmin } from '../../hooks/useAdmins';
|
import { useAdmin, useUpdateAdmin } from '../../hooks/useAdmins';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ const AdminListPage: React.FC = () => {
|
|||||||
|
|
||||||
const handleTableChange = (
|
const handleTableChange = (
|
||||||
pagination: any,
|
pagination: any,
|
||||||
filters: any,
|
_filters: any,
|
||||||
sorter: SorterResult<Admin> | SorterResult<Admin>[]
|
sorter: SorterResult<Admin> | SorterResult<Admin>[]
|
||||||
) => {
|
) => {
|
||||||
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useAudit } from '../../hooks/useAudit';
|
|||||||
import { useAdmin, useAdmins } from '../../hooks/useAdmins';
|
import { useAdmin, useAdmins } from '../../hooks/useAdmins';
|
||||||
import { useUser } from '../../hooks/useUsers';
|
import { useUser } from '../../hooks/useUsers';
|
||||||
import { useEvent } from '../../hooks/useEvents';
|
import { useEvent } from '../../hooks/useEvents';
|
||||||
import { useReview } from '../../hooks/useReviews';
|
|
||||||
import { AuditRecord, AuditListParams } from '../../types/api';
|
import { AuditRecord, AuditListParams } from '../../types/api';
|
||||||
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
|
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
|
||||||
|
|
||||||
@@ -43,7 +42,6 @@ const AuditPage: React.FC = () => {
|
|||||||
const EntityNameCell: React.FC<{ entityType: string; entityId: string }> = ({ entityType, entityId }) => {
|
const EntityNameCell: React.FC<{ entityType: string; entityId: string }> = ({ entityType, entityId }) => {
|
||||||
const { data: user, isLoading: loadingUser } = useUser(entityType === 'user' ? entityId : '');
|
const { data: user, isLoading: loadingUser } = useUser(entityType === 'user' ? entityId : '');
|
||||||
const { data: event, isLoading: loadingEvent } = useEvent(entityType === 'event' ? entityId : '');
|
const { data: event, isLoading: loadingEvent } = useEvent(entityType === 'event' ? entityId : '');
|
||||||
const { data: review, isLoading: loadingReview } = useReview(entityType === 'review' ? entityId : '');
|
|
||||||
|
|
||||||
if (entityType === 'user') {
|
if (entityType === 'user') {
|
||||||
if (loadingUser) return <Spin size="small" />;
|
if (loadingUser) return <Spin size="small" />;
|
||||||
@@ -60,18 +58,16 @@ const AuditPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (entityType === 'review') {
|
if (entityType === 'review') {
|
||||||
if (loadingReview) return <Spin size="small" />;
|
// для review показываем ссылку без загрузки, так как нет API для загрузки названия
|
||||||
// Для отзыва показываем ссылку на страницу отзыва
|
|
||||||
return <Link to={`/reviews/${entityId}`}>{entityId}</Link>;
|
return <Link to={`/reviews/${entityId}`}>{entityId}</Link>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Для остальных типов (calendar, report, ticket, subscription, admin) пока просто ID
|
|
||||||
return <span>{entityId}</span>;
|
return <span>{entityId}</span>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTableChange = (
|
const handleTableChange = (
|
||||||
pagination: any,
|
pagination: any,
|
||||||
filters: any,
|
_filters: any,
|
||||||
sorter: SorterResult<AuditRecord> | SorterResult<AuditRecord>[]
|
sorter: SorterResult<AuditRecord> | SorterResult<AuditRecord>[]
|
||||||
) => {
|
) => {
|
||||||
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Table, Button, Input, Space, Popconfirm, Tooltip, Spin } from 'antd';
|
import { Table, Button, Input, Space, Popconfirm, Tooltip, Spin } from 'antd';
|
||||||
import { SearchOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { DeleteOutlined } from '@ant-design/icons';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useBannedWords, useAddBannedWord, useRemoveBannedWord, BannedWordListParams } from '../../hooks/useBannedWords';
|
import { useBannedWords, useAddBannedWord, useRemoveBannedWord } from '../../hooks/useBannedWords';
|
||||||
|
import { BannedWordListParams } from '../../api/bannedWordsApi';
|
||||||
import { useAdmin } from '../../hooks/useAdmins';
|
import { useAdmin } from '../../hooks/useAdmins';
|
||||||
import { BannedWord } from '../../types/api';
|
import { BannedWord } from '../../types/api';
|
||||||
import type { ColumnsType, SorterResult } from 'antd/es/table';
|
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
const BannedWordsPage: React.FC = () => {
|
const BannedWordsPage: React.FC = () => {
|
||||||
@@ -23,9 +24,10 @@ const BannedWordsPage: React.FC = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Обработчик изменения таблицы (сортировка/пагинация)
|
||||||
const handleTableChange = (
|
const handleTableChange = (
|
||||||
pagination: any,
|
pagination: any,
|
||||||
filters: any,
|
_filters: any,
|
||||||
sorter: SorterResult<BannedWord> | SorterResult<BannedWord>[]
|
sorter: SorterResult<BannedWord> | SorterResult<BannedWord>[]
|
||||||
) => {
|
) => {
|
||||||
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
||||||
@@ -38,6 +40,11 @@ const BannedWordsPage: React.FC = () => {
|
|||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Обработчик поиска
|
||||||
|
const handleSearch = (value: string) => {
|
||||||
|
setParams(prev => ({ ...prev, q: value || undefined, offset: 0 }));
|
||||||
|
};
|
||||||
|
|
||||||
// Компонент для отображения "Кем добавлено"
|
// Компонент для отображения "Кем добавлено"
|
||||||
const AddedByCell: React.FC<{ adminId: string | null }> = ({ adminId }) => {
|
const AddedByCell: React.FC<{ adminId: string | null }> = ({ adminId }) => {
|
||||||
const { data: admin, isLoading: loadingAdmin } = useAdmin(adminId || '');
|
const { data: admin, isLoading: loadingAdmin } = useAdmin(adminId || '');
|
||||||
@@ -108,7 +115,7 @@ const BannedWordsPage: React.FC = () => {
|
|||||||
</Button>
|
</Button>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder="Поиск по слову"
|
placeholder="Поиск по слову"
|
||||||
onSearch={(value) => setParams(prev => ({ ...prev, q: value || undefined, offset: 0 }))}
|
onSearch={handleSearch}
|
||||||
style={{ width: 200 }}
|
style={{ width: 200 }}
|
||||||
allowClear
|
allowClear
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { Card, Descriptions, Spin, Button, Tag, Space } from 'antd';
|
import { Card, Descriptions, Spin, Button, Tag } from 'antd';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useEvent } from '../../hooks/useEvents';
|
import { useEvent } from '../../hooks/useEvents';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Table, Button, Tag, Space, Modal, Form, Select, message, Spin, Input, Tooltip, DatePicker, InputNumber } from 'antd';
|
import { Table, Button, Tag, Space, Modal, Form, Select, Spin, Input, Tooltip, DatePicker, InputNumber } from 'antd';
|
||||||
import { InfoCircleOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { InfoCircleOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useEvents, useUpdateEvent, useDeleteEvent, useEvent } from '../../hooks/useEvents';
|
import { useEvents, useUpdateEvent, useDeleteEvent, useEvent } from '../../hooks/useEvents';
|
||||||
import { Event, EventListParams } from '../../types/api';
|
import { Event, EventListParams } from '../../types/api';
|
||||||
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
|
import type { ColumnsType, SorterResult } from 'antd/es/table/interface';
|
||||||
@@ -106,7 +106,7 @@ const EventListPage: React.FC = () => {
|
|||||||
|
|
||||||
const handleTableChange = (
|
const handleTableChange = (
|
||||||
pagination: any,
|
pagination: any,
|
||||||
filters: any,
|
_filters: any,
|
||||||
sorter: SorterResult<Event> | SorterResult<Event>[]
|
sorter: SorterResult<Event> | SorterResult<Event>[]
|
||||||
) => {
|
) => {
|
||||||
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ const ProfilePage: React.FC = () => {
|
|||||||
);
|
);
|
||||||
if (payload.preferences) {
|
if (payload.preferences) {
|
||||||
try {
|
try {
|
||||||
payload.preferences = JSON.parse(payload.preferences);
|
payload.preferences = JSON.parse(payload.preferences as string);
|
||||||
} catch {
|
} catch {
|
||||||
message.error('Поле «Настройки» должно быть валидным JSON');
|
message.error('Поле «Настройки» должно быть валидным JSON');
|
||||||
return;
|
return;
|
||||||
@@ -72,7 +72,7 @@ const ProfilePage: React.FC = () => {
|
|||||||
{!editing ? (
|
{!editing ? (
|
||||||
<>
|
<>
|
||||||
<div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', gap: 16 }}>
|
<div style={{ marginBottom: 16, display: 'flex', alignItems: 'center', gap: 16 }}>
|
||||||
<Avatar size={64} icon={<UserOutlined />} src={isBadValue(user.avatar_url) ? undefined : user.avatar_url} />
|
<Avatar size={64} icon={<UserOutlined />} src={isBadValue(user.avatar_url) ? '-' : <a href={user.avatar_url || undefined} target="_blank" rel="noreferrer">{user.avatar_url}</a>} />
|
||||||
<div>
|
<div>
|
||||||
<h3>{!isBadValue(user.nickname) ? user.nickname : user.email}</h3>
|
<h3>{!isBadValue(user.nickname) ? user.nickname : user.email}</h3>
|
||||||
<Tag
|
<Tag
|
||||||
@@ -115,7 +115,7 @@ const ProfilePage: React.FC = () => {
|
|||||||
<Descriptions.Item label="Язык">{isBadValue(user.language) ? '-' : user.language}</Descriptions.Item>
|
<Descriptions.Item label="Язык">{isBadValue(user.language) ? '-' : user.language}</Descriptions.Item>
|
||||||
<Descriptions.Item label="Телефон">{isBadValue(user.phone) ? '-' : user.phone}</Descriptions.Item>
|
<Descriptions.Item label="Телефон">{isBadValue(user.phone) ? '-' : user.phone}</Descriptions.Item>
|
||||||
<Descriptions.Item label="Аватар URL">
|
<Descriptions.Item label="Аватар URL">
|
||||||
{isBadValue(user.avatar_url) ? '-' : <a href={user.avatar_url} target="_blank" rel="noreferrer">{user.avatar_url}</a>}
|
{isBadValue(user.avatar_url) ? '-' : <a href={user.avatar_url || undefined} target="_blank" rel="noreferrer">{user.avatar_url}</a>}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
<Descriptions.Item label="Настройки">
|
<Descriptions.Item label="Настройки">
|
||||||
{isBadValue(user.preferences) ? '-' : <pre>{JSON.stringify(user.preferences, null, 2)}</pre>}
|
{isBadValue(user.preferences) ? '-' : <pre>{JSON.stringify(user.preferences, null, 2)}</pre>}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ const ReportListPage: React.FC = () => {
|
|||||||
|
|
||||||
const handleTableChange = (
|
const handleTableChange = (
|
||||||
pagination: any,
|
pagination: any,
|
||||||
filters: any,
|
_filters: any,
|
||||||
sorter: SorterResult<Report> | SorterResult<Report>[]
|
sorter: SorterResult<Report> | SorterResult<Report>[]
|
||||||
) => {
|
) => {
|
||||||
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
||||||
@@ -60,14 +60,13 @@ const ReportListPage: React.FC = () => {
|
|||||||
const { data: user, isLoading: loading } = useUser(reporterId);
|
const { data: user, isLoading: loading } = useUser(reporterId);
|
||||||
if (loading) return <Spin size="small" />;
|
if (loading) return <Spin size="small" />;
|
||||||
if (!user) return <span>{reporterId}</span>;
|
if (!user) return <span>{reporterId}</span>;
|
||||||
// Используем ту же логику, что и в renderLink
|
|
||||||
const nick = user.nickname;
|
const isGoodString = (val: any): val is string => !isBadValue(val);
|
||||||
const email = user.email;
|
|
||||||
let name = '';
|
let name = '';
|
||||||
if (!isBadValue(nick)) {
|
if (isGoodString(user.nickname)) {
|
||||||
name = nick;
|
name = user.nickname;
|
||||||
} else if (!isBadValue(email)) {
|
} else if (isGoodString(user.email)) {
|
||||||
name = email;
|
name = user.email;
|
||||||
} else {
|
} else {
|
||||||
name = user.id;
|
name = user.id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { Card, Descriptions, Spin, Button, Tag, Space, Modal, Form, Select, Input, message } from 'antd';
|
import { Card, Descriptions, Spin, Button, Tag, Space, Modal, Form, Input, message } from 'antd';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useReview, useUpdateReview } from '../../hooks/useReviews';
|
import { useReview, useUpdateReview } from '../../hooks/useReviews';
|
||||||
import { useUser } from '../../hooks/useUsers';
|
import { useUser } from '../../hooks/useUsers';
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ const ReviewListPage: React.FC = () => {
|
|||||||
// ---- Таблица ----
|
// ---- Таблица ----
|
||||||
const handleTableChange = (
|
const handleTableChange = (
|
||||||
pagination: any,
|
pagination: any,
|
||||||
filters: any,
|
_filters: any,
|
||||||
sorter: SorterResult<Review> | SorterResult<Review>[]
|
sorter: SorterResult<Review> | SorterResult<Review>[]
|
||||||
) => {
|
) => {
|
||||||
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
||||||
@@ -111,13 +111,13 @@ const ReviewListPage: React.FC = () => {
|
|||||||
const { data: user, isLoading: loading } = useUser(userId);
|
const { data: user, isLoading: loading } = useUser(userId);
|
||||||
if (loading) return <Spin size="small" />;
|
if (loading) return <Spin size="small" />;
|
||||||
if (!user) return <span>{userId}</span>;
|
if (!user) return <span>{userId}</span>;
|
||||||
const nick = user.nickname;
|
|
||||||
const email = user.email;
|
const isGoodString = (val: any): val is string => !isBadValue(val);
|
||||||
let name = '';
|
let name = '';
|
||||||
if (!isBadValue(nick)) {
|
if (isGoodString(user.nickname)) {
|
||||||
name = nick;
|
name = user.nickname;
|
||||||
} else if (!isBadValue(email)) {
|
} else if (isGoodString(user.email)) {
|
||||||
name = email;
|
name = user.email;
|
||||||
} else {
|
} else {
|
||||||
name = user.id;
|
name = user.id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ const SubscriptionListPage: React.FC = () => {
|
|||||||
|
|
||||||
const handleTableChange = (
|
const handleTableChange = (
|
||||||
pagination: any,
|
pagination: any,
|
||||||
filters: any,
|
_filters: any,
|
||||||
sorter: SorterResult<Subscription> | SorterResult<Subscription>[]
|
sorter: SorterResult<Subscription> | SorterResult<Subscription>[]
|
||||||
) => {
|
) => {
|
||||||
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { Card, Descriptions, Spin, Button, Tag, Space, Form, Select, Input, message } from 'antd';
|
import { Card, Descriptions, Spin, Button, Tag, Space, Form, Select, Input } from 'antd';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { useTicket, useUpdateTicket } from '../../hooks/useTickets';
|
import { useTicket, useUpdateTicket } from '../../hooks/useTickets';
|
||||||
import { useUser } from '../../hooks/useUsers';
|
import { useUser } from '../../hooks/useUsers';
|
||||||
@@ -123,7 +123,7 @@ const TicketDetailPage: React.FC = () => {
|
|||||||
showSearch
|
showSearch
|
||||||
optionFilterProp="label"
|
optionFilterProp="label"
|
||||||
>
|
>
|
||||||
{(admins || []).map((admin) => {
|
{(admins?.data || []).map((admin) => {
|
||||||
const label = `${admin.email}${admin.nickname && admin.nickname !== '-' ? ` – ${admin.nickname}` : ''}`;
|
const label = `${admin.email}${admin.nickname && admin.nickname !== '-' ? ` – ${admin.nickname}` : ''}`;
|
||||||
return (
|
return (
|
||||||
<Select.Option key={admin.id} value={admin.id} label={label}>
|
<Select.Option key={admin.id} value={admin.id} label={label}>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Table, Button, Tag, Space, Popconfirm, Tooltip, Spin, Card, Col, Row, Statistic } from 'antd';
|
import { Table, Button, Tag, Popconfirm, Tooltip, Spin, Card, Col, Row, Statistic } from 'antd';
|
||||||
import { InfoCircleOutlined, DeleteOutlined, BugOutlined, ClockCircleOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
import { InfoCircleOutlined, DeleteOutlined, BugOutlined, ClockCircleOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useTickets, useDeleteTicket, useTicketStats } from '../../hooks/useTickets';
|
import { useTickets, useDeleteTicket, useTicketStats } from '../../hooks/useTickets';
|
||||||
@@ -29,7 +29,7 @@ const TicketListPage: React.FC = () => {
|
|||||||
|
|
||||||
const handleTableChange = (
|
const handleTableChange = (
|
||||||
pagination: any,
|
pagination: any,
|
||||||
filters: any,
|
_filters: any,
|
||||||
sorter: SorterResult<Ticket> | SorterResult<Ticket>[]
|
sorter: SorterResult<Ticket> | SorterResult<Ticket>[]
|
||||||
) => {
|
) => {
|
||||||
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { Table, Button, Tag, Space, Modal, Form, Select, message, Spin, Input, Tooltip } from 'antd';
|
import { Table, Button, Tag, Space, Modal, Form, Select, Spin, Input, Tooltip } from 'antd';
|
||||||
import { InfoCircleOutlined, EditOutlined, LockOutlined, UnlockOutlined, DeleteOutlined } from '@ant-design/icons';
|
import { InfoCircleOutlined, EditOutlined, LockOutlined, UnlockOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { useUsers, useUpdateUser, useDeleteUser, useUser } from '../../hooks/useUsers';
|
import { useUsers, useUpdateUser, useDeleteUser, useUser } from '../../hooks/useUsers';
|
||||||
@@ -150,7 +150,7 @@ const UserListPage: React.FC = () => {
|
|||||||
// ==================== Таблица ====================
|
// ==================== Таблица ====================
|
||||||
const handleTableChange = (
|
const handleTableChange = (
|
||||||
pagination: any,
|
pagination: any,
|
||||||
filters: any,
|
_filters: any,
|
||||||
sorter: SorterResult<User> | SorterResult<User>[]
|
sorter: SorterResult<User> | SorterResult<User>[]
|
||||||
) => {
|
) => {
|
||||||
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
const s = Array.isArray(sorter) ? sorter[0] : sorter;
|
||||||
|
|||||||
Reference in New Issue
Block a user