Initial dev on DataTable component.

This commit is contained in:
Mike Cao 2023-08-25 11:54:44 -07:00
parent 7107336b49
commit d6a27b8e99
19 changed files with 223 additions and 53 deletions

View File

@ -95,7 +95,7 @@
"node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5",
"react": "^18.2.0",
"react-basics": "^0.98.0",
"react-basics": "^0.100.0",
"react-beautiful-dnd": "^13.1.0",
"react-dom": "^18.2.0",
"react-error-boundary": "^4.0.4",

View File

@ -0,0 +1,68 @@
import { createContext } from 'react';
import { SearchField } from 'react-basics';
import { useDataTable } from 'components/hooks/useDataTable';
import { useMessages } from 'components/hooks';
import Empty from 'components/common/Empty';
import Pager from 'components/common/Pager';
import styles from './DataTable.module.css';
const DEFAULT_SEARCH_DELAY = 1000;
export const DataTableStyles = styles;
export const DataTableContext = createContext(null);
export function DataTable({
searchDelay,
showSearch = true,
showPaging = true,
children,
onChange,
}) {
const { formatMessage, labels, messages } = useMessages();
const dataTable = useDataTable();
const { query, setQuery, data, pageInfo, setPageInfo } = dataTable;
const { page, pageSize, count } = pageInfo || {};
const noResults = Boolean(query && data?.length === 0);
const handleChange = () => {
onChange?.({ query, page });
};
const handleSearch = value => {
setQuery(value);
handleChange();
};
const handlePageChange = page => {
setPageInfo(state => ({ ...state, page }));
};
return (
<DataTableContext.Provider value={dataTable}>
{showSearch && (
<SearchField
className={styles.search}
value={query}
onChange={handleSearch}
delay={searchDelay || DEFAULT_SEARCH_DELAY}
autoFocus={true}
placeholder={formatMessage(labels.search)}
/>
)}
{noResults && <Empty message={formatMessage(messages.noResultsFound)} />}
<div className={styles.body}>{children}</div>
{showPaging && (
<Pager
className={styles.pager}
page={page}
pageSize={pageSize}
count={count}
onPageChange={handlePageChange}
/>
)}
</DataTableContext.Provider>
);
}
export default DataTable;

View File

@ -0,0 +1,17 @@
.search {
max-width: 300px;
margin: 20px 0;
}
.action {
justify-content: flex-end;
gap: 5px;
}
.body td {
align-items: center;
}
.pager {
margin-top: 20px;
}

View File

