From 51df752cbaa93905c204217bf3dd84eea3262bc9 Mon Sep 17 00:00:00 2001 From: Matt Hamrick Date: Fri, 27 Aug 2021 13:03:31 -0700 Subject: [PATCH 01/10] update mui versions. --- package.json | 6 +-- yarn.lock | 146 +++++++++++++++++++++++++-------------------------- 2 files changed, 75 insertions(+), 77 deletions(-) diff --git a/package.json b/package.json index 973a54b02..81247ae25 100644 --- a/package.json +++ b/package.json @@ -6,11 +6,11 @@ }, "license": "Apache-2.0", "dependencies": { - "@material-ui/core": "^4.11.3", - "@material-ui/data-grid": "^4.0.0-alpha.25", + "@material-ui/core": "^4.12.3", + "@material-ui/data-grid": "^4.0.0-alpha.37", "@material-ui/icons": "^4.11.2", "@material-ui/lab": "^4.0.0-alpha.60", - "@material-ui/styles": "^4.11.3", + "@material-ui/styles": "^4.11.4", "@reach/router": "^1.3.4", "classnames": "^2.2.6", "copy-to-clipboard": "^3.3.1", diff --git a/yarn.lock b/yarn.lock index a44852813..8c84e42de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1022,7 +1022,7 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.10.0", "@babel/runtime@^7.10.2", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.5", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.14.0" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.14.0.tgz#46794bc20b612c5f75e62dd071e24dfd95f1cbe6" integrity sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA== @@ -1036,7 +1036,7 @@ dependencies: regenerator-runtime "^0.13.4" -"@babel/runtime@^7.14.8": +"@babel/runtime@^7.14.8", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7": version "7.15.3" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.15.3.tgz#2e1c2880ca118e5b2f9988322bd8a7656a32502b" integrity sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA== @@ -2120,14 +2120,14 @@ resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" integrity sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg== -"@material-ui/core@^4.11.3": - version "4.11.4" - resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.11.4.tgz#4fb9fe5dec5dcf780b687e3a40cff78b2b9640a4" - integrity sha512-oqb+lJ2Dl9HXI9orc6/aN8ZIAMkeThufA5iZELf2LQeBn2NtjVilF5D2w7e9RpntAzDb4jK5DsVhkfOvFY/8fg== +"@material-ui/core@^4.12.3": + version "4.12.3" + resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-4.12.3.tgz#80d665caf0f1f034e52355c5450c0e38b099d3ca" + integrity sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw== dependencies: "@babel/runtime" "^7.4.4" "@material-ui/styles" "^4.11.4" - "@material-ui/system" "^4.11.3" + "@material-ui/system" "^4.12.1" "@material-ui/types" "5.1.0" "@material-ui/utils" "^4.11.2" "@types/react-transition-group" "^4.2.0" @@ -2138,12 +2138,13 @@ react-is "^16.8.0 || ^17.0.0" react-transition-group "^4.4.0" -"@material-ui/data-grid@^4.0.0-alpha.25": - version "4.0.0-alpha.27" - resolved "https://registry.yarnpkg.com/@material-ui/data-grid/-/data-grid-4.0.0-alpha.27.tgz#8be77550e967700215f2ba3f95f0d528ee2241f7" - integrity sha512-nZFgtryrhU6c4Q8hl6KhcJMQ4gxYTpFybMyKWPBBJ6Izsgc5RbDrdB3zGy9MEOTbQP6e+RZT1bL/Es/efz9CRw== +"@material-ui/data-grid@^4.0.0-alpha.37": + version "4.0.0-alpha.37" + resolved "https://registry.yarnpkg.com/@material-ui/data-grid/-/data-grid-4.0.0-alpha.37.tgz#89d907c4e94e6a0db4e89e4f59160f7811546ca2" + integrity sha512-3T2AG31aad/lWLMLwn1XUP4mUf3H9YZES17dGuYByzkRLCXbBZHBTPEnCctWukajzwm+v0KGg3QpwitGoiDAjA== dependencies: "@material-ui/utils" "^5.0.0-alpha.14" + clsx "^1.0.4" prop-types "^15.7.2" reselect "^4.0.0" @@ -2165,7 +2166,7 @@ prop-types "^15.7.2" react-is "^16.8.0 || ^17.0.0" -"@material-ui/styles@^4.11.3", "@material-ui/styles@^4.11.4": +"@material-ui/styles@^4.11.4": version "4.11.4" resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.11.4.tgz#eb9dfccfcc2d208243d986457dff025497afa00d" integrity sha512-KNTIZcnj/zprG5LW0Sao7zw+yG3O35pviHzejMdcSGCdWbiO8qzRgOYL8JAxAsWBKOKYwVZxXtHWaB5T2Kvxew== @@ -2187,10 +2188,10 @@ jss-plugin-vendor-prefixer "^10.5.1" prop-types "^15.7.2" -"@material-ui/system@^4.11.3": - version "4.11.3" - resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.11.3.tgz#466bc14c9986798fd325665927c963eb47cc4143" - integrity sha512-SY7otguNGol41Mu2Sg6KbBP1ZRFIbFLHGK81y4KYbsV2yIcaEPOmsCK6zwWlp+2yTV3J/VwT6oSBARtGIVdXPw== +"@material-ui/system@^4.12.1": + version "4.12.1" + resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-4.12.1.tgz#2dd96c243f8c0a331b2bb6d46efd7771a399707c" + integrity sha512-lUdzs4q9kEXZGhbN7BptyiS1rLNHe6kG9o8Y307HCvF4sQxbCgpL2qi+gUk+yI8a2DNk48gISEQxoxpgph0xIw== dependencies: "@babel/runtime" "^7.4.4" "@material-ui/utils" "^4.11.2" @@ -2212,15 +2213,15 @@ react-is "^16.8.0 || ^17.0.0" "@material-ui/utils@^5.0.0-alpha.14": - version "5.0.0-alpha.31" - resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-5.0.0-alpha.31.tgz#a4ff785d4c643f9b25dd3bdf7cc1b8ef00adf954" - integrity sha512-4OzVD12+HbfWMftwiHCBforgjkhzbWMdK9GTQLQcekjdG2qpi41BGvanPpHjlxegzou0A2MEaULBvWqsKrUP9A== + version "5.0.0-beta.5" + resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-5.0.0-beta.5.tgz#de492037e1f1f0910fda32e6f11b66dfcde2a1c2" + integrity sha512-wtJ3ovXWZdTAz5eLBqvMpYH/IBJb3qMQbGCyL1i00+sf7AUlAuv4QLx+QtX/siA6L7IpxUQVfqpoCpQH1eYRpQ== dependencies: - "@babel/runtime" "^7.4.4" - "@types/prop-types" "^15.7.3" + "@babel/runtime" "^7.14.8" + "@types/prop-types" "^15.7.4" "@types/react-is" "^16.7.1 || ^17.0.0" prop-types "^15.7.2" - react-is "^17.0.0" + react-is "^17.0.2" "@maxim_mazurok/gapi.client.analytics@latest": version "3.0.20190807" @@ -2897,11 +2898,16 @@ resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.2.3.tgz#ef65165aea2924c9359205bf748865b8881753c0" integrity sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA== -"@types/prop-types@*", "@types/prop-types@^15.7.3": +"@types/prop-types@*": version "15.7.3" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.3.tgz#2ab0d5da2e5815f94b0b9d4b95d1e5f243ab2ca7" integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== +"@types/prop-types@^15.7.4": + version "15.7.4" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.4.tgz#fcf7205c25dff795ee79af1e30da2c9790808f11" + integrity sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ== + "@types/q@^1.5.1": version "1.5.4" resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" @@ -2929,9 +2935,9 @@ "@types/react" "*" "@types/react-is@^16.7.1 || ^17.0.0": - version "17.0.0" - resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.0.tgz#6b60190ae60591ae0c83d6f3854e61e08f5a7976" - integrity sha512-A0DQ1YWZ0RG2+PV7neAotNCIh8gZ3z7tQnDJyS2xRPDNtAtSPcJ9YyfMP8be36Ha0kQRzbZCrrTMznA4blqO5g== + version "17.0.2" + resolved "https://registry.yarnpkg.com/@types/react-is/-/react-is-17.0.2.tgz#abc4d910bff5b0bc6b3e1bec57575f6b63fd4e05" + integrity sha512-2+L0ilcAEG8udkDnvx8B0upwXFBbNnVwOsSCTxW3SDOkmar9NyEeLG0ZLa3uOEw9zyYf/fQapcnfXAVmDKlyHw== dependencies: "@types/react" "*" @@ -2967,9 +2973,9 @@ "@types/react" "*" "@types/react-transition-group@^4.2.0": - version "4.4.1" - resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.1.tgz#e1a3cb278df7f47f17b5082b1b3da17170bd44b1" - integrity sha512-vIo69qKKcYoJ8wKCJjwSgCTM+z3chw3g18dkrDfVX665tMH7tmbDxEAnPdey4gTlwZz5QuHGzd+hul0OVZDqqQ== + version "4.4.2" + resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.2.tgz#38890fd9db68bf1f2252b99a942998dc7877c5b3" + integrity sha512-KibDWL6nshuOJ0fu8ll7QnV/LVTo3PzQ9aCPnRUYPfX7eZohHwLIdNHj7pftanREzHNP4/nJa8oeM73uSiavMQ== dependencies: "@types/react" "*" @@ -9455,13 +9461,6 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= -indefinite-observable@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/indefinite-observable/-/indefinite-observable-2.0.1.tgz#574af29bfbc17eb5947793797bddc94c9d859400" - integrity sha512-G8vgmork+6H9S8lUAg1gtXEj2JxIQTo0g2PbFiYOdjkziSI0F7UYBiVwhZRuixhBCNGczAls34+5HJPyZysvxQ== - dependencies: - symbol-observable "1.2.0" - indent-string@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" @@ -10893,73 +10892,72 @@ jsprim@^1.2.2: verror "1.10.0" jss-plugin-camel-case@^10.5.1: - version "10.6.0" - resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.6.0.tgz#93d2cd704bf0c4af70cc40fb52d74b8a2554b170" - integrity sha512-JdLpA3aI/npwj3nDMKk308pvnhoSzkW3PXlbgHAzfx0yHWnPPVUjPhXFtLJzgKZge8lsfkUxvYSQ3X2OYIFU6A== + version "10.7.1" + resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.7.1.tgz#e7f7097cf97e9deec599cef3275e213452318b93" + integrity sha512-+ioIyWvmAfgDCWXsQcW1NMnLBvRinOVFkSYJUgewQ6TynOcSj5F1bSU23B7z0p1iqK0PPHIU62xY1iNJD33WGA== dependencies: "@babel/runtime" "^7.3.1" hyphenate-style-name "^1.0.3" - jss "10.6.0" + jss "10.7.1" jss-plugin-default-unit@^10.5.1: - version "10.6.0" - resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.6.0.tgz#af47972486819b375f0f3a9e0213403a84b5ef3b" - integrity sha512-7y4cAScMHAxvslBK2JRK37ES9UT0YfTIXWgzUWD5euvR+JR3q+o8sQKzBw7GmkQRfZijrRJKNTiSt1PBsLI9/w== + version "10.7.1" + resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.7.1.tgz#826270e2ee38d7024a281ac67c30d6944f124786" + integrity sha512-tW+dfYVNARBQb/ONzBwd8uyImigyzMiAEDai+AbH5rcHg5h3TtqhAkxx06iuZiT/dZUiFdSKlbe3q9jZGAPIwA== dependencies: "@babel/runtime" "^7.3.1" - jss "10.6.0" + jss "10.7.1" jss-plugin-global@^10.5.1: - version "10.6.0" - resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.6.0.tgz#3e8011f760f399cbadcca7f10a485b729c50e3ed" - integrity sha512-I3w7ji/UXPi3VuWrTCbHG9rVCgB4yoBQLehGDTmsnDfXQb3r1l3WIdcO8JFp9m0YMmyy2CU7UOV6oPI7/Tmu+w== + version "10.7.1" + resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.7.1.tgz#9725c46d662aac2e596a0a8741944c060e2b90a1" + integrity sha512-FbxCnu44IkK/bw8X3CwZKmcAnJqjAb9LujlAc/aP0bMSdVa3/MugKQRyeQSu00uGL44feJJDoeXXiHOakBr/Zw== dependencies: "@babel/runtime" "^7.3.1" - jss "10.6.0" + jss "10.7.1" jss-plugin-nested@^10.5.1: - version "10.6.0" - resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.6.0.tgz#5f83c5c337d3b38004834e8426957715a0251641" - integrity sha512-fOFQWgd98H89E6aJSNkEh2fAXquC9aZcAVjSw4q4RoQ9gU++emg18encR4AT4OOIFl4lQwt5nEyBBRn9V1Rk8g== + version "10.7.1" + resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.7.1.tgz#35563a7a710a45307fd6b9742ffada1d72a62eb7" + integrity sha512-RNbICk7FlYKaJyv9tkMl7s6FFfeLA3ubNIFKvPqaWtADK0KUaPsPXVYBkAu4x1ItgsWx67xvReMrkcKA0jSXfA== dependencies: "@babel/runtime" "^7.3.1" - jss "10.6.0" + jss "10.7.1" tiny-warning "^1.0.2" jss-plugin-props-sort@^10.5.1: - version "10.6.0" - resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.6.0.tgz#297879f35f9fe21196448579fee37bcde28ce6bc" - integrity sha512-oMCe7hgho2FllNc60d9VAfdtMrZPo9n1Iu6RNa+3p9n0Bkvnv/XX5San8fTPujrTBScPqv9mOE0nWVvIaohNuw== + version "10.7.1" + resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.7.1.tgz#1d12b26048541ed3a2ed1b69f7fc231605728362" + integrity sha512-eyd5FhA+J0QrpqXxO7YNF/HMSXXl4pB0EmUdY4vSJI4QG22F59vQ6AHtP6fSwhmBdQ98Qd9gjfO+RMxcE39P1A== dependencies: "@babel/runtime" "^7.3.1" - jss "10.6.0" + jss "10.7.1" jss-plugin-rule-value-function@^10.5.1: - version "10.6.0" - resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.6.0.tgz#3c1a557236a139d0151e70a82c810ccce1c1c5ea" - integrity sha512-TKFqhRTDHN1QrPTMYRlIQUOC2FFQb271+AbnetURKlGvRl/eWLswcgHQajwuxI464uZk91sPiTtdGi7r7XaWfA== + version "10.7.1" + resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.7.1.tgz#123eb796eb9982f8efa7a7e362daddd90c0c69fe" + integrity sha512-fGAAImlbaHD3fXAHI3ooX6aRESOl5iBt3LjpVjxs9II5u9tzam7pqFUmgTcrip9VpRqYHn8J3gA7kCtm8xKwHg== dependencies: "@babel/runtime" "^7.3.1" - jss "10.6.0" + jss "10.7.1" tiny-warning "^1.0.2" jss-plugin-vendor-prefixer@^10.5.1: - version "10.6.0" - resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.6.0.tgz#e1fcd499352846890c38085b11dbd7aa1c4f2c78" - integrity sha512-doJ7MouBXT1lypLLctCwb4nJ6lDYqrTfVS3LtXgox42Xz0gXusXIIDboeh6UwnSmox90QpVnub7au8ybrb0krQ== + version "10.7.1" + resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.7.1.tgz#217821be2d6dacee31d2d464886760ba7742e19a" + integrity sha512-1UHFmBn7hZNsHXTkLLOL8abRl8vi+D1EVzWD4WmLFj55vawHZfnH1oEz6TUf5Y61XHv0smdHabdXds6BgOXe3A== dependencies: "@babel/runtime" "^7.3.1" css-vendor "^2.0.8" - jss "10.6.0" + jss "10.7.1" -jss@10.6.0, jss@^10.5.1: - version "10.6.0" - resolved "https://registry.yarnpkg.com/jss/-/jss-10.6.0.tgz#d92ff9d0f214f65ca1718591b68e107be4774149" - integrity sha512-n7SHdCozmxnzYGXBHe0NsO0eUf9TvsHVq2MXvi4JmTn3x5raynodDVE/9VQmBdWFyyj9HpHZ2B4xNZ7MMy7lkw== +jss@10.7.1, jss@^10.5.1: + version "10.7.1" + resolved "https://registry.yarnpkg.com/jss/-/jss-10.7.1.tgz#16d846e1a22fb42e857b99f9c6a0c5a27341c804" + integrity sha512-5QN8JSVZR6cxpZNeGfzIjqPEP+ZJwJJfZbXmeABNdxiExyO+eJJDy6WDtqTf8SDKnbL5kZllEpAP71E/Lt7PXg== dependencies: "@babel/runtime" "^7.3.1" csstype "^3.0.2" - indefinite-observable "^2.0.1" is-in-browser "^1.1.3" tiny-warning "^1.0.2" @@ -14270,7 +14268,7 @@ react-is@^16.12.0, react-is@^16.13.1, react-is@^16.7.0, react-is@^16.8.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -"react-is@^16.12.0 || ^17.0.0", "react-is@^16.8.0 || ^17.0.0", react-is@^17.0.0, react-is@^17.0.1, react-is@^17.0.2: +"react-is@^16.12.0 || ^17.0.0", "react-is@^16.8.0 || ^17.0.0", react-is@^17.0.1, react-is@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== @@ -14358,9 +14356,9 @@ react-textarea-autosize@^8.3.2: use-latest "^1.0.0" react-transition-group@^4.4.0: - version "4.4.1" - resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.1.tgz#63868f9325a38ea5ee9535d828327f85773345c9" - integrity sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw== + version "4.4.2" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470" + integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg== dependencies: "@babel/runtime" "^7.5.5" dom-helpers "^5.0.1" @@ -16139,7 +16137,7 @@ svgo@1.3.2, svgo@^1.0.0: unquote "~1.1.1" util.promisify "~1.0.0" -symbol-observable@1.2.0, symbol-observable@^1.0.4: +symbol-observable@^1.0.4: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ== From f73ee6512c9ea49a2c728158d5e4f7a779841fb4 Mon Sep 17 00:00:00 2001 From: Matt Hamrick Date: Fri, 27 Aug 2021 13:06:48 -0700 Subject: [PATCH 02/10] Update from createMuiTheme to createTheme --- gatsby/wrapRootElement.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gatsby/wrapRootElement.js b/gatsby/wrapRootElement.js index 0976df099..4969d46e9 100644 --- a/gatsby/wrapRootElement.js +++ b/gatsby/wrapRootElement.js @@ -1,7 +1,7 @@ import React from "react" import CssBaseline from "@material-ui/core/CssBaseline" import { ThemeProvider } from "@material-ui/core" -import { createMuiTheme, withStyles } from "@material-ui/core/styles" +import { createTheme, withStyles } from "@material-ui/core/styles" import orange from "@material-ui/core/colors/orange" import deepOrange from "@material-ui/core/colors/deepOrange" import Snackbar from "@material-ui/core/Snackbar" @@ -28,7 +28,7 @@ const reducer = (state = {}, action) => { } } -const globalTheme = createMuiTheme({ +const globalTheme = createTheme({ palette: { primary: orange, secondary: deepOrange, From 1031c517343803c54e8412e4dba53ffe0d4519b1 Mon Sep 17 00:00:00 2001 From: Matt Hamrick Date: Fri, 27 Aug 2021 13:31:31 -0700 Subject: [PATCH 03/10] Ran npx @material-ui/codemod@next v5.0.0/theme-breakpoints-width src/ --- src/components/AccountExplorer/index.tsx | 2 +- src/components/ErrorFallback/index.tsx | 2 +- src/components/Layout/useStyles.ts | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/AccountExplorer/index.tsx b/src/components/AccountExplorer/index.tsx index 6b1fc4799..ac703ddeb 100644 --- a/src/components/AccountExplorer/index.tsx +++ b/src/components/AccountExplorer/index.tsx @@ -56,7 +56,7 @@ const useStyles = makeStyles(theme => ({ margin: theme.spacing(1), padding: theme.spacing(1, 1), width: "100%", - "max-width": theme.breakpoints.width("sm"), + "max-width": theme.breakpoints.values.sm, }, table: { "margin-top": theme.spacing(6), diff --git a/src/components/ErrorFallback/index.tsx b/src/components/ErrorFallback/index.tsx index 43af0093f..651813796 100644 --- a/src/components/ErrorFallback/index.tsx +++ b/src/components/ErrorFallback/index.tsx @@ -8,7 +8,7 @@ import Warning from "../Warning" const useStyles = makeStyles(theme => ({ error: { padding: theme.spacing(2), - maxWidth: theme.breakpoints.width("md"), + maxWidth: theme.breakpoints.values.md, }, resetButton: { marginTop: theme.spacing(1), diff --git a/src/components/Layout/useStyles.ts b/src/components/Layout/useStyles.ts index bb1051557..e936809b3 100644 --- a/src/components/Layout/useStyles.ts +++ b/src/components/Layout/useStyles.ts @@ -49,7 +49,7 @@ const useStyles = makeStyles(theme => ({ content: { flexGrow: 1, padding: theme.spacing(2, 4, 0, 4), - maxWidth: theme.breakpoints.width("md"), + maxWidth: theme.breakpoints.values.md, [mobile(theme)]: { maxWidth: "unset", width: "100%", @@ -59,7 +59,7 @@ const useStyles = makeStyles(theme => ({ header: { padding: theme.spacing(4, 4, 2, 4), position: "relative", - maxWidth: theme.breakpoints.width("md"), + maxWidth: theme.breakpoints.values.md, [mobile(theme)]: { maxWidth: "unset", width: "100%", From 8b03370bcac6c6f1d1845f328769b9bcb141ea89 Mon Sep 17 00:00:00 2001 From: Matt Hamrick Date: Fri, 27 Aug 2021 13:37:33 -0700 Subject: [PATCH 04/10] found one outstanding issue. --- src/components/LinkedTextField.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/LinkedTextField.tsx b/src/components/LinkedTextField.tsx index 7655c7be7..4970798b8 100644 --- a/src/components/LinkedTextField.tsx +++ b/src/components/LinkedTextField.tsx @@ -46,7 +46,7 @@ const LinkedTextField: React.FC = ({ variant="outlined" fullWidth label={label} - value={value === undefined ? null : value} + value={value === undefined ? "" : value} onChange={e => onChange(e.target.value)} required={required} helperText={helperText} From d79e9aa84794c97e52d1fb64ffc6d79a47c23cd1 Mon Sep 17 00:00:00 2001 From: Matt Hamrick Date: Thu, 26 Aug 2021 10:58:21 -0700 Subject: [PATCH 05/10] updated useCached to have have a bustCache built-in. --- .../DimensionsMetricsExplorer/useColumns.ts | 2 +- src/components/UAPickers.tsx | 4 +- src/components/ViewSelector/useAccounts.ts | 2 +- .../useDimensionsAndMetrics.ts | 2 +- .../ga4/StreamPicker/useAccounts.ts | 2 +- src/components/ga4/StreamPicker/useStreams.ts | 6 +- src/hooks/useCached.ts | 56 +++++++++++++------ 7 files changed, 47 insertions(+), 27 deletions(-) diff --git a/src/components/DimensionsMetricsExplorer/useColumns.ts b/src/components/DimensionsMetricsExplorer/useColumns.ts index 041c2eb72..8552b8225 100644 --- a/src/components/DimensionsMetricsExplorer/useColumns.ts +++ b/src/components/DimensionsMetricsExplorer/useColumns.ts @@ -34,7 +34,7 @@ const useColumns = (): Requestable< }) }, [metadataAPI, setFailed, setInProgress]) - const columns = useCached( + const { value: columns } = useCached( StorageKey.dimensionsMetricsExplorerColumns, makeRequest, moment.duration(5, "minutes"), diff --git a/src/components/UAPickers.tsx b/src/components/UAPickers.tsx index 36335c7d3..aed3e7c98 100644 --- a/src/components/UAPickers.tsx +++ b/src/components/UAPickers.tsx @@ -190,7 +190,7 @@ export const useUADimensionsAndMetrics = ({ }) }, [metadataAPI, managementAPI, account, property, view]) - const columns = useCached( + const { value: columns } = useCached( // Even though account is sometimes undefined it doesn't really matter // since this hook will re-run once it is. makeRequest is smartEnough to // not do anything when account property or view are undefined. @@ -486,7 +486,7 @@ export const useUASegments = (): UASegment[] | undefined => { return managementAPI !== undefined }, [managementAPI]) - const segments = useCached( + const { value: segments } = useCached( StorageKey.uaSegments, requestSegments, moment.duration(5, "minutes"), diff --git a/src/components/ViewSelector/useAccounts.ts b/src/components/ViewSelector/useAccounts.ts index 5cb9ea846..034382dea 100644 --- a/src/components/ViewSelector/useAccounts.ts +++ b/src/components/ViewSelector/useAccounts.ts @@ -29,7 +29,7 @@ const useAccounts = (): AccountSummary[] | undefined => { managementAPI, ]) - const accounts = useCached( + const { value: accounts } = useCached( StorageKey.uaAccounts, fetchAccounts, moment.duration(5, "minutes"), diff --git a/src/components/ga4/DimensionsMetricsExplorer/useDimensionsAndMetrics.ts b/src/components/ga4/DimensionsMetricsExplorer/useDimensionsAndMetrics.ts index e78be442c..40580588f 100644 --- a/src/components/ga4/DimensionsMetricsExplorer/useDimensionsAndMetrics.ts +++ b/src/components/ga4/DimensionsMetricsExplorer/useDimensionsAndMetrics.ts @@ -60,7 +60,7 @@ export const useDimensionsAndMetrics = ( } }, [dataAPI, propertyName, setFailed, setInProgress]) - const dimsAndMets = useCached( + const { value: dimsAndMets } = useCached( `${StorageKey.ga4DimensionsMetrics}/${propertyName}` as StorageKey, getMetadata, moment.duration(5, "minutes"), diff --git a/src/components/ga4/StreamPicker/useAccounts.ts b/src/components/ga4/StreamPicker/useAccounts.ts index ef1596dac..d123b4bc2 100644 --- a/src/components/ga4/StreamPicker/useAccounts.ts +++ b/src/components/ga4/StreamPicker/useAccounts.ts @@ -44,7 +44,7 @@ const useAccountSummaries = (): Requestable => { setFailed ) - const accountSummaries = useCached( + const { value: accountSummaries } = useCached( StorageKey.ga4AccountSummaries, requestAccountSummaries, moment.duration(5, "minutes"), diff --git a/src/components/ga4/StreamPicker/useStreams.ts b/src/components/ga4/StreamPicker/useStreams.ts index fbcbaafa2..def54ba96 100644 --- a/src/components/ga4/StreamPicker/useStreams.ts +++ b/src/components/ga4/StreamPicker/useStreams.ts @@ -75,7 +75,7 @@ const useStreams = ( [property?.property] ) - const webStreams = useCached( + const { value: webStreams } = useCached( webStorageKey, requestWebStreams, moment.duration(5, "minutes"), @@ -127,7 +127,7 @@ const useStreams = ( [property?.property] ) - const iosStreams = useCached( + const { value: iosStreams } = useCached( iosStorageKey, requestIOSStreams, moment.duration(5, "minutes"), @@ -179,7 +179,7 @@ const useStreams = ( [property?.property] ) - const androidStreams = useCached( + const { value: androidStreams } = useCached( androidStorageKey, requestAndroidStreams, moment.duration(5, "minutes"), diff --git a/src/hooks/useCached.ts b/src/hooks/useCached.ts index 832373180..1f375eadc 100644 --- a/src/hooks/useCached.ts +++ b/src/hooks/useCached.ts @@ -1,42 +1,62 @@ import { StorageKey } from "@/constants" import moment from "moment" -import { useEffect, useMemo } from "react" +import { useCallback, useEffect, useMemo } from "react" import { usePersistantObject } from "." +interface Cached { + "@@_lastFetched": number + "@@_cacheKey": string + value: T +} + const useCached = ( cacheKey: StorageKey, makeRequest: () => Promise, maxAge: moment.Duration, - requestReady: boolean -): T | undefined => { - const [cached, setCached] = usePersistantObject<{ - "@@_lastFetched": number - value: T - }>(cacheKey) + requestReady: boolean, + onError?: (e: any) => void +): { value: T | undefined; bustCache: () => Promise } => { + const [cached, setCached] = usePersistantObject>(cacheKey) - useEffect(() => { + const updateCachedValue = useCallback(async () => { if (requestReady === false) { return } + try { + const t = await makeRequest() + const now = moment.now() + setCached({ "@@_lastFetched": now, value: t, "@@_cacheKey": cacheKey }) + } catch (e) { + onError + ? onError(e) + : console.error("An unhandled error has occured, ", e) + } + }, [makeRequest, setCached, onError, cacheKey, requestReady]) + + useEffect(() => { if (cached === undefined) { - makeRequest().then(t => { - const now = moment.now() - setCached({ "@@_lastFetched": now, value: t }) - }) + updateCachedValue() } else { const now = moment() if (now.isAfter(moment(cached["@@_lastFetched"]).add(maxAge))) { - makeRequest().then(t => { - const now = moment.now() - setCached({ "@@_lastFetched": now, value: t }) - }) + updateCachedValue() } else { return } } - }, [requestReady, cached, setCached, makeRequest, maxAge]) + }, [cached, maxAge, onError, updateCachedValue]) + + const bustCache = useCallback(async () => { + await updateCachedValue() + }, [updateCachedValue]) - return useMemo(() => cached?.value, [cached]) + return useMemo( + () => ({ + value: cached?.value, + bustCache, + }), + [cached, bustCache] + ) } export default useCached From 0733c0322b6a166856bdf1b5086d42ea2ee2b480 Mon Sep 17 00:00:00 2001 From: Matt Hamrick Date: Fri, 27 Aug 2021 13:00:13 -0700 Subject: [PATCH 06/10] stopping point. --- .../ga4/EventBuilder/MPSecret/index.tsx | 195 ++++++++++++++++++ .../MPSecret/useCreateMPSecret.ts | 46 +++++ .../EventBuilder/MPSecret/useGetMPSecrets.ts | 38 ++++ .../ga4/EventBuilder/MPSecret/useInputs.ts | 27 +++ .../MPSecret/useMPSecretsRequest.ts | 98 +++++++++ .../ValidateEvent/useSharableLink.ts | 34 +-- src/components/ga4/EventBuilder/index.tsx | 8 + src/components/ga4/EventBuilder/types.ts | 31 +-- src/components/ga4/EventBuilder/useEvent.ts | 12 +- src/components/ga4/EventBuilder/useInputs.ts | 25 ++- .../ga4/EventBuilder/useUserProperties.ts | 4 +- .../ga4/StreamPicker/index.spec.tsx | 50 +++-- src/components/ga4/StreamPicker/index.tsx | 1 - .../StreamPicker/useAccountPropertyStream.ts | 7 +- src/components/ga4/StreamPicker/useStreams.ts | 38 ++-- src/constants.ts | 3 + src/test-utils.tsx | 27 ++- 17 files changed, 554 insertions(+), 90 deletions(-) create mode 100644 src/components/ga4/EventBuilder/MPSecret/index.tsx create mode 100644 src/components/ga4/EventBuilder/MPSecret/useCreateMPSecret.ts create mode 100644 src/components/ga4/EventBuilder/MPSecret/useGetMPSecrets.ts create mode 100644 src/components/ga4/EventBuilder/MPSecret/useInputs.ts create mode 100644 src/components/ga4/EventBuilder/MPSecret/useMPSecretsRequest.ts diff --git a/src/components/ga4/EventBuilder/MPSecret/index.tsx b/src/components/ga4/EventBuilder/MPSecret/index.tsx new file mode 100644 index 000000000..4c43b48d4 --- /dev/null +++ b/src/components/ga4/EventBuilder/MPSecret/index.tsx @@ -0,0 +1,195 @@ +import { PAB, SAB } from "@/components/Buttons" +import ExternalLink from "@/components/ExternalLink" +import Spinner from "@/components/Spinner" +import Warning from "@/components/Warning" +import WithHelpText from "@/components/WithHelpText" +import { StorageKey, Url } from "@/constants" +import useFormStyles from "@/hooks/useFormStyles" +import { Dispatch, RequestStatus, successful } from "@/types" +import { + Dialog, + DialogTitle, + makeStyles, + TextField, + Typography, +} from "@material-ui/core" +import { Autocomplete } from "@material-ui/lab" +import * as React from "react" +import StreamPicker, { RenderOption } from "../../StreamPicker" +import useAccountPropertyStream from "../../StreamPicker/useAccountPropertyStream" +import { QueryParam } from "../types" +import useInputs, { CreationStatus } from "./useInputs" +import useMPSecretsRequest, { + MPSecret as MPSecretT, +} from "./useMPSecretsRequest" + +const useStyles = makeStyles(theme => ({ + mpSecret: { + "&> :not(:first-child)": { + marginTop: theme.spacing(1), + }, + }, + secret: { + display: "flex", + alignItems: "center", + "&> :not(:first-child)": { + marginLeft: theme.spacing(1), + }, + }, + createSecretDialog: { + padding: theme.spacing(1), + "&> :not(:first-child)": { + marginTop: theme.spacing(1), + }, + }, +})) + +interface Props { + setSecret: Dispatch + secret: MPSecretT | undefined + useFirebase: boolean +} + +const api_secret_reference = ( + api_secret +) + +const MPSecret: React.FC = ({ secret, setSecret, useFirebase }) => { + const formClasses = useFormStyles() + const classes = useStyles() + + const aps = useAccountPropertyStream(StorageKey.eventBuilderAPS, QueryParam, { + androidStreams: useFirebase, + webStreams: !useFirebase, + }) + const secretsRequest = useMPSecretsRequest({ aps }) + const [creationError, setCreationError] = React.useState() + + const { + displayName, + setDisplayName, + creationStatus, + setCreationStatus, + } = useInputs() + + return ( +
+ Choose an account, property, and stream. + + + Select an existing api_secret or create a new secret. + + + The API secret for the property to send the event to. See{" "} + {api_secret_reference} on devsite + + } + > +
+ + className={formClasses.grow} + loading={secretsRequest.status !== RequestStatus.Successful} + options={successful(secretsRequest)?.secrets || []} + noOptionsText="There are no secrets for the selected stream." + loadingText={ + aps.stream === undefined + ? "Choose an account, property, and stream to see existing secrets." + : "Loading..." + } + value={secret || null} + getOptionLabel={secret => secret.secretValue} + getOptionSelected={(a, b) => a.name === b.name} + onChange={(_event, value) => { + if (value === null) { + setSecret(undefined) + return + } + if (typeof value === "string") { + setSecret({ secretValue: value }) + return + } + setSecret(value) + }} + renderOption={secret => ( + + )} + renderInput={params => ( + + )} + /> +
+ { + setCreationStatus(CreationStatus.ShowDialog) + }} + > + new secret + +
+ + setCreationStatus(CreationStatus.NotStarted)} + > + Create new secret +
+ {creationStatus === CreationStatus.ShowDialog ? ( + setDisplayName(e.target.value)} + /> + ) : ( + creating new secret + )} +
+ { + setCreationStatus(CreationStatus.Creating) + try { + const nuSecret = await successful( + secretsRequest + )!.createMPSecret(displayName) + setCreationStatus(CreationStatus.Done) + setSecret(nuSecret) + } catch (e) { + setCreationError(e) + setCreationStatus(CreationStatus.Failed) + } + }} + > + Create + +
+
+
+
+ {creationError && {creationError?.message}} +
+
+ ) +} + +export default MPSecret diff --git a/src/components/ga4/EventBuilder/MPSecret/useCreateMPSecret.ts b/src/components/ga4/EventBuilder/MPSecret/useCreateMPSecret.ts new file mode 100644 index 000000000..7814321dd --- /dev/null +++ b/src/components/ga4/EventBuilder/MPSecret/useCreateMPSecret.ts @@ -0,0 +1,46 @@ +import { useCallback } from "react" +import { useSelector } from "react-redux" +import { AccountPropertyStream } from "../../StreamPicker/useAccountPropertyStream" + +const necessaryScopes = ["https://www.googleapis.com/auth/analytics.edit"] + +const useCreateMPSecret = (aps: AccountPropertyStream) => { + const gapi = useSelector((a: AppState) => a.gapi) + const user = useSelector((a: AppState) => a.user) + return useCallback( + async (displayName: string) => { + if ( + gapi === undefined || + aps.stream === undefined || + user === undefined + ) { + return + } + try { + if (!user.hasGrantedScopes(necessaryScopes.join(","))) { + await user.grant({ + scope: necessaryScopes.join(","), + }) + } + // TODO - Update this once this is available in the client libraries. + const response = await gapi.client.request({ + path: `https://content-analyticsadmin.googleapis.com/v1alpha/${aps.stream.value.name}/measurementProtocolSecrets`, + method: "POST", + body: JSON.stringify({ + display_name: displayName, + }), + }) + return response.result + } catch (e) { + if (e?.result?.error?.message !== undefined) { + throw new Error(e.result.error.message) + } else { + throw e + } + } + }, + [gapi, aps, user] + ) +} + +export default useCreateMPSecret diff --git a/src/components/ga4/EventBuilder/MPSecret/useGetMPSecrets.ts b/src/components/ga4/EventBuilder/MPSecret/useGetMPSecrets.ts new file mode 100644 index 000000000..f8e962187 --- /dev/null +++ b/src/components/ga4/EventBuilder/MPSecret/useGetMPSecrets.ts @@ -0,0 +1,38 @@ +import { useCallback, useMemo } from "react" +import { useSelector } from "react-redux" +import { AccountPropertyStream } from "../../StreamPicker/useAccountPropertyStream" +import { MPSecret } from "./useMPSecretsRequest" + +const useGetMPSecrets = (aps: AccountPropertyStream) => { + const gapi = useSelector((a: AppState) => a.gapi) + + const requestReady = useMemo(() => { + if (gapi === undefined || aps.stream === undefined) { + return false + } + return true + }, [gapi, aps]) + + const getMPSecrets = useCallback(async () => { + if (gapi === undefined || aps.stream === undefined) { + throw new Error("Invalid invariant - gapi & stream must be defined here.") + } + try { + const response = await gapi.client.request({ + path: `https://content-analyticsadmin.googleapis.com/v1alpha/${aps.stream.value.name}/measurementProtocolSecrets`, + }) + console.log({ response }) + return (response.result.measurementProtocolSecrets || []) as MPSecret[] + } catch (e) { + console.error( + "There was an error getting the measurement protocol secrets.", + e + ) + throw e + } + }, [gapi, aps]) + + return { requestReady, getMPSecrets } +} + +export default useGetMPSecrets diff --git a/src/components/ga4/EventBuilder/MPSecret/useInputs.ts b/src/components/ga4/EventBuilder/MPSecret/useInputs.ts new file mode 100644 index 000000000..45691c583 --- /dev/null +++ b/src/components/ga4/EventBuilder/MPSecret/useInputs.ts @@ -0,0 +1,27 @@ +import { useState } from "react" + +import { MPSecret } from "./useMPSecretsRequest" + +export enum CreationStatus { + NotStarted = "not-started", + ShowDialog = "show-dialog", + Creating = "creating", + Done = "done", + Failed = "failed", +} + +const useInputs = () => { + const [displayName, setDisplayName] = useState("") + const [creationStatus, setCreationStatus] = useState( + CreationStatus.NotStarted + ) + + return { + displayName, + setDisplayName, + creationStatus, + setCreationStatus, + } +} + +export default useInputs diff --git a/src/components/ga4/EventBuilder/MPSecret/useMPSecretsRequest.ts b/src/components/ga4/EventBuilder/MPSecret/useMPSecretsRequest.ts new file mode 100644 index 000000000..84a5c86d0 --- /dev/null +++ b/src/components/ga4/EventBuilder/MPSecret/useMPSecretsRequest.ts @@ -0,0 +1,98 @@ +import { StorageKey } from "@/constants" +import useCached from "@/hooks/useCached" +import useRequestStatus from "@/hooks/useRequestStatus" +import { Requestable, RequestStatus } from "@/types" +import moment from "moment" +import { useCallback, useEffect } from "react" +import { AccountPropertyStream } from "../../StreamPicker/useAccountPropertyStream" +import useCreateMPSecret from "./useCreateMPSecret" +import useGetMPSecrets from "./useGetMPSecrets" + +interface MPSecrets { + secrets: MPSecret[] + createMPSecret: (displayName: string) => Promise +} + +export interface MPSecret { + displayName?: string + name?: string + secretValue: string +} + +interface Args { + aps: AccountPropertyStream +} +const useMPSecretsRequest = ({ aps }: Args): Requestable => { + const { status, setFailed, setSuccessful, setInProgress } = useRequestStatus( + RequestStatus.NotStarted + ) + + useEffect(() => { + if (aps.stream === undefined) { + setFailed() + } + }, [setFailed, aps.stream]) + + const { + getMPSecrets: getMPSecretsLocal, + requestReady: getMPSecretsRequestReady, + } = useGetMPSecrets(aps) + + const createMPSecretLocal = useCreateMPSecret(aps) + + const getMPSecrets = useCallback(async () => { + setInProgress() + const secrets = await getMPSecretsLocal() + return secrets + }, [getMPSecretsLocal, setInProgress]) + + const onError = useCallback( + (e: any) => { + setFailed() + // TODO - not sure what to do here yet. + throw e + }, + [setFailed] + ) + + const { value: secrets, bustCache } = useCached( + `${StorageKey.eventBuilderMPSecrets}/${aps.stream?.value.name}` as StorageKey, + getMPSecrets, + moment.duration(5, "minutes"), + getMPSecretsRequestReady, + onError + ) + + const createMPSecret = useCallback( + async (displayName: string) => { + const secret = await createMPSecretLocal(displayName) + await bustCache() + return secret + }, + [createMPSecretLocal, bustCache] + ) + + useEffect(() => { + if (status !== RequestStatus.Successful && secrets !== undefined) { + setSuccessful() + } + }, [secrets, setSuccessful, status]) + + if ( + status === RequestStatus.NotStarted || + status === RequestStatus.InProgress || + status === RequestStatus.Failed + ) { + return { status } + } else { + if (secrets !== undefined) { + return { status, secrets, createMPSecret } + } else { + console.log({ aps, secrets }) + // throw new Error("Invalid invariant - secrets must be defined here.") + return { status: RequestStatus.InProgress } + } + } +} + +export default useMPSecretsRequest diff --git a/src/components/ga4/EventBuilder/ValidateEvent/useSharableLink.ts b/src/components/ga4/EventBuilder/ValidateEvent/useSharableLink.ts index dd2a63aba..46de33449 100644 --- a/src/components/ga4/EventBuilder/ValidateEvent/useSharableLink.ts +++ b/src/components/ga4/EventBuilder/ValidateEvent/useSharableLink.ts @@ -1,6 +1,6 @@ import { useContext, useMemo } from "react" import { EventCtx, UseFirebaseCtx } from ".." -import { MobileIds, UrlParam, WebIds } from "../types" +import { MobileIds, QueryParam, WebIds } from "../types" import { encodeObject, ensureVersion } from "@/url" import { URLVersion } from "@/types" @@ -21,52 +21,52 @@ const useSharableLink = () => { return useMemo(() => { const params = new URLSearchParams() - ensureVersion(params, UrlParam, URLVersion._2) + ensureVersion(params, QueryParam, URLVersion._2) - const addIfTruthy = (p: UrlParam, v: any) => { + const addIfTruthy = (p: QueryParam, v: any) => { v && params.append(p, v) } useFirebase !== undefined && - params.append(UrlParam.UseFirebase, useFirebase ? "1" : "0") + params.append(QueryParam.UseFirebase, useFirebase ? "1" : "0") non_personalized_ads !== undefined && params.append( - UrlParam.NonPersonalizedAds, + QueryParam.NonPersonalizedAds, non_personalized_ads ? "1" : "0" ) addIfTruthy( - UrlParam.AppInstanceId, + QueryParam.AppInstanceId, (clientIds as MobileIds).app_instance_id ) - addIfTruthy(UrlParam.EventType, type) + addIfTruthy(QueryParam.EventType, type) - addIfTruthy(UrlParam.EventName, eventName) + addIfTruthy(QueryParam.EventName, eventName) - addIfTruthy(UrlParam.ClientId, (clientIds as WebIds).client_id) + addIfTruthy(QueryParam.ClientId, (clientIds as WebIds).client_id) - addIfTruthy(UrlParam.UserId, clientIds.user_id) + addIfTruthy(QueryParam.UserId, clientIds.user_id) - addIfTruthy(UrlParam.APISecret, api_secret) + addIfTruthy(QueryParam.APISecret, api_secret) - addIfTruthy(UrlParam.MeasurementId, instanceId.measurement_id) + addIfTruthy(QueryParam.MeasurementId, instanceId.measurement_id) - addIfTruthy(UrlParam.FirebaseAppId, instanceId.firebase_app_id) + addIfTruthy(QueryParam.FirebaseAppId, instanceId.firebase_app_id) - addIfTruthy(UrlParam.TimestampMicros, timestamp_micros) + addIfTruthy(QueryParam.TimestampMicros, timestamp_micros) if (userProperties) { - params.append(UrlParam.UserProperties, encodeObject(userProperties)) + params.append(QueryParam.UserProperties, encodeObject(userProperties)) } if (items) { - params.append(UrlParam.Items, encodeObject(items)) + params.append(QueryParam.Items, encodeObject(items)) } if (parameters.length > 0) { - params.append(UrlParam.Parameters, encodeObject(parameters)) + params.append(QueryParam.Parameters, encodeObject(parameters)) } const urlParams = params.toString() diff --git a/src/components/ga4/EventBuilder/index.tsx b/src/components/ga4/EventBuilder/index.tsx index 2a0125d15..8f465d3d5 100644 --- a/src/components/ga4/EventBuilder/index.tsx +++ b/src/components/ga4/EventBuilder/index.tsx @@ -38,6 +38,7 @@ import { eventsForCategory } from "./event" import useUserProperties from "./useUserProperties" import Items from "./Items" import ValidateEvent from "./ValidateEvent" +import MPSecret from "./MPSecret" export enum Label { APISecret = "api_secret", @@ -171,6 +172,8 @@ const EventBuilder: React.FC = () => { setTimestampMicros, non_personalized_ads, setNonPersonalizedAds, + secret, + setSecret, } = useInputs(categories) return ( @@ -214,6 +217,11 @@ const EventBuilder: React.FC = () => {
+ = { const getVersion = (): string => { const urlParams = new URLSearchParams(window.location.search) - const version = urlParams.get(UrlParam.Version) + const version = urlParams.get(QueryParam.Version) if (version === null) { return "1" } @@ -120,7 +120,7 @@ export const ParametersParam: QueryParamConfig< const useEvent = (initial?: EventType) => { const [typeString, setTypeLocal] = useHydratedPersistantString( StorageKey.ga4EventBuilderLastEventType, - UrlParam.EventType, + QueryParam.EventType, initial || EventType.SelectContent ) @@ -128,21 +128,21 @@ const useEvent = (initial?: EventType) => { const [eventName, setEventName] = useHydratedPersistantString( StorageKey.ga4EventBuilderEventName, - UrlParam.EventName + QueryParam.EventName ) const categories = useMemo(() => suggestedEventFor(type).categories, [type]) const [parameters, setParameters] = useHydratedPersistantObject( StorageKey.ga4EventBuilderParameters, - UrlParam.Parameters, + QueryParam.Parameters, ParametersParam, suggestedEventFor(type).parameters ) const [items, setItems] = useHydratedPersistantObject( StorageKey.ga4EventBuilderItems, - UrlParam.Items, + QueryParam.Items, ItemsParam ) diff --git a/src/components/ga4/EventBuilder/useInputs.ts b/src/components/ga4/EventBuilder/useInputs.ts index f3049480e..42face5b8 100644 --- a/src/components/ga4/EventBuilder/useInputs.ts +++ b/src/components/ga4/EventBuilder/useInputs.ts @@ -4,43 +4,44 @@ import { useHydratedPersistantString, } from "@/hooks/useHydrated" import { useState } from "react" -import { Category, UrlParam } from "./types" +import { MPSecret } from "./MPSecret/useMPSecretsRequest" +import { Category, QueryParam } from "./types" const useInputs = (categories: Category[]) => { const [useFirebase, setUseFirebase] = useHydratedPersistantBoolean( StorageKey.eventBuilderUseFirebase, - UrlParam.UseFirebase, + QueryParam.UseFirebase, true ) const [api_secret, setAPISecret] = useHydratedPersistantString( StorageKey.eventBuilderApiSecret, - UrlParam.APISecret + QueryParam.APISecret ) const [firebase_app_id, setFirebaseAppId] = useHydratedPersistantString( StorageKey.eventBuilderFirebaseAppId, - UrlParam.FirebaseAppId + QueryParam.FirebaseAppId ) const [measurement_id, setMeasurementId] = useHydratedPersistantString( StorageKey.eventBuilderMeasurementId, - UrlParam.MeasurementId + QueryParam.MeasurementId ) const [client_id, setClientId] = useHydratedPersistantString( StorageKey.eventBuilderClientId, - UrlParam.ClientId + QueryParam.ClientId ) const [app_instance_id, setAppInstanceId] = useHydratedPersistantString( StorageKey.eventBuilderAppInstanceId, - UrlParam.AppInstanceId + QueryParam.AppInstanceId ) const [user_id, setUserId] = useHydratedPersistantString( StorageKey.eventBuilderUserId, - UrlParam.UserId + QueryParam.UserId ) const [category, setCategory] = useState(categories[0]) @@ -50,15 +51,17 @@ const useInputs = (categories: Category[]) => { setNonPersonalizedAds, ] = useHydratedPersistantBoolean( StorageKey.eventBuilderNonPersonalizedAds, - UrlParam.NonPersonalizedAds, + QueryParam.NonPersonalizedAds, false ) const [timestamp_micros, setTimestampMicros] = useHydratedPersistantString( StorageKey.eventBuilderTimestampMicros, - UrlParam.TimestampMicros + QueryParam.TimestampMicros ) + const [secret, setSecret] = useState() + return { useFirebase, setUseFirebase, @@ -80,6 +83,8 @@ const useInputs = (categories: Category[]) => { setNonPersonalizedAds, timestamp_micros, setTimestampMicros, + secret, + setSecret, } } diff --git a/src/components/ga4/EventBuilder/useUserProperties.ts b/src/components/ga4/EventBuilder/useUserProperties.ts index a74e77a47..412251e85 100644 --- a/src/components/ga4/EventBuilder/useUserProperties.ts +++ b/src/components/ga4/EventBuilder/useUserProperties.ts @@ -3,7 +3,7 @@ import { useAddToArray, useRemoveByIndex, useUpdateByIndex } from "@/hooks" import { useHydratedPersistantObject } from "@/hooks/useHydrated" import { useCallback } from "react" import { numberParam, stringParam } from "./event" -import { Parameter, UrlParam } from "./types" +import { Parameter, QueryParam } from "./types" import { ParametersParam } from "./useEvent" const useUserProperties = () => { @@ -11,7 +11,7 @@ const useUserProperties = () => { Parameter[] >( StorageKey.ga4EventBuilderUserProperties, - UrlParam.UserProperties, + QueryParam.UserProperties, ParametersParam ) diff --git a/src/components/ga4/StreamPicker/index.spec.tsx b/src/components/ga4/StreamPicker/index.spec.tsx index 0dbd79983..78802772e 100644 --- a/src/components/ga4/StreamPicker/index.spec.tsx +++ b/src/components/ga4/StreamPicker/index.spec.tsx @@ -21,7 +21,6 @@ import { act, within } from "@testing-library/react" import { withProviders } from "@/test-utils" import Sut, { Label } from "./index" -import { AccountSummary, PropertySummary } from "@/types/ga4/StreamPicker" import useAccountPropertyStream from "./useAccountPropertyStream" import { StorageKey } from "@/constants" @@ -32,36 +31,47 @@ enum QueryParam { } describe("StreamPicker", () => { - test("Selects a property & stream when an account is picked.", async () => { - const { result } = renderHook(() => - useAccountPropertyStream("a" as StorageKey, QueryParam) - ) - const { gapi, wrapped } = withProviders() + describe("when autoFill is true", () => { + test("selects a property & stream after an account is picked.", async () => { + const { result } = renderHook(() => + useAccountPropertyStream("a" as StorageKey, QueryParam, { + androidStreams: true, + iosStreams: true, + webStreams: true, + }) + ) + const { gapi, wrapped } = withProviders() - const { findByTestId } = renderer.render(wrapped) + const { findByTestId } = renderer.render(wrapped) - // Await for the mocked accountSummaries method to finish. - await act(async () => { + // Await for the mocked accountSummaries methods to finish. await act(async () => { await gapi.client.analyticsadmin.accountSummaries.list({}) await gapi.client.analyticsadmin.accountSummaries.list({ pageToken: "1", }) }) - }) - const accountPicker = await findByTestId(Label.Account) + const accountPicker = await findByTestId(Label.Account) - await act(async () => { - const accountInput = within(accountPicker).getByRole("textbox") - accountPicker.focus() - renderer.fireEvent.change(accountInput, { target: { value: "" } }) - renderer.fireEvent.keyDown(accountPicker, { key: "ArrowDown" }) - renderer.fireEvent.keyDown(accountPicker, { key: "ArrowDown" }) - renderer.fireEvent.keyDown(accountPicker, { key: "Enter" }) - }) + await act(async () => { + const accountInput = within(accountPicker).getByRole("textbox") + accountPicker.focus() + renderer.fireEvent.change(accountInput, { target: { value: "" } }) + renderer.fireEvent.keyDown(accountPicker, { key: "ArrowDown" }) + renderer.fireEvent.keyDown(accountPicker, { key: "ArrowDown" }) + renderer.fireEvent.keyDown(accountPicker, { key: "Enter" }) + }) - expect(within(accountPicker).getByRole("textbox")).toHaveValue("hi") + // Await for the mocked stream methods to finish. + await act(async () => { + await gapi.client.analyticsadmin.properties.webDataStreams.list() + await gapi.client.analyticsadmin.properties.iosAppDataStreams.list() + await gapi.client.analyticsadmin.properties.androidAppDataStreams.list() + }) + + expect(within(accountPicker).getByRole("textbox")).toHaveValue("hi") + }) }) // describe("with defaults", () => { // test("of { account } selects default", async () => { diff --git a/src/components/ga4/StreamPicker/index.tsx b/src/components/ga4/StreamPicker/index.tsx index 2f751a33b..28b952895 100644 --- a/src/components/ga4/StreamPicker/index.tsx +++ b/src/components/ga4/StreamPicker/index.tsx @@ -33,7 +33,6 @@ interface CommonProps { } interface WithStreams extends CommonProps { - // If needed this can be updated to only show web, firebase, or ios streams. streams: true stream: Stream | undefined setStreamID: Dispatch diff --git a/src/components/ga4/StreamPicker/useAccountPropertyStream.ts b/src/components/ga4/StreamPicker/useAccountPropertyStream.ts index e2c4cb41e..518fb779b 100644 --- a/src/components/ga4/StreamPicker/useAccountPropertyStream.ts +++ b/src/components/ga4/StreamPicker/useAccountPropertyStream.ts @@ -22,6 +22,11 @@ interface AccountPropertyStreamSetters extends AccountPropertySetters { const useAccountPropertyStream = ( prefix: StorageKey, queryParamKeys: { Account: string; Property: string; Stream: string }, + streams: { + androidStreams?: boolean + webStreams?: boolean + iosStreams?: boolean + }, // TODO - This is only here because there seems to be a bug with // use-query-params replaceIn functionality where it also removes the anchor. // Need to do a minimum repro and file a bug to that repo. @@ -45,7 +50,7 @@ const useAccountPropertyStream = ( }, 100) }, []) - const streamsRequest = useStreams(property) + const streamsRequest = useStreams(property, streams) const getStreamsByID = useCallback( (id: string | undefined) => { diff --git a/src/components/ga4/StreamPicker/useStreams.ts b/src/components/ga4/StreamPicker/useStreams.ts index def54ba96..bf72fa25b 100644 --- a/src/components/ga4/StreamPicker/useStreams.ts +++ b/src/components/ga4/StreamPicker/useStreams.ts @@ -26,6 +26,11 @@ const getAndroidPageToken = (response: IOSStreamsResponse) => const useStreams = ( property: PropertySummary | undefined, + streams: { + androidStreams?: boolean + webStreams?: boolean + iosStreams?: boolean + }, onComplete?: () => void ): Requestable<{ streams: Stream[] }> => { const gapi = useGapi() @@ -79,7 +84,7 @@ const useStreams = ( webStorageKey, requestWebStreams, moment.duration(5, "minutes"), - requestReady + requestReady && !!streams.webStreams ) useEffect(() => { @@ -131,7 +136,7 @@ const useStreams = ( iosStorageKey, requestIOSStreams, moment.duration(5, "minutes"), - requestReady + requestReady && !!streams.iosStreams ) useEffect(() => { @@ -183,7 +188,7 @@ const useStreams = ( androidStorageKey, requestAndroidStreams, moment.duration(5, "minutes"), - requestReady + requestReady && !!streams.androidStreams ) useEffect(() => { @@ -194,13 +199,13 @@ const useStreams = ( useEffect(() => { if ( - webStreams !== undefined && - iosStreams !== undefined && - androidStreams !== undefined + (webStreams !== undefined || !streams.webStreams) && + (iosStreams !== undefined || !streams.iosStreams) && + (androidStreams !== undefined || !streams.androidStreams) ) { onComplete && onComplete() } - }, [onComplete, webStreams, iosStreams, androidStreams]) + }, [onComplete, webStreams, iosStreams, androidStreams, streams]) useEffect(() => { setWebStreamNotStarted() @@ -214,36 +219,37 @@ const useStreams = ( ]) if ( - webStreamsStatus === RequestStatus.Successful && - iosStreamsStatus === RequestStatus.Successful && - androidStreamsStatus === RequestStatus.Successful + (webStreamsStatus === RequestStatus.Successful || !streams.webStreams) && + (iosStreamsStatus === RequestStatus.Successful || !streams.iosStreams) && + (androidStreamsStatus === RequestStatus.Successful || + !streams.androidStreams) ) { - if (webStreams === undefined) { + if (webStreams === undefined && streams.webStreams) { throw new Error("Invalid invariant - webStreams must be defined here.") } - if (iosStreams === undefined) { + if (iosStreams === undefined && streams.iosStreams) { throw new Error("Invalid invariant - iosStreams must be defined here.") } - if (androidStreams === undefined) { + if (androidStreams === undefined && streams.androidStreams) { throw new Error( "Invalid invariant - androidStreams must be defined here." ) } return { status: RequestStatus.Successful, - streams: webStreams + streams: (streams.webStreams ? webStreams! : []) .map(s => ({ type: StreamType.WebDataStream, value: s, })) .concat( - iosStreams.map(s => ({ + (streams.iosStreams ? iosStreams! : []).map(s => ({ type: StreamType.IOSDataStream, value: s, })) ) .concat( - androidStreams.map(s => ({ + (streams.androidStreams ? androidStreams! : []).map(s => ({ type: StreamType.AndroidDataStream, value: s, })) diff --git a/src/constants.ts b/src/constants.ts index 03c1e02af..3518e84d9 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -68,6 +68,7 @@ export enum Url { measurementProtocol = "https://developers.google.com/analytics/devguides/collection/protocol/v1", validatingMeasurement = "https://developers.google.com/analytics/devguides/collection/protocol/v1/validating-hits", coreReportingApi = "https://developers.google.com/analytics/devguides/reporting/core/v3/", + ga4MPAPISecretReference = "https://developers.google.com/analytics/devguides/collection/protocol/ga4/reference#api_secret", } export enum GAVersion { @@ -248,6 +249,8 @@ export enum StorageKey { ga4EventBuilderItems = "ga4/event-builder/items", ga4EventBuilderEventName = "ga4/event-builder/event-name", ga4EventBuilderUserProperties = "ga4/event-builder/user-properties", + eventBuilderMPSecrets = "ga4/event-builder/mp-secrets", + eventBuilderAPS = "ga4/event-builder/aps", } export const EventAction = { diff --git a/src/test-utils.tsx b/src/test-utils.tsx index b3ccf26d7..554efc690 100644 --- a/src/test-utils.tsx +++ b/src/test-utils.tsx @@ -431,17 +431,38 @@ export const testGapi = () => ({ iosAppDataStreams: { list: (): Promise<{ result: gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaListIosAppDataStreamsResponse - }> => Promise.resolve({ result: {} }), + }> => + Promise.resolve({ + result: { + iosAppDataStreams: [ + { name: "iosStream", displayName: "My ios stream" }, + ], + }, + }), }, androidAppDataStreams: { list: (): Promise<{ result: gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaListAndroidAppDataStreamsResponse - }> => Promise.resolve({ result: {} }), + }> => + Promise.resolve({ + result: { + androidAppDataStreams: [ + { name: "androidStream", displayName: "my android stream" }, + ], + }, + }), }, webDataStreams: { list: (): Promise<{ result: gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaListWebDataStreamsResponse - }> => Promise.resolve({ result: {} }), + }> => + Promise.resolve({ + result: { + webDataStreams: [ + { name: "webStream", displayName: "my web stream" }, + ], + }, + }), }, }, accountSummaries: { From a20add196c89eb811cbc771872b731687f237ec7 Mon Sep 17 00:00:00 2001 From: Matt Hamrick Date: Mon, 30 Aug 2021 09:08:29 -0700 Subject: [PATCH 07/10] realizing i need more tests for these hooks. --- package.json | 1 + .../ga4/StreamPicker/index.spec.tsx | 67 ++++---- src/components/ga4/StreamPicker/index.tsx | 2 +- .../StreamPicker/useAccountProperty.spec.ts | 160 ++++++++++++++++++ .../ga4/StreamPicker/useAccountProperty.ts | 2 + .../ga4/StreamPicker/useAccounts.ts | 6 +- src/components/ga4/StreamPicker/useStreams.ts | 4 +- ...eHydrated.spec.ts => useHydrated.spec.tsx} | 38 ++++- src/test-utils.tsx | 9 +- yarn.lock | 5 + 10 files changed, 254 insertions(+), 40 deletions(-) create mode 100644 src/components/ga4/StreamPicker/useAccountProperty.spec.ts rename src/hooks/{useHydrated.spec.ts => useHydrated.spec.tsx} (55%) diff --git a/package.json b/package.json index 81247ae25..1db48d55e 100644 --- a/package.json +++ b/package.json @@ -93,6 +93,7 @@ "react-test-renderer": "^17.0.2", "ts-node": "^9.1.1", "tsconfig-paths-webpack-plugin": "^3.5.1", + "type-fest": "^2.1.0", "typescript": "^4.2.4" }, "resolutions": { diff --git a/src/components/ga4/StreamPicker/index.spec.tsx b/src/components/ga4/StreamPicker/index.spec.tsx index 78802772e..a850f8bc8 100644 --- a/src/components/ga4/StreamPicker/index.spec.tsx +++ b/src/components/ga4/StreamPicker/index.spec.tsx @@ -16,11 +16,10 @@ import * as React from "react" import * as renderer from "@testing-library/react" import "@testing-library/jest-dom" -import { renderHook } from "@testing-library/react-hooks" import { act, within } from "@testing-library/react" import { withProviders } from "@/test-utils" -import Sut, { Label } from "./index" +import Sut, { Label, StreamPickerProps } from "./index" import useAccountPropertyStream from "./useAccountPropertyStream" import { StorageKey } from "@/constants" @@ -30,45 +29,49 @@ enum QueryParam { Stream = "c", } +const WithAPS: React.FC> = props => { + console.debug("test component rendering.") + const aps = useAccountPropertyStream("a" as StorageKey, QueryParam, { + androidStreams: true, + iosStreams: true, + webStreams: true, + }) + return +} + describe("StreamPicker", () => { describe("when autoFill is true", () => { test("selects a property & stream after an account is picked.", async () => { - const { result } = renderHook(() => - useAccountPropertyStream("a" as StorageKey, QueryParam, { - androidStreams: true, - iosStreams: true, - webStreams: true, - }) - ) - const { gapi, wrapped } = withProviders() + console.debug("hi") + const { gapi, wrapped } = withProviders() const { findByTestId } = renderer.render(wrapped) - // Await for the mocked accountSummaries methods to finish. - await act(async () => { - await gapi.client.analyticsadmin.accountSummaries.list({}) - await gapi.client.analyticsadmin.accountSummaries.list({ - pageToken: "1", - }) - }) + // // Await for the mocked accountSummaries methods to finish. + // await act(async () => { + // await gapi.client.analyticsadmin.accountSummaries.list({}) + // await gapi.client.analyticsadmin.accountSummaries.list({ + // pageToken: "1", + // }) + // }) const accountPicker = await findByTestId(Label.Account) - await act(async () => { - const accountInput = within(accountPicker).getByRole("textbox") - accountPicker.focus() - renderer.fireEvent.change(accountInput, { target: { value: "" } }) - renderer.fireEvent.keyDown(accountPicker, { key: "ArrowDown" }) - renderer.fireEvent.keyDown(accountPicker, { key: "ArrowDown" }) - renderer.fireEvent.keyDown(accountPicker, { key: "Enter" }) - }) - - // Await for the mocked stream methods to finish. - await act(async () => { - await gapi.client.analyticsadmin.properties.webDataStreams.list() - await gapi.client.analyticsadmin.properties.iosAppDataStreams.list() - await gapi.client.analyticsadmin.properties.androidAppDataStreams.list() - }) + // await act(async () => { + // const accountInput = within(accountPicker).getByRole("textbox") + // accountPicker.focus() + // renderer.fireEvent.change(accountInput, { target: { value: "" } }) + // renderer.fireEvent.keyDown(accountPicker, { key: "ArrowDown" }) + // renderer.fireEvent.keyDown(accountPicker, { key: "ArrowDown" }) + // renderer.fireEvent.keyDown(accountPicker, { key: "Enter" }) + // }) + + // // Await for the mocked stream methods to finish. + // await act(async () => { + // await gapi.client.analyticsadmin.properties.webDataStreams.list() + // await gapi.client.analyticsadmin.properties.iosAppDataStreams.list() + // await gapi.client.analyticsadmin.properties.androidAppDataStreams.list() + // }) expect(within(accountPicker).getByRole("textbox")).toHaveValue("hi") }) diff --git a/src/components/ga4/StreamPicker/index.tsx b/src/components/ga4/StreamPicker/index.tsx index 28b952895..598029ec0 100644 --- a/src/components/ga4/StreamPicker/index.tsx +++ b/src/components/ga4/StreamPicker/index.tsx @@ -44,7 +44,7 @@ interface OnlyProperty extends CommonProps { streams?: false | undefined } -type StreamPickerProps = OnlyProperty | WithStreams +export type StreamPickerProps = OnlyProperty | WithStreams const StreamPicker: React.FC = props => { const { account, property, setAccountID, setPropertyID, autoFill } = props diff --git a/src/components/ga4/StreamPicker/useAccountProperty.spec.ts b/src/components/ga4/StreamPicker/useAccountProperty.spec.ts new file mode 100644 index 000000000..3145d7377 --- /dev/null +++ b/src/components/ga4/StreamPicker/useAccountProperty.spec.ts @@ -0,0 +1,160 @@ +import "@testing-library/jest-dom" +import { renderHook, act } from "@testing-library/react-hooks" + +import useAccountProperty from "./useAccountProperty" +import { wrapperFor } from "@/test-utils" +import { StorageKey } from "@/constants" + +enum QueryParam { + Account = "a", + Property = "b", + Stream = "c", +} + +describe("useAccountProperty hook", () => { + test("with Account & Property values already saved in localStorage", async () => { + const accountID = "account-id" + window.localStorage.setItem( + "a-account", + JSON.stringify({ value: accountID }) + ) + const accountSummariesMock = jest.fn< + Promise<{ + result: gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaListAccountSummariesResponse + }>, + Parameters + >(() => + Promise.resolve({ + result: { + accountSummaries: [ + { + account: accountID, + displayName: "My first account", + propertySummaries: [ + { property: "property-id", displayName: "My first property" }, + ], + }, + ], + }, + }) + ) + const { result, waitForNextUpdate } = renderHook( + () => useAccountProperty("a" as StorageKey, QueryParam), + { + wrapper: wrapperFor({ + gapi: { + client: { + analyticsadmin: { + accountSummaries: { list: accountSummariesMock as any }, + }, + }, + }, + }), + } + ) + + expect(result.current.account).toBeUndefined() + expect(result.current.property).toBeUndefined() + + await act(async () => { + await waitForNextUpdate() + }) + + expect(result.current.account).not.toBeUndefined() + }) + // test("defaults to selectContent", () => { + // const { result } = renderHook(() => useEvent(), options) + // expect(result.current.type).toBe(EventType.SelectContent) + // }) + + // describe("when changing event type", () => { + // // TODO - add this test back in once the keepCommonParameters fix is done. + // // test("keeps values of common parameters", async () => { + // // const { result } = renderHook( + // // () => useEvent(EventType.SelectContent), + // // options + // // ) + + // // act(() => { + // // const idx = result.current.parameters.findIndex( + // // parameter => parameter.name === "content_type" + // // ) + // // if (idx === -1) { + // // fail("select content is expected to have a 'content_type' parameter.") + // // } + // // result.current.setParamValue(idx, "image") + // // result.current.setType(EventType.Share) + // // }) + + // // expect(result.current.type).toBe(EventType.Share) + // // const idx = result.current.parameters.findIndex( + // // p => p.name === "content_type" + // // ) + // // expect(idx).not.toBe(-1) + // // expect(result.current.parameters[idx].value).toBe("image") + // // }) + // test("supports every event type", () => { + // const { result } = renderHook( + // () => useEvent(EventType.SelectContent), + // options + // ) + + // act(() => { + // Object.values(EventType).forEach(eventType => { + // result.current.setType(eventType) + // }) + // }) + // }) + // describe("with no parameters in common", () => { + // test("only keeps new parameters", () => { + // // SelectContent and EarnVirtualCurrency have no parameters in common. + // const { result } = renderHook( + // () => useEvent(EventType.SelectContent), + // options + // ) + + // act(() => { + // result.current.setType(EventType.EarnVirtualCurrency) + // }) + + // const expectedParams = cloneEvent( + // suggestedEventFor(EventType.EarnVirtualCurrency) + // ).parameters + // const actualParams = result.current.parameters + + // expect(actualParams).toHaveLength(expectedParams.length) + // actualParams.forEach((actualP, idx) => { + // const expectedP = expectedParams[idx] + // expect(actualP.name).toBe(expectedP.name) + // expect(actualP.value).toBe(expectedP.value) + // expect(actualP.type).toBe(expectedP.type) + // }) + // }) + // test("double swap only keeps new parameters", () => { + // // SelectContent and EarnVirtualCurrency have no parameters in common. + // const { result } = renderHook( + // () => useEvent(EventType.SelectContent), + // options + // ) + + // act(() => { + // result.current.setType(EventType.EarnVirtualCurrency) + // result.current.setType(EventType.SelectContent) + // }) + + // const expectedParams = cloneEvent( + // suggestedEventFor(EventType.SelectContent) + // ).parameters + // const actualParams = result.current.parameters + + // expect(actualParams).toHaveLength(expectedParams.length) + // actualParams.forEach((actualP, idx) => { + // const expectedP = expectedParams[idx] + // expect(actualP.name).toBe(expectedP.name) + // expect(actualP.value).toBe(expectedP.value) + // expect(actualP.type).toBe(expectedP.type) + // }) + // }) + // }) + // }) +}) diff --git a/src/components/ga4/StreamPicker/useAccountProperty.ts b/src/components/ga4/StreamPicker/useAccountProperty.ts index 5ad09b615..a3d532c82 100644 --- a/src/components/ga4/StreamPicker/useAccountProperty.ts +++ b/src/components/ga4/StreamPicker/useAccountProperty.ts @@ -26,6 +26,8 @@ const useAccountProperty = ( ): AccountProperty & AccountPropertySetters => { const accountsRequest = useAccounts() + console.log({ accountsRequest }) + const getAccountByID = useCallback( (id: string | undefined) => { if (!successful(accountsRequest) || id === undefined) { diff --git a/src/components/ga4/StreamPicker/useAccounts.ts b/src/components/ga4/StreamPicker/useAccounts.ts index d123b4bc2..43d0de3d7 100644 --- a/src/components/ga4/StreamPicker/useAccounts.ts +++ b/src/components/ga4/StreamPicker/useAccounts.ts @@ -1,6 +1,5 @@ import { useCallback, useMemo, useEffect } from "react" -import useGapi from "@/hooks/useGapi" import { Requestable, RequestStatus } from "@/types" import { AccountSummaries } from "@/types/ga4/StreamPicker" import useCached from "@/hooks/useCached" @@ -8,6 +7,7 @@ import { StorageKey } from "@/constants" import moment from "moment" import usePaginatedCallback from "@/hooks/usePaginatedCallback" import useRequestStatus from "@/hooks/useRequestStatus" +import { useSelector } from "react-redux" type AccountSummariesResponse = gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaListAccountSummariesResponse const getAccountSummaries = (response: AccountSummariesResponse) => @@ -16,7 +16,7 @@ const getPageToken = (response: AccountSummariesResponse) => response.nextPageToken const useAccountSummaries = (): Requestable => { - const gapi = useGapi() + const gapi = useSelector((a: AppState) => a.gapi) const adminAPI = useMemo(() => gapi?.client.analyticsadmin, [gapi]) const { status, setInProgress, setFailed, setSuccessful } = useRequestStatus() @@ -51,6 +51,8 @@ const useAccountSummaries = (): Requestable => { requestReady ) + console.log("useAccountSummaries", { accountSummaries }) + useEffect(() => { if (accountSummaries !== undefined) { setSuccessful() diff --git a/src/components/ga4/StreamPicker/useStreams.ts b/src/components/ga4/StreamPicker/useStreams.ts index bf72fa25b..d4e3b1939 100644 --- a/src/components/ga4/StreamPicker/useStreams.ts +++ b/src/components/ga4/StreamPicker/useStreams.ts @@ -1,6 +1,5 @@ import { useCallback, useEffect, useMemo } from "react" -import useGapi from "@/hooks/useGapi" import { Requestable, RequestStatus } from "@/types" import { PropertySummary, Stream, StreamType } from "@/types/ga4/StreamPicker" import usePaginatedCallback from "@/hooks/usePaginatedCallback" @@ -8,6 +7,7 @@ import useCached from "@/hooks/useCached" import { StorageKey } from "@/constants" import moment from "moment" import useRequestStatus from "@/hooks/useRequestStatus" +import { useSelector } from "react-redux" type WebStreamsResponse = gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaListWebDataStreamsResponse type IOSStreamsResponse = gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaListIosAppDataStreamsResponse @@ -33,7 +33,7 @@ const useStreams = ( }, onComplete?: () => void ): Requestable<{ streams: Stream[] }> => { - const gapi = useGapi() + const gapi = useSelector((a: AppState) => a.gapi) const adminAPI = useMemo(() => gapi?.client.analyticsadmin, [gapi]) const { diff --git a/src/hooks/useHydrated.spec.ts b/src/hooks/useHydrated.spec.tsx similarity index 55% rename from src/hooks/useHydrated.spec.ts rename to src/hooks/useHydrated.spec.tsx index ea640428c..9172dba59 100644 --- a/src/hooks/useHydrated.spec.ts +++ b/src/hooks/useHydrated.spec.tsx @@ -2,8 +2,12 @@ import "@testing-library/jest-dom" import { renderHook } from "@testing-library/react-hooks" import { TestWrapper, wrapperFor } from "@/test-utils" -import { useHydratedPersistantString } from "./useHydrated" +import { + useHydratedPersistantString, + useKeyedHydratedPersistantObject, +} from "./useHydrated" import { StorageKey } from "@/constants" +import { useCallback } from "react" describe("useHydratedPersistantString", () => { // The specific storage key shouldn't matter. @@ -44,3 +48,35 @@ describe("useHydratedPersistantString", () => { expect(result.current[0]).toBe(expected) }) }) + +describe("use", () => { + test("", () => { + const key = "a" as StorageKey + const paramName = "paramName" + const complexValue = { id: "a", value: "aaa" } + // TODO - put the key in localStorage so this actually has something to + // grab for the first render. + const { result } = renderHook( + () => { + const getValue = useCallback((key: string | undefined) => { + if (key === "a") { + return complexValue + } else { + return undefined + } + }, []) + return useKeyedHydratedPersistantObject( + key, + paramName, + getValue + ) + }, + { + wrapper: wrapperFor({}), + } + ) + console.log("current", result.current) + console.log("error", result.error) + expect(result.current[0]?.value).toEqual("hi") + }) +}) diff --git a/src/test-utils.tsx b/src/test-utils.tsx index 554efc690..fd307d8fc 100644 --- a/src/test-utils.tsx +++ b/src/test-utils.tsx @@ -23,6 +23,7 @@ import { } from "@reach/router" import { AccountSummary, Column } from "./api" import { QueryParamProvider } from "use-query-params" +import { PartialDeep } from "type-fest" interface WithProvidersConfig { path?: string @@ -33,7 +34,8 @@ export const wrapperFor: (options: { path?: string isLoggedIn?: boolean setUp?: () => void -}) => React.FC = ({ path, isLoggedIn, setUp }) => { + gapi?: PartialDeep +}) => React.FC = ({ path, isLoggedIn, setUp, gapi }) => { path = path || "/" isLoggedIn = isLoggedIn === undefined ? true : isLoggedIn @@ -48,6 +50,9 @@ export const wrapperFor: (options: { } else { store.dispatch({ type: "setUser", user: undefined }) } + if (gapi) { + store.dispatch({ type: "setGapi", gapi }) + } const Wrapper: React.FC = ({ children }) => ( @@ -69,7 +74,7 @@ export const withProviders = ( wrapped: JSX.Element history: History store: any - gapi: ReturnType + gapi?: PartialDeep } => { path = path || "/" isLoggedIn = isLoggedIn === undefined ? true : isLoggedIn diff --git a/yarn.lock b/yarn.lock index 8c84e42de..4948613bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16711,6 +16711,11 @@ type-fest@^0.8.0, type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.1.0.tgz#1f8b20ff51519f3b01b3188d50dea9f9ebfbf1b8" + integrity sha512-2wHUmKDy5wNLmebekbHx/zE9ElYAKOmz34psTLG7OwyEJHaIUr6jnaCd55EvgrawAvliwbwgbyH1LkxIfWFyNg== + type-is@^1.6.4, type-is@~1.6.17, type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" From 0826141a720d690d8e3a444d3321220929c60809 Mon Sep 17 00:00:00 2001 From: Matt Hamrick Date: Mon, 30 Aug 2021 12:12:47 -0700 Subject: [PATCH 08/10] got test working for keyedHydratedPersistantObject. --- src/hooks/useHydrated.spec.tsx | 17 ++++++++--------- src/test-utils.tsx | 13 +++++++++---- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/hooks/useHydrated.spec.tsx b/src/hooks/useHydrated.spec.tsx index 9172dba59..25c5a02a4 100644 --- a/src/hooks/useHydrated.spec.tsx +++ b/src/hooks/useHydrated.spec.tsx @@ -49,17 +49,18 @@ describe("useHydratedPersistantString", () => { }) }) -describe("use", () => { - test("", () => { +describe("useKeyedHydratedPersistantObject", () => { + test("grabs value from localStorage for first render.", () => { const key = "a" as StorageKey + const id = "my-id" + const expectedValue = "abcdef" const paramName = "paramName" - const complexValue = { id: "a", value: "aaa" } - // TODO - put the key in localStorage so this actually has something to - // grab for the first render. + window.localStorage.setItem(key, JSON.stringify({ value: id })) + const complexValue = { id: "my-id", value: expectedValue } const { result } = renderHook( () => { const getValue = useCallback((key: string | undefined) => { - if (key === "a") { + if (key === id) { return complexValue } else { return undefined @@ -75,8 +76,6 @@ describe("use", () => { wrapper: wrapperFor({}), } ) - console.log("current", result.current) - console.log("error", result.error) - expect(result.current[0]?.value).toEqual("hi") + expect(result.current[0]?.value).toEqual(expectedValue) }) }) diff --git a/src/test-utils.tsx b/src/test-utils.tsx index fd307d8fc..bc423a0ae 100644 --- a/src/test-utils.tsx +++ b/src/test-utils.tsx @@ -28,6 +28,7 @@ import { PartialDeep } from "type-fest" interface WithProvidersConfig { path?: string isLoggedIn?: boolean + clearStorage?: boolean } export const wrapperFor: (options: { @@ -35,11 +36,12 @@ export const wrapperFor: (options: { isLoggedIn?: boolean setUp?: () => void gapi?: PartialDeep -}) => React.FC = ({ path, isLoggedIn, setUp, gapi }) => { + clearStorage?: boolean +}) => React.FC = ({ path, isLoggedIn, setUp, gapi, clearStorage }) => { path = path || "/" isLoggedIn = isLoggedIn === undefined ? true : isLoggedIn - window.localStorage.clear() + clearStorage && window.localStorage.clear() setUp && setUp() const history = createHistory(createMemorySource(path)) @@ -69,7 +71,10 @@ export const TestWrapper = wrapperFor({}) export const withProviders = ( component: JSX.Element | null, - { path, isLoggedIn }: WithProvidersConfig = { path: "/", isLoggedIn: true } + { path, isLoggedIn, clearStorage }: WithProvidersConfig = { + path: "/", + isLoggedIn: true, + } ): { wrapped: JSX.Element history: History @@ -79,7 +84,7 @@ export const withProviders = ( path = path || "/" isLoggedIn = isLoggedIn === undefined ? true : isLoggedIn - window.localStorage.clear() + clearStorage && window.localStorage.clear() const history = createHistory(createMemorySource(path)) const store = makeStore() From ea167782634e80fcd2e33e7ac106745f6f6fe5c3 Mon Sep 17 00:00:00 2001 From: Matt Hamrick Date: Mon, 30 Aug 2021 13:03:18 -0700 Subject: [PATCH 09/10] added tests for useCache and found a bug (fixed in test) --- src/hooks/useCached.spec.ts | 100 ++++++++++++++++++++++++++++++++++++ src/hooks/useCached.ts | 19 ++++--- 2 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 src/hooks/useCached.spec.ts diff --git a/src/hooks/useCached.spec.ts b/src/hooks/useCached.spec.ts new file mode 100644 index 000000000..6d831e4ff --- /dev/null +++ b/src/hooks/useCached.spec.ts @@ -0,0 +1,100 @@ +import "@testing-library/jest-dom" +import { renderHook } from "@testing-library/react-hooks" + +import { StorageKey } from "@/constants" +import { useCallback, useMemo } from "react" +import useCached from "./useCached" +import moment from "moment" +import { act } from "react-test-renderer" + +describe("useCached", () => { + // The specific storage key shouldn't matter. + const key: StorageKey = "abc" as StorageKey + const expirey = moment.duration(5, "minutes") + + beforeEach(() => { + window.localStorage.clear() + }) + + describe("when value not in cache", () => { + test("requests value exactly once", async () => { + let madeRequest = false + const { result, waitForNextUpdate } = renderHook(() => { + const makeRequest = useCallback(async () => { + if (madeRequest) { + fail("This function should be called exactly once.") + } else { + madeRequest = true + return "my value" + } + }, []) + const requestReady = useMemo(() => true, []) + return useCached(key, makeRequest, expirey, requestReady) + }) + + // First render the value should be undefined while it's making the async request. + expect(result.current.value).toEqual(undefined) + + await act(async () => { + await waitForNextUpdate() + }) + + expect(result.current.value).toEqual("my value") + }) + }) + + describe("when value in cache", () => { + test("uses cache value before expirey", () => { + const expectedValue = { + hi: "there", + } + + window.localStorage.setItem( + key, + JSON.stringify({ value: expectedValue, "@@_lastFetched": moment.now() }) + ) + + const { result } = renderHook(() => { + const makeRequest = useCallback(async () => { + fail("should not be called if value is in localStorage") + }, []) + const requestReady = useMemo(() => true, []) + return useCached(key, makeRequest, expirey, requestReady) + }) + expect(result.current.value).toEqual(expectedValue) + }) + + test("re-requests value after expirey", async () => { + const expectedValue = { + hi: "there", + } + + window.localStorage.setItem( + key, + JSON.stringify({ + value: "i am out of date", + "@@_lastFetched": moment(moment.now()) + .subtract(expirey) + .subtract(moment.duration(1, "second")) + .unix(), + }) + ) + + const { result, waitForNextUpdate } = renderHook(() => { + const makeRequest = useCallback(async () => { + return expectedValue + }, []) + const requestReady = useMemo(() => true, []) + return useCached(key, makeRequest, expirey, requestReady) + }) + + expect(result.current.value).toEqual(undefined) + + await act(async () => { + await waitForNextUpdate() + }) + + expect(result.current.value).toEqual(expectedValue) + }) + }) +}) diff --git a/src/hooks/useCached.ts b/src/hooks/useCached.ts index 1f375eadc..1883e9555 100644 --- a/src/hooks/useCached.ts +++ b/src/hooks/useCached.ts @@ -37,8 +37,12 @@ const useCached = ( if (cached === undefined) { updateCachedValue() } else { + const cacheTime = cached["@@_lastFetched"] const now = moment() - if (now.isAfter(moment(cached["@@_lastFetched"]).add(maxAge))) { + if ( + cacheTime === undefined || + now.isAfter(moment(cached["@@_lastFetched"]).add(maxAge)) + ) { updateCachedValue() } else { return @@ -50,13 +54,16 @@ const useCached = ( await updateCachedValue() }, [updateCachedValue]) - return useMemo( - () => ({ + return useMemo(() => { + const now = moment() + if (now.isAfter(moment(cached?.["@@_lastFetched"]).add(maxAge))) { + return { value: undefined, bustCache } + } + return { value: cached?.value, bustCache, - }), - [cached, bustCache] - ) + } + }, [cached, bustCache, maxAge]) } export default useCached From 08cf46941ff9e0744aeb3717326c08238626b6e6 Mon Sep 17 00:00:00 2001 From: Matt Hamrick Date: Tue, 31 Aug 2021 08:15:13 -0700 Subject: [PATCH 10/10] stopping point. --- .../ga4/EventBuilder/MPSecret/index.tsx | 36 ++++- .../MPSecret/useCreateMPSecret.ts | 14 +- .../EventBuilder/MPSecret/useGetMPSecrets.ts | 14 +- .../MPSecret/useMPSecretsRequest.ts | 36 ++--- src/components/ga4/StreamPicker/index.tsx | 18 +-- .../StreamPicker/useAccountProperty.spec.ts | 136 ++++++++++++------ .../ga4/StreamPicker/useAccountProperty.ts | 24 +++- .../StreamPicker/useAccountPropertyStream.ts | 43 ++++-- .../ga4/StreamPicker/useAccounts.ts | 2 - src/hooks/useCached.spec.ts | 2 +- src/hooks/useCached.ts | 13 +- src/hooks/useHydrated.ts | 2 +- 12 files changed, 214 insertions(+), 126 deletions(-) diff --git a/src/components/ga4/EventBuilder/MPSecret/index.tsx b/src/components/ga4/EventBuilder/MPSecret/index.tsx index 4c43b48d4..eb52cc288 100644 --- a/src/components/ga4/EventBuilder/MPSecret/index.tsx +++ b/src/components/ga4/EventBuilder/MPSecret/index.tsx @@ -58,11 +58,29 @@ const MPSecret: React.FC = ({ secret, setSecret, useFirebase }) => { const formClasses = useFormStyles() const classes = useStyles() - const aps = useAccountPropertyStream(StorageKey.eventBuilderAPS, QueryParam, { - androidStreams: useFirebase, - webStreams: !useFirebase, + const aps = useAccountPropertyStream( + StorageKey.eventBuilderAPS, + QueryParam, + { + androidStreams: useFirebase, + iosStreams: useFirebase, + webStreams: !useFirebase, + }, + true + ) + + const secretsRequest = useMPSecretsRequest({ + aps, }) - const secretsRequest = useMPSecretsRequest({ aps }) + + React.useEffect(() => { + if (successful(secretsRequest)) { + const secrets = successful(secretsRequest)!.secrets + console.log("setting to first secret") + setSecret(secrets?.[0]) + } + }, [secretsRequest]) + const [creationError, setCreationError] = React.useState() const { @@ -75,7 +93,15 @@ const MPSecret: React.FC = ({ secret, setSecret, useFirebase }) => { return (
Choose an account, property, and stream. - + Select an existing api_secret or create a new secret. diff --git a/src/components/ga4/EventBuilder/MPSecret/useCreateMPSecret.ts b/src/components/ga4/EventBuilder/MPSecret/useCreateMPSecret.ts index 7814321dd..a5b2526d3 100644 --- a/src/components/ga4/EventBuilder/MPSecret/useCreateMPSecret.ts +++ b/src/components/ga4/EventBuilder/MPSecret/useCreateMPSecret.ts @@ -1,19 +1,15 @@ +import { Stream } from "@/types/ga4/StreamPicker" import { useCallback } from "react" import { useSelector } from "react-redux" -import { AccountPropertyStream } from "../../StreamPicker/useAccountPropertyStream" const necessaryScopes = ["https://www.googleapis.com/auth/analytics.edit"] -const useCreateMPSecret = (aps: AccountPropertyStream) => { +const useCreateMPSecret = (stream: Stream | undefined) => { const gapi = useSelector((a: AppState) => a.gapi) const user = useSelector((a: AppState) => a.user) return useCallback( async (displayName: string) => { - if ( - gapi === undefined || - aps.stream === undefined || - user === undefined - ) { + if (gapi === undefined || stream === undefined || user === undefined) { return } try { @@ -24,7 +20,7 @@ const useCreateMPSecret = (aps: AccountPropertyStream) => { } // TODO - Update this once this is available in the client libraries. const response = await gapi.client.request({ - path: `https://content-analyticsadmin.googleapis.com/v1alpha/${aps.stream.value.name}/measurementProtocolSecrets`, + path: `https://content-analyticsadmin.googleapis.com/v1alpha/${stream.value.name}/measurementProtocolSecrets`, method: "POST", body: JSON.stringify({ display_name: displayName, @@ -39,7 +35,7 @@ const useCreateMPSecret = (aps: AccountPropertyStream) => { } } }, - [gapi, aps, user] + [gapi, stream, user] ) } diff --git a/src/components/ga4/EventBuilder/MPSecret/useGetMPSecrets.ts b/src/components/ga4/EventBuilder/MPSecret/useGetMPSecrets.ts index f8e962187..26b3df652 100644 --- a/src/components/ga4/EventBuilder/MPSecret/useGetMPSecrets.ts +++ b/src/components/ga4/EventBuilder/MPSecret/useGetMPSecrets.ts @@ -1,25 +1,25 @@ +import { Stream } from "@/types/ga4/StreamPicker" import { useCallback, useMemo } from "react" import { useSelector } from "react-redux" -import { AccountPropertyStream } from "../../StreamPicker/useAccountPropertyStream" import { MPSecret } from "./useMPSecretsRequest" -const useGetMPSecrets = (aps: AccountPropertyStream) => { +const useGetMPSecrets = (stream: Stream | undefined) => { const gapi = useSelector((a: AppState) => a.gapi) const requestReady = useMemo(() => { - if (gapi === undefined || aps.stream === undefined) { + if (gapi === undefined || stream === undefined) { return false } return true - }, [gapi, aps]) + }, [gapi, stream]) const getMPSecrets = useCallback(async () => { - if (gapi === undefined || aps.stream === undefined) { + if (gapi === undefined || stream === undefined) { throw new Error("Invalid invariant - gapi & stream must be defined here.") } try { const response = await gapi.client.request({ - path: `https://content-analyticsadmin.googleapis.com/v1alpha/${aps.stream.value.name}/measurementProtocolSecrets`, + path: `https://content-analyticsadmin.googleapis.com/v1alpha/${stream.value.name}/measurementProtocolSecrets`, }) console.log({ response }) return (response.result.measurementProtocolSecrets || []) as MPSecret[] @@ -30,7 +30,7 @@ const useGetMPSecrets = (aps: AccountPropertyStream) => { ) throw e } - }, [gapi, aps]) + }, [gapi, stream]) return { requestReady, getMPSecrets } } diff --git a/src/components/ga4/EventBuilder/MPSecret/useMPSecretsRequest.ts b/src/components/ga4/EventBuilder/MPSecret/useMPSecretsRequest.ts index 84a5c86d0..f9c3b4999 100644 --- a/src/components/ga4/EventBuilder/MPSecret/useMPSecretsRequest.ts +++ b/src/components/ga4/EventBuilder/MPSecret/useMPSecretsRequest.ts @@ -2,14 +2,14 @@ import { StorageKey } from "@/constants" import useCached from "@/hooks/useCached" import useRequestStatus from "@/hooks/useRequestStatus" import { Requestable, RequestStatus } from "@/types" +import { Stream } from "@/types/ga4/StreamPicker" import moment from "moment" import { useCallback, useEffect } from "react" -import { AccountPropertyStream } from "../../StreamPicker/useAccountPropertyStream" import useCreateMPSecret from "./useCreateMPSecret" import useGetMPSecrets from "./useGetMPSecrets" interface MPSecrets { - secrets: MPSecret[] + secrets: MPSecret[] | undefined createMPSecret: (displayName: string) => Promise } @@ -20,30 +20,23 @@ export interface MPSecret { } interface Args { - aps: AccountPropertyStream + stream: Stream | undefined } -const useMPSecretsRequest = ({ aps }: Args): Requestable => { +const useMPSecretsRequest = ({ stream }: Args): Requestable => { const { status, setFailed, setSuccessful, setInProgress } = useRequestStatus( RequestStatus.NotStarted ) - useEffect(() => { - if (aps.stream === undefined) { - setFailed() - } - }, [setFailed, aps.stream]) - const { getMPSecrets: getMPSecretsLocal, requestReady: getMPSecretsRequestReady, - } = useGetMPSecrets(aps) + } = useGetMPSecrets(stream) - const createMPSecretLocal = useCreateMPSecret(aps) + const createMPSecretLocal = useCreateMPSecret(stream) const getMPSecrets = useCallback(async () => { setInProgress() - const secrets = await getMPSecretsLocal() - return secrets + return getMPSecretsLocal() }, [getMPSecretsLocal, setInProgress]) const onError = useCallback( @@ -56,7 +49,7 @@ const useMPSecretsRequest = ({ aps }: Args): Requestable => { ) const { value: secrets, bustCache } = useCached( - `${StorageKey.eventBuilderMPSecrets}/${aps.stream?.value.name}` as StorageKey, + `${StorageKey.eventBuilderMPSecrets}/${stream?.value.name}` as StorageKey, getMPSecrets, moment.duration(5, "minutes"), getMPSecretsRequestReady, @@ -78,6 +71,14 @@ const useMPSecretsRequest = ({ aps }: Args): Requestable => { } }, [secrets, setSuccessful, status]) + if (stream === undefined) { + return { + status: RequestStatus.Successful, + secrets: undefined, + createMPSecret, + } + } + if ( status === RequestStatus.NotStarted || status === RequestStatus.InProgress || @@ -88,9 +89,8 @@ const useMPSecretsRequest = ({ aps }: Args): Requestable => { if (secrets !== undefined) { return { status, secrets, createMPSecret } } else { - console.log({ aps, secrets }) - // throw new Error("Invalid invariant - secrets must be defined here.") - return { status: RequestStatus.InProgress } + throw new Error("Invalid invariant - secrets must be defined here.") + // return { status: RequestStatus.InProgress } } } } diff --git a/src/components/ga4/StreamPicker/index.tsx b/src/components/ga4/StreamPicker/index.tsx index 598029ec0..b1e533147 100644 --- a/src/components/ga4/StreamPicker/index.tsx +++ b/src/components/ga4/StreamPicker/index.tsx @@ -29,7 +29,6 @@ interface CommonProps { property: PropertySummary | undefined setAccountID: Dispatch setPropertyID: Dispatch - autoFill?: boolean } interface WithStreams extends CommonProps { @@ -37,7 +36,7 @@ interface WithStreams extends CommonProps { stream: Stream | undefined setStreamID: Dispatch streamsRequest: Requestable<{ streams: Stream[] }> - updateToFirstStream: () => void + noStreamsText?: string } interface OnlyProperty extends CommonProps { @@ -47,7 +46,7 @@ interface OnlyProperty extends CommonProps { export type StreamPickerProps = OnlyProperty | WithStreams const StreamPicker: React.FC = props => { - const { account, property, setAccountID, setPropertyID, autoFill } = props + const { account, property, setAccountID, setPropertyID } = props const classes = useStyles() const accountsAndPropertiesRequest = useAccountsAndProperties(account) @@ -65,12 +64,6 @@ const StreamPicker: React.FC = props => { getOptionSelected={(a, b) => a.name === b.name} onChange={(_event, value) => { setAccountID(value === null ? undefined : value?.name) - - if (autoFill) { - const property = value?.propertySummaries?.[0] - setPropertyID(property?.property) - props.streams && props.updateToFirstStream() - } }} renderOption={account => ( = props => { onChange={(_event, value) => { const property = value === null ? undefined : value setPropertyID(property?.property) - - if (autoFill) { - props.streams && props.updateToFirstStream() - } }} renderOption={summary => ( = props => { noOptionsText={ property === undefined ? "Select an account an property to populate this dropdown." - : "There are no streams for the selected property." + : props.noStreamsText || + "There are no streams for the selected property." } value={props.stream || null} getOptionLabel={stream => stream.value.displayName!} diff --git a/src/components/ga4/StreamPicker/useAccountProperty.spec.ts b/src/components/ga4/StreamPicker/useAccountProperty.spec.ts index 3145d7377..1518e2c9f 100644 --- a/src/components/ga4/StreamPicker/useAccountProperty.spec.ts +++ b/src/components/ga4/StreamPicker/useAccountProperty.spec.ts @@ -1,9 +1,10 @@ import "@testing-library/jest-dom" -import { renderHook, act } from "@testing-library/react-hooks" +import { renderHook } from "@testing-library/react-hooks" import useAccountProperty from "./useAccountProperty" import { wrapperFor } from "@/test-utils" import { StorageKey } from "@/constants" +import moment from "moment" enum QueryParam { Account = "a", @@ -12,56 +13,105 @@ enum QueryParam { } describe("useAccountProperty hook", () => { - test("with Account & Property values already saved in localStorage", async () => { + describe("with accountSummaries cached locally", () => { const accountID = "account-id" - window.localStorage.setItem( - "a-account", - JSON.stringify({ value: accountID }) - ) - const accountSummariesMock = jest.fn< - Promise<{ - result: gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaListAccountSummariesResponse - }>, - Parameters - >(() => - Promise.resolve({ - result: { - accountSummaries: [ + const propertyID = "property-id" + + beforeEach(() => { + window.localStorage.clear() + const summaries: gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaAccountSummary[] = [ + { + name: accountID, + displayName: "my account", + account: "accounts/my-account", + propertySummaries: [ { - account: accountID, - displayName: "My first account", - propertySummaries: [ - { property: "property-id", displayName: "My first property" }, - ], + property: propertyID, + displayName: "my property", }, ], }, - }) - ) - const { result, waitForNextUpdate } = renderHook( - () => useAccountProperty("a" as StorageKey, QueryParam), - { - wrapper: wrapperFor({ - gapi: { - client: { - analyticsadmin: { - accountSummaries: { list: accountSummariesMock as any }, - }, - }, - }, - }), - } - ) - - expect(result.current.account).toBeUndefined() - expect(result.current.property).toBeUndefined() - - await act(async () => { - await waitForNextUpdate() + ] + window.localStorage.setItem( + StorageKey.ga4AccountSummaries, + JSON.stringify({ value: summaries, "@@_last_fetched": moment.now() }) + ) }) - expect(result.current.account).not.toBeUndefined() + test("with Account & Property values in localStorage", async () => { + const storageKey = "a" as StorageKey + window.localStorage.setItem( + "a-account", + JSON.stringify({ value: accountID }) + ) + window.localStorage.setItem( + "a-property", + JSON.stringify({ value: propertyID }) + ) + + const { result } = renderHook( + () => useAccountProperty(storageKey, QueryParam), + { wrapper: wrapperFor({}) } + ) + + expect(result.current.account).not.toBeUndefined() + expect(result.current.account!.name).toBe(accountID) + + expect(result.current.property).not.toBeUndefined() + expect(result.current.property!.property).toBe(propertyID) + }) }) + // test("with Account & Property values already saved in localStorage", async () => { + // const accountID = "account-id" + // window.localStorage.setItem( + // "a-account", + // JSON.stringify({ value: accountID }) + // ) + // const accountSummariesMock = jest.fn< + // Promise<{ + // result: gapi.client.analyticsadmin.GoogleAnalyticsAdminV1alphaListAccountSummariesResponse + // }>, + // Parameters + // >(() => + // Promise.resolve({ + // result: { + // accountSummaries: [ + // { + // account: accountID, + // displayName: "My first account", + // propertySummaries: [ + // { property: "property-id", displayName: "My first property" }, + // ], + // }, + // ], + // }, + // }) + // ) + // const { result, waitForNextUpdate } = renderHook( + // () => useAccountProperty("a" as StorageKey, QueryParam), + // { + // wrapper: wrapperFor({ + // gapi: { + // client: { + // analyticsadmin: { + // accountSummaries: { list: accountSummariesMock as any }, + // }, + // }, + // }, + // }), + // } + // ) + + // expect(result.current.account).toBeUndefined() + // expect(result.current.property).toBeUndefined() + + // await act(async () => { + // await waitForNextUpdate() + // await waitForNextUpdate() + // }) + + // expect(result.current.account).not.toBeUndefined() + // }) // test("defaults to selectContent", () => { // const { result } = renderHook(() => useEvent(), options) // expect(result.current.type).toBe(EventType.SelectContent) diff --git a/src/components/ga4/StreamPicker/useAccountProperty.ts b/src/components/ga4/StreamPicker/useAccountProperty.ts index a3d532c82..a93488b51 100644 --- a/src/components/ga4/StreamPicker/useAccountProperty.ts +++ b/src/components/ga4/StreamPicker/useAccountProperty.ts @@ -18,6 +18,7 @@ export interface AccountPropertySetters { const useAccountProperty = ( prefix: StorageKey, queryParamKeys: { Account: string; Property: string; Stream: string }, + autoFill: boolean = false, // TODO - This is only here because there seems to be a bug with // use-query-params replaceIn functionality where it also removes the anchor. // Need to do a minimum repro and file a bug to that repo. @@ -26,8 +27,6 @@ const useAccountProperty = ( ): AccountProperty & AccountPropertySetters => { const accountsRequest = useAccounts() - console.log({ accountsRequest }) - const getAccountByID = useCallback( (id: string | undefined) => { if (!successful(accountsRequest) || id === undefined) { @@ -40,7 +39,7 @@ const useAccountProperty = ( const [ account, - setAccountID, + setAccountIDLocal, ] = useKeyedHydratedPersistantObject( `${prefix}-account` as StorageKey, queryParamKeys.Account, @@ -72,6 +71,25 @@ const useAccountProperty = ( { keepParam } ) + const setAccountID: Dispatch = useCallback( + v => { + setAccountIDLocal(old => { + let nu: string | undefined + if (typeof v === "function") { + nu = v(old) + } else { + nu = v + } + if (autoFill) { + const nuAccount = getAccountByID(nu) + setPropertyID(nuAccount?.propertySummaries?.[0]?.property) + } + return nu + }) + }, + [autoFill, setAccountIDLocal, getAccountByID] + ) + return { account, setAccountID, diff --git a/src/components/ga4/StreamPicker/useAccountPropertyStream.ts b/src/components/ga4/StreamPicker/useAccountPropertyStream.ts index 518fb779b..ba70d2333 100644 --- a/src/components/ga4/StreamPicker/useAccountPropertyStream.ts +++ b/src/components/ga4/StreamPicker/useAccountPropertyStream.ts @@ -1,4 +1,5 @@ import { StorageKey } from "@/constants" + import { useKeyedHydratedPersistantObject } from "@/hooks/useHydrated" import { Dispatch, Requestable, successful } from "@/types" import { PropertySummary, Stream } from "@/types/ga4/StreamPicker" @@ -16,7 +17,6 @@ export interface AccountPropertyStream extends AccountProperty { interface AccountPropertyStreamSetters extends AccountPropertySetters { setStreamID: Dispatch - updateToFirstStream: () => void } const useAccountPropertyStream = ( @@ -27,6 +27,7 @@ const useAccountPropertyStream = ( webStreams?: boolean iosStreams?: boolean }, + autoFill: boolean = false, // TODO - This is only here because there seems to be a bug with // use-query-params replaceIn functionality where it also removes the anchor. // Need to do a minimum repro and file a bug to that repo. @@ -36,20 +37,13 @@ const useAccountPropertyStream = ( const accountProperty = useAccountProperty( prefix, queryParamKeys, + autoFill, keepParam, onSetProperty ) const { property } = accountProperty - const updateToFirstStream = useCallback(() => { - // I don't really like this, but I'm not sure how else to get this to - // update correctly. - setTimeout(() => { - setSetToFirst(true) - }, 100) - }, []) - const streamsRequest = useStreams(property, streams) const getStreamsByID = useCallback( @@ -73,20 +67,39 @@ const useAccountPropertyStream = ( { keepParam } ) - const [setToFirst, setSetToFirst] = useState(false) + // This seems like a hacky workaround, but I'm not sure what else the pattern + // would be here. + const [needsUpdate, setNeedsUpdate] = useState(false) + useEffect(() => { + if (property === undefined) { + setNeedsUpdate(false) + setStreamID(undefined) + } else { + setNeedsUpdate(true) + } + }, [property]) + useEffect(() => { - if (successful(streamsRequest) && setToFirst) { - setStreamID(successful(streamsRequest)?.streams?.[0].value.name) - setSetToFirst(false) + if (autoFill) { + if (successful(streamsRequest) && needsUpdate) { + console.log("updating stream to first from list", { + autoFill, + streamsRequest, + setStreamID, + }) + const firstStreamID = successful(streamsRequest)!.streams?.[0]?.value + ?.name + setStreamID(firstStreamID) + setNeedsUpdate(false) + } } - }, [streamsRequest, setToFirst, setStreamID]) + }, [autoFill, streamsRequest, setStreamID]) return { ...accountProperty, stream, setStreamID, streamsRequest, - updateToFirstStream, } } diff --git a/src/components/ga4/StreamPicker/useAccounts.ts b/src/components/ga4/StreamPicker/useAccounts.ts index 43d0de3d7..d86f1d871 100644 --- a/src/components/ga4/StreamPicker/useAccounts.ts +++ b/src/components/ga4/StreamPicker/useAccounts.ts @@ -51,8 +51,6 @@ const useAccountSummaries = (): Requestable => { requestReady ) - console.log("useAccountSummaries", { accountSummaries }) - useEffect(() => { if (accountSummaries !== undefined) { setSuccessful() diff --git a/src/hooks/useCached.spec.ts b/src/hooks/useCached.spec.ts index 6d831e4ff..f13274d7f 100644 --- a/src/hooks/useCached.spec.ts +++ b/src/hooks/useCached.spec.ts @@ -88,7 +88,7 @@ describe("useCached", () => { return useCached(key, makeRequest, expirey, requestReady) }) - expect(result.current.value).toEqual(undefined) + expect(result.current.value).toEqual("i am out of date") await act(async () => { await waitForNextUpdate() diff --git a/src/hooks/useCached.ts b/src/hooks/useCached.ts index 1883e9555..300a9e8e9 100644 --- a/src/hooks/useCached.ts +++ b/src/hooks/useCached.ts @@ -54,16 +54,13 @@ const useCached = ( await updateCachedValue() }, [updateCachedValue]) - return useMemo(() => { - const now = moment() - if (now.isAfter(moment(cached?.["@@_lastFetched"]).add(maxAge))) { - return { value: undefined, bustCache } - } - return { + return useMemo( + () => ({ value: cached?.value, bustCache, - } - }, [cached, bustCache, maxAge]) + }), + [cached, bustCache, maxAge] + ) } export default useCached diff --git a/src/hooks/useHydrated.ts b/src/hooks/useHydrated.ts index 7fdca88f3..dc41b64ee 100644 --- a/src/hooks/useHydrated.ts +++ b/src/hooks/useHydrated.ts @@ -85,7 +85,7 @@ export const useKeyedHydratedPersistantObject = ( const setKey: Dispatch = useCallback( key => { setKeyLocal(old => { - let nu: string | undefined = undefined + let nu: string | undefined if (typeof key === "function") { nu = key(old) } else {