diff --git a/package.json b/package.json index 199d3424..a638de54 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/Home/index.tsx b/src/components/Home/index.tsx index 13dccd7e..7341a086 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/components/ga4/DimensionsMetricsExplorer/Compatible.tsx b/src/components/ga4/DimensionsMetricsExplorer/Compatible.tsx index 8116b345..d3a64101 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 513144bc..182fccd9 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 1cbeef95..10ae8a8e 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 ffe3a45e..ab184bf8 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 f4679b79..b68663ad 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 5be7b0c3..00000000 --- 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 31d98d21..17578213 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/", @@ -94,7 +96,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",