diff --git a/src/app/(main)/reports/[reportId]/FieldFilterForm.module.css b/src/app/(main)/reports/[reportId]/FieldFilterForm.module.css index 826cc949..be7bb954 100644 --- a/src/app/(main)/reports/[reportId]/FieldFilterForm.module.css +++ b/src/app/(main)/reports/[reportId]/FieldFilterForm.module.css @@ -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; } diff --git a/src/app/(main)/reports/[reportId]/FieldFilterForm.tsx b/src/app/(main)/reports/[reportId]/FieldFilterForm.tsx index 932f302b..e38f3d65 100644 --- a/src/app/(main)/reports/[reportId]/FieldFilterForm.tsx +++ b/src/app/(main)/reports/[reportId]/FieldFilterForm.tsx @@ -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 (
@@ -77,21 +93,24 @@ export default function FieldFilterForm({ }} )} - setValue(key)} - allowSearch={true} - onSearch={setSearch} - > - {(value: string) => { - return {formattedValues[value]}; - }} - + + setValue(e.target.value)} + /> + {showMenu && ( + + {filteredValues.length > 0 && ( + + {filteredValues.map(value => { + return {safeDecodeURIComponent(value)}; + })} + + )} + + )} + + + + !fields.find(f => f.name === name))} + onSelect={handleAdd} + showType={false} + /> + + + + ); + }; + + return ( + }> + + {fields.map(({ name }) => { + return ( + handleRemove(name)}> + {fieldOptions.find(f => f.name === name)?.label} + + ); + })} + + + ); +} + +export default InsightsFieldParameters; diff --git a/src/app/(main)/reports/insights/InsightsFilterParameters.module.css b/src/app/(main)/reports/insights/InsightsFilterParameters.module.css new file mode 100644 index 00000000..8b1795d2 --- /dev/null +++ b/src/app/(main)/reports/insights/InsightsFilterParameters.module.css @@ -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; +} diff --git a/src/app/(main)/reports/insights/InsightsFilterParameters.tsx b/src/app/(main)/reports/insights/InsightsFilterParameters.tsx new file mode 100644 index 00000000..47554469 --- /dev/null +++ b/src/app/(main)/reports/insights/InsightsFilterParameters.tsx @@ -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 ( + + + + + !filters.find(f => f.name === name))} + onSelect={handleAdd} + /> + + + + ); + }; + + return ( + }> + + {filters.map(({ name, filter, value }) => { + const label = fieldOptions.find(f => f.name === name)?.label; + const isEquals = [OPERATORS.equals, OPERATORS.notEquals].includes(filter); + return ( + handleRemove(name)}> +
+
{label}
+
{filterLabels[filter]}
+
+ {safeDecodeURIComponent(isEquals ? formatValue(value, name) : value)} +
+
+
+ ); + })} +
+
+ ); +} + +export default InsightsFilterParameters; diff --git a/src/app/(main)/reports/insights/InsightsParameters.module.css b/src/app/(main)/reports/insights/InsightsParameters.module.css deleted file mode 100644 index ba089b9a..00000000 --- a/src/app/(main)/reports/insights/InsightsParameters.module.css +++ /dev/null @@ -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; -} diff --git a/src/app/(main)/reports/insights/InsightsParameters.tsx b/src/app/(main)/reports/insights/InsightsParameters.tsx index a3e4e72f..22c57ff0 100644 --- a/src/app/(main)/reports/insights/InsightsParameters.tsx +++ b/src/app/(main)/reports/insights/InsightsParameters.tsx @@ -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 ( - - - - - - - - - {id === 'fields' && ( - - )} - {id === 'filters' && ( - - )} - - - - ); - }; - return ( - {parametersSelected && - parameterGroups.map(({ id, label }) => { - return ( - }> - handleRemove(id, index)}> - {({ name, filter, value }) => { - return ( -
- {id === 'fields' && ( - <> -
{fieldOptions.find(f => f.name === name)?.label}
- - )} - {id === 'filters' && ( - <> -
{fieldOptions.find(f => f.name === name)?.label}
-
{filterLabels[filter]}
-
{formatValue(value, name)}
- - )} -
- ); - }} -
-
- ); - })} + {parametersSelected && } + {parametersSelected && } {formatMessage(labels.runQuery)} diff --git a/src/components/common/Breadcrumb.module.css b/src/components/common/Breadcrumb.module.css index 73e72bc6..81e7524f 100644 --- a/src/components/common/Breadcrumb.module.css +++ b/src/components/common/Breadcrumb.module.css @@ -1,5 +1,5 @@ .bar { - font-size: 14px; + font-size: 12px; font-weight: 700; text-transform: uppercase; color: var(--base600); diff --git a/src/components/hooks/useFilters.ts b/src/components/hooks/useFilters.ts index e1a9a885..714a8c97 100644 --- a/src/components/hooks/useFilters.ts +++ b/src/components/hooks/useFilters.ts @@ -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: [ diff --git a/src/lib/clickhouse.ts b/src/lib/clickhouse.ts index 6206f2f0..4be15db2 100644 --- a/src/lib/clickhouse.ts +++ b/src/lib/clickhouse.ts @@ -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 ''; } diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index 4230cea6..9352eaea 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -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 ''; }