Fixed table rendering issues

This commit is contained in:
Alejandro Celaya 2022-03-07 11:03:41 +01:00
parent 0f43ad59a0
commit 74635281de
5 changed files with 133 additions and 130 deletions

View file

@ -28,7 +28,7 @@ a,
text-decoration: none; text-decoration: none;
} }
a:not(.nav-link):not(.navbar-brand):not(.page-link):hover, a:not(.nav-link):not(.navbar-brand):not(.page-link):not(.highlight-card):hover,
.btn-link:hover { .btn-link:hover {
text-decoration: underline; text-decoration: underline;
} }
@ -164,7 +164,8 @@ hr {
.close, .close,
.close:hover, .close:hover,
.table, .table,
.table-hover > tbody > tr:hover > * { .table-hover > tbody > tr:hover > *,
.table-hover > tbody > tr > * {
color: var(--text-color); color: var(--text-color);
} }

View file

@ -21,13 +21,13 @@ const Settings = (
<NavPills className="mb-3"> <NavPills className="mb-3">
<NavPillItem to="general">General</NavPillItem> <NavPillItem to="general">General</NavPillItem>
<NavPillItem to="short-urls">Short URLs</NavPillItem> <NavPillItem to="short-urls">Short URLs</NavPillItem>
<NavPillItem to="secondary-items">Secondary items</NavPillItem> <NavPillItem to="other-items">Other items</NavPillItem>
</NavPills> </NavPills>
<Routes> <Routes>
<Route path="general" element={<SettingsSections items={[ <UserInterface key="one" />, <RealTimeUpdates key="two" /> ]} />} /> <Route path="general" element={<SettingsSections items={[ <UserInterface key="one" />, <RealTimeUpdates key="two" /> ]} />} />
<Route path="short-urls" element={<SettingsSections items={[ <ShortUrlCreation key="one" />, <ShortUrlsList key="two" /> ]} />} /> <Route path="short-urls" element={<SettingsSections items={[ <ShortUrlCreation key="one" />, <ShortUrlsList key="two" /> ]} />} />
<Route path="secondary-items" element={<SettingsSections items={[ <Tags key="one" />, <Visits key="two" /> ]} />} /> <Route path="other-items" element={<SettingsSections items={[ <Tags key="one" />, <Visits key="two" /> ]} />} />
<Route path="*" element={<Navigate replace to="general" />} /> <Route path="*" element={<Navigate replace to="general" />} />
</Routes> </Routes>
</NoMenuLayout> </NoMenuLayout>

View file

@ -87,137 +87,139 @@ const VisitsTable = ({
}, [ searchTerm ]); }, [ searchTerm ]);
return ( return (
<table className="table table-bordered table-hover table-sm table-responsive-sm visits-table"> <div className="table-responsive-md">
<thead className="visits-table__header"> <table className="table table-bordered table-hover table-sm visits-table">
<tr> <thead className="visits-table__header">
<th
className={`${headerCellsClass} text-center`}
onClick={() => setSelectedVisits(
selectedVisits.length < resultSet.total ? resultSet.visitsGroups.flat() : [],
)}
>
<FontAwesomeIcon icon={checkIcon} className={classNames({ 'text-primary': selectedVisits.length > 0 })} />
</th>
{supportsBots && (
<th className={`${headerCellsClass} text-center`} onClick={orderByColumn('potentialBot')}>
<FontAwesomeIcon icon={botIcon} />
{renderOrderIcon('potentialBot')}
</th>
)}
<th className={headerCellsClass} onClick={orderByColumn('date')}>
Date
{renderOrderIcon('date')}
</th>
<th className={headerCellsClass} onClick={orderByColumn('country')}>
Country
{renderOrderIcon('country')}
</th>
<th className={headerCellsClass} onClick={orderByColumn('city')}>
City
{renderOrderIcon('city')}
</th>
<th className={headerCellsClass} onClick={orderByColumn('browser')}>
Browser
{renderOrderIcon('browser')}
</th>
<th className={headerCellsClass} onClick={orderByColumn('os')}>
OS
{renderOrderIcon('os')}
</th>
<th className={headerCellsClass} onClick={orderByColumn('referer')}>
Referrer
{renderOrderIcon('referer')}
</th>
{isOrphanVisits && (
<th className={headerCellsClass} onClick={orderByColumn('visitedUrl')}>
Visited URL
{renderOrderIcon('visitedUrl')}
</th>
)}
</tr>
<tr>
<td colSpan={fullSizeColSpan} className="p-0">
<SearchField noBorder large={false} onChange={setSearchTerm} />
</td>
</tr>
</thead>
<tbody>
{!resultSet.visitsGroups[page - 1]?.length && (
<tr> <tr>
<td colSpan={fullSizeColSpan} className="text-center"> <th
No visits found with current filtering className={`${headerCellsClass} text-center`}
</td>
</tr>
)}
{resultSet.visitsGroups[page - 1]?.map((visit, index) => {
const isSelected = selectedVisits.includes(visit);
return (
<tr
key={index}
style={{ cursor: 'pointer' }}
className={classNames({ 'table-active': isSelected })}
onClick={() => setSelectedVisits( onClick={() => setSelectedVisits(
isSelected ? selectedVisits.filter((v) => v !== visit) : [ ...selectedVisits, visit ], selectedVisits.length < resultSet.total ? resultSet.visitsGroups.flat() : [],
)} )}
> >
<td className="text-center"> <FontAwesomeIcon icon={checkIcon} className={classNames({ 'text-primary': selectedVisits.length > 0 })} />
{isSelected && <FontAwesomeIcon icon={checkIcon} className="text-primary" />} </th>
</td> {supportsBots && (
{supportsBots && ( <th className={`${headerCellsClass} text-center`} onClick={orderByColumn('potentialBot')}>
<td className="text-center"> <FontAwesomeIcon icon={botIcon} />
{visit.potentialBot && ( {renderOrderIcon('potentialBot')}
<> </th>
<FontAwesomeIcon icon={botIcon} id={`botIcon${index}`} /> )}
<UncontrolledTooltip placement="right" target={`botIcon${index}`}> <th className={headerCellsClass} onClick={orderByColumn('date')}>
Potentially a visit from a bot or crawler Date
</UncontrolledTooltip> {renderOrderIcon('date')}
</> </th>
)} <th className={headerCellsClass} onClick={orderByColumn('country')}>
</td> Country
)} {renderOrderIcon('country')}
<td><Time date={visit.date} /></td> </th>
<td>{visit.country}</td> <th className={headerCellsClass} onClick={orderByColumn('city')}>
<td>{visit.city}</td> City
<td>{visit.browser}</td> {renderOrderIcon('city')}
<td>{visit.os}</td> </th>
<td>{visit.referer}</td> <th className={headerCellsClass} onClick={orderByColumn('browser')}>
{isOrphanVisits && <td>{(visit as NormalizedOrphanVisit).visitedUrl}</td>} Browser
</tr> {renderOrderIcon('browser')}
); </th>
})} <th className={headerCellsClass} onClick={orderByColumn('os')}>
</tbody> OS
{resultSet.total > PAGE_SIZE && ( {renderOrderIcon('os')}
<tfoot> </th>
<th className={headerCellsClass} onClick={orderByColumn('referer')}>
Referrer
{renderOrderIcon('referer')}
</th>
{isOrphanVisits && (
<th className={headerCellsClass} onClick={orderByColumn('visitedUrl')}>
Visited URL
{renderOrderIcon('visitedUrl')}
</th>
)}
</tr>
<tr> <tr>
<td colSpan={fullSizeColSpan} className="visits-table__footer-cell visits-table__sticky"> <td colSpan={fullSizeColSpan} className="p-0">
<div className="row"> <SearchField noBorder large={false} onChange={setSearchTerm} />
<div className="col-md-6">
<SimplePaginator
pagesCount={Math.ceil(resultSet.total / PAGE_SIZE)}
currentPage={page}
setCurrentPage={setPage}
centered={isMobileDevice}
/>
</div>
<div
className={classNames('col-md-6', {
'd-flex align-items-center flex-row-reverse': !isMobileDevice,
'text-center mt-3': isMobileDevice,
})}
>
<div>
Visits <b>{prettify(start + 1)}</b> to{' '}
<b>{prettify(min(end, resultSet.total))}</b> of{' '}
<b>{prettify(resultSet.total)}</b>
</div>
</div>
</div>
</td> </td>
</tr> </tr>
</tfoot> </thead>
)} <tbody>
</table> {!resultSet.visitsGroups[page - 1]?.length && (
<tr>
<td colSpan={fullSizeColSpan} className="text-center">
No visits found with current filtering
</td>
</tr>
)}
{resultSet.visitsGroups[page - 1]?.map((visit, index) => {
const isSelected = selectedVisits.includes(visit);
return (
<tr
key={index}
style={{ cursor: 'pointer' }}
className={classNames({ 'table-active': isSelected })}
onClick={() => setSelectedVisits(
isSelected ? selectedVisits.filter((v) => v !== visit) : [ ...selectedVisits, visit ],
)}
>
<td className="text-center">
{isSelected && <FontAwesomeIcon icon={checkIcon} className="text-primary" />}
</td>
{supportsBots && (
<td className="text-center">
{visit.potentialBot && (
<>
<FontAwesomeIcon icon={botIcon} id={`botIcon${index}`} />
<UncontrolledTooltip placement="right" target={`botIcon${index}`}>
Potentially a visit from a bot or crawler
</UncontrolledTooltip>
</>
)}
</td>
)}
<td><Time date={visit.date} /></td>
<td>{visit.country}</td>
<td>{visit.city}</td>
<td>{visit.browser}</td>
<td>{visit.os}</td>
<td>{visit.referer}</td>
{isOrphanVisits && <td>{(visit as NormalizedOrphanVisit).visitedUrl}</td>}
</tr>
);
})}
</tbody>
{resultSet.total > PAGE_SIZE && (
<tfoot>
<tr>
<td colSpan={fullSizeColSpan} className="visits-table__footer-cell visits-table__sticky">
<div className="row">
<div className="col-md-6">
<SimplePaginator
pagesCount={Math.ceil(resultSet.total / PAGE_SIZE)}
currentPage={page}
setCurrentPage={setPage}
centered={isMobileDevice}
/>
</div>
<div
className={classNames('col-md-6', {
'd-flex align-items-center flex-row-reverse': !isMobileDevice,
'text-center mt-3': isMobileDevice,
})}
>
<div>
Visits <b>{prettify(start + 1)}</b> to{' '}
<b>{prettify(min(end, resultSet.total))}</b> of{' '}
<b>{prettify(resultSet.total)}</b>
</div>
</div>
</div>
</td>
</tr>
</tfoot>
)}
</table>
</div>
); );
}; };

View file

@ -1,4 +1,4 @@
import { DropdownItem, DropdownItemProps } from 'reactstrap'; // eslint-disable-line import/named import { DropdownItem, DropdownItemProps } from 'reactstrap';
import { OrphanVisitType, VisitsFilter } from '../types'; import { OrphanVisitType, VisitsFilter } from '../types';
import { DropdownBtn } from '../../utils/DropdownBtn'; import { DropdownBtn } from '../../utils/DropdownBtn';
import { hasValue } from '../../utils/utils'; import { hasValue } from '../../utils/utils';

View file

@ -24,6 +24,6 @@ describe('<Settings />', () => {
expect(items).toHaveLength(3); expect(items).toHaveLength(3);
expect(items.first().prop('to')).toEqual('general'); expect(items.first().prop('to')).toEqual('general');
expect(items.at(1).prop('to')).toEqual('short-urls'); expect(items.at(1).prop('to')).toEqual('short-urls');
expect(items.last().prop('to')).toEqual('secondary-items'); expect(items.last().prop('to')).toEqual('other-items');
}); });
}); });