diff --git a/web/packages/shared/components/ClusterDropdown/ClusterDropdown.story.tsx b/web/packages/shared/components/ClusterDropdown/ClusterDropdown.story.tsx new file mode 100644 index 0000000000000..df241fdda130c --- /dev/null +++ b/web/packages/shared/components/ClusterDropdown/ClusterDropdown.story.tsx @@ -0,0 +1,119 @@ +/** + * Teleport + * Copyright (C) 2023 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import React from 'react'; +import { MemoryRouter } from 'react-router'; + +import { Cluster } from 'teleport/services/clusters'; +import { createTeleportContext } from 'teleport/mocks/contexts'; +import { ContextProvider } from 'teleport/index'; +import { Text, Box } from 'design'; + +import { ClusterDropdown } from './ClusterDropdown'; + +export default { + title: 'ClusterDropdown', +}; + +const fetchClusters = () => null; + +export const Dropdown = () => { + const ctx = createTeleportContext(); + return ( + + + + 500 clusters + null} + /> + + + 2 clusters + null} + /> + + + 20 clusters + null} + /> + + + 5 clusters + null} + /> + + + no clusters (shouldn't be displayed) + null} + /> + + + + ); +}; + +Dropdown.storyName = 'ClusterDropdown'; + +const lotsOfClusters = new Array(500).fill(null).map( + (_, i) => + ({ + clusterId: `cluster-${i}`, + } as Cluster) +); + +const twoClusters = new Array(2).fill(null).map( + (_, i) => + ({ + clusterId: `cluster-${i}`, + } as Cluster) +); + +const twentyClusters = new Array(20).fill(null).map( + (_, i) => + ({ + clusterId: `cluster-${i}`, + } as Cluster) +); + +const fiveClusters = new Array(5).fill(null).map( + (_, i) => + ({ + clusterId: `cluster-${i}`, + } as Cluster) +); diff --git a/web/packages/shared/components/ClusterDropdown/ClusterDropdown.tsx b/web/packages/shared/components/ClusterDropdown/ClusterDropdown.tsx index 7e73a61ee1f21..dd1b0ab118411 100644 --- a/web/packages/shared/components/ClusterDropdown/ClusterDropdown.tsx +++ b/web/packages/shared/components/ClusterDropdown/ClusterDropdown.tsx @@ -17,8 +17,9 @@ */ import React, { useState, useEffect } from 'react'; +import styled from 'styled-components'; import { useHistory } from 'react-router'; -import { ButtonSecondary, Flex, Menu, MenuItem, Text } from 'design'; +import { Box, ButtonSecondary, Flex, Menu, MenuItem, Text } from 'design'; import { ChevronDown } from 'design/Icon'; import cfg from 'teleport/config'; import { Cluster } from 'teleport/services/clusters'; @@ -64,6 +65,8 @@ export function ClusterDropdown({ const [options, setOptions] = React.useState( createOptions(initialClusters) ); + const showInput = options.length > 5 ? true : false; + const [clusterFilter, setClusterFilter] = useState(''); const history = useHistory(); const [anchorEl, setAnchorEl] = useState(null); @@ -130,6 +133,17 @@ export function ClusterDropdown({ return null; } + const onClusterFilterChange = (e: React.ChangeEvent) => { + setClusterFilter(e.target.value); + }; + + let filteredOptions = options; + if (clusterFilter) { + filteredOptions = options.filter(cluster => + cluster.label.toLowerCase().includes(clusterFilter.toLowerCase()) + ); + } + return ( @@ -147,7 +161,11 @@ export function ClusterDropdown({ `margin-top: 36px;`} + popoverCss={() => ` + margin-top: ${showInput ? '40px' : '4px'}; + max-height: 265px; + overflow: hidden; + `} transformOrigin={{ vertical: 'top', horizontal: 'left', @@ -160,20 +178,74 @@ export function ClusterDropdown({ open={Boolean(anchorEl)} onClose={handleClose} > - {options.map(cluster => ( - onChangeOption(cluster.value)} + {showInput ? ( + p.theme.space[2]}px; + `} > - - {cluster.label} - - - ))} + + + ) : ( + // without this empty box, the entire positioning is way out of whack + // TODO (avatus): find out why during menu/popover rework + + )} + + {filteredOptions.map(cluster => ( + onChangeOption(cluster.value)} + > + + {cluster.label} + + + ))} + ); } type Option = { value: string; label: string }; + +const ClusterFilter = styled.input( + ({ theme }) => ` + background-color: ${theme.colors.spotBackground[0]}; + padding-left: ${theme.space[3]}px; + width: 100%; + border-radius: 29px; + box-sizing: border-box; + color: ${theme.colors.text.main}; + height: 32px; + font-size: ${theme.fontSizes[1]}px; + outline: none; + border: none; + &:focus { + border: none; + } + + ::placeholder { + color: ${theme.colors.text.muted}; + opacity: 1; + } +` +);