@ -1,14 +1,15 @@
import styles from './Pager.module.css';
import classNames from 'classnames';
import { Button, Flexbox, Icon, Icons } from 'react-basics';
import useMessages from 'components/hooks/useMessages';
import styles from './Pager.module.css';
export function Pager({ page, pageSize, count, onPageChange }) {
export function Pager({ page, pageSize, count, onPageChange, className }) {
const { formatMessage, labels } = useMessages();
const maxPage = Math.ceil(count / pageSize);
const maxPage = pageSize && count ? Math.ceil(count / pageSize) : 0;
const lastPage = page === maxPage;
const firstPage = page === 1;
if (count === 0) {
if (count === 0 || !maxPage) {
return null;
}
@ -24,7 +25,7 @@ export function Pager({ page, pageSize, count, onPageChange }) {
}
return (
<Flexbox justifyContent="center" className={styles.container}>
<Flexbox justifyContent="center" className={classNames(styles.container, className)}>
<Button onClick={() => handlePageChange(-1)} disabled={firstPage}>
<Icon rotate={90}>
<Icons.ChevronDown />

View File

@ -1,7 +1,4 @@
.container {
margin-top: 20px;
}
.text {
font-size: var(--font-size-md);
margin: 0 16px;
}

View File

@ -0,0 +1,13 @@
import { useState } from 'react';
import { usePaging } from 'components/hooks/usePaging';
export function useDataTable(config = {}) {
const { initialData, initialQuery, initialPageInfo } = config;
const [data, setData] = useState(initialData ?? null);
const [query, setQuery] = useState(initialQuery ?? '');
const { pageInfo, setPageInfo } = usePaging(initialPageInfo);
return { data, setData, query, setQuery, pageInfo, setPageInfo };
}
export default useDataTable;

View File

@ -0,0 +1,9 @@
import { useState } from 'react';
const DEFAULT_PAGE_INFO = { page: 1, pageSize: 10, total: 0 };
export function usePaging(initialPageInfo) {
const [pageInfo, setPageInfo] = useState(initialPageInfo ?? { ...DEFAULT_PAGE_INFO });
return { pageInfo, setPageInfo };
}

View File

@ -178,6 +178,7 @@ export const labels = defineMessages({
day: { id: 'label.day', defaultMessage: 'Day' },
date: { id: 'label.date', defaultMessage: 'Date' },
pageOf: { id: 'label.page-of', defaultMessage: 'Page {current} of {total}' },
search: { id: 'label.search', defaultMessage: 'Search' },
});
export const messages = defineMessages({

View File

@ -13,17 +13,17 @@ export function EventsChart({ websiteId, className, token }) {
const { locale } = useLocale();
const [timezone] = useTimezone();
const {
query: { url, eventName },
query: { url, event },
} = usePageQuery();
const { data, isLoading } = useQuery(['events', websiteId, modified, eventName], () =>
const { data, isLoading } = useQuery(['events', websiteId, modified, event], () =>
get(`/websites/${websiteId}/events`, {
startAt: +startDate,
endAt: +endDate,
unit,
timezone,
url,
eventName,
event,
token,
}),
);

View File

@ -6,9 +6,9 @@ import classNames from 'classnames';
import Empty from 'components/common/Empty';
import { formatNumber, formatLongNumber } from 'lib/format';
import useMessages from 'components/hooks/useMessages';
import styles from './DataTable.module.css';
import styles from './ListTable.module.css';
export function DataTable({
export function ListTable({
data = [],
title,
metric,
@ -102,4 +102,4 @@ const AnimatedRow = ({
);
};
export default DataTable;
export default ListTable;

View File

@ -8,7 +8,7 @@ import { percentFilter } from 'lib/filters';
import useDateRange from 'components/hooks/useDateRange';
import usePageQuery from 'components/hooks/usePageQuery';
import ErrorMessage from 'components/common/ErrorMessage';
import DataTable from './DataTable';
import ListTable from './ListTable';
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
import Icons from 'components/icons';
import useMessages from 'components/hooks/useMessages';
@ -104,7 +104,7 @@ export function MetricsTable({
<div className={classNames(styles.container, className)}>
{!data && isLoading && !isFetched && <Loading icon="dots" />}
{error && <ErrorMessage />}
{data && !error && <DataTable {...props} data={filteredData} className={className} />}
{data && !error && <ListTable {...props} data={filteredData} className={className} />}
<div className={styles.footer}>
{data && !error && limit && (
<Link href={router.pathname} as={resolveUrl({ view: type })}>

View File

@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { useRouter } from 'next/router';
import DataTable from 'components/metrics/DataTable';
import ListTable from 'components/metrics/ListTable';
import useLocale from 'components/hooks/useLocale';
import useCountryNames from 'components/hooks/useCountryNames';
import useMessages from 'components/hooks/useMessages';
@ -24,7 +24,7 @@ export function RealtimeCountries({ data }) {
);
return (
<DataTable
<ListTable
title={formatMessage(labels.countries)}
metric={formatMessage(labels.visitors)}
data={data}

View File

@ -2,7 +2,7 @@ import { useMemo, useState } from 'react';
import { ButtonGroup, Button, Flexbox } from 'react-basics';
import firstBy from 'thenby';
import { percentFilter } from 'lib/filters';
import DataTable from 'components/metrics/DataTable';
import ListTable from 'components/metrics/ListTable';
import { FILTER_PAGES, FILTER_REFERRERS } from 'lib/constants';
import useMessages from 'components/hooks/useMessages';
@ -82,7 +82,7 @@ export function RealtimeUrls({ websiteDomain, data = {} }) {
</ButtonGroup>
</Flexbox>
{filter === FILTER_REFERRERS && (
<DataTable
<ListTable
title={formatMessage(labels.referrers)}
metric={formatMessage(labels.views)}
renderLabel={renderLink}
@ -90,7 +90,7 @@ export function RealtimeUrls({ websiteDomain, data = {} }) {
/>
)}
{filter === FILTER_PAGES && (
<DataTable
<ListTable
title={formatMessage(labels.pages)}
metric={formatMessage(labels.views)}
renderLabel={renderLink}

View File

@ -1,5 +1,5 @@
import { useContext } from 'react';
import DataTable from 'components/metrics/DataTable';
import ListTable from 'components/metrics/ListTable';
import { useMessages } from 'components/hooks';
import { ReportContext } from '../Report';
@ -7,7 +7,7 @@ export function FunnelTable() {
const { report } = useContext(ReportContext);
const { formatMessage, labels } = useMessages();
return (
<DataTable
<ListTable
data={report?.data}
title={formatMessage(labels.url)}
metric={formatMessage(labels.visitors)}

View File

@ -3,11 +3,11 @@ import PageHeader from 'components/layout/PageHeader';
import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm';
import WebsitesTable from 'components/pages/settings/websites/WebsitesTable';
import useApi from 'components/hooks/useApi';
import useApiFilter from 'components/hooks/useApiFilter';
import useMessages from 'components/hooks/useMessages';
import useUser from 'components/hooks/useUser';
import { ROLES } from 'lib/constants';
import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
import { useState } from 'react';
export function WebsitesList({
showTeam,
@ -15,28 +15,27 @@ export function WebsitesList({
showHeader = true,
includeTeams,
onlyTeams,
fetch,
}) {
const { formatMessage, labels, messages } = useMessages();
const { user } = useUser();
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
useApiFilter();
const [params, setParams] = useState({});
const { get, useQuery } = useApi();
const { data, isLoading, error, refetch } = useQuery(
['websites', fetch, user?.id, filter, page, pageSize, includeTeams, onlyTeams],
['websites', includeTeams, onlyTeams],
() =>
get(`/users/${user?.id}/websites`, {
filter,
page,
pageSize,
includeTeams,
onlyTeams,
...params,
}),
{ enabled: !!user },
);
const { showToast } = useToasts();
const handleChange = params => {
setParams(params);
};
const handleSave = async () => {
await refetch();
showToast({ message: formatMessage(messages.saved), variant: 'success' });
@ -67,10 +66,7 @@ export function WebsitesList({
data={data}
showTeam={showTeam}
showEditButton={showEditButton}
onFilterChange={handleFilterChange}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
filterValue={filter}
onChange={handleChange}
/>
</Page>
);

View File

@ -1,11 +1,78 @@
import Link from 'next/link';
import { Button, Text, Icon, Icons } from 'react-basics';
import { Button, Text, Icon, Icons, GridTable, GridColumn } from 'react-basics';
import SettingsTable from 'components/common/SettingsTable';
import Empty from 'components/common/Empty';
import useMessages from 'components/hooks/useMessages';
import useUser from 'components/hooks/useUser';
import DataTable, { DataTableStyles } from 'components/common/DataTable';
export function WebsitesTable({
data = [],
filterValue,
showTeam,
showEditButton,
openExternal = false,
onChange,
}) {
const { formatMessage, labels } = useMessages();
const { user } = useUser();
const showTable = data && (filterValue || data?.data?.length !== 0);
return (
<DataTable onChange={onChange}>
{showTable && (
<GridTable data={data?.data}>
<GridColumn name="name" label={formatMessage(labels.name)} />
<GridColumn name="domain" label={formatMessage(labels.domain)} />
{showTeam && (
<GridColumn name="teamName" label={formatMessage(labels.teamName)}>
{row => row.teamWebsite[0]?.team.name}
</GridColumn>
)}
{showTeam && (
<GridColumn name="owner" label={formatMessage(labels.owner)}>
{row => row.user.username}
</GridColumn>
)}
<GridColumn name="action" label=" " className={DataTableStyles.action}>
{row => {
const {
id,
user: { id: ownerId },
} = row;
return (
<>
{showEditButton && (!showTeam || ownerId === user.id) && (
<Link href={`/settings/websites/${id}`}>
<Button>
<Icon>
<Icons.Edit />
</Icon>
<Text>{formatMessage(labels.edit)}</Text>
</Button>
</Link>
)}
<Link href={`/websites/${id}`} target={openExternal ? '_blank' : null}>
<Button>
<Icon>
<Icons.External />
</Icon>
<Text>{formatMessage(labels.view)}</Text>
</Button>
</Link>
</>
);
}}
</GridColumn>
</GridTable>
)}
</DataTable>
);
}
export function WebsitesTable2({
data = [],
filterValue,
onFilterChange,

View File

@ -19,16 +19,19 @@ import {
useToasts,
} from 'react-basics';
const TABS = {
myWebsites: 'my-websites',
teamWebsites: 'team-websites',
};
export function WebsitesPage() {
const { formatMessage, labels, messages } = useMessages();
const [tab, setTab] = useState('my-websites');
const [fetch, setFetch] = useState(1);
const [tab, setTab] = useState(TABS.myWebsites);
const { user } = useUser();
const { cloudMode } = useConfig();
const { showToast } = useToasts();
const handleSave = async () => {
setFetch(fetch + 1);
const handleSave = () => {
showToast({ message: formatMessage(messages.saved), variant: 'success' });
};
@ -54,18 +57,16 @@ export function WebsitesPage() {
<Page>
<PageHeader title={formatMessage(labels.websites)}>{!cloudMode && addButton}</PageHeader>
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30 }}>
<Item key="my-websites">{formatMessage(labels.myWebsites)}</Item>
<Item key="team-webaites">{formatMessage(labels.teamWebsites)}</Item>
<Item key={TABS.myWebsites}>{formatMessage(labels.myWebsites)}</Item>
<Item key={TABS.teamWebsites}>{formatMessage(labels.teamWebsites)}</Item>
</Tabs>
{tab === 'my-websites' && (
{tab === TABS.myWebsites && (
<WebsiteList showEditButton={!cloudMode} showHeader={false} fetch={fetch} />
)}
{tab === 'team-webaites' && (
{tab === TABS.teamWebsites && (
<WebsiteList
showEditButton={!cloudMode}
showHeader={false}
fetch={fetch}
showTeam={true}
onlyTeams={true}
/>

View File

@ -7642,10 +7642,10 @@ rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
react-basics@^0.98.0:
version "0.98.0"
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.98.0.tgz#b207bedbd9dac749d28ea6de2197a0efe648b78c"
integrity sha512-ebUigu+s6Iusq14EZTFTTUzdDPYFQEZjeD4feeq3o7dE+ndOVnajEdQ2va/x6CsRBUsWgjLJipfQi0XIrxYupA==
react-basics@^0.100.0:
version "0.100.0"
resolved "https://registry.yarnpkg.com/react-basics/-/react-basics-0.100.0.tgz#14a36769af89f3e01641997f897e4073f16f5035"
integrity sha512-ET6DX/FYAcjGRauBE4jwqwVpd/hKmA2Nu/fi1dakwsv17hkyV5FEAhdWhQAxJX3VnaCH//QysN8+ae12KuNA9g==
dependencies:
classnames "^2.3.1"
date-fns "^2.29.3"