mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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",
|
||||
|
||||
Generated
+71
-84
@@ -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…</>,
|
||||
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>
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user