mirror of
https://github.com/umami-software/umami.git
synced 2026-02-05 21:27:20 +01:00
Cohorts editing.
This commit is contained in:
parent
07665f4824
commit
8c8e36c63b
26 changed files with 1066 additions and 985 deletions
|
|
@ -3,6 +3,7 @@ import { Grid, Column, TextField, Label, Select, Icon, Button, ListItem } from '
|
|||
import { useFilters, useFormat, useWebsiteValuesQuery } from '@/components/hooks';
|
||||
import { Close } from '@/components/icons';
|
||||
import { isSearchOperator } from '@/lib/params';
|
||||
import { Empty } from '@/components/common/Empty';
|
||||
|
||||
export interface FilterRecordProps {
|
||||
websiteId: string;
|
||||
|
|
@ -41,20 +42,25 @@ export function FilterRecord({
|
|||
endDate,
|
||||
});
|
||||
const isSearch = isSearchOperator(operator);
|
||||
const items = data?.filter(({ value }) => value) || [];
|
||||
|
||||
const handleSearch = value => {
|
||||
const handleSearch = (value: string) => {
|
||||
setSearch(value);
|
||||
};
|
||||
|
||||
const handleSelectOperator = value => {
|
||||
const handleSelectOperator = (value: any) => {
|
||||
onSelect?.(name, value);
|
||||
};
|
||||
|
||||
const handleSelectValue = value => {
|
||||
const handleSelectValue = (value: string) => {
|
||||
setSelected(value);
|
||||
onChange?.(name, value);
|
||||
};
|
||||
|
||||
const renderValue = () => {
|
||||
return formatValue(selected, type);
|
||||
};
|
||||
|
||||
return (
|
||||
<Column>
|
||||
<Label>{fields.find(f => f.name === name)?.label}</Label>
|
||||
|
|
@ -78,15 +84,17 @@ export function FilterRecord({
|
|||
)}
|
||||
{!isSearch && (
|
||||
<Select
|
||||
items={data}
|
||||
items={items}
|
||||
value={selected}
|
||||
onChange={handleSelectValue}
|
||||
searchValue={search}
|
||||
renderValue={renderValue}
|
||||
onSearch={handleSearch}
|
||||
isLoading={isLoading}
|
||||
listProps={{ renderEmptyState: () => <Empty /> }}
|
||||
allowSearch
|
||||
>
|
||||
{data?.map(({ value }) => {
|
||||
{items?.map(({ value }) => {
|
||||
return (
|
||||
<ListItem key={value} id={value}>
|
||||
{formatValue(value, type)}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export function useWebsiteCohortQuery(
|
|||
|
||||
return useQuery({
|
||||
queryKey: ['website:cohorts', { websiteId, cohortId, modified }],
|
||||
queryFn: () => get(`/websites/${websiteId}/cohorts/${cohortId}`),
|
||||
queryFn: () => get(`/websites/${websiteId}/segments/${cohortId}`),
|
||||
enabled: !!(websiteId && cohortId),
|
||||
placeholderData: keepPreviousData,
|
||||
...options,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useMemo } from 'react';
|
||||
import { getMinimumUnit, parseDateRange, getOffsetDateRange } from '@/lib/date';
|
||||
import { setItem } from '@/lib/storage';
|
||||
import { DATE_RANGE_CONFIG, DEFAULT_DATE_COMPARE, DEFAULT_DATE_RANGE_VALUE } from '@/lib/constants';
|
||||
|
|
@ -6,10 +7,10 @@ import { setDateRangeValue, useApp } from '@/store/app';
|
|||
import { useLocale } from './useLocale';
|
||||
import { useApi } from './useApi';
|
||||
import { useNavigation } from './useNavigation';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export interface UseDateRangeOptions {
|
||||
ignoreOffset?: boolean;
|
||||
useQueryParameter?: boolean;
|
||||
}
|
||||
|
||||
export function useDateRange(websiteId?: string, options: UseDateRangeOptions = {}) {
|
||||
|
|
@ -20,7 +21,11 @@ export function useDateRange(websiteId?: string, options: UseDateRangeOptions =
|
|||
} = useNavigation();
|
||||
const websiteConfig = useWebsites(state => state[websiteId]?.dateRange);
|
||||
const globalConfig = useApp(state => state.dateRangeValue);
|
||||
const dateValue = date || websiteConfig?.value || globalConfig || DEFAULT_DATE_RANGE_VALUE;
|
||||
const dateValue =
|
||||
(options.useQueryParameter ? date : false) ||
|
||||
websiteConfig?.value ||
|
||||
globalConfig ||
|
||||
DEFAULT_DATE_RANGE_VALUE;
|
||||
|
||||
const dateRange = useMemo(() => {
|
||||
const dateRangeObject = parseDateRange(dateValue, locale);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useIntl } from 'react-intl';
|
||||
import { messages, labels } from '@/components/messages';
|
||||
|
||||
export function useMessages(): any {
|
||||
export function useMessages() {
|
||||
const intl = useIntl();
|
||||
|
||||
const getMessage = (id: string) => {
|
||||
|
|
|
|||
|
|
@ -4,31 +4,31 @@ import { endOfYear } from 'date-fns';
|
|||
import { DatePickerForm } from '@/components/metrics/DatePickerForm';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { DateDisplay } from '@/components/common/DateDisplay';
|
||||
import { parseDateRange } from '@/lib/date';
|
||||
|
||||
export interface DateFilterProps {
|
||||
value: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
value?: string;
|
||||
onChange?: (value: string) => void;
|
||||
showAllTime?: boolean;
|
||||
renderDate?: boolean;
|
||||
placement?: string;
|
||||
}
|
||||
|
||||
export function DateFilter({
|
||||
value,
|
||||
startDate,
|
||||
endDate,
|
||||
onChange,
|
||||
showAllTime,
|
||||
renderDate,
|
||||
placement = 'bottom',
|
||||
}: DateFilterProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const [showPicker, setShowPicker] = useState(false);
|
||||
const { startDate, endDate } = parseDateRange(value) || {};
|
||||
|
||||
const options = [
|
||||
{ label: formatMessage(labels.today), value: '0day' },
|
||||
{
|
||||
label: formatMessage(labels.lastHours, { x: 24 }),
|
||||
label: formatMessage(labels.lastHours, { x: '24' }),
|
||||
value: '24hour',
|
||||
},
|
||||
{
|
||||
|
|
@ -37,7 +37,7 @@ export function DateFilter({
|
|||
divider: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.lastDays, { x: 7 }),
|
||||
label: formatMessage(labels.lastDays, { x: '7' }),
|
||||
value: '7day',
|
||||
},
|
||||
{
|
||||
|
|
@ -46,21 +46,21 @@ export function DateFilter({
|
|||
divider: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.lastDays, { x: 30 }),
|
||||
label: formatMessage(labels.lastDays, { x: '30' }),
|
||||
value: '30day',
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.lastDays, { x: 90 }),
|
||||
label: formatMessage(labels.lastDays, { x: '90' }),
|
||||
value: '90day',
|
||||
},
|
||||
{ label: formatMessage(labels.thisYear), value: '0year' },
|
||||
{
|
||||
label: formatMessage(labels.lastMonths, { x: 6 }),
|
||||
label: formatMessage(labels.lastMonths, { x: '6' }),
|
||||
value: '6month',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
label: formatMessage(labels.lastMonths, { x: 12 }),
|
||||
label: formatMessage(labels.lastMonths, { x: '12' }),
|
||||
value: '12month',
|
||||
},
|
||||
showAllTime && {
|
||||
|
|
@ -105,7 +105,7 @@ export function DateFilter({
|
|||
placeholder={formatMessage(labels.selectDate)}
|
||||
onChange={handleChange}
|
||||
renderValue={renderValue}
|
||||
popoverProps={{ placement: 'bottom' }}
|
||||
popoverProps={{ placement: placement as any }}
|
||||
>
|
||||
{options.map(({ label, value, divider }: any) => {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Key } from 'react';
|
||||
import { subMonths, endOfDay } from 'date-fns';
|
||||
import { Grid, Column, List, ListItem } from '@umami/react-zen';
|
||||
import { useFields, useMessages } from '@/components/hooks';
|
||||
import { FilterRecord } from '@/components/common/FilterRecord';
|
||||
|
|
@ -6,28 +7,23 @@ import { Empty } from '@/components/common/Empty';
|
|||
|
||||
export interface FieldFiltersProps {
|
||||
websiteId: string;
|
||||
filters: { name: string; operator: string; value: string }[];
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
onSave?: (data: any) => void;
|
||||
value?: { name: string; operator: string; value: string }[];
|
||||
exclude?: string[];
|
||||
onChange?: (data: any) => void;
|
||||
}
|
||||
|
||||
export function FieldFilters({
|
||||
websiteId,
|
||||
filters,
|
||||
startDate,
|
||||
endDate,
|
||||
onSave,
|
||||
}: FieldFiltersProps) {
|
||||
export function FieldFilters({ websiteId, value, exclude = [], onChange }: FieldFiltersProps) {
|
||||
const { formatMessage, messages } = useMessages();
|
||||
const { fields } = useFields();
|
||||
const startDate = subMonths(endOfDay(new Date()), 6);
|
||||
const endDate = endOfDay(new Date());
|
||||
|
||||
const updateFilter = (name: string, props: Record<string, any>) => {
|
||||
onSave(filters.map(filter => (filter.name === name ? { ...filter, ...props } : filter)));
|
||||
onChange(value.map(filter => (filter.name === name ? { ...filter, ...props } : filter)));
|
||||
};
|
||||
|
||||
const handleAdd = (name: Key) => {
|
||||
onSave(filters.concat({ name: name.toString(), operator: 'eq', value: '' }));
|
||||
onChange(value.concat({ name: name.toString(), operator: 'eq', value: '' }));
|
||||
};
|
||||
|
||||
const handleChange = (name: string, value: Key) => {
|
||||
|
|
@ -39,25 +35,27 @@ export function FieldFilters({
|
|||
};
|
||||
|
||||
const handleRemove = (name: string) => {
|
||||
onSave(filters.filter(filter => filter.name !== name));
|
||||
onChange(value.filter(filter => filter.name !== name));
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid columns="160px 1fr" overflow="hidden" gapY="6">
|
||||
<Column border="right" paddingRight="3">
|
||||
<List onAction={handleAdd}>
|
||||
{fields.map((field: any) => {
|
||||
const isDisabled = !!filters.find(({ name }) => name === field.name);
|
||||
return (
|
||||
<ListItem key={field.name} id={field.name} isDisabled={isDisabled}>
|
||||
{field.label}
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
{fields
|
||||
.filter(({ name }) => !exclude.includes(name))
|
||||
.map(field => {
|
||||
const isDisabled = !!value.find(({ name }) => name === field.name);
|
||||
return (
|
||||
<ListItem key={field.name} id={field.name} isDisabled={isDisabled}>
|
||||
{field.label}
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</Column>
|
||||
<Column paddingLeft="6" overflow="auto" gapY="4" height="500px" style={{ contain: 'layout' }}>
|
||||
{filters.map(filter => {
|
||||
{value.map(filter => {
|
||||
return (
|
||||
<FilterRecord
|
||||
key={filter.name}
|
||||
|
|
@ -72,7 +70,7 @@ export function FieldFilters({
|
|||
/>
|
||||
);
|
||||
})}
|
||||
{!filters.length && <Empty message={formatMessage(messages.nothingSelected)} />}
|
||||
{!value.length && <Empty message={formatMessage(messages.nothingSelected)} />}
|
||||
</Column>
|
||||
</Grid>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ export function FilterBar({ websiteId }: { websiteId: string }) {
|
|||
label={label}
|
||||
operator={operatorLabels[operator]}
|
||||
value={paramValue}
|
||||
onRemove={name => handleCloseFilter(name)}
|
||||
onRemove={(name: string) => handleCloseFilter(name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
@ -143,7 +143,7 @@ const FilterItem = ({ name, label, operator, value, onRemove }) => {
|
|||
{value}
|
||||
</Text>
|
||||
</Row>
|
||||
<Icon onClick={() => onRemove(name)} size="xs">
|
||||
<Icon onClick={() => onRemove(name)} size="xs" style={{ cursor: 'pointer' }}>
|
||||
<Close />
|
||||
</Icon>
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ export function FilterEditForm({
|
|||
<TabPanel id="fields">
|
||||
<FieldFilters
|
||||
websiteId={websiteId}
|
||||
filters={currentFilters}
|
||||
value={currentFilters}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onSave={setCurrentFilters}
|
||||
|
|
|
|||
|
|
@ -38,8 +38,8 @@ export function TeamsButton({ showText = true }: { showText?: boolean }) {
|
|||
return (
|
||||
<MenuTrigger>
|
||||
<Pressable>
|
||||
<Row width="100%" backgroundColor="2" border borderRadius>
|
||||
<SidebarItem label={label} icon={teamId ? <Users /> : <User />}>
|
||||
<Row role="button" width="100%" backgroundColor="2" border borderRadius>
|
||||
<SidebarItem role="button" label={label} icon={teamId ? <Users /> : <User />}>
|
||||
{showText && (
|
||||
<Icon rotate={90} size="sm">
|
||||
<Chevron />
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export function WebsiteDateFilter({
|
|||
allowCompare,
|
||||
}: WebsiteDateFilterProps) {
|
||||
const { dateRange } = useDateRange(websiteId);
|
||||
const { value, startDate, endDate } = dateRange;
|
||||
const { value, endDate } = dateRange;
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const {
|
||||
router,
|
||||
|
|
@ -61,8 +61,6 @@ export function WebsiteDateFilter({
|
|||
)}
|
||||
<DateFilter
|
||||
value={value}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
onChange={handleChange}
|
||||
showAllTime={showAllTime}
|
||||
renderDate={+offset !== 0}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export const labels = defineMessages({
|
|||
data: { id: 'label.data', defaultMessage: 'Data' },
|
||||
trackingCode: { id: 'label.tracking-code', defaultMessage: 'Tracking code' },
|
||||
shareUrl: { id: 'label.share-url', defaultMessage: 'Share URL' },
|
||||
action: { id: 'label.action', defaultMessage: 'Action' },
|
||||
actions: { id: 'label.actions', defaultMessage: 'Actions' },
|
||||
domain: { id: 'label.domain', defaultMessage: 'Domain' },
|
||||
websiteId: { id: 'label.website-id', defaultMessage: 'Website ID' },
|
||||
|
|
@ -357,6 +358,7 @@ export const labels = defineMessages({
|
|||
audience: { id: 'label.audience', defaultMessage: 'Audience' },
|
||||
invalidUrl: { id: 'label.invalid-url', defaultMessage: 'Invalid URL' },
|
||||
environment: { id: 'label.environment', defaultMessage: 'Environment' },
|
||||
criteria: { id: 'label.criteria', defaultMessage: 'Criteria' },
|
||||
});
|
||||
|
||||
export const messages = defineMessages({
|
||||
|
|
|
|||
|
|
@ -85,6 +85,7 @@ export function WeeklyTraffic({ websiteId }: { websiteId: string }) {
|
|||
height="16px"
|
||||
borderRadius="full"
|
||||
style={{ margin: '0 auto' }}
|
||||
role="cell"
|
||||
>
|
||||
<Row
|
||||
backgroundColor="primary"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue