feat(admin): E2E content/system mock-сьюты, row testids, IFT smoke. Refs EventHub/EventHubFrontAdmin#33 #37 #38 #39
This commit is contained in:
@@ -126,15 +126,19 @@ export function DataTable<TData, TParams extends ServerTableParams>({
|
||||
</TableRow>
|
||||
))
|
||||
) : table.getRowModel().rows.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow key={row.id}>
|
||||
table.getRowModel().rows.map((row) => {
|
||||
const original = row.original as { id?: string; word?: string };
|
||||
const rowKey = original.id ?? original.word;
|
||||
return (
|
||||
<TableRow key={row.id} data-testid={rowKey ? `row-${rowKey}` : undefined}>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center text-muted-foreground">
|
||||
|
||||
@@ -60,7 +60,7 @@ export function DenseRowList<TParams extends ServerTableParams>({
|
||||
) : (
|
||||
<ul className="divide-y">
|
||||
{items.map((item) => (
|
||||
<li key={item.id}>
|
||||
<li key={item.id} data-testid={`row-${item.id}`}>
|
||||
<div
|
||||
className={cn(
|
||||
'group flex items-start gap-3 px-4 py-3 transition-colors',
|
||||
@@ -69,6 +69,7 @@ export function DenseRowList<TParams extends ServerTableParams>({
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-testid={`row-activate-${item.id}`}
|
||||
className={cn(
|
||||
'min-w-0 flex-1 text-left',
|
||||
!item.onActivate && 'cursor-default'
|
||||
|
||||
@@ -11,6 +11,7 @@ interface ExploreSearchProps {
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
export function ExploreSearch({
|
||||
@@ -20,10 +21,12 @@ export function ExploreSearch({
|
||||
placeholder,
|
||||
className,
|
||||
children,
|
||||
'data-testid': dataTestId,
|
||||
}: ExploreSearchProps) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<form
|
||||
data-testid={dataTestId}
|
||||
className={cn('flex flex-wrap items-center gap-2', className)}
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -33,6 +36,7 @@ export function ExploreSearch({
|
||||
<div className="relative min-w-[200px] max-w-md flex-1">
|
||||
<Search className="absolute left-2.5 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
data-testid={dataTestId ? `${dataTestId}-input` : undefined}
|
||||
className="pl-8"
|
||||
placeholder={placeholder ?? t('common.searchPlaceholder')}
|
||||
value={value}
|
||||
|
||||
@@ -118,7 +118,7 @@ const AdminDetailPage: React.FC = () => {
|
||||
const titleName = !isBadValue(admin.nickname) ? admin.nickname : admin.email || admin.id;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="page-admin-detail">
|
||||
<PageHeader
|
||||
title={t('admins.detailTitle', { name: titleName })}
|
||||
breadcrumbs={[
|
||||
@@ -127,11 +127,16 @@ const AdminDetailPage: React.FC = () => {
|
||||
]}
|
||||
actions={
|
||||
<>
|
||||
<Button variant="outline" size="sm" onClick={() => setEditModal(true)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
data-testid="btn-edit-admin"
|
||||
onClick={() => setEditModal(true)}
|
||||
>
|
||||
<Pencil className="h-4 w-4" />
|
||||
{t('common.edit')}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => navigate('/admins')}>
|
||||
<Button variant="outline" data-testid="btn-back" onClick={() => navigate('/admins')}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
{t('common.backToList')}
|
||||
</Button>
|
||||
@@ -301,7 +306,7 @@ const AdminDetailPage: React.FC = () => {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -236,12 +236,13 @@ const AdminListPage: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<ExploreListShell
|
||||
data-testid="page-admins"
|
||||
title={t('admins.title')}
|
||||
stats={exploreStats}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
actions={
|
||||
<Button onClick={() => setCreateModal(true)}>
|
||||
<Button data-testid="btn-create-admin" onClick={() => setCreateModal(true)}>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{t('admins.add')}
|
||||
</Button>
|
||||
@@ -261,7 +262,7 @@ const AdminListPage: React.FC = () => {
|
||||
</ExploreListShell>
|
||||
|
||||
<Dialog open={createModal} onOpenChange={setCreateModal}>
|
||||
<DialogContent>
|
||||
<DialogContent data-testid="dialog-create-admin">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('admins.createTitle')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -186,13 +186,14 @@ const AuditPage: React.FC = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div data-testid="page-audit" className="space-y-6">
|
||||
<ListPageHeader title={t('audit.title')} description={t('audit.description')} />
|
||||
|
||||
<div className="flex flex-wrap items-end gap-4">
|
||||
<div className="flex flex-wrap items-end gap-4" data-testid="filter-audit">
|
||||
<div className="space-y-2">
|
||||
<Label>{t('audit.searchAdmin')}</Label>
|
||||
<Input
|
||||
data-testid="filter-audit-admin-search"
|
||||
placeholder={t('audit.searchAdminPlaceholder')}
|
||||
value={adminSearch}
|
||||
onChange={(e) => setAdminSearch(e.target.value)}
|
||||
|
||||
@@ -118,7 +118,7 @@ const BannedWordsPage: React.FC = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div data-testid="page-banned-words" className="space-y-6">
|
||||
<ListPageHeader
|
||||
title={t('bannedWords.title')}
|
||||
description={t('bannedWords.description')}
|
||||
@@ -126,24 +126,36 @@ const BannedWordsPage: React.FC = () => {
|
||||
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Input
|
||||
data-testid="input-banned-word"
|
||||
placeholder={t('bannedWords.placeholder')}
|
||||
value={newWord}
|
||||
onChange={(e) => setNewWord(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
|
||||
className="w-[200px]"
|
||||
/>
|
||||
<Button onClick={handleAdd} disabled={addWord.isPending || !newWord.trim()}>
|
||||
<Button
|
||||
data-testid="btn-add-banned-word"
|
||||
onClick={handleAdd}
|
||||
disabled={addWord.isPending || !newWord.trim()}
|
||||
>
|
||||
{t('common.add')}
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
data-testid="filter-banned-words-search"
|
||||
placeholder={t('explore.searchWord')}
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
className="w-[200px]"
|
||||
/>
|
||||
<Button variant="outline" size="icon" className="h-9 w-9" onClick={handleSearch}>
|
||||
<Button
|
||||
data-testid="btn-search-banned-words"
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-9 w-9"
|
||||
onClick={handleSearch}
|
||||
>
|
||||
<Search className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,158 +1,158 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
|
||||
import { useCalendar } from '@/hooks/useCalendars';
|
||||
|
||||
import { formatDisplayValue } from '@/lib/utils';
|
||||
|
||||
import { PageHeader } from '@/components/PageHeader';
|
||||
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
import { Button } from '@/components/ui/button';
|
||||
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
|
||||
|
||||
const isBadValue = (val: unknown) =>
|
||||
|
||||
val === '-' || val === 'undefined' || val === '' || val === null || val === undefined;
|
||||
|
||||
|
||||
|
||||
const DetailValue: React.FC<{ value: unknown; emptyLabel: string }> = ({ value, emptyLabel }) => {
|
||||
|
||||
const text = formatDisplayValue(value);
|
||||
|
||||
if (text === '-') return <>{emptyLabel}</>;
|
||||
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
|
||||
return <pre className="overflow-auto rounded-md bg-muted p-2 text-xs">{text}</pre>;
|
||||
|
||||
}
|
||||
|
||||
return <>{text}</>;
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
const CalendarDetailPage: React.FC = () => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data: calendar, isLoading } = useCalendar(id || '');
|
||||
|
||||
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
|
||||
if (isBadValue(dateStr)) return t('common.emDash');
|
||||
|
||||
const d = dayjs(dateStr);
|
||||
|
||||
return d.isValid() ? d.format('DD.MM.YYYY HH:mm') : dateStr;
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (isLoading) {
|
||||
|
||||
return (
|
||||
|
||||
<div className="space-y-4">
|
||||
|
||||
<Skeleton className="h-8 w-64" />
|
||||
|
||||
<Skeleton className="h-96 w-full" />
|
||||
|
||||
</div>
|
||||
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (!calendar) return <p className="text-muted-foreground">{t('calendars.notFound')}</p>;
|
||||
|
||||
|
||||
|
||||
const emDash = t('common.emDash');
|
||||
|
||||
const titleLabel = calendar.title || calendar.id;
|
||||
|
||||
|
||||
|
||||
return (
|
||||
|
||||
<>
|
||||
|
||||
<PageHeader
|
||||
|
||||
title={t('calendars.detailTitle', { name: titleLabel })}
|
||||
|
||||
breadcrumbs={[
|
||||
|
||||
{ label: t('calendars.title'), href: '/calendars' },
|
||||
|
||||
{ label: String(titleLabel) },
|
||||
|
||||
]}
|
||||
|
||||
actions={
|
||||
|
||||
<Button variant="outline" onClick={() => navigate('/calendars')}>
|
||||
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
|
||||
{t('common.backToList')}
|
||||
|
||||
</Button>
|
||||
|
||||
}
|
||||
|
||||
/>
|
||||
|
||||
<Card>
|
||||
|
||||
<CardContent className="space-y-6">
|
||||
|
||||
<dl className="grid gap-3 text-sm">
|
||||
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
|
||||
<dt className="text-muted-foreground">{t('common.id')}</dt>
|
||||
|
||||
<dd>{calendar.id}</dd>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
|
||||
<dt className="text-muted-foreground">{t('common.title')}</dt>
|
||||
|
||||
<dd>{calendar.title}</dd>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
|
||||
<dt className="text-muted-foreground">{t('common.shortName')}</dt>
|
||||
|
||||
import React from 'react';
|
||||
import { useParams, useNavigate, Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs from 'dayjs';
|
||||
import { ArrowLeft } from 'lucide-react';
|
||||
import { useCalendar } from '@/hooks/useCalendars';
|
||||
import { formatDisplayValue } from '@/lib/utils';
|
||||
import { PageHeader } from '@/components/PageHeader';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
|
||||
const isBadValue = (val: unknown) =>
|
||||
val === '-' || val === 'undefined' || val === '' || val === null || val === undefined;
|
||||
|
||||
const DetailValue: React.FC<{ value: unknown; emptyLabel: string }> = ({ value, emptyLabel }) => {
|
||||
const text = formatDisplayValue(value);
|
||||
if (text === '-') return <>{emptyLabel}</>;
|
||||
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
||||
return <pre className="overflow-auto rounded-md bg-muted p-2 text-xs">{text}</pre>;
|
||||
}
|
||||
return <>{text}</>;
|
||||
};
|
||||
|
||||
const CalendarDetailPage: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { data: calendar, isLoading } = useCalendar(id || '');
|
||||
|
||||
const formatDate = (dateStr: string) => {
|
||||
if (isBadValue(dateStr)) return t('common.emDash');
|
||||
const d = dayjs(dateStr);
|
||||
return d.isValid() ? d.format('DD.MM.YYYY HH:mm') : dateStr;
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Skeleton className="h-8 w-64" />
|
||||
<Skeleton className="h-96 w-full" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!calendar) return <p className="text-muted-foreground">{t('calendars.notFound')}</p>;
|
||||
|
||||
const emDash = t('common.emDash');
|
||||
const titleLabel = calendar.title || calendar.id;
|
||||
|
||||
return (
|
||||
<div data-testid="page-calendar-detail">
|
||||
<PageHeader
|
||||
title={t('calendars.detailTitle', { name: titleLabel })}
|
||||
breadcrumbs={[
|
||||
{ label: t('calendars.title'), href: '/calendars' },
|
||||
{ label: String(titleLabel) },
|
||||
]}
|
||||
actions={
|
||||
<Button variant="outline" data-testid="btn-back" onClick={() => navigate('/calendars')}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
{t('common.backToList')}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Card>
|
||||
<CardContent className="space-y-6">
|
||||
<dl className="grid gap-3 text-sm">
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.id')}</dt>
|
||||
<dd>{calendar.id}</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.title')}</dt>
|
||||
<dd>{calendar.title}</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.shortName')}</dt>
|
||||
<dd><DetailValue value={calendar.short_name} emptyLabel={emDash} /></dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.description')}</dt>
|
||||
<dd><DetailValue value={calendar.description} emptyLabel={emDash} /></dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.type')}</dt>
|
||||
<dd>
|
||||
<Badge variant={calendar.type === 'commercial' ? 'warning' : 'default'}>{calendar.type}</Badge>
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.status')}</dt>
|
||||
<dd>
|
||||
<Badge variant={calendar.status === 'active' ? 'success' : calendar.status === 'frozen' ? 'warning' : 'destructive'}>
|
||||
{calendar.status}
|
||||
</Badge>
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.reason')}</dt>
|
||||
<dd><DetailValue value={calendar.reason} emptyLabel={emDash} /></dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.category')}</dt>
|
||||
<dd><DetailValue value={calendar.category} emptyLabel={emDash} /></dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.owner')}</dt>
|
||||
<dd>
|
||||
{!isBadValue(calendar.owner_id) ? (
|
||||
<Link to={`/users/${calendar.owner_id}`} className="text-primary hover:underline">
|
||||
{calendar.owner_id}
|
||||
</Link>
|
||||
) : emDash}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.color')}</dt>
|
||||
<dd><DetailValue value={calendar.color} emptyLabel={emDash} /></dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.image')}</dt>
|
||||
<dd><DetailValue value={calendar.image_url} emptyLabel={emDash} /></dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.confirmation')}</dt>
|
||||
<dd><DetailValue value={calendar.confirmation} emptyLabel={emDash} /></dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('events.tags')}</dt>
|
||||
<dd><DetailValue value={calendar.tags} emptyLabel={emDash} /></dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.rating')}</dt>
|
||||
<dd>{calendar.rating_avg} ({calendar.rating_count})</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.settings')}</dt>
|
||||
<dd><DetailValue value={calendar.settings} emptyLabel={emDash} /></dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.createdAt')}</dt>
|
||||
<dd>{formatDate(calendar.created_at)}</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[140px_1fr] gap-2">
|
||||
<dt className="text-muted-foreground">{t('common.updatedAt')}</dt>
|
||||
<dd>{formatDate(calendar.updated_at)}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CalendarDetailPage;
|
||||
|
||||
@@ -286,12 +286,14 @@ const CalendarListPage: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<ExploreListShell
|
||||
data-testid="page-calendars"
|
||||
title={t('calendars.title')}
|
||||
stats={exploreStats}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
toolbar={
|
||||
<ExploreSearch
|
||||
data-testid="filter-calendars-search"
|
||||
value={searchDraft}
|
||||
onChange={setSearchDraft}
|
||||
onSubmit={applySearch}
|
||||
|
||||
@@ -61,7 +61,7 @@ const EventDetailPage: React.FC = () => {
|
||||
const titleLabel = event.title || event.id;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div data-testid="page-event-detail">
|
||||
<PageHeader
|
||||
title={t('events.detailTitle', { name: titleLabel })}
|
||||
breadcrumbs={[
|
||||
@@ -69,7 +69,7 @@ const EventDetailPage: React.FC = () => {
|
||||
{ label: String(titleLabel) },
|
||||
]}
|
||||
actions={
|
||||
<Button variant="outline" onClick={() => navigate('/events')}>
|
||||
<Button variant="outline" data-testid="btn-back" onClick={() => navigate('/events')}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
{t('common.backToList')}
|
||||
</Button>
|
||||
@@ -190,7 +190,7 @@ const EventDetailPage: React.FC = () => {
|
||||
|
||||
</CardContent>
|
||||
</Card>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -376,6 +376,7 @@ const EventListPage: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<ExploreListShell
|
||||
data-testid="page-events"
|
||||
title={t('events.title')}
|
||||
stats={exploreStats}
|
||||
viewMode={viewMode}
|
||||
@@ -384,6 +385,7 @@ const EventListPage: React.FC = () => {
|
||||
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap sm:items-center">
|
||||
<div className="min-w-0 flex-1">
|
||||
<ExploreSearch
|
||||
data-testid="filter-events-search"
|
||||
value={searchDraft}
|
||||
onChange={setSearchDraft}
|
||||
onSubmit={applySearch}
|
||||
|
||||
@@ -286,7 +286,7 @@ const MonitoringPage: React.FC = () => {
|
||||
const healthy = summary.online > 0 && summary.cpuPeak < 85;
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div data-testid="page-monitoring" className="space-y-6">
|
||||
<div className="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
@@ -303,11 +303,12 @@ const MonitoringPage: React.FC = () => {
|
||||
{t('monitoring.subtitle', { minutes })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
<div className="flex flex-wrap gap-1.5" data-testid="filter-monitoring-range">
|
||||
{TIME_RANGE_KEYS.map((r) => (
|
||||
<Button
|
||||
key={r.value}
|
||||
size="sm"
|
||||
data-testid={`filter-monitoring-range-${r.value}`}
|
||||
variant={minutes === r.value ? 'default' : 'outline'}
|
||||
onClick={() => setMinutes(r.value)}
|
||||
>
|
||||
|
||||
@@ -150,7 +150,7 @@ const ProfilePage: React.FC = () => {
|
||||
const emDash = t('common.emDash');
|
||||
|
||||
return (
|
||||
<Card className="max-w-2xl">
|
||||
<Card data-testid="page-profile" className="max-w-2xl">
|
||||
<CardHeader>
|
||||
<CardTitle>{t('profile.title')}</CardTitle>
|
||||
<CardDescription>{t('profile.description')}</CardDescription>
|
||||
@@ -232,7 +232,7 @@ const ProfilePage: React.FC = () => {
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<Button onClick={startEditing}>
|
||||
<Button data-testid="btn-edit-profile" onClick={startEditing}>
|
||||
<Pencil className="h-4 w-4" />
|
||||
{t('profile.edit')}
|
||||
</Button>
|
||||
@@ -309,10 +309,10 @@ const ProfilePage: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button type="submit" disabled={updateProfile.isPending}>
|
||||
<Button type="submit" data-testid="btn-save-profile" disabled={updateProfile.isPending}>
|
||||
{updateProfile.isPending ? t('common.saving') : t('common.save')}
|
||||
</Button>
|
||||
<Button type="button" variant="outline" onClick={() => setEditing(false)}>
|
||||
<Button type="button" data-testid="btn-cancel-profile" variant="outline" onClick={() => setEditing(false)}>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -277,12 +277,13 @@ const SubscriptionListPage: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<ExploreListShell
|
||||
data-testid="page-subscriptions"
|
||||
title={t('subscriptions.title')}
|
||||
stats={exploreStats}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
toolbar={
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="flex flex-wrap gap-2" data-testid="filter-subscriptions">
|
||||
<Select
|
||||
value={params.plan ?? ALL}
|
||||
onValueChange={(val) =>
|
||||
|
||||
@@ -62,7 +62,7 @@ const UserDetailPage: React.FC = () => {
|
||||
const emDash = t('common.emDash');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="page-user-detail">
|
||||
<PageHeader
|
||||
title={String(titleName)}
|
||||
description={`ID ${user.id}`}
|
||||
@@ -71,7 +71,7 @@ const UserDetailPage: React.FC = () => {
|
||||
{ label: String(titleName) },
|
||||
]}
|
||||
actions={
|
||||
<Button variant="outline" onClick={() => navigate('/users')}>
|
||||
<Button variant="outline" data-testid="btn-back" onClick={() => navigate('/users')}>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
{t('common.backToList')}
|
||||
</Button>
|
||||
|
||||
@@ -329,12 +329,14 @@ const UserListPage: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<ExploreListShell
|
||||
data-testid="page-users"
|
||||
title={t('users.title')}
|
||||
stats={exploreStats}
|
||||
viewMode={viewMode}
|
||||
onViewModeChange={setViewMode}
|
||||
toolbar={
|
||||
<ExploreSearch
|
||||
data-testid="filter-users-search"
|
||||
value={searchDraft}
|
||||
onChange={setSearchDraft}
|
||||
onSubmit={applySearch}
|
||||
@@ -356,7 +358,7 @@ const UserListPage: React.FC = () => {
|
||||
</ExploreListShell>
|
||||
|
||||
<Dialog open={editModal.open} onOpenChange={(open) => !open && setEditModal({ open: false, userId: null })}>
|
||||
<DialogContent>
|
||||
<DialogContent data-testid="dialog-edit-user">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('users.editTitle')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
Reference in New Issue
Block a user