From 9a7455fb1e753d1822fd98147e7cf3de55bab36e Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Thu, 3 Oct 2024 09:30:37 -0700 Subject: [PATCH 1/2] Dimensions and Metrics Explorer UX update (#2027) * Add screen_view, ad_impression, campaign_details events to EventBuilder * remove UA toggle * update the react-spinner version * remove unimplemented LineItem component * Force Typography to use span tag instead of div to avoid compilation warnings. Remove duplicate code and fix tests by renaming labels... * remove UA code and tests * fix tests * 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 | 4 +- .../DimensionsMetricsExplorer/Compatible.tsx | 196 +++++++---- .../ga4/DimensionsMetricsExplorer/Field.tsx | 149 +++------ .../ga4/DimensionsMetricsExplorer/index.tsx | 304 ++++++++++-------- .../useCompatibility.tsx | 9 +- .../useDimensionsAndMetrics.ts | 2 + .../DimensionsMetricsExplorer/useInputs.ts | 12 - src/constants.ts | 1 - 8 files changed, 344 insertions(+), 333 deletions(-) delete mode 100644 src/components/ga4/DimensionsMetricsExplorer/useInputs.ts diff --git a/package.json b/package.json index 199d34240..a638de547 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-markdown": "^9.0.1", "react-loader-spinner": "^6.1.6", "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..d3a641012 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), @@ -56,16 +59,23 @@ const StyledPaper = styled(Paper)(( } })); -const WithProperty: React.FC< - CompatibleHook & { property: PropertySummary | undefined } -> = ({ - dimensions, - metrics, - removeMetric, - removeDimension, - property, - hasFieldSelected, -}) => { +type CompatibleProps = + CompatibleHook + & { + property: PropertySummary | undefined, + allMetrics: Metric[], + allDimensions: Dimension[] +} +const WithProperty: React.FC = ({ + dimensions, + metrics, + removeMetric, + removeDimension, setDimensions, setMetrics, + property, + hasFieldSelected, incompatibleDimensions, incompatibleMetrics, + allDimensions, + allMetrics, + }) => { if (property === undefined) { @@ -73,72 +83,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..182fccd97 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'; @@ -56,68 +58,6 @@ const Root = styled('div')(( } })); -const knownLinks: [string, JSX.Element][] = [ - [ - "", - - Set up and manage conversion events - , - ], - [ - "", - - Keywords: Definition - , - ], - [ - "", - - User-ID for cross-platform analysis - , - ], - [ - "", - - Data filters - , - ], -] - -const linkifyText = ( - remainingString: string, - elements: (JSX.Element | string)[] -): [string, (JSX.Element | string)[]] => { - const firstMatch = knownLinks.reduce( - (acc, [inText], idx) => { - const { matchIndex } = acc - const currentMatchIndex = remainingString.indexOf(inText) - if (currentMatchIndex !== -1) { - if (currentMatchIndex < matchIndex || matchIndex === -1) { - return { - matchIndex: currentMatchIndex, - knownLinksIndex: idx, - } - } - } - return acc - }, - { knownLinksIndex: -1, matchIndex: -1 } - ) - if (firstMatch.matchIndex === -1) { - elements.push(remainingString) - return ["", elements] - } else { - const [inText, link] = knownLinks[firstMatch.knownLinksIndex] - const before = remainingString.substring(0, firstMatch.matchIndex) - const after = remainingString.substring( - firstMatch.matchIndex + inText.length - ) - elements.push(before) - elements.push(link) - return [after, elements] - } -} - interface FieldProps extends CompatibleHook { field: | { type: "dimension"; value: Dimension } @@ -128,7 +68,6 @@ interface FieldProps extends CompatibleHook { const Field: React.FC = props => { - const { field, account, @@ -158,27 +97,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 +126,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..10ae8a8e2 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,166 @@ 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), + })), + [categories, fieldDisplayFilter] ) - 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 = {} as any + 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", From 180a62266635f0b24c71e0895e761d4361e90198 Mon Sep 17 00:00:00 2001 From: ikuleshov Date: Thu, 3 Oct 2024 13:53:03 -0700 Subject: [PATCH 2/2] Add HaTS link to the main page (#2028) * Add screen_view, ad_impression, campaign_details events to EventBuilder * remove UA toggle * update the react-spinner version * remove unimplemented LineItem component * Force Typography to use span tag instead of div to avoid compilation warnings. Remove duplicate code and fix tests by renaming labels... * remove UA code and tests * fix tests * 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. * add HATS survey link to the main page under the "Help & Feedback" section --- src/components/Home/index.tsx | 7 +++++++ src/constants.ts | 2 ++ 2 files changed, 9 insertions(+) diff --git a/src/components/Home/index.tsx b/src/components/Home/index.tsx index 13dccd7ed..7341a0861 100644 --- a/src/components/Home/index.tsx +++ b/src/components/Home/index.tsx @@ -88,6 +88,13 @@ const IndexPage: React.FC = () => { For this site +
  • + Fill out a{" "} + + brief customer survey + {" "} + and let us know what you think! +
  • You may report bugs by{" "} diff --git a/src/constants.ts b/src/constants.ts index f93f31f3b..175782135 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -52,6 +52,8 @@ export enum Url { enhancedEcommerceDemo = "https://enhancedecommerce.appspot.com/", gaDevToolsGitHub = "https://github.com/googleanalytics/ga-dev-tools", gaDevToolsGitHubNewIssue = "https://github.com/googleanalytics/ga-dev-tools/issues/new?assignees=&labels=&template=bug_report.md&title=", + + gaDevToolsHatsSurvey = "https://forms.gle/khM6TxwTpjRfkPk69", gaDevToolsGitHubNewFeatureRequest = "https://github.com/googleanalytics/ga-dev-tools/issues/new?assignees=&labels=&template=feature_request.md&title=", gaDevsite = "http://developers.google.com/analytics", gaDevsiteHelp = "http://developers.google.com/analytics/help/",