mirror of
https://github.com/umami-software/umami.git
synced 2026-02-08 22:57:12 +01:00
Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
1b4c5c80d3
91 changed files with 1454 additions and 373 deletions
37
components/common/Pager.js
Normal file
37
components/common/Pager.js
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import styles from './Pager.module.css';
|
||||
import { Button, Flexbox, Icon, Icons } from 'react-basics';
|
||||
|
||||
export function Pager({ page, pageSize, count, onPageChange, onPageSizeChange }) {
|
||||
const maxPage = Math.ceil(count / pageSize);
|
||||
const lastPage = page === maxPage;
|
||||
const firstPage = page === 1;
|
||||
|
||||
if (count === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handlePageChange = value => {
|
||||
const nextPage = page + value;
|
||||
if (nextPage > 0 && nextPage <= maxPage) {
|
||||
onPageChange(nextPage);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Flexbox justifyContent="center" className={styles.container}>
|
||||
<Button onClick={() => handlePageChange(-1)} disabled={firstPage}>
|
||||
<Icon size="lg" className={styles.icon} rotate={90}>
|
||||
<Icons.ChevronDown />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Flexbox alignItems="center" className={styles.text}>{`Page ${page} of ${maxPage}`}</Flexbox>
|
||||
<Button onClick={() => handlePageChange(1)} disabled={lastPage}>
|
||||
<Icon size="lg" className={styles.icon} rotate={270}>
|
||||
<Icons.ChevronDown />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
export default Pager;
|
||||
7
components/common/Pager.module.css
Normal file
7
components/common/Pager.module.css
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
.container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
|
@ -1,37 +1,98 @@
|
|||
import { Table, TableHeader, TableBody, TableRow, TableCell, TableColumn } from 'react-basics';
|
||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
SearchField,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableColumn,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from 'react-basics';
|
||||
import styles from './SettingsTable.module.css';
|
||||
import Pager from 'components/common/Pager';
|
||||
|
||||
export function SettingsTable({
|
||||
columns = [],
|
||||
data,
|
||||
children,
|
||||
cellRender,
|
||||
showSearch,
|
||||
showPaging,
|
||||
onFilterChange,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
filterValue,
|
||||
}) {
|
||||
const { formatMessage, messages } = useMessages();
|
||||
const [filter, setFilter] = useState(filterValue);
|
||||
const { data: value, page, count, pageSize } = data;
|
||||
|
||||
const handleFilterChange = value => {
|
||||
setFilter(value);
|
||||
onFilterChange(value);
|
||||
};
|
||||
|
||||
export function SettingsTable({ columns = [], data = [], children, cellRender }) {
|
||||
return (
|
||||
<Table columns={columns} rows={data}>
|
||||
<TableHeader className={styles.header}>
|
||||
{(column, index) => {
|
||||
return (
|
||||
<TableColumn key={index} className={styles.cell} style={columns[index].style}>
|
||||
{column.label}
|
||||
</TableColumn>
|
||||
);
|
||||
}}
|
||||
</TableHeader>
|
||||
<TableBody className={styles.body}>
|
||||
{(row, keys, rowIndex) => {
|
||||
row.action = children(row, keys, rowIndex);
|
||||
<>
|
||||
{showSearch && (
|
||||
<SearchField
|
||||
onChange={handleFilterChange}
|
||||
delay={1000}
|
||||
value={filter}
|
||||
placeholder="Search"
|
||||
style={{ maxWidth: '300px', marginBottom: '10px' }}
|
||||
/>
|
||||
)}
|
||||
{value.length === 0 && filterValue && (
|
||||
<EmptyPlaceholder message={formatMessage(messages.noResultsFound)}></EmptyPlaceholder>
|
||||
)}
|
||||
{value.length > 0 && (
|
||||
<Table columns={columns} rows={value}>
|
||||
<TableHeader className={styles.header}>
|
||||
{(column, index) => {
|
||||
return (
|
||||
<TableColumn key={index} className={styles.cell} style={columns[index].style}>
|
||||
{column.label}
|
||||
</TableColumn>
|
||||
);
|
||||
}}
|
||||
</TableHeader>
|
||||
<TableBody className={styles.body}>
|
||||
{(row, keys, rowIndex) => {
|
||||
row.action = children(row, keys, rowIndex);
|
||||
|
||||
return (
|
||||
<TableRow key={rowIndex} data={row} keys={keys} className={styles.row}>
|
||||
{(data, key, colIndex) => {
|
||||
return (
|
||||
<TableCell key={colIndex} className={styles.cell} style={columns[colIndex].style}>
|
||||
<label className={styles.label}>{columns[colIndex].label}</label>
|
||||
{cellRender ? cellRender(row, data, key, colIndex) : data[key]}
|
||||
</TableCell>
|
||||
);
|
||||
}}
|
||||
</TableRow>
|
||||
);
|
||||
}}
|
||||
</TableBody>
|
||||
</Table>
|
||||
return (
|
||||
<TableRow key={rowIndex} data={row} keys={keys} className={styles.row}>
|
||||
{(data, key, colIndex) => {
|
||||
return (
|
||||
<TableCell
|
||||
key={colIndex}
|
||||
className={styles.cell}
|
||||
style={columns[colIndex].style}
|
||||
>
|
||||
<label className={styles.label}>{columns[colIndex].label}</label>
|
||||
{cellRender ? cellRender(row, data, key, colIndex) : data[key]}
|
||||
</TableCell>
|
||||
);
|
||||
}}
|
||||
</TableRow>
|
||||
);
|
||||
}}
|
||||
</TableBody>
|
||||
{showPaging && (
|
||||
<Pager
|
||||
page={page}
|
||||
pageSize={pageSize}
|
||||
count={count}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
/>
|
||||
)}
|
||||
</Table>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@ export function WebsiteSelect({ websiteId, onSelect }) {
|
|||
const { data } = useQuery(['websites:me'], () => get('/me/websites'));
|
||||
|
||||
const renderValue = value => {
|
||||
return data?.find(({ id }) => id === value)?.name;
|
||||
return data?.data?.find(({ id }) => id === value)?.name;
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
items={data}
|
||||
items={data?.data}
|
||||
value={websiteId}
|
||||
renderValue={renderValue}
|
||||
onChange={onSelect}
|
||||
|
|
|
|||
|
|
@ -163,6 +163,7 @@ export const labels = defineMessages({
|
|||
overview: { id: 'label.overview', defaultMessage: 'Overview' },
|
||||
totalRecords: { id: 'label.total-records', defaultMessage: 'Total records' },
|
||||
insights: { id: 'label.insights', defaultMessage: 'Insights' },
|
||||
retention: { id: 'label.retention', defaultMessage: 'Retention' },
|
||||
dropoff: { id: 'label.dropoff', defaultMessage: 'Dropoff' },
|
||||
referrer: { id: 'label.referrer', defaultMessage: 'Referrer' },
|
||||
country: { id: 'label.country', defaultMessage: 'Country' },
|
||||
|
|
|
|||
|
|
@ -12,16 +12,17 @@ import useDashboard from 'store/dashboard';
|
|||
import useMessages from 'hooks/useMessages';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
||||
export function Dashboard({ userId }) {
|
||||
export function Dashboard() {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const dashboard = useDashboard();
|
||||
const { showCharts, limit, editing } = dashboard;
|
||||
const [max, setMax] = useState(limit);
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, isLoading, error } = useQuery(['websites'], () =>
|
||||
get('/websites', { userId, includeTeams: 1 }),
|
||||
get('/websites', { includeTeams: 1 }),
|
||||
);
|
||||
const hasData = data && data.length !== 0;
|
||||
const hasData = data && data?.data.length !== 0;
|
||||
|
||||
const { dir } = useLocale();
|
||||
|
||||
function handleMore() {
|
||||
|
|
@ -47,8 +48,10 @@ export function Dashboard({ userId }) {
|
|||
)}
|
||||
{hasData && (
|
||||
<>
|
||||
{editing && <DashboardEdit websites={data} />}
|
||||
{!editing && <WebsiteChartList websites={data} showCharts={showCharts} limit={max} />}
|
||||
{editing && <DashboardEdit websites={data?.data} />}
|
||||
{!editing && (
|
||||
<WebsiteChartList websites={data?.data} showCharts={showCharts} limit={max} />
|
||||
)}
|
||||
{max < data.length && (
|
||||
<Flexbox justifyContent="center">
|
||||
<Button onClick={handleMore}>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import FunnelReport from './funnel/FunnelReport';
|
||||
import EventDataReport from './event-data/EventDataReport';
|
||||
import InsightsReport from './insights/InsightsReport';
|
||||
import RetentionReport from './retention/RetentionReport';
|
||||
|
||||
const reports = {
|
||||
funnel: FunnelReport,
|
||||
'event-data': EventDataReport,
|
||||
insights: InsightsReport,
|
||||
retention: RetentionReport,
|
||||
};
|
||||
|
||||
export default function ReportDetails({ reportId, reportType }) {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,12 @@ export function ReportTemplates() {
|
|||
url: '/reports/funnel',
|
||||
icon: <Funnel />,
|
||||
},
|
||||
{
|
||||
title: formatMessage(labels.retention),
|
||||
description: 'Track your websites user retention',
|
||||
url: '/reports/retention',
|
||||
icon: <Funnel />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -5,7 +5,14 @@ import SettingsTable from 'components/common/SettingsTable';
|
|||
import ConfirmDeleteForm from 'components/common/ConfirmDeleteForm';
|
||||
import { useMessages } from 'hooks';
|
||||
|
||||
export function ReportsTable({ data = [], onDelete = () => {} }) {
|
||||
export function ReportsTable({
|
||||
data = [],
|
||||
onDelete = () => {},
|
||||
filterValue,
|
||||
onFilterChange,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
}) {
|
||||
const [report, setReport] = useState(null);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
|
|
@ -22,7 +29,16 @@ export function ReportsTable({ data = [], onDelete = () => {} }) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<SettingsTable columns={columns} data={data}>
|
||||
<SettingsTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
showSearch={true}
|
||||
showPaging={true}
|
||||
onFilterChange={onFilterChange}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
filterValue={filterValue}
|
||||
>
|
||||
{row => {
|
||||
const { id } = row;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { Loading } from 'react-basics';
|
||||
import { Loading, StatusLight } from 'react-basics';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import useTheme from 'hooks/useTheme';
|
||||
import BarChart from 'components/metrics/BarChart';
|
||||
|
|
@ -22,14 +22,25 @@ export function FunnelChart({ className, loading }) {
|
|||
);
|
||||
|
||||
const renderTooltipPopup = useCallback((setTooltipPopup, model) => {
|
||||
const { opacity, dataPoints } = model.tooltip;
|
||||
const { opacity, labelColors, dataPoints } = model.tooltip;
|
||||
|
||||
if (!dataPoints?.length || !opacity) {
|
||||
setTooltipPopup(null);
|
||||
return;
|
||||
}
|
||||
|
||||
setTooltipPopup(`${formatLongNumber(dataPoints[0].raw.y)} ${formatMessage(labels.visitors)}`);
|
||||
setTooltipPopup(
|
||||
<>
|
||||
<div>
|
||||
{formatLongNumber(dataPoints[0].raw.y)} {formatMessage(labels.visitors)}
|
||||
</div>
|
||||
<div>
|
||||
<StatusLight color={labelColors?.[0]?.backgroundColor}>
|
||||
{formatLongNumber(dataPoints[0].raw.z)}% {formatMessage(labels.dropoff)}
|
||||
</StatusLight>
|
||||
</div>
|
||||
</>,
|
||||
);
|
||||
}, []);
|
||||
|
||||
const datasets = useMemo(() => {
|
||||
|
|
|
|||
41
components/pages/reports/retention/RetentionParameters.js
Normal file
41
components/pages/reports/retention/RetentionParameters.js
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import { useContext, useRef } from 'react';
|
||||
import { useMessages } from 'hooks';
|
||||
import { Form, FormButtons, FormInput, FormRow, SubmitButton, TextField } from 'react-basics';
|
||||
import { ReportContext } from 'components/pages/reports/Report';
|
||||
import BaseParameters from '../BaseParameters';
|
||||
|
||||
const fieldOptions = [
|
||||
{ name: 'daily', type: 'string' },
|
||||
{ name: 'weekly', type: 'string' },
|
||||
];
|
||||
|
||||
export function RetentionParameters() {
|
||||
const { report, runReport, isRunning } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const ref = useRef(null);
|
||||
|
||||
const { parameters } = report || {};
|
||||
const { websiteId, dateRange } = parameters || {};
|
||||
const queryDisabled = !websiteId || !dateRange;
|
||||
|
||||
const handleSubmit = (data, e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (!queryDisabled) {
|
||||
runReport(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form ref={ref} values={parameters} onSubmit={handleSubmit} preventSubmit={true}>
|
||||
<BaseParameters />
|
||||
<FormButtons>
|
||||
<SubmitButton variant="primary" disabled={queryDisabled} loading={isRunning}>
|
||||
{formatMessage(labels.runQuery)}
|
||||
</SubmitButton>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
||||
export default RetentionParameters;
|
||||
26
components/pages/reports/retention/RetentionReport.js
Normal file
26
components/pages/reports/retention/RetentionReport.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import RetentionTable from './RetentionTable';
|
||||
import RetentionParameters from './RetentionParameters';
|
||||
import Report from '../Report';
|
||||
import ReportHeader from '../ReportHeader';
|
||||
import ReportMenu from '../ReportMenu';
|
||||
import ReportBody from '../ReportBody';
|
||||
import Funnel from 'assets/funnel.svg';
|
||||
|
||||
const defaultParameters = {
|
||||
type: 'retention',
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
export default function RetentionReport({ reportId }) {
|
||||
return (
|
||||
<Report reportId={reportId} defaultParameters={defaultParameters}>
|
||||
<ReportHeader icon={<Funnel />} />
|
||||
<ReportMenu>
|
||||
<RetentionParameters />
|
||||
</ReportMenu>
|
||||
<ReportBody>
|
||||
<RetentionTable />
|
||||
</ReportBody>
|
||||
</Report>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
.filters {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
border: 1px solid var(--base400);
|
||||
border-radius: var(--border-radius);
|
||||
line-height: 32px;
|
||||
padding: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
31
components/pages/reports/retention/RetentionTable.js
Normal file
31
components/pages/reports/retention/RetentionTable.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { useContext } from 'react';
|
||||
import { GridTable, GridColumn } from 'react-basics';
|
||||
import { useMessages } from 'hooks';
|
||||
import { ReportContext } from '../Report';
|
||||
|
||||
export function RetentionTable() {
|
||||
const { report } = useContext(ReportContext);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
return (
|
||||
<GridTable data={report?.data || []}>
|
||||
<GridColumn name="date" label={'Date'}>
|
||||
{row => row.date}
|
||||
</GridColumn>
|
||||
<GridColumn name="day" label={'Day'}>
|
||||
{row => row.day}
|
||||
</GridColumn>
|
||||
<GridColumn name="visitors" label={formatMessage(labels.visitors)}>
|
||||
{row => row.visitors}
|
||||
</GridColumn>
|
||||
<GridColumn name="returnVisitors" label={'Return Visitors'}>
|
||||
{row => row.returnVisitors}
|
||||
</GridColumn>
|
||||
<GridColumn name="percentage" label={'Percentage'}>
|
||||
{row => row.percentage}
|
||||
</GridColumn>
|
||||
</GridTable>
|
||||
);
|
||||
}
|
||||
|
||||
export default RetentionTable;
|
||||
|
|
@ -12,6 +12,8 @@ export function TeamAddWebsiteForm({ teamId, onSave, onClose }) {
|
|||
const [newWebsites, setNewWebsites] = useState([]);
|
||||
const formRef = useRef();
|
||||
|
||||
const hasData = websites && websites.data.length > 0;
|
||||
|
||||
const handleSubmit = () => {
|
||||
mutate(
|
||||
{ websiteIds: newWebsites },
|
||||
|
|
@ -42,20 +44,22 @@ export function TeamAddWebsiteForm({ teamId, onSave, onClose }) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<Form onSubmit={handleSubmit} error={error} ref={formRef}>
|
||||
<FormRow label={formatMessage(labels.websites)}>
|
||||
<Dropdown items={websites} onChange={handleAddWebsite} style={{ width: 300 }}>
|
||||
{({ id, name }) => <Item key={id}>{name}</Item>}
|
||||
</Dropdown>
|
||||
</FormRow>
|
||||
<WebsiteTags items={websites} websites={newWebsites} onClick={handleRemoveWebsite} />
|
||||
<FormButtons flex>
|
||||
<SubmitButton disabled={newWebsites && newWebsites.length === 0}>
|
||||
{formatMessage(labels.addWebsite)}
|
||||
</SubmitButton>
|
||||
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
{hasData && (
|
||||
<Form onSubmit={handleSubmit} error={error} ref={formRef}>
|
||||
<FormRow label={formatMessage(labels.websites)}>
|
||||
<Dropdown items={websites.data} onChange={handleAddWebsite} style={{ width: 300 }}>
|
||||
{({ id, name }) => <Item key={id}>{name}</Item>}
|
||||
</Dropdown>
|
||||
</FormRow>
|
||||
<WebsiteTags items={websites.data} websites={newWebsites} onClick={handleRemoveWebsite} />
|
||||
<FormButtons flex>
|
||||
<SubmitButton disabled={newWebsites && newWebsites.length === 0}>
|
||||
{formatMessage(labels.addWebsite)}
|
||||
</SubmitButton>
|
||||
<Button onClick={onClose}>{formatMessage(labels.cancel)}</Button>
|
||||
</FormButtons>
|
||||
</Form>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,13 +2,22 @@ import { Loading, useToasts } from 'react-basics';
|
|||
import TeamMembersTable from 'components/pages/settings/teams/TeamMembersTable';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import useApiFilter from 'hooks/useApiFilter';
|
||||
|
||||
export function TeamMembers({ teamId, readOnly }) {
|
||||
const { showToast } = useToasts();
|
||||
const { get, useQuery } = useApi();
|
||||
const { formatMessage, messages } = useMessages();
|
||||
const { data, isLoading, refetch } = useQuery(['teams:users', teamId], () =>
|
||||
get(`/teams/${teamId}/users`),
|
||||
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
|
||||
useApiFilter();
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, isLoading, refetch } = useQuery(
|
||||
['teams:users', teamId, filter, page, pageSize],
|
||||
() =>
|
||||
get(`/teams/${teamId}/users`, {
|
||||
filter,
|
||||
page,
|
||||
pageSize,
|
||||
}),
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
|
|
@ -22,7 +31,15 @@ export function TeamMembers({ teamId, readOnly }) {
|
|||
|
||||
return (
|
||||
<>
|
||||
<TeamMembersTable onSave={handleSave} data={data} readOnly={readOnly} />
|
||||
<TeamMembersTable
|
||||
onSave={handleSave}
|
||||
data={data}
|
||||
readOnly={readOnly}
|
||||
onFilterChange={handleFilterChange}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
filterValue={filter}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,15 @@ import { ROLES } from 'lib/constants';
|
|||
import TeamMemberRemoveButton from './TeamMemberRemoveButton';
|
||||
import SettingsTable from 'components/common/SettingsTable';
|
||||
|
||||
export function TeamMembersTable({ data = [], onSave, readOnly }) {
|
||||
export function TeamMembersTable({
|
||||
data = [],
|
||||
onSave,
|
||||
readOnly,
|
||||
filterValue,
|
||||
onFilterChange,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
|
||||
|
|
@ -16,7 +24,7 @@ export function TeamMembersTable({ data = [], onSave, readOnly }) {
|
|||
|
||||
const cellRender = (row, data, key) => {
|
||||
if (key === 'username') {
|
||||
return row?.user?.username;
|
||||
return row?.username;
|
||||
}
|
||||
if (key === 'role') {
|
||||
return formatMessage(
|
||||
|
|
@ -27,13 +35,23 @@ export function TeamMembersTable({ data = [], onSave, readOnly }) {
|
|||
};
|
||||
|
||||
return (
|
||||
<SettingsTable data={data} columns={columns} cellRender={cellRender}>
|
||||
<SettingsTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
cellRender={cellRender}
|
||||
showSearch={true}
|
||||
showPaging={true}
|
||||
onFilterChange={onFilterChange}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
filterValue={filterValue}
|
||||
>
|
||||
{row => {
|
||||
return (
|
||||
!readOnly && (
|
||||
<TeamMemberRemoveButton
|
||||
teamId={row.teamId}
|
||||
userId={row.userId}
|
||||
userId={row.id}
|
||||
disabled={user.id === row?.user?.id || row.role === ROLES.teamOwner}
|
||||
onSave={onSave}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -13,13 +13,22 @@ import TeamWebsitesTable from 'components/pages/settings/teams/TeamWebsitesTable
|
|||
import TeamAddWebsiteForm from 'components/pages/settings/teams/TeamAddWebsiteForm';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import useApiFilter from 'hooks/useApiFilter';
|
||||
|
||||
export function TeamWebsites({ teamId }) {
|
||||
const { showToast } = useToasts();
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
|
||||
useApiFilter();
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, isLoading, refetch } = useQuery(['teams:websites', teamId], () =>
|
||||
get(`/teams/${teamId}/websites`),
|
||||
const { data, isLoading, refetch } = useQuery(
|
||||
['teams:websites', teamId, filter, page, pageSize],
|
||||
() =>
|
||||
get(`/teams/${teamId}/websites`, {
|
||||
filter,
|
||||
page,
|
||||
pageSize,
|
||||
}),
|
||||
);
|
||||
const hasData = data && data.length !== 0;
|
||||
|
||||
|
|
@ -49,7 +58,17 @@ export function TeamWebsites({ teamId }) {
|
|||
return (
|
||||
<div>
|
||||
<ActionForm description={formatMessage(messages.teamWebsitesInfo)}>{addButton}</ActionForm>
|
||||
{hasData && <TeamWebsitesTable teamId={teamId} data={data} onSave={handleSave} />}
|
||||
{hasData && (
|
||||
<TeamWebsitesTable
|
||||
teamId={teamId}
|
||||
data={data}
|
||||
onSave={handleSave}
|
||||
onFilterChange={handleFilterChange}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
filterValue={filter}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,17 @@ import TeamWebsiteRemoveButton from './TeamWebsiteRemoveButton';
|
|||
import SettingsTable from 'components/common/SettingsTable';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
|
||||
export function TeamWebsitesTable({ data = [], onSave }) {
|
||||
export function TeamWebsitesTable({
|
||||
data = [],
|
||||
onSave,
|
||||
filterValue,
|
||||
onFilterChange,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { openExternal } = useConfig();
|
||||
|
||||
const { user } = useUser();
|
||||
const columns = [
|
||||
{ name: 'name', label: formatMessage(labels.name) },
|
||||
|
|
@ -17,11 +25,19 @@ export function TeamWebsitesTable({ data = [], onSave }) {
|
|||
];
|
||||
|
||||
return (
|
||||
<SettingsTable columns={columns} data={data}>
|
||||
<SettingsTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
showSearch={true}
|
||||
showPaging={true}
|
||||
onFilterChange={onFilterChange}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
filterValue={filterValue}
|
||||
>
|
||||
{row => {
|
||||
const { teamId } = row;
|
||||
const { id: websiteId, name, domain, userId } = row.website;
|
||||
const { teamUser } = row.team;
|
||||
const { id: teamId, teamUser } = row.teamWebsite[0].team;
|
||||
const { id: websiteId, name, domain, userId } = row;
|
||||
const owner = teamUser[0];
|
||||
const canRemove = user.id === userId || user.id === owner.userId;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,24 +1,37 @@
|
|||
import { useState } from 'react';
|
||||
import { Button, Icon, Modal, ModalTrigger, useToasts, Text, Flexbox } from 'react-basics';
|
||||
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||
import TeamAddForm from 'components/pages/settings/teams/TeamAddForm';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import TeamsTable from 'components/pages/settings/teams/TeamsTable';
|
||||
import Page from 'components/layout/Page';
|
||||
import Icons from 'components/icons';
|
||||
import TeamJoinForm from './TeamJoinForm';
|
||||
import Page from 'components/layout/Page';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import TeamAddForm from 'components/pages/settings/teams/TeamAddForm';
|
||||
import TeamsTable from 'components/pages/settings/teams/TeamsTable';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import useUser from 'hooks/useUser';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import { useState } from 'react';
|
||||
import { Button, Flexbox, Icon, Modal, ModalTrigger, Text, useToasts } from 'react-basics';
|
||||
import TeamJoinForm from './TeamJoinForm';
|
||||
import useApiFilter from 'hooks/useApiFilter';
|
||||
|
||||
export default function TeamsList() {
|
||||
const { user } = useUser();
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
|
||||
useApiFilter();
|
||||
const [update, setUpdate] = useState(0);
|
||||
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, isLoading, error } = useQuery(['teams', update], () => get(`/teams`));
|
||||
const hasData = data && data.length !== 0;
|
||||
const { data, isLoading, error } = useQuery(['teams', update, filter, page, pageSize], () => {
|
||||
return get(`/teams`, {
|
||||
filter,
|
||||
page,
|
||||
pageSize,
|
||||
});
|
||||
});
|
||||
|
||||
const hasData = data && data?.data.length !== 0;
|
||||
const isFiltered = filter;
|
||||
|
||||
const { showToast } = useToasts();
|
||||
|
||||
const handleSave = () => {
|
||||
|
|
@ -71,15 +84,26 @@ export default function TeamsList() {
|
|||
return (
|
||||
<Page loading={isLoading} error={error}>
|
||||
<PageHeader title={formatMessage(labels.teams)}>
|
||||
{hasData && (
|
||||
{(hasData || isFiltered) && (
|
||||
<Flexbox gap={10}>
|
||||
{joinButton}
|
||||
{createButton}
|
||||
</Flexbox>
|
||||
)}
|
||||
</PageHeader>
|
||||
{hasData && <TeamsTable data={data} onDelete={handleDelete} />}
|
||||
{!hasData && (
|
||||
|
||||
{(hasData || isFiltered) && (
|
||||
<TeamsTable
|
||||
data={data}
|
||||
onDelete={handleDelete}
|
||||
onFilterChange={handleFilterChange}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
filterValue={filter}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!hasData && !isFiltered && (
|
||||
<EmptyPlaceholder message={formatMessage(messages.noTeams)}>
|
||||
<Flexbox gap={10}>
|
||||
{joinButton}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,21 @@
|
|||
import SettingsTable from 'components/common/SettingsTable';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import useUser from 'hooks/useUser';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import Link from 'next/link';
|
||||
import { Button, Icon, Icons, Modal, ModalTrigger, Text } from 'react-basics';
|
||||
import TeamDeleteForm from './TeamDeleteForm';
|
||||
import TeamLeaveForm from './TeamLeaveForm';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import useUser from 'hooks/useUser';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import SettingsTable from 'components/common/SettingsTable';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
||||
export function TeamsTable({ data = [], onDelete }) {
|
||||
export function TeamsTable({
|
||||
data = { data: [] },
|
||||
onDelete,
|
||||
filterValue,
|
||||
onFilterChange,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
const { dir } = useLocale();
|
||||
|
|
@ -27,7 +34,17 @@ export function TeamsTable({ data = [], onDelete }) {
|
|||
};
|
||||
|
||||
return (
|
||||
<SettingsTable data={data} columns={columns} cellRender={cellRender}>
|
||||
<SettingsTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
cellRender={cellRender}
|
||||
showSearch={true}
|
||||
showPaging={true}
|
||||
onFilterChange={onFilterChange}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
filterValue={filterValue}
|
||||
>
|
||||
{row => {
|
||||
const { id, teamUser } = row;
|
||||
const owner = teamUser.find(({ role }) => role === ROLES.teamOwner);
|
||||
|
|
|
|||
|
|
@ -7,14 +7,27 @@ import UserAddButton from './UserAddButton';
|
|||
import useApi from 'hooks/useApi';
|
||||
import useUser from 'hooks/useUser';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import useApiFilter from 'hooks/useApiFilter';
|
||||
|
||||
export function UsersList() {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { user } = useUser();
|
||||
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
|
||||
useApiFilter();
|
||||
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, isLoading, error, refetch } = useQuery(['user'], () => get(`/users`), {
|
||||
enabled: !!user,
|
||||
});
|
||||
const { data, isLoading, error, refetch } = useQuery(
|
||||
['user', filter, page, pageSize],
|
||||
() =>
|
||||
get(`/users`, {
|
||||
filter,
|
||||
page,
|
||||
pageSize,
|
||||
}),
|
||||
{
|
||||
enabled: !!user,
|
||||
},
|
||||
);
|
||||
const { showToast } = useToasts();
|
||||
const hasData = data && data.length !== 0;
|
||||
|
||||
|
|
@ -33,8 +46,17 @@ export function UsersList() {
|
|||
<PageHeader title={formatMessage(labels.users)}>
|
||||
<UserAddButton onSave={handleSave} />
|
||||
</PageHeader>
|
||||
{hasData && <UsersTable data={data} onDelete={handleDelete} />}
|
||||
{!hasData && (
|
||||
{(hasData || filter) && (
|
||||
<UsersTable
|
||||
data={data}
|
||||
onDelete={handleDelete}
|
||||
onFilterChange={handleFilterChange}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
filterValue={filter}
|
||||
/>
|
||||
)}
|
||||
{!hasData && !filter && (
|
||||
<EmptyPlaceholder message={formatMessage(messages.noUsers)}>
|
||||
<UserAddButton onSave={handleSave} />
|
||||
</EmptyPlaceholder>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,14 @@ import useMessages from 'hooks/useMessages';
|
|||
import SettingsTable from 'components/common/SettingsTable';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
|
||||
export function UsersTable({ data = [], onDelete }) {
|
||||
export function UsersTable({
|
||||
data = { data: [] },
|
||||
onDelete,
|
||||
filterValue,
|
||||
onFilterChange,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { user } = useUser();
|
||||
const { dateLocale } = useLocale();
|
||||
|
|
@ -36,7 +43,17 @@ export function UsersTable({ data = [], onDelete }) {
|
|||
};
|
||||
|
||||
return (
|
||||
<SettingsTable data={data} columns={columns} cellRender={cellRender}>
|
||||
<SettingsTable
|
||||
data={data}
|
||||
columns={columns}
|
||||
cellRender={cellRender}
|
||||
showSearch={true}
|
||||
showPaging={true}
|
||||
onFilterChange={onFilterChange}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
filterValue={filterValue}
|
||||
>
|
||||
{(row, keys, rowIndex) => {
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -8,14 +8,22 @@ import useApi from 'hooks/useApi';
|
|||
import useUser from 'hooks/useUser';
|
||||
import useMessages from 'hooks/useMessages';
|
||||
import { ROLES } from 'lib/constants';
|
||||
import useApiFilter from 'hooks/useApiFilter';
|
||||
|
||||
export function WebsitesList() {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { user } = useUser();
|
||||
const { filter, page, pageSize, handleFilterChange, handlePageChange, handlePageSizeChange } =
|
||||
useApiFilter();
|
||||
const { get, useQuery } = useApi();
|
||||
const { data, isLoading, error, refetch } = useQuery(
|
||||
['websites', user?.id],
|
||||
() => get(`/users/${user?.id}/websites`),
|
||||
['websites', user?.id, filter, page, pageSize],
|
||||
() =>
|
||||
get(`/users/${user?.id}/websites`, {
|
||||
filter,
|
||||
page,
|
||||
pageSize,
|
||||
}),
|
||||
{ enabled: !!user },
|
||||
);
|
||||
const { showToast } = useToasts();
|
||||
|
|
@ -47,7 +55,15 @@ export function WebsitesList() {
|
|||
return (
|
||||
<Page loading={isLoading} error={error}>
|
||||
<PageHeader title={formatMessage(labels.websites)}>{addButton}</PageHeader>
|
||||
{hasData && <WebsitesTable data={data} />}
|
||||
{hasData && (
|
||||
<WebsitesTable
|
||||
data={data}
|
||||
onFilterChange={handleFilterChange}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
filterValue={filter}
|
||||
/>
|
||||
)}
|
||||
{!hasData && (
|
||||
<EmptyPlaceholder message={formatMessage(messages.noWebsitesConfigured)}>
|
||||
{addButton}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,13 @@ import SettingsTable from 'components/common/SettingsTable';
|
|||
import useMessages from 'hooks/useMessages';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
|
||||
export function WebsitesTable({ data = [] }) {
|
||||
export function WebsitesTable({
|
||||
data = [],
|
||||
filterValue,
|
||||
onFilterChange,
|
||||
onPageChange,
|
||||
onPageSizeChange,
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { openExternal } = useConfig();
|
||||
|
||||
|
|
@ -15,7 +21,16 @@ export function WebsitesTable({ data = [] }) {
|
|||
];
|
||||
|
||||
return (
|
||||
<SettingsTable columns={columns} data={data}>
|
||||
<SettingsTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
showSearch={true}
|
||||
showPaging={true}
|
||||
onFilterChange={onFilterChange}
|
||||
onPageChange={onPageChange}
|
||||
onPageSizeChange={onPageSizeChange}
|
||||
filterValue={filterValue}
|
||||
>
|
||||
{row => {
|
||||
const { id } = row;
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,16 @@ import WebsiteHeader from './WebsiteHeader';
|
|||
|
||||
export function WebsiteReportsPage({ websiteId }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { reports, error, isLoading, deleteReport } = useReports(websiteId);
|
||||
const {
|
||||
reports,
|
||||
error,
|
||||
isLoading,
|
||||
deleteReport,
|
||||
filter,
|
||||
handleFilterChange,
|
||||
handlePageChange,
|
||||
handlePageSizeChange,
|
||||
} = useReports(websiteId);
|
||||
|
||||
const handleDelete = async id => {
|
||||
await deleteReport(id);
|
||||
|
|
@ -26,7 +35,14 @@ export function WebsiteReportsPage({ websiteId }) {
|
|||
</Button>
|
||||
</Link>
|
||||
</Flexbox>
|
||||
<ReportsTable data={reports} onDelete={handleDelete} />
|
||||
<ReportsTable
|
||||
data={reports}
|
||||
onDelete={handleDelete}
|
||||
onFilterChange={handleFilterChange}
|
||||
onPageChange={handlePageChange}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
filterValue={filter}
|
||||
/>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue