From 8a6b0e5b5cebcaee766125a55e3fe0d9bf7c0b7a Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Thu, 26 Sep 2024 06:55:19 +0000 Subject: [PATCH] Dimensions and Metrics Explorer UX update Use the autocomplete component to search metrics/dimensions. The main list of all dimensions/metrics remains unchanged when selecting fields for compatibility check. The list of options available in Autocomplete components is updated based on fields' compatibility. Display field categories in accordion with Expand all / Collapse all options. Add option to display compatible only/incompatible only/all fields. Render field description using Markdown. --- package.json | 6 +- .../DimensionsMetricsExplorer/Compatible.tsx | 188 +++++++---- .../ga4/DimensionsMetricsExplorer/Field.tsx | 87 +++-- .../ga4/DimensionsMetricsExplorer/index.tsx | 306 ++++++++++-------- .../useCompatibility.tsx | 9 +- .../useDimensionsAndMetrics.ts | 2 + .../DimensionsMetricsExplorer/useInputs.ts | 12 - src/constants.ts | 1 - 8 files changed, 341 insertions(+), 270 deletions(-) delete mode 100644 src/components/ga4/DimensionsMetricsExplorer/useInputs.ts diff --git a/package.json b/package.json index 199d34240..ad2bfae36 100644 --- a/package.json +++ b/package.json @@ -44,21 +44,23 @@ "react-helmet": "^6.1.0", "react-icons": "^4.8.0", "react-json-view": "^1.21.3", - "react-loader-spinner": "^6.1.6", + "react-loader-spinner": "^5.3.4", + "react-markdown": "^9.0.1", "react-redux": "^8.0.5", "react-syntax-highlighter": "^15.5.0", "redux": "^4.2.1", + "remark-gfm": "^4.0.0", "tsconfig-paths-webpack-plugin": "^4.0.1", "use-debounce": "^9.0.4", "use-query-params": "^0.4.3" }, "devDependencies": { + "@reach/router": "^1.3.4", "@testing-library/jest-dom": "^6.1.2", "@testing-library/react": "^14.0.0", "@testing-library/user-event": "^13.1.8", "@types/gapi": "^0.0.39", "@types/gapi.auth2": "^0.0.54", - "@reach/router": "^1.3.4", "@types/gapi.client.analytics": "^3.0.7", "@types/gapi.client.analyticsadmin": "^1.0.0", "@types/gapi.client.analyticsdata": "^1.0.2", diff --git a/src/components/ga4/DimensionsMetricsExplorer/Compatible.tsx b/src/components/ga4/DimensionsMetricsExplorer/Compatible.tsx index 8116b345f..e83657a98 100644 --- a/src/components/ga4/DimensionsMetricsExplorer/Compatible.tsx +++ b/src/components/ga4/DimensionsMetricsExplorer/Compatible.tsx @@ -11,6 +11,9 @@ import { navigate } from "gatsby" import * as React from "react" import QueryExplorerLink from "../QueryExplorer/BasicReport/QueryExplorerLink" import { CompatibleHook } from "./useCompatibility" +import Autocomplete from '@mui/material/Autocomplete'; +import {Dimension, Metric} from '@/components/ga4/DimensionsMetricsExplorer/useDimensionsAndMetrics'; +import TextField from '@mui/material/TextField'; const PREFIX = 'Compatible'; @@ -23,9 +26,9 @@ const classes = { }; const StyledPaper = styled(Paper)(( - { - theme - } + { + theme + } ) => ({ [`&.${classes.compatible}`]: { padding: theme.spacing(2), @@ -57,15 +60,18 @@ const StyledPaper = styled(Paper)(( })); const WithProperty: React.FC< - CompatibleHook & { property: PropertySummary | undefined } + CompatibleHook & { property: PropertySummary | undefined } > = ({ - dimensions, - metrics, - removeMetric, - removeDimension, - property, - hasFieldSelected, -}) => { + dimensions, + metrics, + addDimension, + removeMetric, + removeDimension, setDimensions, setMetrics, + property, + hasFieldSelected, incompatibleDimensions, incompatibleMetrics, + allDimensions, + allMetrics, + }) => { if (property === undefined) { @@ -73,72 +79,124 @@ const WithProperty: React.FC< } return ( - <> - - As you choose dimensions & metrics (by clicking the checkbox next to - their name), they will be added here. Incompatible dimensions & metrics - will be grayed out. - -
- Dimensions: -
- {dimensions !== undefined && dimensions.length > 0 - ? dimensions.map(d => ( - navigate(`#${d.apiName}`)} - onDelete={() => removeDimension(d)} - /> - )) - : "No dimensions selected."} -
- Metrics: -
- {metrics !== undefined && metrics.length > 0 - ? metrics?.map(m => ( - removeMetric(m)} - /> - )) - : "No metrics selected."} -
-
- {hasFieldSelected && ( + <> - Use these fields in the{" "} - + As you choose dimensions & metrics, they will be added here. + Incompatible dimensions & metrics will be grayed out. - )} - +
+ Dimensions: +
+ + fullWidth + autoComplete + multiple + isOptionEqualToValue={(a, b) => a.apiName === b.apiName} + onChange={(event, value) => setDimensions(value)} + value={dimensions || []} + options={allDimensions} + getOptionDisabled={(option) => + incompatibleDimensions?.find(d => d.apiName === option.apiName) !== undefined + } + getOptionLabel={dimension => `${dimension.apiName}: ${dimension.uiName}` || ""} + renderInput={params => ( + + Select dimensions. + + } + /> + )} + renderTags={(tagValue, getTagProps) => + tagValue.map((option, index) => { + return ( + navigate(`#${option.apiName}`)} + onDelete={() => removeDimension(option)} + /> + ); + }) + } + /> +
+ Metrics: +
+ + fullWidth + autoComplete + multiple + isOptionEqualToValue={(a, b) => a.apiName === b.apiName} + onChange={(event, value) => setMetrics(value)} + value={metrics || []} + options={allMetrics} + getOptionDisabled={(option) => + incompatibleMetrics?.find(d => d.apiName === option.apiName) !== undefined + } + getOptionLabel={metric => `${metric.apiName}: ${metric.uiName}` || ""} + renderInput={params => ( + + Select metrics. + + } + /> + )} + renderTags={(tagValue, getTagProps) => + tagValue.map((option, index) => { + return ( + navigate(`#${option.apiName}`)} + onDelete={() => removeMetric(option)} + /> + ); + }) + } + /> +
+
+ {hasFieldSelected && ( + + Use these fields in the{" "} + + + )} + ) } const Compatible: React.FC< - CompatibleHook & { property: PropertySummary | undefined } + CompatibleHook & { allDimensions: Dimension[], allMetrics: Metric[], property: PropertySummary | undefined } > = props => { const { reset, property, hasFieldSelected } = props return ( - - - Compatible Fields - - - - - - {property === undefined && ( - - Pick a property above to enable this functionality. + + + Compatible Fields + + + - )} - + + {property === undefined && ( + + Pick a property above to enable this functionality. + + )} + ); } diff --git a/src/components/ga4/DimensionsMetricsExplorer/Field.tsx b/src/components/ga4/DimensionsMetricsExplorer/Field.tsx index 513144bcb..34c2b4dec 100644 --- a/src/components/ga4/DimensionsMetricsExplorer/Field.tsx +++ b/src/components/ga4/DimensionsMetricsExplorer/Field.tsx @@ -1,18 +1,20 @@ import * as React from "react" -import { styled } from '@mui/material/styles'; +import {styled} from '@mui/material/styles'; import IconLink from "@mui/icons-material/Link" import Typography from "@mui/material/Typography" import InlineCode from "@/components/InlineCode" -import { CopyIconButton } from "@/components/CopyButton" +import {CopyIconButton} from "@/components/CopyButton" import ExternalLink from "@/components/ExternalLink" -import { Dimension, Metric } from "./useDimensionsAndMetrics" -import { QueryParam } from "." -import { AccountSummary, PropertySummary } from "@/types/ga4/StreamPicker" +import {Dimension, Metric} from "./useDimensionsAndMetrics" +import {QueryParam} from "." +import {AccountSummary, PropertySummary} from "@/types/ga4/StreamPicker" import LabeledCheckbox from "@/components/LabeledCheckbox" -import { CompatibleHook } from "./useCompatibility" +import {CompatibleHook} from "./useCompatibility" +import Markdown from 'react-markdown' +import remarkGfm from 'remark-gfm' const PREFIX = 'Field'; @@ -128,7 +130,6 @@ interface FieldProps extends CompatibleHook { const Field: React.FC = props => { - const { field, account, @@ -158,27 +159,6 @@ const Field: React.FC = props => { return `${baseURL}${search}#${apiName}` }, [field, apiName, account, property]) - const withLinks = React.useMemo(() => { - let remainingText = description - let elements: (JSX.Element | string)[] = [] - let mightHaveLinks = true - while (mightHaveLinks) { - const result = linkifyText(remainingText, elements) - remainingText = result[0] - elements = result[1] - if (remainingText === "") { - mightHaveLinks = false - } - } - return ( - <> - {elements.map((e, idx) => ( - {e} - ))} - - ) - }, [description]) - const isCompatible = React.useMemo(() => { return ( incompatibleDimensions?.find(d => d.apiName === field.value.apiName) === @@ -208,29 +188,34 @@ const Field: React.FC = props => { }, [checked, addDimension, addMetric, removeDimension, removeMetric, field]) return ( - - - {property === undefined ? ( - uiName - ) : ( - - {uiName} - - )} - {apiName} - } - toCopy={link} - tooltipText={`Copy link to ${apiName}`} - /> - - {withLinks} - + <> + { + + + {property === undefined ? ( + uiName + ) : ( + + {uiName} + + )} + {apiName} + } + toCopy={link} + tooltipText={`Copy link to ${apiName}`} + /> + + {description} + + } + ); } diff --git a/src/components/ga4/DimensionsMetricsExplorer/index.tsx b/src/components/ga4/DimensionsMetricsExplorer/index.tsx index 1cbeef95f..cb210cc95 100644 --- a/src/components/ga4/DimensionsMetricsExplorer/index.tsx +++ b/src/components/ga4/DimensionsMetricsExplorer/index.tsx @@ -1,33 +1,29 @@ import * as React from "react" -import { styled } from '@mui/material/styles'; -import { useMemo } from "react" -import ExternalLink from "@/components/ExternalLink" +import {Link} from "gatsby" + +import {styled} from '@mui/material/styles'; import Typography from "@mui/material/Typography" -import TextField from "@mui/material/TextField" -import IconButton from "@mui/material/IconButton" -import Clear from "@mui/icons-material/Clear" +import MenuItem from '@mui/material/MenuItem'; +import FormControl from '@mui/material/FormControl'; +import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; +import Accordion from '@mui/material/Accordion'; +import AccordionDetails from '@mui/material/AccordionDetails'; +import AccordionSummary from '@mui/material/AccordionSummary'; +import {Button, InputLabel, Select, SelectChangeEvent} from '@mui/material'; -import { Url, StorageKey } from "@/constants" -import { useScrollTo } from "@/hooks" +import ExternalLink from "@/components/ExternalLink" +import {StorageKey, Url} from "@/constants" +import {useScrollTo} from "@/hooks" import Loadable from "@/components/Loadable" -import Info from "@/components/Info" +import ScrollToTop from "@/components/ScrollToTop" + import Field from "./Field" -import useInputs from "./useInputs" -import { - useDimensionsAndMetrics, - Successful, - Dimension, - Metric, -} from "./useDimensionsAndMetrics" +import {Dimension, Metric, Successful, useDimensionsAndMetrics,} from "./useDimensionsAndMetrics" import StreamPicker from "../StreamPicker" -import useAccountProperty, { - AccountProperty, -} from "../StreamPicker/useAccountProperty" -import { Link } from "gatsby" +import useAccountProperty, {AccountProperty} from "../StreamPicker/useAccountProperty" import useCompatibility from "./useCompatibility" import Compatible from "./Compatible" -import ScrollToTop from "@/components/ScrollToTop" const PREFIX = 'DimensionsMetricsExplorer'; @@ -64,132 +60,168 @@ const dataAPI = ( ) const RenderSuccessful: React.FC = ({ - categories, - aps, -}) => { - - const { search, setSearch } = useInputs() - const searchRegex = useMemo( - () => - search - ? new RegExp( - // Escape all "special" regex characters. We're only creating a regex - // here to make the testing code more simple. - search.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"), - "gi" - ) - : undefined, - [search] - ) + categories, + metrics, + dimensions, + aps, + }) => { - const compability = useCompatibility(aps) + type ViewMode = 'all' | 'compatible' | 'incompatible' + const [viewMode, setViewMode] = React.useState('all') - const searchFilter = React.useCallback( - (c: Dimension | Metric) => { - if (searchRegex === undefined) { - return true - } - return searchRegex.test(c.uiName!) || searchRegex.test(c.apiName!) - }, - [searchRegex] + const compatibility = useCompatibility(aps) + + useScrollTo() + const handleViewModeChange = (event: SelectChangeEvent) => { + setViewMode(event.target.value as ViewMode); + }; + + const fieldDisplayFilter = React.useCallback( + (c: Dimension | Metric) => { + const isCompatible = compatibility.incompatibleDimensions?.find(d => + d.apiName === c.apiName) === undefined && + compatibility.incompatibleMetrics?.find(d => + d.apiName === c.apiName) === undefined + return viewMode === 'all' || viewMode === 'compatible' && + isCompatible || viewMode === 'incompatible' && !isCompatible; + }, + [viewMode, + compatibility.incompatibleDimensions, + compatibility.incompatibleMetrics] ) const filteredCategories = React.useMemo( - () => - categories.map(c => ({ - ...c, - dimensions: c.dimensions.filter(searchFilter), - metrics: c.metrics.filter(searchFilter), - })), - [searchFilter, categories] + () => + categories.map(c => ({ + ...c, + dimensions: c.dimensions.filter(fieldDisplayFilter), + metrics: c.metrics.filter(fieldDisplayFilter), + })), + [viewMode, compatibility.metrics, + compatibility.dimensions, compatibility.incompatibleDimensions, + compatibility.incompatibleMetrics, categories] ) - const notAllFields = useMemo(() => { - if (searchRegex !== undefined) { - return ( - - You are only viewing a subset of the available metrics and dimensions. - - ) - } - }, [searchRegex]) + const resetAllCategoryAccordions = (expanded: boolean) => + { + const initialCategoryAccordionState = {} + categories.forEach( (x) => initialCategoryAccordionState[x.category]=expanded ) + return initialCategoryAccordionState + } - useScrollTo() + const [categoryAccordionState, + setCategoryAccordionState] = React.useState(resetAllCategoryAccordions(true)); - return ( - ( - - setSearch(e.target.value)} - InputProps={{ - endAdornment: ( - setSearch("")}> - - - ), - }} - /> - {notAllFields} - {filteredCategories.map(({ category, dimensions, metrics }) => { - if (dimensions.length === 0 && metrics.length === 0) { - return null + const handleCategoryAccordionStateChange = + (category: string) => (event: React.SyntheticEvent, isExpanded: boolean) => { + const newState = { + ...categoryAccordionState } - const baseAnchor = encodeURIComponent(category) - return ( - - - {category} - - {dimensions.length > 0 && ( - <> - - Dimensions - - {dimensions.map(dimension => ( - - ))} - - )} - {metrics.length > 0 && ( - <> - - Metrics - - {metrics.map(metric => ( - - ))} - - )} - - ) + newState[category] = isExpanded + setCategoryAccordionState(newState); + }; + + return ( + ( + + + + View mode + + + + + Dimensions & Metrics + + + + + + {filteredCategories.map(({category, dimensions, metrics}) => { + if (dimensions.length === 0 && metrics.length === 0) { + return null + } + const baseAnchor = encodeURIComponent(category) + return ( + + + } + > + + {category} + + + + {dimensions.length > 0 && ( + <> + + Dimensions + + {dimensions.map(dimension => ( + + ))} + + )} + {metrics.length > 0 && ( + <> + + Metrics + + {metrics.map(metric => ( + + ))} + + )} + + + + ) })} ) ); diff --git a/src/components/ga4/DimensionsMetricsExplorer/useCompatibility.tsx b/src/components/ga4/DimensionsMetricsExplorer/useCompatibility.tsx index ffe3a45e7..ab184bf83 100644 --- a/src/components/ga4/DimensionsMetricsExplorer/useCompatibility.tsx +++ b/src/components/ga4/DimensionsMetricsExplorer/useCompatibility.tsx @@ -6,10 +6,13 @@ import { Dimension, Metric } from "./useDimensionsAndMetrics" type CheckCompatibilityResponse = gapi.client.analyticsdata.CheckCompatibilityResponse export interface CompatibleHook { - dimensions: Dimension[] | undefined - incompatibleDimensions: Dimension[] | undefined + dimensions?: Dimension[] metrics: Metric[] | undefined + incompatibleDimensions: Dimension[] | undefined incompatibleMetrics: Metric[] | undefined + setDimensions: (value: (((prevState: (Dimension[] | undefined)) => (Dimension[] | undefined)) | Dimension[] | undefined)) => void + setMetrics: (value: (((prevState: (Metric[] | undefined)) => (Metric[] | undefined)) | Metric[] | undefined)) => void + addDimension: (d: Dimension) => void removeDimension: (d: Dimension) => void addMetric: (m: Metric) => void @@ -95,6 +98,8 @@ const useCompatibility = (ap: AccountProperty): CompatibleHook => { return { dimensions, metrics, + setDimensions, + setMetrics, addDimension, removeDimension, addMetric, diff --git a/src/components/ga4/DimensionsMetricsExplorer/useDimensionsAndMetrics.ts b/src/components/ga4/DimensionsMetricsExplorer/useDimensionsAndMetrics.ts index f4679b798..b68663adc 100644 --- a/src/components/ga4/DimensionsMetricsExplorer/useDimensionsAndMetrics.ts +++ b/src/components/ga4/DimensionsMetricsExplorer/useDimensionsAndMetrics.ts @@ -10,6 +10,8 @@ import { AccountProperty } from "../StreamPicker/useAccountProperty" export type Dimension = gapi.client.analyticsdata.DimensionMetadata export type Metric = gapi.client.analyticsdata.MetricMetadata +export type DimensionOrMetric = Dimension | Metric + export type Successful = { dimensions: Dimension[] metrics: Metric[] diff --git a/src/components/ga4/DimensionsMetricsExplorer/useInputs.ts b/src/components/ga4/DimensionsMetricsExplorer/useInputs.ts deleted file mode 100644 index 5be7b0c3d..000000000 --- a/src/components/ga4/DimensionsMetricsExplorer/useInputs.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { usePersistentString } from "@/hooks" -import { StorageKey } from "@/constants" - -const useInputs = () => { - const [search, setSearch] = usePersistentString( - StorageKey.ga4DimensionsMetricsSearch - ) - - return { search, setSearch } -} - -export default useInputs diff --git a/src/constants.ts b/src/constants.ts index 31d98d214..f93f31f3b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -94,7 +94,6 @@ export enum StorageKey { // GA4 Dimensions and metrics explorer ga4DimensionsMetrics = "/ga4/dimensions-metrics/", ga4DimensionsMetricsExplorerAPS = "/ga4/dimensions-metrics-explorer/aps", - ga4DimensionsMetricsSearch = "/ga4/dimensions-metrics-explorer/search", ga4DimensionsMetricsFields = "/ga4/dimensions-metrics-explorer/fields", ga4DimensionsMetricsAccountSummaries = "/ga4/dimensions-metrics-explorer/account-summaries", ga4DimensionsMetricsSelectedAccount = "/ga4/dimensions-metrics-explorer/selected-account",