diff --git a/src/AutoUI/index.tsx b/src/AutoUI/index.tsx index b5f5b157..79529e59 100644 --- a/src/AutoUI/index.tsx +++ b/src/AutoUI/index.tsx @@ -72,10 +72,14 @@ import { useAnalyticsContext, } from '@balena/ui-shared-components'; import type { FiltersView } from '../components/Filters'; -import { ajvFilter } from '../components/Filters/SchemaSieve'; +import { + ajvFilter, + convertFilterToHumanReadable, +} from '../components/Filters/SchemaSieve'; import type { Format } from '../components/Widget/utils'; import type { Dictionary } from '../common'; import { defaultFormats } from '../components/Widget/Formats'; + const { Box, styled } = Material; const HeaderGrid = styled(Box)(({ theme }) => ({ @@ -179,6 +183,18 @@ export const AutoUI = >({ const { t } = useTranslation(); const { state: analytics } = useAnalyticsContext(); const history = useHistory(); + // Use a flag to make sure table view event is only triggered once (without the tag + // it will be triggered whenever the data is updated) + const shouldTableViewEventBeTriggered = React.useRef(true); + const totalItems = React.useMemo( + () => + pagination && 'totalItems' in pagination + ? pagination.totalItems + : Array.isArray(data) + ? data.length + : null, + [pagination, data], + ); const modelRef = React.useRef(modelRaw); // This allows the component to work even if @@ -192,10 +208,7 @@ export const AutoUI = >({ const [filters, setFilters] = React.useState([]); const [sort, setSort] = React.useState | null>( - () => - (getFromLocalStorage(`${model.resource}__sort`) as - | TableSortOptions - | undefined) || null, + () => getFromLocalStorage(`${model.resource}__sort`) || null, ); const [internalPagination, setInternalPagination] = React.useState<{ page: number; @@ -431,6 +444,30 @@ export const AutoUI = >({ }); }; + React.useEffect(() => { + if (!lens || !shouldTableViewEventBeTriggered) { + return; + } + + const columns = properties.map((property) => [ + property.field, + property.selected, + ]); + + const amplitudeFilter = filters.map((f) => convertFilterToHumanReadable(f)); + + analytics.webTracker?.track('Resource List View', { + lens: lens.slug, + resource: model.resource, + totalItems, + filters: Object.assign({}, amplitudeFilter), + columns: Object.fromEntries(columns), + sort, + }); + + shouldTableViewEventBeTriggered.current = false; + }, [lens, model.resource, filters, sort, totalItems, properties]); + if (loading && data == null) { return ( >({ {actionData?.action?.renderer?.({ schema: actionData.schema, affectedEntries: actionData.affectedEntries, - onDone: () => setActionData(undefined), + onDone: () => { + setActionData(undefined); + }, setSelected: $setSelected, })} diff --git a/src/components/Filters/FilterDescription.tsx b/src/components/Filters/FilterDescription.tsx index af29b930..fa19d917 100644 --- a/src/components/Filters/FilterDescription.tsx +++ b/src/components/Filters/FilterDescription.tsx @@ -1,59 +1,8 @@ import type { JSONSchema7 as JSONSchema } from 'json-schema'; import * as React from 'react'; -import { Tag, type TagProps, TagItem } from 'rendition'; -import { - FULL_TEXT_SLUG, - parseFilterDescription, - type FilterDescription as SieveFilterDescription, -} from './SchemaSieve'; -import { isDateTimeFormat } from '../../DataTypes'; -import { format as dateFormat } from 'date-fns'; -import { isJSONSchema } from '../../AutoUI/schemaOps'; -import isEqual from 'lodash/isEqual'; -import { findInObject } from '../../AutoUI/utils'; - -const transformToReadableValue = ( - parsedFilterDescription: SieveFilterDescription, -): string => { - const { schema, value } = parsedFilterDescription; - if (schema && isDateTimeFormat(schema.format)) { - return dateFormat(value, 'PPPppp'); - } - const schemaEnum: JSONSchema['enum'] = findInObject(schema, 'enum'); - const schemaEnumNames: string[] | undefined = findInObject( - schema, - 'enumNames', - ); - if (schemaEnum && schemaEnumNames) { - const index = schemaEnum.findIndex((a) => isEqual(a, value)); - return (schemaEnumNames as string[])[index]; - } - - const oneOf: JSONSchema['oneOf'] = findInObject(schema, 'oneOf'); - if (oneOf) { - const selected = oneOf.find( - (o) => isJSONSchema(o) && isEqual(o.const, value), - ); - - return isJSONSchema(selected) && selected.title ? selected.title : value; - } - - if (typeof value === 'object') { - if (Object.keys(value).length > 1) { - return Object.entries(value) - .map(([key, value]) => { - const property = schema.properties?.[key]; - return isJSONSchema(property) - ? `${property.title ?? key}: ${value}` - : `${key}: ${value}`; - }) - .join(', '); - } - return Object.values(value)[0] as string; - } - - return String(value); -}; +import type { TagItem } from 'rendition'; +import { Tag, type TagProps } from 'rendition'; +import { convertFilterToHumanReadable } from './SchemaSieve'; export interface FilterDescriptionProps extends Omit { filter: JSONSchema; @@ -63,44 +12,10 @@ export const FilterDescription = ({ filter, ...props }: FilterDescriptionProps) => { - const tagProps = React.useMemo(() => { - if (filter.title === FULL_TEXT_SLUG) { - const parsedFilterDescription = parseFilterDescription(filter); - if (!parsedFilterDescription) { - return; - } - return parsedFilterDescription - ? [ - { - name: parsedFilterDescription.field, - operator: 'contains', - value: transformToReadableValue(parsedFilterDescription), - }, - ] - : undefined; - } - - return filter.anyOf - ?.map((f, index) => { - if (!isJSONSchema(f)) { - return; - } - const parsedFilterDescription = parseFilterDescription(f); - if (!parsedFilterDescription) { - return; - } - const value = transformToReadableValue(parsedFilterDescription); - return { - name: - parsedFilterDescription?.schema?.title ?? - parsedFilterDescription.field, - operator: parsedFilterDescription.operator, - value, - prefix: index > 0 ? 'or' : undefined, - }; - }) - .filter((f): f is TagItem => Boolean(f)); - }, [filter]); + const tagProps = React.useMemo( + () => convertFilterToHumanReadable(filter) as TagItem[], + [filter], + ); return tagProps ? : null; }; diff --git a/src/components/Filters/SchemaSieve.ts b/src/components/Filters/SchemaSieve.ts index 14eeb23f..4a0f3071 100644 --- a/src/components/Filters/SchemaSieve.ts +++ b/src/components/Filters/SchemaSieve.ts @@ -1,4 +1,4 @@ -import { +import type { JSONSchema7 as JSONSchema, JSONSchema7Definition as JSONSchemaDefinition, } from 'json-schema'; @@ -12,6 +12,9 @@ import ajvKeywords from 'ajv-keywords'; import addFormats from 'ajv-formats'; import pickBy from 'lodash/pickBy'; import { enqueueSnackbar } from '@balena/ui-shared-components'; +import { format as dateFormat } from 'date-fns'; +import isEqual from 'lodash/isEqual'; +import { findInObject } from '../../AutoUI/utils'; const ajv = new Ajv(); // TODO: remove the any cast as soon as we remove rendition @@ -253,3 +256,85 @@ export const parseFilterDescription = ( return; } }; + +const transformToReadableValue = ( + parsedFilterDescription: FilterDescription, +): string => { + const { schema, value } = parsedFilterDescription; + if (schema && isDateTimeFormat(schema.format)) { + return dateFormat(value, 'PPPppp'); + } + const schemaEnum: JSONSchema['enum'] = findInObject(schema, 'enum'); + const schemaEnumNames: string[] | undefined = findInObject( + schema, + 'enumNames', + ); + if (schemaEnum && schemaEnumNames) { + const index = schemaEnum.findIndex((a) => isEqual(a, value)); + return schemaEnumNames[index]; + } + + const oneOf: JSONSchema['oneOf'] = findInObject(schema, 'oneOf'); + if (oneOf) { + const selected = oneOf.find( + (o) => isJSONSchema(o) && isEqual(o.const, value), + ); + + return isJSONSchema(selected) && selected.title ? selected.title : value; + } + + if (typeof value === 'object') { + if (Object.keys(value).length > 1) { + return Object.entries(value) + .map(([key, value]) => { + const property = schema.properties?.[key]; + return isJSONSchema(property) + ? `${property.title ?? key}: ${value}` + : `${key}: ${value}`; + }) + .join(', '); + } + return Object.values(value)[0] as string; + } + + return String(value); +}; + +export const convertFilterToHumanReadable = (filter: JSONSchema) => { + if (filter.title === FULL_TEXT_SLUG) { + const parsedFilterDescription = parseFilterDescription(filter); + if (!parsedFilterDescription) { + return; + } + return parsedFilterDescription + ? [ + { + name: parsedFilterDescription.field, + operator: 'contains', + value: transformToReadableValue(parsedFilterDescription), + }, + ] + : undefined; + } + + return filter.anyOf + ?.map((f, index) => { + if (!isJSONSchema(f)) { + return; + } + const parsedFilterDescription = parseFilterDescription(f); + if (!parsedFilterDescription) { + return; + } + const value = transformToReadableValue(parsedFilterDescription); + return { + name: + parsedFilterDescription?.schema?.title ?? + parsedFilterDescription.field, + operator: parsedFilterDescription.operator, + value, + prefix: index > 0 ? 'or' : undefined, + }; + }) + .filter((f) => Boolean(f)); +};