feat(site): add WorkspacesButton component (#10011)

* chore: Add OverflowY component

* chore: Add PopoverContainer component

* chore: Add SearchBox

* feat: add WorkspacesButton

* chore: Install MUI utils package

* chore: integrate WorkspacesButton

* chore: reorganize files

* fix: resolve hover state visual glitch

* chore: Add story for OverflowY

* fix: remove dynamic name from OverflowY story

* chore: update stories again

* fix: remove all references to icons (for now)

* refactor: move flex shrink to be OverflowY concern

* fix: remove needless render key

* fix: make sure popover closes before navigation

* refactor: clean up WorkspacesButton to use more native MUI

* fix: update integration into rest of view

* fix: remove JS security concern

* refactor: parameterize button language

* revert: undo sql/go file change

* fix: remove permissions dependency

* fix: simplify button prop types

* fix: lift data dependencies to page component

* refactor: clean up props

* fix: update dependencies again for Storybook
This commit is contained in:
Michael Smith
2023-10-05 20:46:42 +00:00
committed by GitHub
parent ad47ef17e8
commit 2d6c4fe90a
12 changed files with 779 additions and 105 deletions
+1
View File
@@ -45,6 +45,7 @@
"@mui/material": "5.14.0",
"@mui/styles": "5.14.0",
"@mui/system": "5.14.0",
"@mui/utils": "5.14.11",
"@tanstack/react-query": "4.35.3",
"@vitejs/plugin-react": "4.1.0",
"@xstate/inspect": "0.8.0",
+71 -84
View File
@@ -51,6 +51,9 @@ dependencies:
'@mui/system':
specifier: 5.14.0
version: 5.14.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0)
'@mui/utils':
specifier: 5.14.11
version: 5.14.11(@types/react@18.2.6)(react@18.2.0)
'@tanstack/react-query':
specifier: 4.35.3
version: 4.35.3(react-dom@18.2.0)(react@18.2.0)
@@ -2112,6 +2115,7 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.14.0
dev: true
/@babel/runtime@7.22.6:
resolution: {integrity: sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==}
@@ -2124,7 +2128,6 @@ packages:
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.14.0
dev: true
/@babel/template@7.22.15:
resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==}
@@ -2239,7 +2242,7 @@ packages:
resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==}
dependencies:
'@babel/helper-module-imports': 7.22.5
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@emotion/hash': 0.9.1
'@emotion/memoize': 0.8.1
'@emotion/serialize': 1.1.2
@@ -3260,10 +3263,10 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@emotion/is-prop-valid': 1.2.1
'@mui/types': 7.2.4(@types/react@18.2.6)
'@mui/utils': 5.14.3(react@18.2.0)
'@mui/utils': 5.14.11(@types/react@18.2.6)(react@18.2.0)
'@popperjs/core': 2.11.8
'@types/react': 18.2.6
clsx: 1.2.1
@@ -3284,10 +3287,10 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@emotion/is-prop-valid': 1.2.1
'@mui/types': 7.2.4(@types/react@18.2.6)
'@mui/utils': 5.14.3(react@18.2.0)
'@mui/utils': 5.14.11(@types/react@18.2.6)(react@18.2.0)
'@popperjs/core': 2.11.8
'@types/react': 18.2.6
clsx: 1.2.1
@@ -3343,7 +3346,7 @@ packages:
'@mui/material': 5.14.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@mui/system': 5.14.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0)
'@mui/types': 7.2.4(@types/react@18.2.6)
'@mui/utils': 5.14.1(react@18.2.0)
'@mui/utils': 5.14.11(@types/react@18.2.6)(react@18.2.0)
'@types/react': 18.2.6
clsx: 1.2.1
prop-types: 15.8.1
@@ -3376,7 +3379,7 @@ packages:
'@mui/core-downloads-tracker': 5.14.2
'@mui/system': 5.14.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0)
'@mui/types': 7.2.4(@types/react@18.2.6)
'@mui/utils': 5.14.1(react@18.2.0)
'@mui/utils': 5.14.11(@types/react@18.2.6)(react@18.2.0)
'@types/react': 18.2.6
'@types/react-transition-group': 4.4.6
clsx: 1.2.1
@@ -3398,8 +3401,8 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@mui/utils': 5.14.3(react@18.2.0)
'@babel/runtime': 7.23.1
'@mui/utils': 5.14.11(@types/react@18.2.6)(react@18.2.0)
'@types/react': 18.2.6
prop-types: 15.8.1
react: 18.2.0
@@ -3418,7 +3421,7 @@ packages:
'@emotion/styled':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@emotion/cache': 11.11.0
'@emotion/react': 11.11.1(@types/react@18.2.6)(react@18.2.0)
'@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.6)(react@18.2.0)
@@ -3441,7 +3444,7 @@ packages:
'@emotion/hash': 0.9.1
'@mui/private-theming': 5.13.7(@types/react@18.2.6)(react@18.2.0)
'@mui/types': 7.2.4(@types/react@18.2.6)
'@mui/utils': 5.14.1(react@18.2.0)
'@mui/utils': 5.14.11(@types/react@18.2.6)(react@18.2.0)
'@types/react': 18.2.6
clsx: 1.2.1
csstype: 3.1.2
@@ -3480,7 +3483,7 @@ packages:
'@mui/private-theming': 5.13.7(@types/react@18.2.6)(react@18.2.0)
'@mui/styled-engine': 5.13.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0)
'@mui/types': 7.2.4(@types/react@18.2.6)
'@mui/utils': 5.14.3(react@18.2.0)
'@mui/utils': 5.14.11(@types/react@18.2.6)(react@18.2.0)
'@types/react': 18.2.6
clsx: 1.2.1
csstype: 3.1.2
@@ -3499,29 +3502,19 @@ packages:
'@types/react': 18.2.6
dev: false
/@mui/utils@5.14.1(react@18.2.0):
resolution: {integrity: sha512-39KHKK2JeqRmuUcLDLwM+c2XfVC136C5/yUyQXmO2PVbOb2Bol4KxtkssEqCbTwg87PSCG3f1Tb0keRsK7cVGw==}
/@mui/utils@5.14.11(@types/react@18.2.6)(react@18.2.0):
resolution: {integrity: sha512-fmkIiCPKyDssYrJ5qk+dime1nlO3dmWfCtaPY/uVBqCRMBZ11JhddB9m8sjI2mgqQQwRJG5bq3biaosNdU/s4Q==}
engines: {node: '>=12.0.0'}
peerDependencies:
'@types/react': ^17.0.0 || ^18.0.0
react: ^17.0.0 || ^18.0.0
peerDependenciesMeta:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@types/prop-types': 15.7.5
'@types/react-is': 18.2.1
prop-types: 15.8.1
react: 18.2.0
react-is: 18.2.0
dev: false
/@mui/utils@5.14.3(react@18.2.0):
resolution: {integrity: sha512-gZ6Etw+ppO43GYc1HFZSLjwd4DoZoa+RrYTD25wQLfzcSoPjVoC/zZqA2Lkq0zjgwGBQOSxKZI6jfp9uXR+kgw==}
engines: {node: '>=12.0.0'}
peerDependencies:
react: ^17.0.0 || ^18.0.0
dependencies:
'@babel/runtime': 7.22.15
'@types/prop-types': 15.7.5
'@types/react-is': 18.2.1
'@types/react': 18.2.6
prop-types: 15.8.1
react: 18.2.0
react-is: 18.2.0
@@ -3622,13 +3615,13 @@ packages:
/@radix-ui/number@1.0.1:
resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==}
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
dev: true
/@radix-ui/primitive@1.0.1:
resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
dev: true
/@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0):
@@ -3644,7 +3637,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.6
'@types/react-dom': 18.2.4
@@ -3665,7 +3658,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.6)(react@18.2.0)
'@radix-ui/react-context': 1.0.1(@types/react@18.2.6)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
@@ -3685,7 +3678,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@types/react': 18.2.6
react: 18.2.0
dev: true
@@ -3699,7 +3692,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@types/react': 18.2.6
react: 18.2.0
dev: true
@@ -3713,7 +3706,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@types/react': 18.2.6
react: 18.2.0
dev: true
@@ -3731,7 +3724,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.6)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
@@ -3752,7 +3745,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@types/react': 18.2.6
react: 18.2.0
dev: true
@@ -3770,7 +3763,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.6)(react@18.2.0)
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.6)(react@18.2.0)
@@ -3789,7 +3782,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.6)(react@18.2.0)
'@types/react': 18.2.6
react: 18.2.0
@@ -3808,7 +3801,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@floating-ui/react-dom': 2.0.2(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.6)(react@18.2.0)
@@ -3838,7 +3831,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.6
'@types/react-dom': 18.2.4
@@ -3859,7 +3852,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@radix-ui/react-slot': 1.0.2(@types/react@18.2.6)(react@18.2.0)
'@types/react': 18.2.6
'@types/react-dom': 18.2.4
@@ -3880,7 +3873,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.6)(react@18.2.0)
@@ -3909,7 +3902,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@radix-ui/number': 1.0.1
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
@@ -3950,7 +3943,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.6
'@types/react-dom': 18.2.4
@@ -3967,7 +3960,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.6)(react@18.2.0)
'@types/react': 18.2.6
react: 18.2.0
@@ -3986,7 +3979,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-context': 1.0.1(@types/react@18.2.6)(react@18.2.0)
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.6)(react@18.2.0)
@@ -4013,7 +4006,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.6)(react@18.2.0)
@@ -4036,7 +4029,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@radix-ui/primitive': 1.0.1
'@radix-ui/react-context': 1.0.1(@types/react@18.2.6)(react@18.2.0)
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.6)(react@18.2.0)
@@ -4059,7 +4052,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@types/react': 18.2.6
react: 18.2.0
dev: true
@@ -4073,7 +4066,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.6)(react@18.2.0)
'@types/react': 18.2.6
react: 18.2.0
@@ -4088,7 +4081,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.6)(react@18.2.0)
'@types/react': 18.2.6
react: 18.2.0
@@ -4103,7 +4096,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@types/react': 18.2.6
react: 18.2.0
dev: true
@@ -4117,7 +4110,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@types/react': 18.2.6
react: 18.2.0
dev: true
@@ -4131,7 +4124,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@radix-ui/rect': 1.0.1
'@types/react': 18.2.6
react: 18.2.0
@@ -4146,7 +4139,7 @@ packages:
'@types/react':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.6)(react@18.2.0)
'@types/react': 18.2.6
react: 18.2.0
@@ -4165,7 +4158,7 @@ packages:
'@types/react-dom':
optional: true
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0)
'@types/react': 18.2.6
'@types/react-dom': 18.2.4
@@ -4176,7 +4169,7 @@ packages:
/@radix-ui/rect@1.0.1:
resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==}
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
dev: true
/@remix-run/router@1.9.0:
@@ -5547,7 +5540,7 @@ packages:
engines: {node: '>=14'}
dependencies:
'@babel/code-frame': 7.22.13
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
'@types/aria-query': 5.0.1
aria-query: 5.1.3
chalk: 4.1.2
@@ -5980,12 +5973,6 @@ packages:
'@types/react': 18.2.6
dev: true
/@types/react-is@18.2.1:
resolution: {integrity: sha512-wyUkmaaSZEzFZivD8F2ftSyAfk6L+DfFliVj/mYdOXbVjRcS87fQJLTnhk6dRZPuJjI+9g6RZJO4PNCngUrmyw==}
dependencies:
'@types/react': 18.2.6
dev: false
/@types/react-syntax-highlighter@15.5.5:
resolution: {integrity: sha512-QH3JZQXa2usAvJvSsdSUJ4Yu4j8ReuZpgRrEW+XP+Rmosbn425YshW9iGEb/pAARm8496axHhHUPRH3UmTiB6A==}
dependencies:
@@ -6843,7 +6830,7 @@ packages:
resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
engines: {node: '>=10', npm: '>=6'}
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
cosmiconfig: 7.1.0
resolve: 1.22.4
dev: false
@@ -7562,7 +7549,7 @@ packages:
/css-vendor@2.0.8:
resolution: {integrity: sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==}
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
is-in-browser: 1.1.3
dev: false
@@ -7862,7 +7849,7 @@ packages:
/dom-helpers@5.2.1:
resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
csstype: 3.1.2
dev: false
@@ -10549,7 +10536,7 @@ packages:
/jss-plugin-camel-case@10.10.0:
resolution: {integrity: sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==}
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
hyphenate-style-name: 1.0.4
jss: 10.10.0
dev: false
@@ -10557,21 +10544,21 @@ packages:
/jss-plugin-default-unit@10.10.0:
resolution: {integrity: sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==}
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
jss: 10.10.0
dev: false
/jss-plugin-global@10.10.0:
resolution: {integrity: sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==}
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
jss: 10.10.0
dev: false
/jss-plugin-nested@10.10.0:
resolution: {integrity: sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==}
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
jss: 10.10.0
tiny-warning: 1.0.3
dev: false
@@ -10579,14 +10566,14 @@ packages:
/jss-plugin-props-sort@10.10.0:
resolution: {integrity: sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==}
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
jss: 10.10.0
dev: false
/jss-plugin-rule-value-function@10.10.0:
resolution: {integrity: sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==}
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
jss: 10.10.0
tiny-warning: 1.0.3
dev: false
@@ -10594,7 +10581,7 @@ packages:
/jss-plugin-vendor-prefixer@10.10.0:
resolution: {integrity: sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==}
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
css-vendor: 2.0.8
jss: 10.10.0
dev: false
@@ -10602,7 +10589,7 @@ packages:
/jss@10.10.0:
resolution: {integrity: sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==}
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
csstype: 3.1.2
is-in-browser: 1.1.3
tiny-warning: 1.0.3
@@ -12230,7 +12217,7 @@ packages:
peerDependencies:
react: '>=16.13.1'
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
react: 18.2.0
dev: true
@@ -12411,7 +12398,7 @@ packages:
react: '>=16.6.0'
react-dom: '>=16.6.0'
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
dom-helpers: 5.2.1
loose-envify: 1.4.0
prop-types: 15.8.1
@@ -12602,7 +12589,7 @@ packages:
/regenerator-transform@0.15.2:
resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==}
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
dev: true
/regexp-tree@0.1.27:
@@ -12826,7 +12813,7 @@ packages:
/rtl-css-js@1.16.1:
resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==}
dependencies:
'@babel/runtime': 7.22.15
'@babel/runtime': 7.23.1
dev: false
/run-async@2.4.1:
@@ -14133,7 +14120,7 @@ packages:
peerDependencies:
eslint: '>=7'
meow: ^9.0.0
optionator: 0.9.3
optionator: ^0.9.1
stylelint: '>=13'
typescript: '*'
vite: '>=2.0.0'
@@ -0,0 +1,34 @@
import { Meta, StoryObj } from "@storybook/react";
import { OverflowY } from "./OverflowY";
const numbers: number[] = [];
for (let i = 0; i < 20; i++) {
numbers.push(i + 1);
}
const meta: Meta<typeof OverflowY> = {
title: "components/OverflowY",
component: OverflowY,
args: {
maxHeight: 400,
children: numbers.map((num, i) => (
<p
key={num}
css={{
height: "50px",
padding: 0,
margin: 0,
color: "black",
backgroundColor: i % 2 === 0 ? "white" : "gray",
}}
>
Element {num}
</p>
)),
},
};
export default meta;
type Story = StoryObj<typeof OverflowY>;
export const Example: Story = {};
@@ -0,0 +1,39 @@
/**
* @file Provides reusable vertical overflow behavior.
*/
import { type ReactNode } from "react";
import { type SystemStyleObject } from "@mui/system";
import Box from "@mui/system/Box";
type Props = {
children: ReactNode;
height?: number;
maxHeight?: number;
sx?: SystemStyleObject;
};
export function OverflowY({ children, height, maxHeight, sx }: Props) {
const computedHeight = height === undefined ? "100%" : `${height}px`;
// Doing Math.max check to catch cases where height is accidentally larger
// than maxHeight
const computedMaxHeight =
maxHeight === undefined
? computedHeight
: `${Math.max(height ?? 0, maxHeight)}px`;
return (
<Box
sx={{
width: "100%",
height: computedHeight,
maxHeight: computedMaxHeight,
overflowY: "auto",
flexShrink: 1,
...sx,
}}
>
{children}
</Box>
);
}
@@ -36,7 +36,6 @@ export const PageHeader: FC<PropsWithChildren<PageHeaderProps>> = ({
marginLeft: "auto",
[theme.breakpoints.down("md")]: {
marginTop: theme.spacing(3),
marginLeft: "initial",
width: "100%",
},
@@ -0,0 +1,23 @@
import { Meta, StoryObj } from "@storybook/react";
import { PopoverContainer } from "./PopoverContainer";
import Button from "@mui/material/Button";
const numbers: number[] = [];
for (let i = 0; i < 20; i++) {
numbers.push(i + 1);
}
const meta: Meta<typeof PopoverContainer> = {
title: "components/PopoverContainer",
component: PopoverContainer,
args: {
anchorButton: <Button>I have no hooks/refs</Button>,
children: <p>Hiya!</p>,
originY: "bottom",
},
};
export default meta;
type Story = StoryObj<typeof PopoverContainer>;
export const Example: Story = {};
@@ -0,0 +1,244 @@
/**
* @file Abstracts over MUI's Popover component to simplify using it (and hide
* some of the wonkier parts of the API).
*
* Just place a button and some content in the component, and things just work.
* No setup needed with hooks or refs.
*/
import {
type KeyboardEvent,
type MouseEvent,
type PropsWithChildren,
type ReactElement,
createContext,
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";
import { type Theme, type SystemStyleObject, Box } from "@mui/system";
import Popover, { type PopoverOrigin } from "@mui/material/Popover";
import { useNavigate, type LinkProps } from "react-router-dom";
import { useTheme } from "@emotion/react";
function getButton(container: HTMLElement) {
return (
container.querySelector("button") ??
container.querySelector('[aria-role="button"]')
);
}
const ClosePopoverContext = createContext<(() => void) | null>(null);
type PopoverLinkProps = LinkProps & {
to: string;
sx?: SystemStyleObject<Theme>;
};
/**
* A custom version of a React Router Link that makes sure to close the popover
* before starting a navigation.
*
* This is necessary because React Router's navigation logic doesn't work well
* with modals (including MUI's base Popover component).
*
* ---
* If the page being navigated to has lazy loading and isn't available yet, the
* previous components are supposed to be hidden during the transition, but
* because most React modals use React.createPortal to put content outside of
* the main DOM tree, React Router has no way of knowing about them. So open
* modals have a high risk of not disappearing until the page transition
* finishes and the previous components fully unmount.
*/
export function PopoverLink({
children,
to,
sx,
...linkProps
}: PopoverLinkProps) {
const closePopover = useContext(ClosePopoverContext);
if (closePopover === null) {
throw new Error("PopoverLink is not located inside of a PopoverContainer");
}
// Luckily, useNavigate and Link are designed to be imperative/declarative
// mirrors of each other, so their inputs should never get out of sync
const navigate = useNavigate();
const theme = useTheme();
const onClick = (event: MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.stopPropagation();
closePopover();
// Hacky, but by using a promise to push the navigation to resolve via the
// micro-task queue, there's guaranteed to be a period for the popover to
// close. Tried React DOM's flushSync function, but it was unreliable.
void Promise.resolve().then(() => {
navigate(to, linkProps);
});
};
return (
<Box
component="a"
// Href still needed for accessibility reasons and semantic markup
href=""
onClick={onClick}
sx={{
outline: "none",
textDecoration: "none",
"&:focus": {
backgroundColor: theme.palette.action.focus,
},
"&:hover": {
textDecoration: "none",
backgroundColor: theme.palette.action.hover,
},
...sx,
}}
>
{children}
</Box>
);
}
type PopoverContainerProps = PropsWithChildren<{
/**
* Does not require any hooks or refs to work. Also does not override any refs
* or event handlers attached to the button.
*/
anchorButton: ReactElement;
width?: number;
originX?: PopoverOrigin["horizontal"];
originY?: PopoverOrigin["vertical"];
sx?: SystemStyleObject<Theme>;
}>;
export function PopoverContainer({
children,
anchorButton,
originX = 0,
originY = 0,
width = 320,
sx = {},
}: PopoverContainerProps) {
const parentClosePopover = useContext(ClosePopoverContext);
if (parentClosePopover !== null) {
throw new Error(
"Popover detected inside of Popover - this will always be a bad user experience",
);
}
const buttonContainerRef = useRef<HTMLDivElement>(null);
// Ref value is for effects and event listeners; state value is for React
// renders. Have to duplicate state because after the initial render, it's
// never safe to reference ref contents inside a render path, especially with
// React 18 concurrency. Duplication is a necessary evil because of MUI's
// weird, clunky APIs
const anchorButtonRef = useRef<HTMLButtonElement | null>(null);
const [loadedButton, setLoadedButton] = useState<HTMLButtonElement>();
// Makes container listen to changes in button. If this approach becomes
// untenable in the future, it can be replaced with React.cloneElement, but
// the trade-off there is that every single anchorButton will need to be
// wrapped inside React.forwardRef, making the abstraction leak a little more
useEffect(() => {
const buttonContainer = buttonContainerRef.current;
if (buttonContainer === null) {
throw new Error("Please attach container ref to button container");
}
const initialButton = getButton(buttonContainer);
if (initialButton === null) {
throw new Error("Initial ref query failed");
}
anchorButtonRef.current = initialButton;
const onContainerMutation: MutationCallback = () => {
const newButton = getButton(buttonContainer);
if (newButton === null) {
throw new Error("Semantic button removed after DOM update");
}
anchorButtonRef.current = newButton;
setLoadedButton((current) => {
return current === undefined ? undefined : newButton;
});
};
const observer = new MutationObserver(onContainerMutation);
observer.observe(buttonContainer, {
childList: true,
subtree: true,
});
return () => observer.disconnect();
}, []);
// Not using useInteractive because the container element is just meant to
// catch events from the inner button, not act as a button itself
const onInnerButtonInteraction = () => {
if (anchorButtonRef.current === null) {
throw new Error("Usable ref value is unavailable");
}
setLoadedButton(anchorButtonRef.current);
};
const onInnerButtonKeydown = (event: KeyboardEvent) => {
if (event.key === "Enter" || event.key === " ") {
onInnerButtonInteraction();
}
};
const closePopover = useCallback(() => {
setLoadedButton(undefined);
}, []);
return (
<>
{/* Cannot switch with Box component; breaks implementation */}
<div
// Disabling semantics for the container does not affect the button
// placed inside; the button should still be fully accessible
role="none"
tabIndex={-1}
ref={buttonContainerRef}
onClick={onInnerButtonInteraction}
onKeyDown={onInnerButtonKeydown}
// Only style that container should ever need
style={{ width: "fit-content" }}
>
{anchorButton}
</div>
<ClosePopoverContext.Provider value={closePopover}>
<Popover
open={loadedButton !== undefined}
anchorEl={loadedButton}
onClose={closePopover}
anchorOrigin={{ horizontal: originX, vertical: originY }}
sx={{
"& .MuiPaper-root": {
overflowY: "hidden",
width,
paddingY: 0,
...sx,
},
}}
transitionDuration={{
enter: 300,
exit: 0,
}}
>
{children}
</Popover>
</ClosePopoverContext.Provider>
</>
);
}
@@ -0,0 +1,222 @@
import { type PropsWithChildren, type ReactNode, useState } from "react";
import { useTheme } from "@emotion/react";
import { Language } from "./WorkspacesPageView";
import { type Template } from "api/typesGenerated";
import { type UseQueryResult } from "@tanstack/react-query";
import { Link as RouterLink } from "react-router-dom";
import Box from "@mui/system/Box";
import Button from "@mui/material/Button";
import Link from "@mui/material/Link";
import AddIcon from "@mui/icons-material/AddOutlined";
import OpenIcon from "@mui/icons-material/OpenInNewOutlined";
import Typography from "@mui/material/Typography";
import { Loader } from "components/Loader/Loader";
import { OverflowY } from "components/OverflowY/OverflowY";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Avatar } from "components/Avatar/Avatar";
import { SearchBox } from "./WorkspacesSearchBox";
import {
PopoverContainer,
PopoverLink,
} from "components/PopoverContainer/PopoverContainer";
const ICON_SIZE = 18;
const COLUMN_GAP = 1.5;
function sortTemplatesByUsersDesc(
templates: readonly Template[],
searchTerm: string,
) {
const allWhitespace = /^\s+$/.test(searchTerm);
if (allWhitespace) {
return templates;
}
const termMatcher = new RegExp(searchTerm.replaceAll(/[^\w]/g, "."), "i");
return templates
.filter((template) => termMatcher.test(template.display_name))
.sort((t1, t2) => t2.active_user_count - t1.active_user_count)
.slice(0, 10);
}
function WorkspaceResultsRow({ template }: { template: Template }) {
const theme = useTheme();
return (
<PopoverLink to={`/templates/${template.name}/workspace`}>
<Box
sx={{
display: "flex",
columnGap: COLUMN_GAP,
alignItems: "center",
paddingX: 2,
paddingY: 1,
overflowY: "hidden",
}}
>
<Avatar
src={template.icon}
fitImage
alt={template.display_name || "Coder template"}
sx={{
width: `${ICON_SIZE}px`,
height: `${ICON_SIZE}px`,
fontSize: `${ICON_SIZE * 0.5}px`,
fontWeight: 700,
}}
>
{template.display_name || "-"}
</Avatar>
<Box
sx={{
lineHeight: 1,
width: "100%",
overflow: "hidden",
color: "white",
}}
>
<Typography
component="p"
sx={{ marginY: 0, paddingBottom: 0.5, lineHeight: 1 }}
noWrap
>
{template.display_name || "[Unnamed]"}
</Typography>
<Box
component="p"
sx={{
marginY: 0,
fontSize: 14,
color: theme.palette.text.secondary,
}}
>
{/*
* There are some templates that have -1 as their user count –
* basically functioning like a null value in JS. Can safely just
* treat them as if they were 0.
*/}
{template.active_user_count <= 0
? "No"
: template.active_user_count}{" "}
developer
{template.active_user_count === 1 ? "" : "s"}
</Box>
</Box>
</Box>
</PopoverLink>
);
}
type TemplatesQuery = UseQueryResult<Template[]>;
type WorkspacesButtonProps = PropsWithChildren<{
templatesFetchStatus: TemplatesQuery["status"];
templates: TemplatesQuery["data"];
}>;
export function WorkspacesButton({
children,
templatesFetchStatus,
templates,
}: WorkspacesButtonProps) {
const theme = useTheme();
// Dataset should always be small enough that client-side filtering should be
// good enough. Can swap out down the line if it becomes an issue
const [searchTerm, setSearchTerm] = useState("");
const processed = sortTemplatesByUsersDesc(templates ?? [], searchTerm);
let emptyState: ReactNode = undefined;
if (templates?.length === 0) {
emptyState = (
<EmptyState
message="No templates yet"
cta={
<Link to="/templates" component={RouterLink}>
Create one now.
</Link>
}
/>
);
} else if (processed.length === 0) {
emptyState = <EmptyState message="No templates match your text" />;
}
return (
<PopoverContainer
// Stopgap value until bug where string-based horizontal origin isn't
// being applied consistently can get figured out
originX={-115}
originY="bottom"
sx={{ display: "flex", flexFlow: "column nowrap" }}
anchorButton={
<Button startIcon={<AddIcon />} variant="contained">
{children}
</Button>
}
>
<SearchBox
value={searchTerm}
onValueChange={(newValue) => setSearchTerm(newValue)}
placeholder="Type/select a workspace template"
label="Template select for workspace"
sx={{ flexShrink: 0, columnGap: COLUMN_GAP }}
/>
<OverflowY
maxHeight={380}
sx={{
display: "flex",
flexFlow: "column nowrap",
paddingY: 1,
}}
>
{templatesFetchStatus === "loading" ? (
<Loader size={14} />
) : (
<>
{processed.map((template) => (
<WorkspaceResultsRow key={template.id} template={template} />
))}
{emptyState}
</>
)}
</OverflowY>
<Link
component={RouterLink}
to="/templates"
sx={{
outline: "none",
"&:focus": {
backgroundColor: theme.palette.action.focus,
},
}}
>
<Box
sx={{
padding: 2,
display: "flex",
flexFlow: "row nowrap",
alignItems: "center",
columnGap: COLUMN_GAP,
borderTop: `1px solid ${theme.palette.divider}`,
}}
>
<Box component="span" sx={{ width: `${ICON_SIZE}px` }}>
<OpenIcon
sx={{ fontSize: "16px", marginX: "auto", display: "block" }}
/>
</Box>
<span>{Language.seeAllTemplates}</span>
</Box>
</Link>
</PopoverContainer>
);
}
@@ -19,6 +19,8 @@ import TextField from "@mui/material/TextField";
import { displayError } from "components/GlobalSnackbar/utils";
import { getErrorMessage } from "api/errors";
import { useEffectEvent } from "hooks/hookPolyfills";
import { useQuery } from "@tanstack/react-query";
import { templates } from "api/queries/templates";
function useSafeSearchParams() {
// Have to wrap setSearchParams because React Router doesn't make sure that
@@ -41,8 +43,13 @@ const WorkspacesPage: FC = () => {
// each hook.
const searchParamsResult = useSafeSearchParams();
const pagination = usePagination({ searchParamsResult });
const organizationId = useOrganizationId();
const templatesQuery = useQuery(templates(organizationId));
const filterProps = useWorkspacesFilter({
searchParamsResult,
organizationId,
onFilterChange: () => pagination.goToPage(1),
});
@@ -106,6 +113,8 @@ const WorkspacesPage: FC = () => {
checkedWorkspaces={checkedWorkspaces}
onCheckChange={setCheckedWorkspaces}
canCheckWorkspaces={canCheckWorkspaces}
templates={templatesQuery.data}
templatesFetchStatus={templatesQuery.status}
workspaces={data?.workspaces}
dormantWorkspaces={dormantWorkspaces}
error={error}
@@ -142,11 +151,13 @@ export default WorkspacesPage;
type UseWorkspacesFilterOptions = {
searchParamsResult: ReturnType<typeof useSearchParams>;
onFilterChange: () => void;
organizationId: string;
};
const useWorkspacesFilter = ({
searchParamsResult,
onFilterChange,
organizationId,
}: UseWorkspacesFilterOptions) => {
const filter = useFilter({
fallbackFilter: "owner:me",
@@ -163,9 +174,8 @@ const useWorkspacesFilter = ({
enabled: canFilterByUser,
});
const orgId = useOrganizationId();
const templateMenu = useTemplateFilterMenu({
orgId,
orgId: organizationId,
value: filter.values.template,
onChange: (option) =>
filter.update({ ...filter.values, template: option?.value }),
@@ -16,6 +16,7 @@ import {
mockApiError,
MockUser,
MockPendingProvisionerJob,
MockTemplate,
} from "testHelpers/entities";
import { WorkspacesPageView } from "./WorkspacesPageView";
import { DashboardProviderContext } from "components/Dashboard/DashboardProvider";
@@ -92,6 +93,19 @@ const defaultFilterProps = getDefaultFilterProps<FilterProps>({
},
});
const mockTemplates = [
MockTemplate,
...[1, 2, 3, 4].map((num) => {
return {
...MockTemplate,
active_user_count: Math.floor(Math.random() * 10) * num,
display_name: `Extra Template ${num}`,
description: "Auto-Generated template",
icon: num % 2 === 0 ? "" : "/icon/goland.svg",
};
}),
];
const meta: Meta<typeof WorkspacesPageView> = {
title: "pages/WorkspacesPageView",
component: WorkspacesPageView,
@@ -100,6 +114,8 @@ const meta: Meta<typeof WorkspacesPageView> = {
filterProps: defaultFilterProps,
checkedWorkspaces: [],
canCheckWorkspaces: true,
templates: mockTemplates,
templatesFetchStatus: "success",
},
decorators: [
(Story) => (
@@ -1,14 +1,8 @@
import Link from "@mui/material/Link";
import { Workspace } from "api/typesGenerated";
import { Template, Workspace } from "api/typesGenerated";
import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase";
import { ComponentProps, FC } from "react";
import { Link as RouterLink } from "react-router-dom";
import { Margins } from "components/Margins/Margins";
import {
PageHeader,
PageHeaderSubtitle,
PageHeaderTitle,
} from "components/PageHeader/PageHeader";
import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader";
import { Stack } from "components/Stack/Stack";
import { WorkspaceHelpTooltip } from "./WorkspaceHelpTooltip";
import { WorkspacesTable } from "pages/WorkspacesPage/WorkspacesTable";
@@ -24,16 +18,21 @@ import {
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import DeleteOutlined from "@mui/icons-material/DeleteOutlined";
import { WorkspacesButton } from "./WorkspacesButton";
import { UseQueryResult } from "@tanstack/react-query";
export const Language = {
pageTitle: "Workspaces",
yourWorkspacesButton: "Your workspaces",
allWorkspacesButton: "All workspaces",
runningWorkspacesButton: "Running workspaces",
createANewWorkspace: `Create a new workspace from a `,
createWorkspace: <>Create Workspace&hellip;</>,
seeAllTemplates: "See all templates",
template: "Template",
};
type TemplateQuery = UseQueryResult<Template[]>;
export interface WorkspacesPageViewProps {
error: unknown;
workspaces?: Workspace[];
@@ -48,6 +47,9 @@ export interface WorkspacesPageViewProps {
onCheckChange: (checkedWorkspaces: Workspace[]) => void;
onDeleteAll: () => void;
canCheckWorkspaces: boolean;
templatesFetchStatus: TemplateQuery["status"];
templates: TemplateQuery["data"];
}
export const WorkspacesPageView: FC<
@@ -66,6 +68,8 @@ export const WorkspacesPageView: FC<
onCheckChange,
onDeleteAll,
canCheckWorkspaces,
templates,
templatesFetchStatus,
}) => {
const { saveLocal } = useLocalStorage();
@@ -78,21 +82,22 @@ export const WorkspacesPageView: FC<
return (
<Margins>
<PageHeader>
<PageHeader
actions={
<WorkspacesButton
templates={templates}
templatesFetchStatus={templatesFetchStatus}
>
{Language.createWorkspace}
</WorkspacesButton>
}
>
<PageHeaderTitle>
<Stack direction="row" spacing={1} alignItems="center">
<span>{Language.pageTitle}</span>
<WorkspaceHelpTooltip />
</Stack>
</PageHeaderTitle>
<PageHeaderSubtitle>
{Language.createANewWorkspace}
<Link component={RouterLink} to="/templates">
{Language.template}
</Link>
.
</PageHeaderSubtitle>
</PageHeader>
<Stack>
@@ -0,0 +1,94 @@
/**
* @file Defines a controlled searchbox component for processing form state.
*
* Not defined as a top-level component just yet, because it's not clear how
* reusable this is outside of workspace dropdowns.
*/
import {
type ForwardedRef,
type KeyboardEvent,
forwardRef,
useId,
} from "react";
import Box from "@mui/system/Box";
import SearchIcon from "@mui/icons-material/SearchOutlined";
import { visuallyHidden } from "@mui/utils";
import { type SystemStyleObject } from "@mui/system";
type Props = {
value: string;
onValueChange: (newValue: string) => void;
placeholder?: string;
label?: string;
onKeyDown?: (event: KeyboardEvent) => void;
sx?: SystemStyleObject;
};
export const SearchBox = forwardRef(function SearchBox(
{
value,
onValueChange,
onKeyDown,
label = "Search",
placeholder = "Search...",
sx = {},
}: Props,
ref?: ForwardedRef<HTMLInputElement>,
) {
const hookId = useId();
const inputId = `${hookId}-${SearchBox.name}-input`;
return (
<Box
sx={{
display: "flex",
flexFlow: "row nowrap",
alignItems: "center",
paddingX: 2,
height: "40px",
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
...sx,
}}
onKeyDown={onKeyDown}
>
<Box component="div" sx={{ width: "18px" }}>
<SearchIcon
sx={{
display: "block",
fontSize: "14px",
marginX: "auto",
color: (theme) => theme.palette.text.secondary,
}}
/>
</Box>
<Box component="label" sx={visuallyHidden} htmlFor={inputId}>
{label}
</Box>
<Box
component="input"
type="text"
ref={ref}
id={inputId}
autoFocus
tabIndex={0}
placeholder={placeholder}
value={value}
onChange={(e) => onValueChange(e.target.value)}
sx={{
height: "100%",
border: 0,
background: "none",
width: "100%",
outline: 0,
"&::placeholder": {
color: (theme) => theme.palette.text.secondary,
},
}}
/>
</Box>
);
});