mirror of
https://github.com/kremalicious/umami.git
synced 2024-11-22 18:00:17 +01:00
Merge branch 'data-table' into dev
# Conflicts: # src/components/messages.js
This commit is contained in:
commit
92ccc64e47
68
src/components/common/DataTable.js
Normal file
68
src/components/common/DataTable.js
Normal 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;
|
17
src/components/common/DataTable.module.css
Normal file
17
src/components/common/DataTable.module.css
Normal 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;
|
||||||
|
}
|
@ -1,14 +1,15 @@
|
|||||||
import styles from './Pager.module.css';
|
import classNames from 'classnames';
|
||||||
import { Button, Flexbox, Icon, Icons } from 'react-basics';
|
import { Button, Flexbox, Icon, Icons } from 'react-basics';
|
||||||
import useMessages from 'components/hooks/useMessages';
|
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 { formatMessage, labels } = useMessages();
|
||||||
const maxPage = Math.ceil(count / pageSize);
|
const maxPage = pageSize && count ? Math.ceil(count / pageSize) : 0;
|
||||||
const lastPage = page === maxPage;
|
const lastPage = page === maxPage;
|
||||||
const firstPage = page === 1;
|
const firstPage = page === 1;
|
||||||
|
|
||||||
if (count === 0) {
|
if (count === 0 || !maxPage) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,7 +25,7 @@ export function Pager({ page, pageSize, count, onPageChange }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flexbox justifyContent="center" className={styles.container}>
|
<Flexbox justifyContent="center" className={classNames(styles.container, className)}>
|
||||||
<Button onClick={() => handlePageChange(-1)} disabled={firstPage}>
|
<Button onClick={() => handlePageChange(-1)} disabled={firstPage}>
|
||||||
<Icon rotate={90}>
|
<Icon rotate={90}>
|
||||||
<Icons.ChevronDown />
|
<Icons.ChevronDown />
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
.container {
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
|
font-size: var(--font-size-md);
|
||||||
margin: 0 16px;
|
margin: 0 16px;
|
||||||
}
|
}
|
||||||
|
13
src/components/hooks/useDataTable.js
Normal file
13
src/components/hooks/useDataTable.js
Normal 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;
|
9
src/components/hooks/usePaging.js
Normal file
9
src/components/hooks/usePaging.js
Normal 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 };
|
||||||
|
}
|
@ -13,17 +13,17 @@ export function EventsChart({ websiteId, className, token }) {
|
|||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
const [timezone] = useTimezone();
|
const [timezone] = useTimezone();
|
||||||
const {
|
const {
|
||||||
query: { url, eventName },
|
query: { url, event },
|
||||||
} = usePageQuery();
|
} = usePageQuery();
|
||||||
|
|
||||||
const { data, isLoading } = useQuery(['events', websiteId, modified, eventName], () =>
|
const { data, isLoading } = useQuery(['events', websiteId, modified, event], () =>
|
||||||
get(`/websites/${websiteId}/events`, {
|
get(`/websites/${websiteId}/events`, {
|
||||||
startAt: +startDate,
|
startAt: +startDate,
|
||||||
endAt: +endDate,
|
endAt: +endDate,
|
||||||
unit,
|
unit,
|
||||||
timezone,
|
timezone,
|
||||||
url,
|
url,
|
||||||
eventName,
|
event,
|
||||||
token,
|
token,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -6,7 +6,7 @@ import classNames from 'classnames';
|
|||||||
import Empty from 'components/common/Empty';
|
import Empty from 'components/common/Empty';
|
||||||
import { formatNumber, formatLongNumber } from 'lib/format';
|
import { formatNumber, formatLongNumber } from 'lib/format';
|
||||||
import useMessages from 'components/hooks/useMessages';
|
import useMessages from 'components/hooks/useMessages';
|
||||||
import styles from './DataTable.module.css';
|
import styles from './ListTable.module.css';
|
||||||
|
|
||||||
export function ListTable({
|
export function ListTable({
|
||||||
data = [],
|
data = [],
|
||||||
|
@ -3,11 +3,11 @@ import PageHeader from 'components/layout/PageHeader';
|
|||||||
import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm';
|
import WebsiteAddForm from 'components/pages/settings/websites/WebsiteAddForm';
|
||||||
import WebsitesTable from 'components/pages/settings/websites/WebsitesTable';
|
import WebsitesTable from 'components/pages/settings/websites/WebsitesTable';
|
||||||
import useApi from 'components/hooks/useApi';
|
import useApi from 'components/hooks/useApi';
|
||||||
import useApiFilter from 'components/hooks/useApiFilter';
|
|
||||||
import useMessages from 'components/hooks/useMessages';
|
import useMessages from 'components/hooks/useMessages';
|
||||||
import useUser from 'components/hooks/useUser';
|
import useUser from 'components/hooks/useUser';
|
||||||
import { ROLES } from 'lib/constants';
|
import { ROLES } from 'lib/constants';
|
||||||
import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
|
import { Button, Icon, Icons, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
export function WebsitesList({
|
export function WebsitesList({
|
||||||
showTeam,
|
showTeam,
|
||||||
@ -15,28 +15,27 @@ export function WebsitesList({
|
|||||||
showHeader = true,
|
showHeader = true,
|
||||||
includeTeams,
|
includeTeams,
|
||||||
onlyTeams,
|
onlyTeams,
|
||||||
fetch,
|
|
||||||
}) {
|
}) {
|
||||||
const { formatMessage, labels, messages } = useMessages();
|
const { formatMessage, labels, messages } = useMessages();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
const [params, setParams] = useState({});
|
||||||
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
|
|
||||||
useApiFilter();
|
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { data, isLoading, error, refetch } = useQuery(
|
const { data, isLoading, error, refetch } = useQuery(
|
||||||
['websites', fetch, user?.id, filter, page, pageSize, includeTeams, onlyTeams],
|
['websites', includeTeams, onlyTeams],
|
||||||
() =>
|
() =>
|
||||||
get(`/users/${user?.id}/websites`, {
|
get(`/users/${user?.id}/websites`, {
|
||||||
filter,
|
|
||||||
page,
|
|
||||||
pageSize,
|
|
||||||
includeTeams,
|
includeTeams,
|
||||||
onlyTeams,
|
onlyTeams,
|
||||||
|
...params,
|
||||||
}),
|
}),
|
||||||
{ enabled: !!user },
|
{ enabled: !!user },
|
||||||
);
|
);
|
||||||
const { showToast } = useToasts();
|
const { showToast } = useToasts();
|
||||||
|
|
||||||
|
const handleChange = params => {
|
||||||
|
setParams(params);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
await refetch();
|
await refetch();
|
||||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||||
@ -67,10 +66,7 @@ export function WebsitesList({
|
|||||||
data={data}
|
data={data}
|
||||||
showTeam={showTeam}
|
showTeam={showTeam}
|
||||||
showEditButton={showEditButton}
|
showEditButton={showEditButton}
|
||||||
onFilterChange={handleFilterChange}
|
onChange={handleChange}
|
||||||
onPageChange={handlePageChange}
|
|
||||||
onPageSizeChange={handlePageSizeChange}
|
|
||||||
filterValue={filter}
|
|
||||||
/>
|
/>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
@ -1,11 +1,78 @@
|
|||||||
import Link from 'next/link';
|
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 SettingsTable from 'components/common/SettingsTable';
|
||||||
import Empty from 'components/common/Empty';
|
import Empty from 'components/common/Empty';
|
||||||
import useMessages from 'components/hooks/useMessages';
|
import useMessages from 'components/hooks/useMessages';
|
||||||
import useUser from 'components/hooks/useUser';
|
import useUser from 'components/hooks/useUser';
|
||||||
|
import DataTable, { DataTableStyles } from 'components/common/DataTable';
|
||||||
|
|
||||||
export function WebsitesTable({
|
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 = [],
|
data = [],
|
||||||
filterValue,
|
filterValue,
|
||||||
onFilterChange,
|
onFilterChange,
|
||||||
|
@ -18,16 +18,19 @@ import {
|
|||||||
useToasts,
|
useToasts,
|
||||||
} from 'react-basics';
|
} from 'react-basics';
|
||||||
|
|
||||||
|
const TABS = {
|
||||||
|
myWebsites: 'my-websites',
|
||||||
|
teamWebsites: 'team-websites',
|
||||||
|
};
|
||||||
|
|
||||||
export function WebsitesPage() {
|
export function WebsitesPage() {
|
||||||
const { formatMessage, labels, messages } = useMessages();
|
const { formatMessage, labels, messages } = useMessages();
|
||||||
const [tab, setTab] = useState('my-websites');
|
const [tab, setTab] = useState(TABS.myWebsites);
|
||||||
const [fetch, setFetch] = useState(1);
|
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
const { showToast } = useToasts();
|
const { showToast } = useToasts();
|
||||||
const cloudMode = Boolean(process.env.cloudMode);
|
const cloudMode = Boolean(process.env.cloudMode);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = () => {
|
||||||
setFetch(fetch + 1);
|
|
||||||
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
showToast({ message: formatMessage(messages.saved), variant: 'success' });
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -53,18 +56,16 @@ export function WebsitesPage() {
|
|||||||
<Page>
|
<Page>
|
||||||
<PageHeader title={formatMessage(labels.websites)}>{!cloudMode && addButton}</PageHeader>
|
<PageHeader title={formatMessage(labels.websites)}>{!cloudMode && addButton}</PageHeader>
|
||||||
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30 }}>
|
<Tabs selectedKey={tab} onSelect={setTab} style={{ marginBottom: 30 }}>
|
||||||
<Item key="my-websites">{formatMessage(labels.myWebsites)}</Item>
|
<Item key={TABS.myWebsites}>{formatMessage(labels.myWebsites)}</Item>
|
||||||
<Item key="team-webaites">{formatMessage(labels.teamWebsites)}</Item>
|
<Item key={TABS.teamWebsites}>{formatMessage(labels.teamWebsites)}</Item>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
{tab === TABS.myWebsites && (
|
||||||
{tab === 'my-websites' && (
|
|
||||||
<WebsiteList showEditButton={!cloudMode} showHeader={false} fetch={fetch} />
|
<WebsiteList showEditButton={!cloudMode} showHeader={false} fetch={fetch} />
|
||||||
)}
|
)}
|
||||||
{tab === 'team-webaites' && (
|
{tab === TABS.teamWebsites && (
|
||||||
<WebsiteList
|
<WebsiteList
|
||||||
showEditButton={!cloudMode}
|
showEditButton={!cloudMode}
|
||||||
showHeader={false}
|
showHeader={false}
|
||||||
fetch={fetch}
|
|
||||||
showTeam={true}
|
showTeam={true}
|
||||||
onlyTeams={true}
|
onlyTeams={true}
|
||||||
/>
|
/>
|
||||||
|
Loading…
Reference in New Issue
Block a user