Skip to content
This repository has been archived by the owner on Jan 16, 2025. It is now read-only.

Add a Resource List View event #145

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 45 additions & 6 deletions src/AutoUI/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => ({
Expand Down Expand Up @@ -179,6 +183,18 @@ export const AutoUI = <T extends AutoUIBaseResource<T>>({
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
Expand All @@ -192,10 +208,7 @@ export const AutoUI = <T extends AutoUIBaseResource<T>>({

const [filters, setFilters] = React.useState<JSONSchema[]>([]);
const [sort, setSort] = React.useState<TableSortOptions<T> | null>(
() =>
(getFromLocalStorage(`${model.resource}__sort`) as
| TableSortOptions<T>
| undefined) || null,
() => getFromLocalStorage(`${model.resource}__sort`) || null,
);
const [internalPagination, setInternalPagination] = React.useState<{
page: number;
Expand Down Expand Up @@ -431,6 +444,30 @@ export const AutoUI = <T extends AutoUIBaseResource<T>>({
});
};

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 (
<Spinner
Expand Down Expand Up @@ -611,7 +648,9 @@ export const AutoUI = <T extends AutoUIBaseResource<T>>({
{actionData?.action?.renderer?.({
schema: actionData.schema,
affectedEntries: actionData.affectedEntries,
onDone: () => setActionData(undefined),
onDone: () => {
setActionData(undefined);
},
setSelected: $setSelected,
})}
</Box>
Expand Down
99 changes: 7 additions & 92 deletions src/components/Filters/FilterDescription.tsx
Original file line number Diff line number Diff line change
@@ -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<TagProps, 'value'> {
filter: JSONSchema;
Expand All @@ -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<TagItem | undefined>((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 ? <Tag mt={2} multiple={tagProps} {...props} /> : null;
};
87 changes: 86 additions & 1 deletion src/components/Filters/SchemaSieve.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {
import type {
JSONSchema7 as JSONSchema,
JSONSchema7Definition as JSONSchemaDefinition,
} from 'json-schema';
Expand All @@ -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
Expand Down Expand Up @@ -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));
};