Update insights report parameters. Added contains logic.

This commit is contained in:
Mike Cao 2024-03-22 23:33:45 -07:00
parent d59477deb5
commit 5daad2726e
15 changed files with 280 additions and 190 deletions

View File

@ -1,7 +1,7 @@
.popup {
display: flex;
max-width: 300px;
max-height: 400px;
max-height: 210px;
overflow-x: hidden;
}
@ -19,6 +19,6 @@
min-width: 180px;
}
.menu {
min-width: 200px;
.text {
min-width: 180px;
}

View File

@ -1,6 +1,19 @@
import { useState, useMemo } from 'react';
import { Form, FormRow, Item, Flexbox, Dropdown, Button } from 'react-basics';
import {
Form,
FormRow,
Item,
Flexbox,
Dropdown,
Button,
TextField,
Menu,
Popup,
PopupTrigger,
} from 'react-basics';
import { useMessages, useFilters, useFormat, useLocale } from 'components/hooks';
import { safeDecodeURIComponent } from 'next-basics';
import { OPERATORS } from 'lib/constants';
import styles from './FieldFilterForm.module.css';
export interface FieldFilterFormProps {
@ -22,12 +35,11 @@ export default function FieldFilterForm({
}: FieldFilterFormProps) {
const { formatMessage, labels } = useMessages();
const [filter, setFilter] = useState('eq');
const [value, setValue] = useState();
const [value, setValue] = useState('');
const { getFilters } = useFilters();
const { formatValue } = useFormat();
const { locale } = useLocale();
const filters = getFilters(type);
const [search, setSearch] = useState('');
const formattedValues = useMemo(() => {
const formatted = {};
@ -45,21 +57,25 @@ export default function FieldFilterForm({
}, [formatValue, locale, name, values]);
const filteredValues = useMemo(() => {
return search ? values.filter(n => n.includes(search)) : values;
}, [search, formattedValues]);
return value ? values.filter(n => n.includes(value)) : values;
}, [value, formattedValues]);
const renderFilterValue = value => {
return filters.find(f => f.value === value)?.label;
};
const renderValue = value => {
return formattedValues[value];
};
const handleAdd = () => {
onSelect({ name, type, filter, value });
};
const handleMenuSelect = value => {
setValue(value);
};
const showMenu =
[OPERATORS.equals, OPERATORS.notEquals].includes(filter as any) &&
!(filteredValues.length === 1 && filteredValues[0] === value);
return (
<Form>
<FormRow label={label} className={styles.filter}>
@ -77,21 +93,24 @@ export default function FieldFilterForm({
}}
</Dropdown>
)}
<Dropdown
className={styles.dropdown}
popupProps={{ className: styles.popup }}
menuProps={{ className: styles.menu }}
items={filteredValues}
value={value}
renderValue={renderValue}
onChange={(key: any) => setValue(key)}
allowSearch={true}
onSearch={setSearch}
>
{(value: string) => {
return <Item key={value}>{formattedValues[value]}</Item>;
}}
</Dropdown>
<PopupTrigger>
<TextField
className={styles.text}
value={decodeURIComponent(value)}
onChange={e => setValue(e.target.value)}
/>
{showMenu && (
<Popup className={styles.popup} alignment="end">
{filteredValues.length > 0 && (
<Menu variant="popup" onSelect={handleMenuSelect}>
{filteredValues.map(value => {
return <Item key={value}>{safeDecodeURIComponent(value)}</Item>;
})}
</Menu>
)}
</Popup>
)}
</PopupTrigger>
</Flexbox>
<Button variant="primary" onClick={handleAdd} disabled={!filter || !value}>
{formatMessage(labels.add)}

View File

@ -24,14 +24,14 @@ function useValues(websiteId: string, type: string) {
export interface FilterSelectFormProps {
websiteId: string;
items: any[];
fields: any[];
onSelect?: (key: any) => void;
allowFilterSelect?: boolean;
}
export default function FilterSelectForm({
websiteId,
items,
fields,
onSelect,
allowFilterSelect,
}: FilterSelectFormProps) {
@ -39,7 +39,7 @@ export default function FilterSelectForm({
const { data, isLoading } = useValues(websiteId, field?.name);
if (!field) {
return <FieldSelectForm fields={items} onSelect={setField} showType={false} />;
return <FieldSelectForm fields={fields} onSelect={setField} showType={false} />;
}
if (isLoading) {

View File

@ -13,9 +13,4 @@
border: 1px solid var(--base400);
border-radius: var(--border-radius);
box-shadow: 1px 1px 1px var(--base400);
gap: 10px;
}
.icon {
align-self: center;
}

View File

@ -1,40 +1,36 @@
import { ReactNode } from 'react';
import { Icon, TooltipPopup } from 'react-basics';
import { Icon } from 'react-basics';
import Icons from 'components/icons';
import Empty from 'components/common/Empty';
import { useMessages } from 'components/hooks';
import styles from './ParameterList.module.css';
export interface ParameterListProps {
items: any[];
children?: ReactNode | ((item: any) => ReactNode);
onRemove: (index: number, e: any) => void;
children?: ReactNode;
}
export function ParameterList({ items = [], children, onRemove }: ParameterListProps) {
export function ParameterList({ children }: ParameterListProps) {
const { formatMessage, labels } = useMessages();
return (
<div className={styles.list}>
{!items.length && <Empty message={formatMessage(labels.none)} />}
{items.map((item, index) => {
return (
<div key={index} className={styles.item}>
{typeof children === 'function' ? children(item) : item}
<TooltipPopup
className={styles.icon}
label={formatMessage(labels.remove)}
position="right"
>
<Icon onClick={onRemove.bind(null, index)}>
<Icons.Close />
</Icon>
</TooltipPopup>
</div>
);
})}
{!children && <Empty message={formatMessage(labels.none)} />}
{children}
</div>
);
}
const Item = ({ children, onRemove }: { children?: ReactNode; onRemove?: () => void }) => {
return (
<div className={styles.item}>
{children}
<Icon onClick={onRemove}>
<Icons.Close />
</Icon>
</div>
);
};
ParameterList.Item = Item;
export default ParameterList;

View File

@ -2,4 +2,5 @@
display: grid;
grid-template-rows: max-content 1fr;
grid-template-columns: max-content 1fr;
margin-bottom: 60px;
}

View File

@ -0,0 +1,75 @@
import { useMessages } from 'components/hooks';
import Icons from 'components/icons';
import { useContext } from 'react';
import { Button, FormRow, Icon, Popup, PopupTrigger } from 'react-basics';
import FieldSelectForm from '../[reportId]/FieldSelectForm';
import ParameterList from '../[reportId]/ParameterList';
import PopupForm from '../[reportId]/PopupForm';
import { ReportContext } from '../[reportId]/Report';
export function InsightsFieldParameters() {
const { report, updateReport } = useContext(ReportContext);
const { formatMessage, labels } = useMessages();
const { parameters } = report || {};
const { fields } = parameters || {};
const fieldOptions = [
{ name: 'url', type: 'string', label: formatMessage(labels.url) },
{ name: 'title', type: 'string', label: formatMessage(labels.pageTitle) },
{ name: 'referrer', type: 'string', label: formatMessage(labels.referrer) },
{ name: 'query', type: 'string', label: formatMessage(labels.query) },
{ name: 'browser', type: 'string', label: formatMessage(labels.browser) },
{ name: 'os', type: 'string', label: formatMessage(labels.os) },
{ name: 'device', type: 'string', label: formatMessage(labels.device) },
{ name: 'country', type: 'string', label: formatMessage(labels.country) },
{ name: 'region', type: 'string', label: formatMessage(labels.region) },
{ name: 'city', type: 'string', label: formatMessage(labels.city) },
];
const handleAdd = (value: { name: any }) => {
if (!fields.find(({ name }) => name === value.name)) {
updateReport({ parameters: { fields: fields.concat(value) } });
}
};
const handleRemove = (name: string) => {
updateReport({ parameters: { fields: fields.filter(f => f.name !== name) } });
};
const AddButton = () => {
return (
<PopupTrigger>
<Button size="sm">
<Icon>
<Icons.Plus />
</Icon>
</Button>
<Popup position="bottom" alignment="start">
<PopupForm>
<FieldSelectForm
fields={fieldOptions.filter(({ name }) => !fields.find(f => f.name === name))}
onSelect={handleAdd}
showType={false}
/>
</PopupForm>
</Popup>
</PopupTrigger>
);
};
return (
<FormRow label={formatMessage(labels.fields)} action={<AddButton />}>
<ParameterList>
{fields.map(({ name }) => {
return (
<ParameterList.Item key={name} onRemove={() => handleRemove(name)}>
{fieldOptions.find(f => f.name === name)?.label}
</ParameterList.Item>
);
})}
</ParameterList>
</FormRow>
);
}
export default InsightsFieldParameters;

View File

@ -0,0 +1,36 @@
.item {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 10px;
overflow: hidden;
}
.label {
color: var(--base800);
border: 1px solid var(--base300);
font-weight: 900;
padding: 2px 8px;
border-radius: 5px;
white-space: nowrap;
}
.filter {
color: var(--blue900);
background-color: var(--blue100);
font-size: 12px;
font-weight: 900;
padding: 2px 8px;
border-radius: 5px;
text-transform: uppercase;
white-space: nowrap;
}
.value {
color: var(--base900);
background-color: var(--base100);
font-weight: 900;
padding: 2px 8px;
border-radius: 5px;
white-space: nowrap;
}

View File

@ -0,0 +1,88 @@
import { useMessages, useFormat, useFilters } from 'components/hooks';
import Icons from 'components/icons';
import { useContext } from 'react';
import { Button, FormRow, Icon, Popup, PopupTrigger } from 'react-basics';
import FilterSelectForm from '../[reportId]/FilterSelectForm';
import ParameterList from '../[reportId]/ParameterList';
import PopupForm from '../[reportId]/PopupForm';
import { ReportContext } from '../[reportId]/Report';
import styles from './InsightsFilterParameters.module.css';
import { safeDecodeURIComponent } from 'next-basics';
import { OPERATORS } from 'lib/constants';
export function InsightsFilterParameters() {
const { report, updateReport } = useContext(ReportContext);
const { formatMessage, labels } = useMessages();
const { formatValue } = useFormat();
const { filterLabels } = useFilters();
const { parameters } = report || {};
const { websiteId, filters } = parameters || {};
const fieldOptions = [
{ name: 'url', type: 'string', label: formatMessage(labels.url) },
{ name: 'title', type: 'string', label: formatMessage(labels.pageTitle) },
{ name: 'referrer', type: 'string', label: formatMessage(labels.referrer) },
{ name: 'query', type: 'string', label: formatMessage(labels.query) },
{ name: 'browser', type: 'string', label: formatMessage(labels.browser) },
{ name: 'os', type: 'string', label: formatMessage(labels.os) },
{ name: 'device', type: 'string', label: formatMessage(labels.device) },
{ name: 'country', type: 'string', label: formatMessage(labels.country) },
{ name: 'region', type: 'string', label: formatMessage(labels.region) },
{ name: 'city', type: 'string', label: formatMessage(labels.city) },
];
const handleAdd = (value: { name: any }) => {
if (!filters.find(({ name }) => name === value.name)) {
updateReport({ parameters: { filters: filters.concat(value) } });
}
};
const handleRemove = (name: string) => {
updateReport({ parameters: { filters: filters.filter(f => f.name !== name) } });
};
const AddButton = () => {
return (
<PopupTrigger>
<Button size="sm">
<Icon>
<Icons.Plus />
</Icon>
</Button>
<Popup position="bottom" alignment="start">
<PopupForm>
<FilterSelectForm
websiteId={websiteId}
fields={fieldOptions.filter(({ name }) => !filters.find(f => f.name === name))}
onSelect={handleAdd}
/>
</PopupForm>
</Popup>
</PopupTrigger>
);
};
return (
<FormRow label={formatMessage(labels.filters)} action={<AddButton />}>
<ParameterList>
{filters.map(({ name, filter, value }) => {
const label = fieldOptions.find(f => f.name === name)?.label;
const isEquals = [OPERATORS.equals, OPERATORS.notEquals].includes(filter);
return (
<ParameterList.Item key={name} onRemove={() => handleRemove(name)}>
<div className={styles.item}>
<div className={styles.label}>{label}</div>
<div className={styles.filter}>{filterLabels[filter]}</div>
<div className={styles.value}>
{safeDecodeURIComponent(isEquals ? formatValue(value, name) : value)}
</div>
</div>
</ParameterList.Item>
);
})}
</ParameterList>
</FormRow>
);
}
export default InsightsFilterParameters;

View File

@ -1,17 +0,0 @@
.parameter {
display: flex;
gap: 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
min-width: 0;
}
.op {
font-weight: bold;
}
.popup {
margin-top: -10px;
margin-inline-start: 30px;
}

View File

@ -1,136 +1,29 @@
import { useFilters, useFormat, useMessages } from 'components/hooks';
import Icons from 'components/icons';
import { useMessages } from 'components/hooks';
import { useContext } from 'react';
import {
Form,
FormButtons,
FormRow,
Icon,
Popup,
PopupTrigger,
SubmitButton,
TooltipPopup,
} from 'react-basics';
import { Form, FormButtons, SubmitButton } from 'react-basics';
import BaseParameters from '../[reportId]/BaseParameters';
import FieldSelectForm from '../[reportId]/FieldSelectForm';
import FilterSelectForm from '../[reportId]/FilterSelectForm';
import ParameterList from '../[reportId]/ParameterList';
import PopupForm from '../[reportId]/PopupForm';
import { ReportContext } from '../[reportId]/Report';
import styles from './InsightsParameters.module.css';
import InsightsFieldParameters from './InsightsFieldParameters';
import InsightsFilterParameters from './InsightsFilterParameters';
export function InsightsParameters() {
const { report, runReport, updateReport, isRunning } = useContext(ReportContext);
const { report, runReport, isRunning } = useContext(ReportContext);
const { formatMessage, labels } = useMessages();
const { formatValue } = useFormat();
const { filterLabels } = useFilters();
const { id, parameters } = report || {};
const { websiteId, dateRange, fields, filters } = parameters || {};
const { startDate, endDate } = dateRange || {};
const parametersSelected = websiteId && startDate && endDate;
const queryEnabled = websiteId && dateRange && (fields?.length || filters?.length);
const fieldOptions = [
{ name: 'url', type: 'string', label: formatMessage(labels.url) },
{ name: 'title', type: 'string', label: formatMessage(labels.pageTitle) },
{ name: 'referrer', type: 'string', label: formatMessage(labels.referrer) },
{ name: 'query', type: 'string', label: formatMessage(labels.query) },
{ name: 'browser', type: 'string', label: formatMessage(labels.browser) },
{ name: 'os', type: 'string', label: formatMessage(labels.os) },
{ name: 'device', type: 'string', label: formatMessage(labels.device) },
{ name: 'country', type: 'string', label: formatMessage(labels.country) },
{ name: 'region', type: 'string', label: formatMessage(labels.region) },
{ name: 'city', type: 'string', label: formatMessage(labels.city) },
];
const parameterGroups = [
{ id: 'fields', label: formatMessage(labels.fields) },
{ id: 'filters', label: formatMessage(labels.filters) },
];
const parameterData = {
fields,
filters,
};
const handleSubmit = (values: any) => {
runReport(values);
};
const handleAdd = (id: string | number, value: { name: any }) => {
const data = parameterData[id];
if (!data.find(({ name }) => name === value.name)) {
updateReport({ parameters: { [id]: data.concat(value) } });
}
};
const handleRemove = (id: string, index: number) => {
const data = [...parameterData[id]];
data.splice(index, 1);
updateReport({ parameters: { [id]: data } });
};
const AddButton = ({ id, onAdd }) => {
return (
<PopupTrigger>
<TooltipPopup label={formatMessage(labels.add)} position="top">
<Icon>
<Icons.Plus />
</Icon>
</TooltipPopup>
<Popup position="bottom" alignment="start" className={styles.popup}>
<PopupForm>
{id === 'fields' && (
<FieldSelectForm
fields={fieldOptions}
onSelect={onAdd.bind(null, id)}
showType={false}
/>
)}
{id === 'filters' && (
<FilterSelectForm
websiteId={websiteId}
items={fieldOptions}
onSelect={onAdd.bind(null, id)}
/>
)}
</PopupForm>
</Popup>
</PopupTrigger>
);
};
return (
<Form values={parameters} onSubmit={handleSubmit}>
<BaseParameters allowWebsiteSelect={!id} />
{parametersSelected &&
parameterGroups.map(({ id, label }) => {
return (
<FormRow key={label} label={label} action={<AddButton id={id} onAdd={handleAdd} />}>
<ParameterList items={parameterData[id]} onRemove={index => handleRemove(id, index)}>
{({ name, filter, value }) => {
return (
<div className={styles.parameter}>
{id === 'fields' && (
<>
<div>{fieldOptions.find(f => f.name === name)?.label}</div>
</>
)}
{id === 'filters' && (
<>
<div>{fieldOptions.find(f => f.name === name)?.label}</div>
<div className={styles.op}>{filterLabels[filter]}</div>
<div>{formatValue(value, name)}</div>
</>
)}
</div>
);
}}
</ParameterList>
</FormRow>
);
})}
{parametersSelected && <InsightsFieldParameters />}
{parametersSelected && <InsightsFilterParameters />}
<FormButtons>
<SubmitButton variant="primary" disabled={!queryEnabled} isLoading={isRunning}>
{formatMessage(labels.runQuery)}

View File

@ -1,5 +1,5 @@
.bar {
font-size: 14px;
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
color: var(--base600);

View File

@ -22,7 +22,7 @@ export function useFilters() {
};
const typeFilters = {
string: [OPERATORS.equals, OPERATORS.notEquals],
string: [OPERATORS.equals, OPERATORS.notEquals, OPERATORS.contains, OPERATORS.doesNotContain],
array: [OPERATORS.contains, OPERATORS.doesNotContain],
boolean: [OPERATORS.true, OPERATORS.false],
number: [

View File

@ -69,6 +69,8 @@ function mapFilter(column: string, filter: string, name: string, type: string =
return `${column} != {${name}:${type}}`;
case OPERATORS.contains:
return `positionCaseInsensitive(${column}, {${name}:${type}}) > 0`;
case OPERATORS.doesNotContain:
return `positionCaseInsensitive(${column}, {${name}:${type}}) = 0`;
default:
return '';
}

View File

@ -100,6 +100,8 @@ function mapFilter(column: string, filter: string, name: string, type = 'varchar
return `${column} != {{${name}::${type}}}`;
case OPERATORS.contains:
return `${column} like {{${name}::${type}}}`;
case OPERATORS.doesNotContain:
return `${column} not like {{${name}::${type}}}`;
default:
return '';
}