From caf6411768b5d039478760e82b07d66ab08a26df Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 17 Jun 2022 12:34:49 -0500 Subject: [PATCH 01/15] update esbuild --- web/src/esbuild.config.js | 6 +- web/src/package.json | 2 +- yarn.lock | 246 +++++++++++++++++++------------------- 3 files changed, 129 insertions(+), 125 deletions(-) diff --git a/web/src/esbuild.config.js b/web/src/esbuild.config.js index ef5781d258..936cc1626a 100755 --- a/web/src/esbuild.config.js +++ b/web/src/esbuild.config.js @@ -33,7 +33,11 @@ const dynamicPublicPathPlugin = { require('esbuild') .build({ - entryPoints: { explore: 'explore/explore.tsx', app: 'app/index.tsx' }, + entryPoints: { + explore: 'explore/explore.tsx', + app: 'app/index.tsx', + worker: 'app/worker/worker.ts', + }, outdir: 'build/static/', logLevel: 'info', bundle: true, diff --git a/web/src/package.json b/web/src/package.json index f2aa9e78d9..e89ce9a51b 100644 --- a/web/src/package.json +++ b/web/src/package.json @@ -36,7 +36,7 @@ "bowser": "2.11.0", "classnames": "2.3.1", "diff": "5.0.0", - "esbuild": "0.14.39", + "esbuild": "0.14.45", "fuse.js": "6.6.1", "graphiql": "1.9.2", "graphql": "16.5.0", diff --git a/yarn.lock b/yarn.lock index 91913e3181..ac2fd50d9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2930,35 +2930,35 @@ es-to-primitive@^1.2.1: is-date-object "^1.0.1" is-symbol "^1.0.2" -esbuild-android-64@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.39.tgz#09f12a372eed9743fd77ff6d889ac14f7b340c21" - integrity sha512-EJOu04p9WgZk0UoKTqLId9VnIsotmI/Z98EXrKURGb3LPNunkeffqQIkjS2cAvidh+OK5uVrXaIP229zK6GvhQ== - -esbuild-android-arm64@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.39.tgz#f608d00ea03fe26f3b1ab92a30f99220390f3071" - integrity sha512-+twajJqO7n3MrCz9e+2lVOnFplRsaGRwsq1KL/uOy7xK7QdRSprRQcObGDeDZUZsacD5gUkk6OiHiYp6RzU3CA== - -esbuild-darwin-64@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.39.tgz#31528daa75b4c9317721ede344195163fae3e041" - integrity sha512-ImT6eUw3kcGcHoUxEcdBpi6LfTRWaV6+qf32iYYAfwOeV+XaQ/Xp5XQIBiijLeo+LpGci9M0FVec09nUw41a5g== - -esbuild-darwin-arm64@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.39.tgz#247f770d86d90a215fa194f24f90e30a0bd97245" - integrity sha512-/fcQ5UhE05OiT+bW5v7/up1bDsnvaRZPJxXwzXsMRrr7rZqPa85vayrD723oWMT64dhrgWeA3FIneF8yER0XTw== - -esbuild-freebsd-64@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.39.tgz#479414d294905055eb396ebe455ed42213284ee0" - integrity sha512-oMNH8lJI4wtgN5oxuFP7BQ22vgB/e3Tl5Woehcd6i2r6F3TszpCnNl8wo2d/KvyQ4zvLvCWAlRciumhQg88+kQ== - -esbuild-freebsd-arm64@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.39.tgz#cedeb10357c88533615921ae767a67dc870a474c" - integrity sha512-1GHK7kwk57ukY2yI4ILWKJXaxfr+8HcM/r/JKCGCPziIVlL+Wi7RbJ2OzMcTKZ1HpvEqCTBT/J6cO4ZEwW4Ypg== +esbuild-android-64@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.45.tgz#696f078d97a1350ceea5db626b3bc4a60fc13555" + integrity sha512-krVmwL2uXQN1A+Ci4u2MR+Y0IAvQK0u3no5TsgguHVhTy138szjuohScCGjkpvLCpGLk7P4kFP1LKuntvJ0d4A== + +esbuild-android-arm64@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.45.tgz#d229da8040cea4a0041756d1abcea375b4e3f9db" + integrity sha512-62POGdzAjM+XOXEM5MmFW6k9Pjdjg1hTrXKKBbPE700LFF36B+1Jv9QkskT5UadbTk4cdH9BQ7bGiRPYY0p/Dw== + +esbuild-darwin-64@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.45.tgz#d7d38d91e4c03890d58c297131e9b82895a37a6a" + integrity sha512-dbkVUAnGx5IeZesWnIhnvxy7dSvgUQvfy0TVLzd9XVP3oI/VsKs8UNsfPrxI5HiN4SINv7oPAbxWceMpB7IqNA== + +esbuild-darwin-arm64@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.45.tgz#d4df2871b94351d7d5561e41f91aadba46857838" + integrity sha512-O6Bz7nnOae5rvbh2Ueo8ibSr7+/eLjsbPdgeMsAZri+LkOa7nsVPnhmocpO3Hy/LWfagTtHy1O9HRPIaArPmTg== + +esbuild-freebsd-64@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.45.tgz#e90106a2db3f5b0771392d3f9d8801b3138825c6" + integrity sha512-y1X2nr3XSWnDC7MRcy21EVAT0TiCtdefOntJ+SQcZnPBTURzX77f99S8lDF2KswukChkiacpd2Wd4VZieo7w7Q== + +esbuild-freebsd-arm64@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.45.tgz#e734c8ae8042cd9224406d45d6992261382bcbde" + integrity sha512-r3ZNejkx1BKXQ6sYOP6C5rTwgiUajyAh03wygLWZtl+SLyygvAnu+OouqtveesufjBDgujp4wZXP/n8PVqXkqg== esbuild-jest@0.5.0: version "0.5.0" @@ -2969,101 +2969,101 @@ esbuild-jest@0.5.0: "@babel/plugin-transform-modules-commonjs" "^7.12.13" babel-jest "^26.6.3" -esbuild-linux-32@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.39.tgz#d9f008c4322d771f3958f59c1eee5a05cdf92485" - integrity sha512-g97Sbb6g4zfRLIxHgW2pc393DjnkTRMeq3N1rmjDUABxpx8SjocK4jLen+/mq55G46eE2TA0MkJ4R3SpKMu7dg== - -esbuild-linux-64@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.39.tgz#ba58d7f66858913aeb1ab5c6bde1bbd824731795" - integrity sha512-4tcgFDYWdI+UbNMGlua9u1Zhu0N5R6u9tl5WOM8aVnNX143JZoBZLpCuUr5lCKhnD0SCO+5gUyMfupGrHtfggQ== - -esbuild-linux-arm64@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.39.tgz#708785a30072702b5b1c16b65cf9c25c51202529" - integrity sha512-23pc8MlD2D6Px1mV8GMglZlKgwgNKAO8gsgsLLcXWSs9lQsCYkIlMo/2Ycfo5JrDIbLdwgP8D2vpfH2KcBqrDQ== - -esbuild-linux-arm@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.39.tgz#4e8b5deaa7ab60d0d28fab131244ef82b40684f4" - integrity sha512-t0Hn1kWVx5UpCzAJkKRfHeYOLyFnXwYynIkK54/h3tbMweGI7dj400D1k0Vvtj2u1P+JTRT9tx3AjtLEMmfVBQ== - -esbuild-linux-mips64le@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.39.tgz#6f3bf3023f711084e5a1e8190487d2020f39f0f7" - integrity sha512-epwlYgVdbmkuRr5n4es3B+yDI0I2e/nxhKejT9H0OLxFAlMkeQZxSpxATpDc9m8NqRci6Kwyb/SfmD1koG2Zuw== - -esbuild-linux-ppc64le@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.39.tgz#900e718a4ea3f6aedde8424828eeefdd4b48d4b9" - integrity sha512-W/5ezaq+rQiQBThIjLMNjsuhPHg+ApVAdTz2LvcuesZFMsJoQAW2hutoyg47XxpWi7aEjJGrkS26qCJKhRn3QQ== - -esbuild-linux-riscv64@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.39.tgz#dcbff622fa37047a75d2ff7a1d8d2949d80277e4" - integrity sha512-IS48xeokcCTKeQIOke2O0t9t14HPvwnZcy+5baG13Z1wxs9ZrC5ig5ypEQQh4QMKxURD5TpCLHw2W42CLuVZaA== - -esbuild-linux-s390x@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.39.tgz#3f725a7945b419406c99d93744b28552561dcdfd" - integrity sha512-zEfunpqR8sMomqXhNTFEKDs+ik7HC01m3M60MsEjZOqaywHu5e5682fMsqOlZbesEAAaO9aAtRBsU7CHnSZWyA== - -esbuild-netbsd-64@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.39.tgz#e10e40b6a765798b90d4eb85901cc85c8b7ff85e" - integrity sha512-Uo2suJBSIlrZCe4E0k75VDIFJWfZy+bOV6ih3T4MVMRJh1lHJ2UyGoaX4bOxomYN3t+IakHPyEoln1+qJ1qYaA== - -esbuild-openbsd-64@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.39.tgz#935ec143f75ce10bd9cdb1c87fee00287eb0edbc" - integrity sha512-secQU+EpgUPpYjJe3OecoeGKVvRMLeKUxSMGHnK+aK5uQM3n1FPXNJzyz1LHFOo0WOyw+uoCxBYdM4O10oaCAA== - -esbuild-sunos-64@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.39.tgz#0e7aa82b022a2e6d55b0646738b2582c2d72c3c0" - integrity sha512-qHq0t5gePEDm2nqZLb+35p/qkaXVS7oIe32R0ECh2HOdiXXkj/1uQI9IRogGqKkK+QjDG+DhwiUw7QoHur/Rwg== - -esbuild-windows-32@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.39.tgz#3f1538241f31b538545f4b5841b248cac260fa35" - integrity sha512-XPjwp2OgtEX0JnOlTgT6E5txbRp6Uw54Isorm3CwOtloJazeIWXuiwK0ONJBVb/CGbiCpS7iP2UahGgd2p1x+Q== - -esbuild-windows-64@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.39.tgz#b100c59f96d3c2da2e796e42fee4900d755d3e03" - integrity sha512-E2wm+5FwCcLpKsBHRw28bSYQw0Ikxb7zIMxw3OPAkiaQhLVr3dnVO8DofmbWhhf6b97bWzg37iSZ45ZDpLw7Ow== - -esbuild-windows-arm64@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.39.tgz#00268517e665b33c89778d61f144e4256b39f631" - integrity sha512-sBZQz5D+Gd0EQ09tZRnz/PpVdLwvp/ufMtJ1iDFYddDaPpZXKqPyaxfYBLs3ueiaksQ26GGa7sci0OqFzNs7KA== - -esbuild@0.14.39: - version "0.14.39" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.39.tgz#c926b2259fe6f6d3a94f528fb42e103c5a6d909a" - integrity sha512-2kKujuzvRWYtwvNjYDY444LQIA3TyJhJIX3Yo4+qkFlDDtGlSicWgeHVJqMUP/2sSfH10PGwfsj+O2ro1m10xQ== +esbuild-linux-32@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.45.tgz#0f814a784c941bfd5a5dca7ff742b57744c5324b" + integrity sha512-Qk9cCO3PJig/Y+SdslN/Th4pbAjgaH9oUjVH28eMsLTPf6QDUuK6EED91jepJdR3vxhcnVjyl6JqtOWmP+uxCg== + +esbuild-linux-64@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.45.tgz#2028a6dfb3fea7a5dd5e87cf00d02a36a3373e3c" + integrity sha512-IybO2ugqvc/Zzn1Kih3x0FVjYAy3GTCwhtcp91dbdqk3wPqxYCzObYspa8ca0s+OovI0Cnb+rhXrUtq8gBqlqw== + +esbuild-linux-arm64@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.45.tgz#6fef4165583215918d45150148ac0a18d5ef6323" + integrity sha512-UNEyuHTwztrkEU/+mWIxGzKrYBo2cEtjYAZQVZB1kliANKgRITktg2miaO/b/VtNe84ob1aXSvW8XOPEn5RTGQ== + +esbuild-linux-arm@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.45.tgz#e454f701ae5371322c8265f4356e098611e51f96" + integrity sha512-qKWJ4A4TAcxXV2TBLPw3Av5X2SYNfyNnBHNJTQJ5VuevK6Aq5i6XEMvUgdlcVuZ9MYPfS5aJZWglzDzJMf1Lpw== + +esbuild-linux-mips64le@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.45.tgz#093cc86be71f9b0cd2876da23ee2f5d35b09a247" + integrity sha512-s/jcfw3Vpku5dIVSFVY7idJsGdIpIJ88IrkyprVgCG2yBeXatb67B7yIt0E1tL+OHrJJdNBw6GikCiMPAAu1VA== + +esbuild-linux-ppc64le@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.45.tgz#8bddbf67157027e917b6e5b04230d7cf14d62cf2" + integrity sha512-lJItl6ESZnhSx951U9R7MTBopgwIELHlQzC6SBR023V5JC1rPRFDZ/UEBsV+7BFcCAfqlyb+odGEAmcBSf4XCA== + +esbuild-linux-riscv64@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.45.tgz#0f234f63595159cfc976fd5310e7902e8b5a7ad7" + integrity sha512-8anMu+QLl9MununVCGJN2I/JvUWPm1EVsBBLq/J+Nz4hr8t6QOCuEp0HRaeMohyl2XiMFBchVu0mwa05rF7GFQ== + +esbuild-linux-s390x@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.45.tgz#272288e0f30e4fa0f01d6df6572602c7df78c256" + integrity sha512-1TPeNvNCoahMw745KNTA6POKaFfSqQrBb3fdOL82GXZqyKU/6rHNwGP0NgHe88bAUMp3QZfjGfCGKxfBHL77RQ== + +esbuild-netbsd-64@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.45.tgz#0f9af25773e025cd1ca5e22277661cb15d36b406" + integrity sha512-55f2eZ8EQhhOZosqX0mApgRoI9PrVyXlHd9Uivk+B0B4WTKUgzkoHaVk4EkIUtNRQTpDWPciTlpb/C2tUYVejA== + +esbuild-openbsd-64@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.45.tgz#6d47a564eb4b80d9e3aeb4493918b36d17440228" + integrity sha512-Z5sNcT3oN9eryMW3mGn5HAgg7XCxiUS4isqH1tZXpsdOdOESbgbTEP0mBEJU0WU7Vt2gIN5XMbAp7Oigm0k71g== + +esbuild-sunos-64@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.45.tgz#cca97d7d31214bddfa1970386883d1f3f2868cec" + integrity sha512-WmWu4wAm8mIxxK9aWFCj5VHunY3BHQDT3dAPexMLLszPyMF7RDtUYf+Dash9tjyitvnoxPAvR7DpWpirDLQIlQ== + +esbuild-windows-32@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.45.tgz#afd98449fd6a741a4655e3e4f0e5ae7081765baf" + integrity sha512-DPPehKwPJFBoSG+jILc/vcJNN8pTwz1m6FWojxqtqPhgw8OabTgN4vL7gNMqL/FSeDxF+zyvZeeMrZFYF1d81Q== + +esbuild-windows-64@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.45.tgz#5d284b712b119c6306ac5f32e2a611ac6eec2842" + integrity sha512-t6bxFZcp9bLmSs+1pCNL/BW2bq3QEQHxU4HoiMEyWfF8QBU8iNXFI1iLGdyCzB1Ue2739h55tpOvojFrfyNPWA== + +esbuild-windows-arm64@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.45.tgz#08089d4cc921939ed352d9c2d928b5d867a6dc67" + integrity sha512-DnhrvjECBJ2L0owoznPb4RqQKZ498SM8J+YHqmqzi0Gf/enkUwwTjB8vPCK6dDuFnNU/NE8f94FhKdkBHYruDQ== + +esbuild@0.14.45: + version "0.14.45" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.45.tgz#3e3192894f91c32cf19207726f136278be46e968" + integrity sha512-JOxGUD8jcs8xE8DjyGWC8by/vLMCXTJ/wuauWipL5kJRZx1dhpqIntb31QHjA45GZJWaXv7SjC/Zwu1bCkXWtQ== optionalDependencies: - esbuild-android-64 "0.14.39" - esbuild-android-arm64 "0.14.39" - esbuild-darwin-64 "0.14.39" - esbuild-darwin-arm64 "0.14.39" - esbuild-freebsd-64 "0.14.39" - esbuild-freebsd-arm64 "0.14.39" - esbuild-linux-32 "0.14.39" - esbuild-linux-64 "0.14.39" - esbuild-linux-arm "0.14.39" - esbuild-linux-arm64 "0.14.39" - esbuild-linux-mips64le "0.14.39" - esbuild-linux-ppc64le "0.14.39" - esbuild-linux-riscv64 "0.14.39" - esbuild-linux-s390x "0.14.39" - esbuild-netbsd-64 "0.14.39" - esbuild-openbsd-64 "0.14.39" - esbuild-sunos-64 "0.14.39" - esbuild-windows-32 "0.14.39" - esbuild-windows-64 "0.14.39" - esbuild-windows-arm64 "0.14.39" + esbuild-android-64 "0.14.45" + esbuild-android-arm64 "0.14.45" + esbuild-darwin-64 "0.14.45" + esbuild-darwin-arm64 "0.14.45" + esbuild-freebsd-64 "0.14.45" + esbuild-freebsd-arm64 "0.14.45" + esbuild-linux-32 "0.14.45" + esbuild-linux-64 "0.14.45" + esbuild-linux-arm "0.14.45" + esbuild-linux-arm64 "0.14.45" + esbuild-linux-mips64le "0.14.45" + esbuild-linux-ppc64le "0.14.45" + esbuild-linux-riscv64 "0.14.45" + esbuild-linux-s390x "0.14.45" + esbuild-netbsd-64 "0.14.45" + esbuild-openbsd-64 "0.14.45" + esbuild-sunos-64 "0.14.45" + esbuild-windows-32 "0.14.45" + esbuild-windows-64 "0.14.45" + esbuild-windows-arm64 "0.14.45" escalade@^3.1.1: version "3.1.1" From 4a792575b3cf0db4a0f015111547c2e5660be0d0 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Fri, 17 Jun 2022 15:06:06 -0500 Subject: [PATCH 02/15] use worker for calc --- .../services/AlertMetrics/AlertMetrics.tsx | 215 +++--------------- .../services/AlertMetrics/AlertMetricsCSV.tsx | 56 ----- .../AlertMetrics/AlertMetricsTable.tsx | 34 ++- .../app/services/AlertMetrics/useAlertCSV.ts | 55 +++++ .../services/AlertMetrics/useAlertMetrics.ts | 68 ++++++ .../app/services/AlertMetrics/useAlerts.ts | 137 +++++++++++ web/src/app/util/AppLink.tsx | 2 +- web/src/app/worker/index.ts | 49 ++++ web/src/app/worker/methods.ts | 5 + web/src/app/worker/worker.ts | 8 + 10 files changed, 381 insertions(+), 248 deletions(-) delete mode 100644 web/src/app/services/AlertMetrics/AlertMetricsCSV.tsx create mode 100644 web/src/app/services/AlertMetrics/useAlertCSV.ts create mode 100644 web/src/app/services/AlertMetrics/useAlertMetrics.ts create mode 100644 web/src/app/services/AlertMetrics/useAlerts.ts create mode 100644 web/src/app/worker/index.ts create mode 100644 web/src/app/worker/methods.ts create mode 100644 web/src/app/worker/worker.ts diff --git a/web/src/app/services/AlertMetrics/AlertMetrics.tsx b/web/src/app/services/AlertMetrics/AlertMetrics.tsx index f12f16acd6..1409663e29 100644 --- a/web/src/app/services/AlertMetrics/AlertMetrics.tsx +++ b/web/src/app/services/AlertMetrics/AlertMetrics.tsx @@ -1,12 +1,5 @@ -import React, { - useMemo, - useState, - useEffect, - useRef, - useDeferredValue, -} from 'react' +import React, { useMemo } from 'react' import { Card, CardContent, CardHeader, Grid } from '@mui/material' -import { gql, useClient } from 'urql' import { DateTime, Duration, Interval } from 'luxon' import { useURLParams } from '../../actions/hooks' import AlertMetricsFilter, { @@ -16,144 +9,18 @@ import AlertMetricsFilter, { import AlertCountGraph from './AlertCountGraph' import AlertMetricsTable from './AlertMetricsTable' import AlertAveragesGraph from './AlertAveragesGraph' -import { Alert } from '../../../schema' -import { GenericError } from '../../error-pages' +import { GenericError, ObjectNotFound } from '../../error-pages' import _ from 'lodash' - -const alertsQuery = gql` - query alerts($input: AlertSearchOptions!) { - alerts(input: $input) { - nodes { - id - alertID - summary - status - service { - name - id - } - createdAt - metrics { - closedAt - timeToClose - timeToAck - escalated - } - } - pageInfo { - hasNextPage - endCursor - } - } - } -` - -const QUERY_LIMIT = 100 +import { useWorker } from '../../worker' +import { AlertMetricsOpts, useAlertMetrics } from './useAlertMetrics' +import { useAlerts } from './useAlerts' +import { useQuery } from 'urql' +import Spinner from '../../loading/components/Spinner' export type AlertMetricsProps = { serviceID: string } -type AlertsData = { - alerts: Alert[] - loading: boolean - error: Error | undefined -} - -function useAlerts( - serviceID: string, - since: string, - until: string, - isValidRange: boolean, -): AlertsData { - const depKey = `${serviceID}-${since}-${until}` - const [alerts, setAlerts] = useState([]) - const [loading, setLoading] = useState(true) - const [error, setError] = useState() - const key = useRef(depKey) - key.current = depKey - const renderAlerts = useDeferredValue(alerts) - - useEffect(() => { - return () => { - // cancel on unmount - key.current = '' - } - }, []) - - const client = useClient() - const fetch = React.useCallback(async () => { - setAlerts([]) - setLoading(true) - setError(undefined) - if (!isValidRange) { - return - } - async function fetchAlerts( - cursor: string, - ): Promise<[Alert[], boolean, string, Error | undefined]> { - const q = await client - .query(alertsQuery, { - input: { - filterByServiceID: [serviceID], - first: QUERY_LIMIT, - notClosedBefore: since, - closedBefore: until, - filterByStatus: ['StatusClosed'], - after: cursor, - }, - }) - .toPromise() - - if (q.error) { - return [[], false, '', q.error] - } - - return [ - q.data.alerts.nodes, - q.data.alerts.pageInfo.hasNextPage, - q.data.alerts.pageInfo.endCursor, - undefined, - ] - } - - const throttledSetAlerts = _.throttle(setAlerts, 1000) - - let [alerts, hasNextPage, endCursor, error] = await fetchAlerts('') - if (key.current !== depKey) return // abort if the key has changed - if (error) { - setError(error) - throttledSetAlerts.cancel() - return - } - let allAlerts = alerts - setAlerts(allAlerts) - while (hasNextPage) { - ;[alerts, hasNextPage, endCursor, error] = await fetchAlerts(endCursor) - if (key.current !== depKey) return // abort if the key has changed - if (error) { - setError(error) - throttledSetAlerts.cancel() - return - } - allAlerts = allAlerts.concat(alerts) - throttledSetAlerts(allAlerts) - } - - setLoading(false) - }, [depKey]) - - useEffect(() => { - fetch() - }, [depKey]) - - return { - alerts: renderAlerts, - loading, - error, - } -} - export default function AlertMetrics({ serviceID, }: AlertMetricsProps): JSX.Element { @@ -161,6 +28,11 @@ export default function AlertMetrics({ const minDate = now.minus({ days: MAX_DAY_COUNT - 1 }).startOf('day') const maxDate = now.endOf('day') + const [svc] = useQuery({ + query: 'query Svc($id: ID!) {service(id:$id){id,name}}', + variables: { id: serviceID }, + }) + const [params] = useURLParams({ since: minDate.toFormat(DATE_FORMAT), until: maxDate.toFormat(DATE_FORMAT), @@ -182,7 +54,20 @@ export default function AlertMetrics({ until.toISO(), isValidRange, ) + const graphInterval = Interval.fromDateTimes(since, until).toISO() + const graphDur = Duration.fromObject({ days: 1 }).toISO() + // useMemo to use same object reference + const metricsOpts: AlertMetricsOpts = useMemo( + () => ({ int: graphInterval, dur: graphDur, alerts: alertsData.alerts }), + [graphInterval, graphDur, alertsData.alerts], + ) + + const useAlertMetricsFn = useWorker(useAlertMetrics) + const graphData = useAlertMetricsFn(metricsOpts) + + if (svc.fetching) return + if (!svc.data?.service?.name) return if (!isValidRange) { return } @@ -191,48 +76,6 @@ export default function AlertMetrics({ return } - const ivl = Interval.fromDateTimes(since, until) - - const graphData = ivl.splitBy({ days: 1 }).map((i) => { - const date = i.start.toLocaleString({ month: 'short', day: 'numeric' }) - const label = i.start.toLocaleString({ - month: 'short', - day: 'numeric', - year: 'numeric', - }) - - const bucket = alertsData.alerts.filter((a) => - i.contains(DateTime.fromISO(a.metrics?.closedAt as string)), - ) - - const escalatedCount = bucket.filter((a) => a.metrics?.escalated).length - - return { - date, - label, - count: bucket.length, - nonEscalatedCount: bucket.length - escalatedCount, - escalatedCount, - - // get average of a.metrics.timeToClose values - avgTimeToClose: bucket.length - ? bucket.reduce((acc, a) => { - if (!a.metrics?.timeToClose) return acc - const timeToClose = Duration.fromISO(a.metrics.timeToClose) - return acc + Math.ceil(timeToClose.as('minutes')) - }, 0) / bucket.length - : 0, - - avgTimeToAck: bucket.length - ? bucket.reduce((acc, a) => { - if (!a.metrics?.timeToAck) return acc - const timeToAck = Duration.fromISO(a.metrics.timeToAck) - return acc + Math.ceil(timeToAck.as('minutes')) - }, 0) / bucket.length - : 0, - } - }) - const daycount = Math.floor(now.diff(since, 'days').plus({ day: 1 }).days) return ( @@ -248,10 +91,10 @@ export default function AlertMetrics({ ({ - ...a, - ...a.metrics, - }))} + alerts={alertsData.alerts} + serviceName={svc.data.service.name} + startTime={since.toFormat('yyyy-MM-dd')} + endTime={until.toFormat('yyyy-MM-dd')} loading={alertsData.loading} /> diff --git a/web/src/app/services/AlertMetrics/AlertMetricsCSV.tsx b/web/src/app/services/AlertMetrics/AlertMetricsCSV.tsx deleted file mode 100644 index 82aaca2491..0000000000 --- a/web/src/app/services/AlertMetrics/AlertMetricsCSV.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import React from 'react' -import { Button } from '@mui/material' -import DownloadIcon from '@mui/icons-material/Download' -import { CSVLink } from 'react-csv' -import { Alert } from '../../../schema' -import { DateTime, Duration } from 'luxon' - -interface AlertMetricsCSVProps { - alerts: Alert[] -} - -export default function AlertMetricsCSV( - props: AlertMetricsCSVProps, -): JSX.Element { - const zoneAbbr = DateTime.local().toFormat('ZZZZ Z') - - // Note: the data object is ordered - const data = props.alerts.map((a) => ({ - [`createdAt (${zoneAbbr})`]: DateTime.fromISO(a.createdAt).toLocal().toSQL({ - includeOffset: false, - }), - [`closedAt (${zoneAbbr})`]: DateTime.fromISO(a.metrics?.closedAt as string) - .toLocal() - .toSQL({ - includeOffset: false, - }), - timeToAck: Duration.fromISO(a.metrics?.timeToAck as string).toFormat( - 'hh:mm:ss', - ), - timeToClose: Duration.fromISO(a.metrics?.timeToClose as string).toFormat( - 'hh:mm:ss', - ), - alertID: a.alertID, - escalated: (a.metrics?.escalated as boolean).toString(), - status: a.status.replace('Status', ''), - summary: a.summary, - details: a.details, - serviceID: a.service?.id, - serviceName: a.service?.name, - })) - - const getFileName = (): string => { - if (props.alerts.length) { - return 'GoAlert_Alert_Metrics[' + props.alerts[0].service?.name + '].csv' - } - return 'GoAlert_Alert_Metrics.csv' - } - - return ( - - - - ) -} diff --git a/web/src/app/services/AlertMetrics/AlertMetricsTable.tsx b/web/src/app/services/AlertMetrics/AlertMetricsTable.tsx index e0ef56e9f5..98847c155c 100644 --- a/web/src/app/services/AlertMetrics/AlertMetricsTable.tsx +++ b/web/src/app/services/AlertMetrics/AlertMetricsTable.tsx @@ -1,4 +1,4 @@ -import React from 'react' +import React, { useMemo } from 'react' import { DataGrid, GridRenderCellParams, @@ -10,17 +10,20 @@ import { GridToolbarFilterButton, gridClasses, } from '@mui/x-data-grid' -import { Grid } from '@mui/material' +import { Button, Grid } from '@mui/material' import DownloadIcon from '@mui/icons-material/Download' import { makeStyles } from '@mui/styles' import { Alert } from '../../../schema' import { DateTime, Duration } from 'luxon' import AppLink from '../../util/AppLink' -import AlertMetricsCSV from './AlertMetricsCSV' +import { useAlertCSV } from './useAlertCSV' interface AlertMetricsTableProps { alerts: Alert[] loading: boolean + serviceName: string + startTime: string + endTime: string } const useStyles = makeStyles(() => ({ @@ -33,7 +36,15 @@ export default function AlertMetricsTable( props: AlertMetricsTableProps, ): JSX.Element { const classes = useStyles() - const { alerts } = props + const alerts = useMemo( + () => props.alerts.map((a) => ({ ...a, ...a.metrics })), + [props.alerts], + ) + const csvData = useAlertCSV(props.alerts) + const link = useMemo( + () => URL.createObjectURL(new Blob([csvData], { type: 'text/csv' })), + [csvData], + ) const columns = [ { @@ -136,7 +147,20 @@ export default function AlertMetricsTable( - + + + diff --git a/web/src/app/services/AlertMetrics/useAlertCSV.ts b/web/src/app/services/AlertMetrics/useAlertCSV.ts new file mode 100644 index 0000000000..787e95147a --- /dev/null +++ b/web/src/app/services/AlertMetrics/useAlertCSV.ts @@ -0,0 +1,55 @@ +import { DateTime, Duration } from 'luxon' +import { Alert } from '../../../schema' +import { pathPrefix } from '../../env' + +function formatCSVField(data: string): string { + if (!/[,"\r\n]/.test(data)) return data + + return `"${data.replace(/"/g, '""')}"` +} + +export function useAlertCSV(alerts: Alert[]): string { + let data = '' + const zoneAbbr = DateTime.local().toFormat('ZZZZ Z') + const cols = [ + `Created At (${zoneAbbr})`, + `Closed At (${zoneAbbr})`, + `Time to Ack`, + `Time to Close`, + `Alert ID`, + `Escalated`, + `Status`, + `Summary`, + `Details`, + `Service Name`, + `Service URL`, + ] + data += cols.map(formatCSVField).join(',') + '\r\n' + + const rows = alerts.map( + (a) => + [ + DateTime.fromISO(a.createdAt).toLocal().toSQL({ + includeOffset: false, + }), + DateTime.fromISO(a.metrics?.closedAt as string) + .toLocal() + .toSQL({ + includeOffset: false, + }), + Duration.fromISO(a.metrics?.timeToAck as string).toFormat('hh:mm:ss'), + Duration.fromISO(a.metrics?.timeToClose as string).toFormat('hh:mm:ss'), + a.alertID.toString(), + (a.metrics?.escalated as boolean).toString(), + a.status.replace('Status', ''), + a.summary, + a.details, + a.service?.name || '', + `${location.origin}${pathPrefix}/services/${a.service?.id}`, + ] + .map(formatCSVField) + .join(',') + '\r\n', + ) + + return data + rows.join('') +} diff --git a/web/src/app/services/AlertMetrics/useAlertMetrics.ts b/web/src/app/services/AlertMetrics/useAlertMetrics.ts new file mode 100644 index 0000000000..c9f51082fa --- /dev/null +++ b/web/src/app/services/AlertMetrics/useAlertMetrics.ts @@ -0,0 +1,68 @@ +import { DateTime, Duration, Interval } from 'luxon' +import { Alert } from '../../../schema' + +export type AlertMetricPoint = { + date: string + label: string + count: number + nonEscalatedCount: number + escalatedCount: number + avgTimeToClose: number + avgTimeToAck: number +} + +export type AlertMetricsOpts = { + int: string // iso-formatted interval + dur: string // iso-formatted duration + alerts: Alert[] +} + +export function useAlertMetrics(opts: AlertMetricsOpts): AlertMetricPoint[] { + let alerts = opts.alerts + return Interval.fromISO(opts.int) + .splitBy(Duration.fromISO(opts.dur)) + .map((i) => { + const date = i.start.toLocaleString({ + month: 'short', + day: 'numeric', + }) + const label = i.start.toLocaleString({ + month: 'short', + day: 'numeric', + year: 'numeric', + }) + + const nextIvl = alerts.findIndex( + (a) => !i.contains(DateTime.fromISO(a.metrics?.closedAt as string)), + ) + const bucket = nextIvl === -1 ? alerts : alerts.slice(0, nextIvl) + alerts = alerts.slice(nextIvl) + + const escalatedCount = bucket.filter((a) => a.metrics?.escalated).length + + return { + date, + label, + count: bucket.length, + nonEscalatedCount: bucket.length - escalatedCount, + escalatedCount, + + // get average of a.metrics.timeToClose values + avgTimeToClose: bucket.length + ? bucket.reduce((acc, a) => { + if (!a.metrics?.timeToClose) return acc + const timeToClose = Duration.fromISO(a.metrics.timeToClose) + return acc + Math.ceil(timeToClose.as('minutes')) + }, 0) / bucket.length + : 0, + + avgTimeToAck: bucket.length + ? bucket.reduce((acc, a) => { + if (!a.metrics?.timeToAck) return acc + const timeToAck = Duration.fromISO(a.metrics.timeToAck) + return acc + Math.ceil(timeToAck.as('minutes')) + }, 0) / bucket.length + : 0, + } + }) +} diff --git a/web/src/app/services/AlertMetrics/useAlerts.ts b/web/src/app/services/AlertMetrics/useAlerts.ts new file mode 100644 index 0000000000..a39ea9fb30 --- /dev/null +++ b/web/src/app/services/AlertMetrics/useAlerts.ts @@ -0,0 +1,137 @@ +import _ from 'lodash' +import React from 'react' +import { useEffect, useRef, useState } from 'react' +import { gql, useClient } from 'urql' +import { Alert } from '../../../schema' + +const alertsQuery = gql` + query alerts($input: AlertSearchOptions!) { + alerts(input: $input) { + nodes { + id + alertID + summary + status + service { + name + id + } + createdAt + metrics { + closedAt + timeToClose + timeToAck + escalated + } + } + pageInfo { + hasNextPage + endCursor + } + } + } +` + +const QUERY_LIMIT = 100 + +export type AlertsData = { + alerts: Alert[] + loading: boolean + error: Error | undefined +} + +export function useAlerts( + serviceID: string, + since: string, + until: string, + isValidRange: boolean, +): AlertsData { + const depKey = `${serviceID}-${since}-${until}` + const [alerts, setAlerts] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState() + const key = useRef(depKey) + key.current = depKey + + useEffect(() => { + return () => { + // cancel on unmount + key.current = '' + } + }, []) + + const client = useClient() + const fetch = React.useCallback(async () => { + setAlerts([]) + setLoading(true) + setError(undefined) + if (!isValidRange) { + return + } + async function fetchAlerts( + cursor: string, + ): Promise<[Alert[], boolean, string, Error | undefined]> { + const q = await client + .query(alertsQuery, { + input: { + filterByServiceID: [serviceID], + first: QUERY_LIMIT, + notClosedBefore: since, + closedBefore: until, + filterByStatus: ['StatusClosed'], + after: cursor, + }, + }) + .toPromise() + + if (q.error) { + return [[], false, '', q.error] + } + + return [ + q.data.alerts.nodes, + q.data.alerts.pageInfo.hasNextPage, + q.data.alerts.pageInfo.endCursor, + undefined, + ] + } + + const throttledSetAlerts = _.throttle( + (alerts) => setAlerts(_.sortBy(alerts, 'metrics.closedAt')), + 1000, + ) + + let [alerts, hasNextPage, endCursor, error] = await fetchAlerts('') + if (key.current !== depKey) return // abort if the key has changed + if (error) { + setError(error) + throttledSetAlerts.cancel() + return + } + let allAlerts = alerts + throttledSetAlerts(allAlerts) + while (hasNextPage) { + ;[alerts, hasNextPage, endCursor, error] = await fetchAlerts(endCursor) + if (key.current !== depKey) return // abort if the key has changed + if (error) { + setError(error) + throttledSetAlerts.cancel() + return + } + allAlerts = allAlerts.concat(alerts) + throttledSetAlerts(allAlerts) + } + + setLoading(false) + }, [depKey]) + + useEffect(() => { + fetch() + }, [depKey]) + + return { + alerts, + loading, + error, + } +} diff --git a/web/src/app/util/AppLink.tsx b/web/src/app/util/AppLink.tsx index 0047ba5b8d..471e38c3e8 100644 --- a/web/src/app/util/AppLink.tsx +++ b/web/src/app/util/AppLink.tsx @@ -41,7 +41,7 @@ const AppLink: ForwardRefRenderFunction = other.rel = 'noopener noreferrer' } - const external = /^(tel:|mailto:|https?:\/\/)/.test(to) + const external = /^(tel:|mailto:|blob:|https?:\/\/)/.test(to) // handle relative URLs if (!external && !to.startsWith('/')) { diff --git a/web/src/app/worker/index.ts b/web/src/app/worker/index.ts new file mode 100644 index 0000000000..ebe2b62ddc --- /dev/null +++ b/web/src/app/worker/index.ts @@ -0,0 +1,49 @@ +import { useDeferredValue, useEffect, useMemo, useState } from 'react' +import _ from 'lodash' +import { pathPrefix } from '../env' +import methods from './methods' + +interface Nameable { + name: string +} + +export function useWorker(method: string | Nameable): (arg: any) => any | null { + const methodName = typeof method === 'string' ? method : method.name + if (!(methodName in methods)) { + throw new Error(`method must be a valid method from app/worker/methods.ts`) + } + // fallback to a simple memo if workers are unsupported + if (!window.Worker) + return useMemo( + () => + function doWork(arg: any) { + const _arg = useDeferredValue(arg) + return useMemo(() => methods[methodName](_arg), [_arg]) + }, + [methodName], + ) + + const [worker, setWorker] = useState() + const [result, setResult] = useState(null) + + useEffect(() => { + const w = new Worker(`${pathPrefix}/static/worker.js`) + w.onmessage = (e) => { + setResult(e.data) + } + setWorker(w) + return () => w.terminate() + }, [methodName]) + + return useMemo( + () => + function doWork(arg: any) { + useMemo(() => { + if (worker) worker.postMessage({ method: methodName, arg }) + }, [worker, arg]) + + return result + }, + [worker, methodName, result], + ) +} diff --git a/web/src/app/worker/methods.ts b/web/src/app/worker/methods.ts new file mode 100644 index 0000000000..b2949bd9a8 --- /dev/null +++ b/web/src/app/worker/methods.ts @@ -0,0 +1,5 @@ +import { useAlertMetrics } from '../services/AlertMetrics/useAlertMetrics' + +export default { + useAlertMetrics, +} as Record any> diff --git a/web/src/app/worker/worker.ts b/web/src/app/worker/worker.ts new file mode 100644 index 0000000000..89d76fa231 --- /dev/null +++ b/web/src/app/worker/worker.ts @@ -0,0 +1,8 @@ +import _ from 'lodash' + +import methods from './methods' + +self.onmessage = (e) => { + const result = methods[e.data.method](e.data.arg) + self.postMessage(result) +} From 74a9887d7fe7483114bc48ef4a2f3b69b39c6ce1 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Tue, 21 Jun 2022 12:09:00 -0500 Subject: [PATCH 03/15] up query limit --- alert/search.go | 2 +- graphql2/graphqlapp/alert.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/alert/search.go b/alert/search.go index 11f0373c0b..6148fdfda7 100644 --- a/alert/search.go +++ b/alert/search.go @@ -171,7 +171,7 @@ func (opts renderData) Normalize() (*renderData, error) { err := validate.Many( validate.Search("Search", opts.Search), - validate.Range("Limit", opts.Limit, 0, search.MaxResults), + validate.Range("Limit", opts.Limit, 0, 1001), validate.Range("Status", len(opts.Status), 0, 3), validate.ManyUUID("Services", opts.ServiceFilter.IDs, 50), validate.Range("Omit", len(opts.Omit), 0, 50), diff --git a/graphql2/graphqlapp/alert.go b/graphql2/graphqlapp/alert.go index d366b2e1d9..ad9624dcc5 100644 --- a/graphql2/graphqlapp/alert.go +++ b/graphql2/graphqlapp/alert.go @@ -294,7 +294,7 @@ func (q *Query) Alerts(ctx context.Context, opts *graphql2.AlertSearchOptions) ( err = validate.Many( validate.Range("ServiceIDs", len(opts.FilterByServiceID), 0, 50), - validate.Range("First", s.Limit, 1, 100), + validate.Range("First", s.Limit, 1, 1000), ) if err != nil { return nil, err From 74a2754966ef0a9c2136caebc6474b2b88033e5b Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Tue, 21 Jun 2022 12:09:31 -0500 Subject: [PATCH 04/15] update params --- .../services/AlertMetrics/AlertMetrics.tsx | 37 +++---------- .../AlertMetrics/AlertMetricsFilter.tsx | 52 +++++++++---------- 2 files changed, 34 insertions(+), 55 deletions(-) diff --git a/web/src/app/services/AlertMetrics/AlertMetrics.tsx b/web/src/app/services/AlertMetrics/AlertMetrics.tsx index 1409663e29..07238df640 100644 --- a/web/src/app/services/AlertMetrics/AlertMetrics.tsx +++ b/web/src/app/services/AlertMetrics/AlertMetrics.tsx @@ -1,7 +1,7 @@ import React, { useMemo } from 'react' import { Card, CardContent, CardHeader, Grid } from '@mui/material' import { DateTime, Duration, Interval } from 'luxon' -import { useURLParams } from '../../actions/hooks' +import { useURLParam, useURLParams } from '../../actions/hooks' import AlertMetricsFilter, { DATE_FORMAT, MAX_DAY_COUNT, @@ -25,37 +25,20 @@ export default function AlertMetrics({ serviceID, }: AlertMetricsProps): JSX.Element { const now = useMemo(() => DateTime.now(), []) - const minDate = now.minus({ days: MAX_DAY_COUNT - 1 }).startOf('day') - const maxDate = now.endOf('day') const [svc] = useQuery({ query: 'query Svc($id: ID!) {service(id:$id){id,name}}', variables: { id: serviceID }, }) + const [range] = useURLParam('range', 'P1M') + const [ivl] = useURLParam('interval', 'P1D') - const [params] = useURLParams({ - since: minDate.toFormat(DATE_FORMAT), - until: maxDate.toFormat(DATE_FORMAT), - }) - - const since = DateTime.fromFormat(params.since, DATE_FORMAT).startOf('day') - const until = DateTime.fromFormat(params.until, DATE_FORMAT).endOf('day') - - const isValidRange = - since >= minDate && - until >= minDate && - since <= maxDate && - until <= maxDate && - since <= until + const since = now.minus(Duration.fromISO(range)).startOf('day') + const until = now.endOf('day') - const alertsData = useAlerts( - serviceID, - since.toISO(), - until.toISO(), - isValidRange, - ) + const alertsData = useAlerts(serviceID, since.toISO(), until.toISO(), true) const graphInterval = Interval.fromDateTimes(since, until).toISO() - const graphDur = Duration.fromObject({ days: 1 }).toISO() + const graphDur = Duration.fromISO(ivl).toISO() // useMemo to use same object reference const metricsOpts: AlertMetricsOpts = useMemo( @@ -63,14 +46,10 @@ export default function AlertMetrics({ [graphInterval, graphDur, alertsData.alerts], ) - const useAlertMetricsFn = useWorker(useAlertMetrics) - const graphData = useAlertMetricsFn(metricsOpts) + const graphData = useWorker(useAlertMetrics, metricsOpts, []) if (svc.fetching) return if (!svc.data?.service?.name) return - if (!isValidRange) { - return - } if (alertsData.error) { return diff --git a/web/src/app/services/AlertMetrics/AlertMetricsFilter.tsx b/web/src/app/services/AlertMetrics/AlertMetricsFilter.tsx index 3b7088c52e..3f38297b9d 100644 --- a/web/src/app/services/AlertMetrics/AlertMetricsFilter.tsx +++ b/web/src/app/services/AlertMetrics/AlertMetricsFilter.tsx @@ -20,24 +20,8 @@ export const DATE_FORMAT = 'y-MM-dd' export default function AlertMetricsFilter({ now, }: AlertMetricsFilterProps): JSX.Element { - const [since, setSince] = useURLParam('since', '') - - const dateRangeValue = since - ? Math.ceil( - now.diff(DateTime.fromFormat(since, DATE_FORMAT), 'weeks').weeks, - ) - : MAX_DAY_COUNT / 7 // default - - const handleDateRangeChange = (e: SelectChangeEvent): void => { - const weeks = e.target.value as number - setSince( - now - .minus({ weeks }) - .plus({ days: 1 }) - .startOf('day') - .toFormat(DATE_FORMAT), - ) - } + const [range, setRange] = useURLParam('range', 'P1M') + const [ivl, setIvl] = useURLParam('interval', 'P1D') return ( @@ -48,17 +32,33 @@ export default function AlertMetricsFilter({ + + + Interval + From 4e0dad9cd86e5cee04253b282d034e7ce008f11a Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Tue, 21 Jun 2022 12:10:05 -0500 Subject: [PATCH 05/15] add worker and hooks --- .../AlertMetrics/AlertMetricsTable.tsx | 12 ++- .../app/services/AlertMetrics/useAlertCSV.ts | 10 +- .../app/services/AlertMetrics/useAlerts.ts | 16 ++-- web/src/app/worker/index.ts | 92 +++++++++++-------- web/src/app/worker/methods.ts | 2 + 5 files changed, 84 insertions(+), 48 deletions(-) diff --git a/web/src/app/services/AlertMetrics/AlertMetricsTable.tsx b/web/src/app/services/AlertMetrics/AlertMetricsTable.tsx index 98847c155c..3214821cfa 100644 --- a/web/src/app/services/AlertMetrics/AlertMetricsTable.tsx +++ b/web/src/app/services/AlertMetrics/AlertMetricsTable.tsx @@ -17,6 +17,8 @@ import { Alert } from '../../../schema' import { DateTime, Duration } from 'luxon' import AppLink from '../../util/AppLink' import { useAlertCSV } from './useAlertCSV' +import { useWorker } from '../../worker' +import { pathPrefix } from '../../env' interface AlertMetricsTableProps { alerts: Alert[] @@ -40,7 +42,15 @@ export default function AlertMetricsTable( () => props.alerts.map((a) => ({ ...a, ...a.metrics })), [props.alerts], ) - const csvData = useAlertCSV(props.alerts) + + const csvOpts = useMemo( + () => ({ + urlPrefix: location.origin + pathPrefix, + alerts: alerts, + }), + [props.alerts], + ) + const csvData = useWorker(useAlertCSV, csvOpts, '') const link = useMemo( () => URL.createObjectURL(new Blob([csvData], { type: 'text/csv' })), [csvData], diff --git a/web/src/app/services/AlertMetrics/useAlertCSV.ts b/web/src/app/services/AlertMetrics/useAlertCSV.ts index 787e95147a..7872827c7f 100644 --- a/web/src/app/services/AlertMetrics/useAlertCSV.ts +++ b/web/src/app/services/AlertMetrics/useAlertCSV.ts @@ -1,6 +1,5 @@ import { DateTime, Duration } from 'luxon' import { Alert } from '../../../schema' -import { pathPrefix } from '../../env' function formatCSVField(data: string): string { if (!/[,"\r\n]/.test(data)) return data @@ -8,7 +7,12 @@ function formatCSVField(data: string): string { return `"${data.replace(/"/g, '""')}"` } -export function useAlertCSV(alerts: Alert[]): string { +export type useAlertCSVOpts = { + alerts: Alert[] + urlPrefix: string +} + +export function useAlertCSV({ urlPrefix, alerts }: useAlertCSVOpts): string { let data = '' const zoneAbbr = DateTime.local().toFormat('ZZZZ Z') const cols = [ @@ -45,7 +49,7 @@ export function useAlertCSV(alerts: Alert[]): string { a.summary, a.details, a.service?.name || '', - `${location.origin}${pathPrefix}/services/${a.service?.id}`, + `${urlPrefix}/services/${a.service?.id}`, ] .map(formatCSVField) .join(',') + '\r\n', diff --git a/web/src/app/services/AlertMetrics/useAlerts.ts b/web/src/app/services/AlertMetrics/useAlerts.ts index a39ea9fb30..83736c6354 100644 --- a/web/src/app/services/AlertMetrics/useAlerts.ts +++ b/web/src/app/services/AlertMetrics/useAlerts.ts @@ -32,7 +32,7 @@ const alertsQuery = gql` } ` -const QUERY_LIMIT = 100 +const QUERY_LIMIT = 1000 export type AlertsData = { alerts: Alert[] @@ -96,10 +96,10 @@ export function useAlerts( ] } - const throttledSetAlerts = _.throttle( - (alerts) => setAlerts(_.sortBy(alerts, 'metrics.closedAt')), - 1000, - ) + const throttledSetAlerts = _.throttle((alerts, loading) => { + setAlerts(_.sortBy(alerts, 'metrics.closedAt')) + setLoading(loading) + }, 3000) let [alerts, hasNextPage, endCursor, error] = await fetchAlerts('') if (key.current !== depKey) return // abort if the key has changed @@ -109,7 +109,7 @@ export function useAlerts( return } let allAlerts = alerts - throttledSetAlerts(allAlerts) + throttledSetAlerts(allAlerts, true) while (hasNextPage) { ;[alerts, hasNextPage, endCursor, error] = await fetchAlerts(endCursor) if (key.current !== depKey) return // abort if the key has changed @@ -119,10 +119,10 @@ export function useAlerts( return } allAlerts = allAlerts.concat(alerts) - throttledSetAlerts(allAlerts) + throttledSetAlerts(allAlerts, true) } - setLoading(false) + throttledSetAlerts(allAlerts, false) }, [depKey]) useEffect(() => { diff --git a/web/src/app/worker/index.ts b/web/src/app/worker/index.ts index ebe2b62ddc..4e5d5bab49 100644 --- a/web/src/app/worker/index.ts +++ b/web/src/app/worker/index.ts @@ -1,49 +1,69 @@ -import { useDeferredValue, useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import _ from 'lodash' import { pathPrefix } from '../env' import methods from './methods' -interface Nameable { - name: string +type NextRun = { + arg: any + method: string } -export function useWorker(method: string | Nameable): (arg: any) => any | null { - const methodName = typeof method === 'string' ? method : method.name - if (!(methodName in methods)) { +class Runner { + private worker: Worker | null = null + private next: NextRun | null = null + private onChange: (result: V) => void = () => {} + private isBusy: boolean = false + + private _send = () => { + if (!this.next) return + if (!this.worker) { + this.worker = new Worker(`${pathPrefix}/static/worker.js`) + this.worker.onmessage = (e) => { + this.isBusy = false + this.onChange(e.data) + this._send() + } + } + if (this.isBusy) return + this.worker.postMessage(this.next) + this.isBusy = true + this.next = null + } + + run = (method: string, arg: T, onChange: (result: V) => void) => { + this.onChange = onChange + this.next = { method, arg } + this._send() + } + + shutdown = () => { + if (!this.worker) return + this.worker.terminate() + this.worker = null + } +} + +export function useWorker(method: (arg: T) => V, arg: T, def: V): V { + if (!(method.name in methods)) { throw new Error(`method must be a valid method from app/worker/methods.ts`) } + // fallback to a simple memo if workers are unsupported - if (!window.Worker) - return useMemo( - () => - function doWork(arg: any) { - const _arg = useDeferredValue(arg) - return useMemo(() => methods[methodName](_arg), [_arg]) - }, - [methodName], - ) - - const [worker, setWorker] = useState() - const [result, setResult] = useState(null) + if (!window.Worker) return useMemo(() => method(arg), [arg]) + + const [result, setResult] = useState(def) + const [worker, setWorker] = useState | null>(null) useEffect(() => { - const w = new Worker(`${pathPrefix}/static/worker.js`) - w.onmessage = (e) => { - setResult(e.data) - } + const w = new Runner() setWorker(w) - return () => w.terminate() - }, [methodName]) - - return useMemo( - () => - function doWork(arg: any) { - useMemo(() => { - if (worker) worker.postMessage({ method: methodName, arg }) - }, [worker, arg]) - - return result - }, - [worker, methodName, result], - ) + return w.shutdown + }, []) + + useMemo(() => { + if (!worker) return + worker.run(method.name, arg, setResult) + }, [worker, arg]) + + return result } diff --git a/web/src/app/worker/methods.ts b/web/src/app/worker/methods.ts index b2949bd9a8..1ccef7751c 100644 --- a/web/src/app/worker/methods.ts +++ b/web/src/app/worker/methods.ts @@ -1,5 +1,7 @@ +import { useAlertCSV } from '../services/AlertMetrics/useAlertCSV' import { useAlertMetrics } from '../services/AlertMetrics/useAlertMetrics' export default { useAlertMetrics, + useAlertCSV, } as Record any> From 7e4dbf6607782ebbd635eb07d0eb6e77315316c2 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Tue, 21 Jun 2022 12:10:15 -0500 Subject: [PATCH 06/15] remove react-csv --- web/src/package.json | 2 -- yarn.lock | 12 ------------ 2 files changed, 14 deletions(-) diff --git a/web/src/package.json b/web/src/package.json index 792a31757d..4a51c649ec 100644 --- a/web/src/package.json +++ b/web/src/package.json @@ -49,7 +49,6 @@ "react-big-calendar": "0.40.0", "react-colorful": "5.5.1", "react-countdown": "2.3.2", - "react-csv": "2.2.1", "react-dom": "18.1.0", "react-infinite-scroll-component": "6.1.0", "react-markdown": "8.0.0", @@ -71,7 +70,6 @@ "@types/react": "18.0.5", "@types/react-beautiful-dnd": "13.1.1", "@types/react-big-calendar": "0.38.0", - "@types/react-csv": "1.1.2", "@types/react-dom": "18.0.1", "chance": "1.1.7", "cypress": "9.7.0", diff --git a/yarn.lock b/yarn.lock index 0ecedb24eb..70ed839be2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1410,13 +1410,6 @@ "@types/prop-types" "*" "@types/react" "*" -"@types/react-csv@1.1.2": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@types/react-csv/-/react-csv-1.1.2.tgz#a5694d7e5cbf4bc1d4baa178a3fa7ac3466ea8c5" - integrity sha512-hCtZyXAubxBtn3Oi3I9kNAx2liRTaMtl1eWpO2M98aYkHuoSTbYO8OcZEjyr9aJJ30Xnoxj+uES3G6L6O1qgtg== - dependencies: - "@types/react" "*" - "@types/react-dom@18.0.1": version "18.0.1" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.1.tgz#cb3cc10ea91141b12c71001fede1017acfbce4db" @@ -6453,11 +6446,6 @@ react-countdown@2.3.2: dependencies: prop-types "^15.7.2" -react-csv@2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/react-csv/-/react-csv-2.2.1.tgz#2ec723dd3ce8069269bffb3a95d968302fc87740" - integrity sha512-QdImVwsFQIUcOti2dJFTLnxZ8dW/q+DDpjTmD1m1UVBxh2OaEwIBg7PSGA71m7GQEEoz8M5BfvEc1rd7q8rgPw== - react-dom@18.1.0: version "18.1.0" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.1.0.tgz#7f6dd84b706408adde05e1df575b3a024d7e8a2f" From 5d6134069d81adc7c8a940150ed88c7a32134455 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Tue, 21 Jun 2022 12:24:01 -0500 Subject: [PATCH 07/15] fix labels --- .../services/AlertMetrics/AlertMetrics.tsx | 28 +++++++++++-------- .../app/services/AlertMetrics/useAlerts.ts | 24 +++++++++------- 2 files changed, 30 insertions(+), 22 deletions(-) diff --git a/web/src/app/services/AlertMetrics/AlertMetrics.tsx b/web/src/app/services/AlertMetrics/AlertMetrics.tsx index 07238df640..3ad4d1d857 100644 --- a/web/src/app/services/AlertMetrics/AlertMetrics.tsx +++ b/web/src/app/services/AlertMetrics/AlertMetrics.tsx @@ -1,11 +1,8 @@ import React, { useMemo } from 'react' import { Card, CardContent, CardHeader, Grid } from '@mui/material' -import { DateTime, Duration, Interval } from 'luxon' -import { useURLParam, useURLParams } from '../../actions/hooks' -import AlertMetricsFilter, { - DATE_FORMAT, - MAX_DAY_COUNT, -} from './AlertMetricsFilter' +import { DateTime, DateTimeUnit, Duration, Interval } from 'luxon' +import { useURLParam } from '../../actions/hooks' +import AlertMetricsFilter from './AlertMetricsFilter' import AlertCountGraph from './AlertCountGraph' import AlertMetricsTable from './AlertMetricsTable' import AlertAveragesGraph from './AlertAveragesGraph' @@ -21,6 +18,12 @@ export type AlertMetricsProps = { serviceID: string } +const units: Record = { + P1D: 'day', + P1W: 'week', + P1M: 'month', +} + export default function AlertMetrics({ serviceID, }: AlertMetricsProps): JSX.Element { @@ -32,13 +35,14 @@ export default function AlertMetrics({ }) const [range] = useURLParam('range', 'P1M') const [ivl] = useURLParam('interval', 'P1D') + const graphDur = Duration.fromISO(ivl).toISO() - const since = now.minus(Duration.fromISO(range)).startOf('day') - const until = now.endOf('day') + const unit = units[ivl] + const since = now.minus(Duration.fromISO(range)).startOf(unit) + const until = now.startOf(unit) - const alertsData = useAlerts(serviceID, since.toISO(), until.toISO(), true) + const alertsData = useAlerts(serviceID, since.toISO(), until.toISO()) const graphInterval = Interval.fromDateTimes(since, until).toISO() - const graphDur = Duration.fromISO(ivl).toISO() // useMemo to use same object reference const metricsOpts: AlertMetricsOpts = useMemo( @@ -55,7 +59,7 @@ export default function AlertMetrics({ return } - const daycount = Math.floor(now.diff(since, 'days').plus({ day: 1 }).days) + const daycount = Math.ceil(until.diff(since, unit).as(unit)) return ( @@ -63,7 +67,7 @@ export default function AlertMetrics({ diff --git a/web/src/app/services/AlertMetrics/useAlerts.ts b/web/src/app/services/AlertMetrics/useAlerts.ts index 83736c6354..2dce40b624 100644 --- a/web/src/app/services/AlertMetrics/useAlerts.ts +++ b/web/src/app/services/AlertMetrics/useAlerts.ts @@ -1,5 +1,5 @@ import _ from 'lodash' -import React from 'react' +import React, { useLayoutEffect } from 'react' import { useEffect, useRef, useState } from 'react' import { gql, useClient } from 'urql' import { Alert } from '../../../schema' @@ -44,7 +44,7 @@ export function useAlerts( serviceID: string, since: string, until: string, - isValidRange: boolean, + pause?: boolean, ): AlertsData { const depKey = `${serviceID}-${since}-${until}` const [alerts, setAlerts] = useState([]) @@ -65,7 +65,7 @@ export function useAlerts( setAlerts([]) setLoading(true) setError(undefined) - if (!isValidRange) { + if (pause) { return } async function fetchAlerts( @@ -96,10 +96,14 @@ export function useAlerts( ] } - const throttledSetAlerts = _.throttle((alerts, loading) => { - setAlerts(_.sortBy(alerts, 'metrics.closedAt')) - setLoading(loading) - }, 3000) + const throttledSetAlerts = _.throttle( + (alerts, loading) => { + setAlerts(_.sortBy(alerts, 'metrics.closedAt')) + setLoading(loading) + }, + 3000, + { leading: true }, + ) let [alerts, hasNextPage, endCursor, error] = await fetchAlerts('') if (key.current !== depKey) return // abort if the key has changed @@ -123,11 +127,11 @@ export function useAlerts( } throttledSetAlerts(allAlerts, false) - }, [depKey]) + }, [depKey, pause]) - useEffect(() => { + useLayoutEffect(() => { fetch() - }, [depKey]) + }, [depKey, pause]) return { alerts, From dcd1ed00037ac7281ad242b61a27309c5472e3a4 Mon Sep 17 00:00:00 2001 From: Nathaniel Caza Date: Tue, 21 Jun 2022 14:45:31 -0500 Subject: [PATCH 08/15] cleanup --- .../AlertMetrics/AlertMetricsFilter.tsx | 2 + .../AlertMetrics/AlertMetricsTable.tsx | 182 +++++++++--------- 2 files changed, 93 insertions(+), 91 deletions(-) diff --git a/web/src/app/services/AlertMetrics/AlertMetricsFilter.tsx b/web/src/app/services/AlertMetrics/AlertMetricsFilter.tsx index 3f38297b9d..edba4dae3b 100644 --- a/web/src/app/services/AlertMetrics/AlertMetricsFilter.tsx +++ b/web/src/app/services/AlertMetrics/AlertMetricsFilter.tsx @@ -47,6 +47,8 @@ export default function AlertMetricsFilter({ Past Year + + Interval