diff --git a/web/packages/shared/components/Controls/MultiselectMenu.story.tsx b/web/packages/shared/components/Controls/MultiselectMenu.story.tsx new file mode 100644 index 0000000000000..3016d892c64a5 --- /dev/null +++ b/web/packages/shared/components/Controls/MultiselectMenu.story.tsx @@ -0,0 +1,137 @@ +/** + * Teleport + * Copyright (C) 2024 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, { useState } from 'react'; +import { Flex } from 'design'; + +import { MultiselectMenu } from './MultiselectMenu'; + +import type { Meta, StoryFn, StoryObj } from '@storybook/react'; + +type OptionValue = `option-${number}`; + +const options: { + value: OptionValue; + label: string | React.ReactNode; + disabled?: boolean; + disabledTooltip?: string; +}[] = [ + { value: 'option-1', label: 'Option 1' }, + { value: 'option-2', label: 'Option 2' }, + { value: 'option-3', label: 'Option 3' }, + { value: 'option-4', label: 'Option 4' }, +]; + +const optionsWithCustomLabels: typeof options = [ + { + value: 'option-1', + label: Bold Option 1, + }, + { + value: 'option-3', + label: Italic Option 3, + }, +]; + +export default { + title: 'Shared/Controls/MultiselectMenu', + component: MultiselectMenu, + argTypes: { + buffered: { + control: { type: 'boolean' }, + description: 'Buffer selections until "Apply" is clicked', + table: { defaultValue: { summary: 'false' } }, + }, + showIndicator: { + control: { type: 'boolean' }, + description: 'Show indicator when there are selected options', + table: { defaultValue: { summary: 'true' } }, + }, + showSelectControls: { + control: { type: 'boolean' }, + description: 'Show select controls (Select All/Select None)', + table: { defaultValue: { summary: 'true' } }, + }, + label: { + control: { type: 'text' }, + description: 'Label for the multiselect', + }, + tooltip: { + control: { type: 'text' }, + description: 'Tooltip for the label', + }, + selected: { + control: false, + description: 'Currently selected options', + table: { type: { summary: 'T[]' } }, + }, + onChange: { + control: false, + description: 'Callback when selection changes', + table: { type: { summary: 'selected: T[]' } }, + }, + options: { + control: false, + description: 'Options to select from', + table: { + type: { + summary: + 'Array<{ value: T; label: string | ReactNode; disabled?: boolean; disabledTooltip?: string; }>', + }, + }, + }, + }, + args: { + label: 'Select Options', + tooltip: 'Choose multiple options', + buffered: false, + showIndicator: true, + showSelectControls: true, + }, + parameters: { controls: { expanded: true, exclude: ['userContext'] } }, + render: (args => { + const [selected, setSelected] = useState([]); + return ( + + + + ); + }) satisfies StoryFn>, +} satisfies Meta>; + +type Story = StoryObj>; + +const Default: Story = { args: { options } }; + +const WithCustomLabels: Story = { args: { options: optionsWithCustomLabels } }; + +const WithDisabledOption: Story = { + args: { + options: [ + ...options, + { + value: 'option-5', + label: 'Option 5', + disabled: true, + disabledTooltip: 'Lorum ipsum dolor sit amet', + }, + ], + }, +}; + +export { Default, WithCustomLabels, WithDisabledOption }; diff --git a/web/packages/shared/components/Controls/MultiselectMenu.tsx b/web/packages/shared/components/Controls/MultiselectMenu.tsx new file mode 100644 index 0000000000000..f252cf7aa21be --- /dev/null +++ b/web/packages/shared/components/Controls/MultiselectMenu.tsx @@ -0,0 +1,243 @@ +/** + * Teleport + * Copyright (C) 2024 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, { ReactNode, useState } from 'react'; +import styled from 'styled-components'; +import { + ButtonPrimary, + ButtonSecondary, + Flex, + Menu, + MenuItem, + Text, +} from 'design'; +import { ChevronDown } from 'design/Icon'; +import { CheckboxInput } from 'design/Checkbox'; + +import { HoverTooltip } from 'shared/components/ToolTip'; + +type MultiselectMenuProps = { + options: { + value: T; + label: string | ReactNode; + disabled?: boolean; + disabledTooltip?: string; + }[]; + selected: T[]; + onChange: (selected: T[]) => void; + label: string | ReactNode; + tooltip: string; + buffered?: boolean; + showIndicator?: boolean; + showSelectControls?: boolean; +}; + +export const MultiselectMenu = ({ + onChange, + options, + selected, + label, + tooltip, + buffered = false, + showIndicator = true, + showSelectControls = true, +}: MultiselectMenuProps) => { + // we have a separate state in the filter so we can select a few different things and then click "apply" + const [intSelected, setIntSelected] = useState([]); + const [anchorEl, setAnchorEl] = useState(null); + const handleOpen = ( + event: React.MouseEvent + ) => { + setIntSelected(selected || []); + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + // if we cancel, we reset the options to what is already selected in the params + const cancelUpdate = () => { + setIntSelected(selected || []); + handleClose(); + }; + + const handleSelect = (value: T) => { + let newSelected = (buffered ? intSelected : selected).slice(); + + if (newSelected.includes(value)) { + newSelected = newSelected.filter(v => v !== value); + } else { + newSelected.push(value); + } + + (buffered ? setIntSelected : onChange)(newSelected); + }; + + const handleSelectAll = () => { + (buffered ? setIntSelected : onChange)( + options.filter(o => !o.disabled).map(o => o.value) + ); + }; + + const handleClearAll = () => { + (buffered ? setIntSelected : onChange)([]); + }; + + const applyFilters = () => { + onChange(intSelected); + handleClose(); + }; + + return ( + + + + {label} {selected?.length > 0 ? `(${selected?.length})` : ''} + + {selected?.length > 0 && showIndicator && } + + + `margin-top: 36px;`} + menuListCss={() => `overflow-y: auto;`} + transformOrigin={{ + vertical: 'top', + horizontal: 'left', + }} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', + }} + anchorEl={anchorEl} + open={Boolean(anchorEl)} + onClose={cancelUpdate} + > + {showSelectControls && ( + + + Select All + + + Clear All + + + )} + {options.map(opt => { + const $checkbox = ( + <> + { + handleSelect(opt.value); + }} + id={opt.value} + checked={(buffered ? intSelected : selected)?.includes( + opt.value + )} + /> + + {opt.label} + + + ); + return ( + (!opt.disabled ? handleSelect(opt.value) : null)} + > + {opt.disabled && opt.disabledTooltip ? ( + + {$checkbox} + + ) : ( + $checkbox + )} + + ); + })} + {buffered && ( + + + Apply Filters + + + Cancel + + + )} + + + ); +}; + +const MultiselectMenuOptionsContainer = styled(Flex)<{ + position: 'top' | 'bottom'; +}>` + position: sticky; + ${p => (p.position === 'top' ? 'top: 0;' : 'bottom: 0;')} + background-color: ${p => p.theme.colors.levels.elevated}; + z-index: 1; +`; + +const FiltersExistIndicator = styled.div` + position: absolute; + top: -4px; + right: -4px; + height: 12px; + width: 12px; + background-color: ${p => p.theme.colors.brand}; + border-radius: 50%; + display: inline-block; +`; diff --git a/web/packages/shared/components/Controls/SortMenu.story.tsx b/web/packages/shared/components/Controls/SortMenu.story.tsx new file mode 100644 index 0000000000000..56158b8a8d228 --- /dev/null +++ b/web/packages/shared/components/Controls/SortMenu.story.tsx @@ -0,0 +1,83 @@ +/** + * Teleport + * Copyright (C) 2024 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, { useState } from 'react'; +import { Flex } from 'design'; + +import { SortMenu } from './SortMenu'; + +import type { Meta, StoryFn, StoryObj } from '@storybook/react'; + +export default { + title: 'Shared/Controls/SortMenu', + component: SortMenu, + argTypes: { + current: { + control: false, + description: 'Current sort', + table: { + type: { + summary: + "Array<{ fieldName: Exclude; dir: 'ASC' | 'DESC'>", + }, + }, + }, + fields: { + control: false, + description: 'Fields to sort by', + table: { + type: { + summary: + '{ value: Exclude; label: string }[]', + }, + }, + }, + onChange: { + control: false, + description: 'Callback when fieldName or dir is changed', + table: { + type: { + summary: + "(value: { fieldName: Exclude; dir: 'ASC' | 'DESC' }) => void", + }, + }, + }, + }, + args: { + current: { fieldName: 'name', dir: 'ASC' }, + fields: [ + { value: 'name', label: 'Name' }, + { value: 'created', label: 'Created' }, + { value: 'updated', label: 'Updated' }, + ], + }, + parameters: { controls: { expanded: true, exclude: ['userContext'] } }, +} satisfies Meta>; + +const Default: StoryObj = { + render: (({ current, fields }) => { + const [sort, setSort] = useState(current); + return ( + + + + ); + }) satisfies StoryFn, +}; + +export { Default as SortMenu }; diff --git a/web/packages/shared/components/Controls/SortMenu.tsx b/web/packages/shared/components/Controls/SortMenu.tsx new file mode 100644 index 0000000000000..d6bbc5cdf0d2d --- /dev/null +++ b/web/packages/shared/components/Controls/SortMenu.tsx @@ -0,0 +1,120 @@ +/** + * Teleport + * Copyright (C) 2024 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, { useState } from 'react'; +import { ButtonBorder, Flex, Menu, MenuItem } from 'design'; +import { ArrowDown, ArrowUp } from 'design/Icon'; + +import { HoverTooltip } from 'shared/components/ToolTip'; + +type SortMenuSort = { + fieldName: Exclude; + dir: 'ASC' | 'DESC'; +}; + +export const SortMenu = ({ + current, + fields, + onChange, +}: { + current: SortMenuSort; + fields: { value: SortMenuSort['fieldName']; label: string }[]; + onChange: (value: SortMenuSort) => void; +}) => { + const [anchorEl, setAnchorEl] = useState(null); + + const handleOpen = (event: React.MouseEvent) => + setAnchorEl(event.currentTarget); + + const handleClose = () => setAnchorEl(null); + + const handleSelect = (value: (typeof fields)[number]['value']) => { + handleClose(); + onChange({ + fieldName: value, + dir: current.dir, + }); + }; + + return ( + + + props.theme.colors.spotBackground[2]}; + `} + textTransform="none" + size="small" + px={2} + onClick={handleOpen} + aria-label="Sort by" + aria-haspopup="true" + aria-expanded={!!anchorEl} + > + {fields.find(f => f.value === current.fieldName)?.label} + + + `margin-top: 36px; margin-left: 28px;`} + transformOrigin={{ + vertical: 'top', + horizontal: 'right', + }} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'right', + }} + anchorEl={anchorEl} + open={Boolean(anchorEl)} + onClose={handleClose} + > + {fields.map(({ value, label }) => ( + handleSelect(value)}> + {label} + + ))} + + + + onChange({ + fieldName: current.fieldName, + dir: current.dir === 'ASC' ? 'DESC' : 'ASC', + }) + } + textTransform="none" + css={` + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-color: ${props => props.theme.colors.spotBackground[2]}; + `} + size="small" + > + {current.dir === 'ASC' ? ( + + ) : ( + + )} + + + + ); +}; diff --git a/web/packages/shared/components/Controls/ViewModeSwitch.story.tsx b/web/packages/shared/components/Controls/ViewModeSwitch.story.tsx new file mode 100644 index 0000000000000..adc77e1975715 --- /dev/null +++ b/web/packages/shared/components/Controls/ViewModeSwitch.story.tsx @@ -0,0 +1,61 @@ +/** + * Teleport + * Copyright (C) 2024 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, { useState } from 'react'; +import { Flex } from 'design'; + +import { ViewMode } from 'gen-proto-ts/teleport/userpreferences/v1/unified_resource_preferences_pb'; + +import { ViewModeSwitch } from './ViewModeSwitch'; + +import type { Meta, StoryFn, StoryObj } from '@storybook/react'; + +export default { + title: 'Shared/Controls/ViewModeSwitch', + component: ViewModeSwitch, + argTypes: { + currentViewMode: { + control: { type: 'radio', options: [ViewMode.CARD, ViewMode.LIST] }, + description: 'Current view mode', + table: { defaultValue: { summary: ViewMode.CARD.toString() } }, + }, + setCurrentViewMode: { + control: false, + description: 'Callback to set current view mode', + table: { type: { summary: '(newViewMode: ViewMode) => void' } }, + }, + }, + args: { currentViewMode: ViewMode.CARD }, + parameters: { controls: { expanded: true, exclude: ['userContext'] } }, +} satisfies Meta; + +const Default: StoryObj = { + render: (({ currentViewMode }) => { + const [viewMode, setViewMode] = useState(currentViewMode); + return ( + + + + ); + }) satisfies StoryFn, +}; + +export { Default as ViewModeSwitch }; diff --git a/web/packages/shared/components/Controls/ViewModeSwitch.tsx b/web/packages/shared/components/Controls/ViewModeSwitch.tsx new file mode 100644 index 0000000000000..7997f2de29f66 --- /dev/null +++ b/web/packages/shared/components/Controls/ViewModeSwitch.tsx @@ -0,0 +1,120 @@ +/** + * Teleport + * Copyright (C) 2024 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 styled from 'styled-components'; +import { Rows, SquaresFour } from 'design/Icon'; + +import { ViewMode } from 'gen-proto-ts/teleport/userpreferences/v1/unified_resource_preferences_pb'; + +import { HoverTooltip } from 'shared/components/ToolTip'; + +export const ViewModeSwitch = ({ + currentViewMode, + setCurrentViewMode, +}: { + currentViewMode: ViewMode; + setCurrentViewMode: (viewMode: ViewMode) => void; +}) => { + return ( + + + setCurrentViewMode(ViewMode.CARD)} + role="radio" + aria-label="Card View" + aria-checked={currentViewMode === ViewMode.CARD} + first + > + + + + + setCurrentViewMode(ViewMode.LIST)} + role="radio" + aria-label="List View" + aria-checked={currentViewMode === ViewMode.LIST} + last + > + + + + + ); +}; + +const ViewModeSwitchContainer = styled.div` + height: 22px; + border: ${p => p.theme.borders[1]} ${p => p.theme.colors.spotBackground[2]}; + border-radius: ${p => p.theme.radii[2]}px; + display: flex; + + .selected { + background-color: ${p => p.theme.colors.spotBackground[1]}; + + &:focus-visible, + &:hover { + background-color: ${p => p.theme.colors.spotBackground[1]}; + } + } +`; + +const ViewModeSwitchButton = styled.button<{ first?: boolean; last?: boolean }>` + height: 100%; + width: 100%; + overflow: hidden; + border: none; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + background-color: transparent; + outline: none; + transition: outline-width 150ms ease; + + ${p => + p.first && + ` + border-top-left-radius: ${p.theme.radii[2]}px; + border-bottom-left-radius: ${p.theme.radii[2]}px; + border-right: ${p.theme.borders[1]} ${p.theme.colors.spotBackground[2]}; + `} + ${p => + p.last && + ` + border-top-right-radius: ${p.theme.radii[2]}px; + border-bottom-right-radius: ${p.theme.radii[2]}px; + `} + + &:focus-visible { + outline: ${p => p.theme.borders[1]} + ${p => p.theme.colors.text.slightlyMuted}; + } + + &:focus-visible, + &:hover { + background-color: ${p => p.theme.colors.spotBackground[0]}; + } +`; diff --git a/web/packages/shared/components/UnifiedResources/FilterPanel.tsx b/web/packages/shared/components/UnifiedResources/FilterPanel.tsx index eb3763d7a5ae7..6e62a50a3d4c9 100644 --- a/web/packages/shared/components/UnifiedResources/FilterPanel.tsx +++ b/web/packages/shared/components/UnifiedResources/FilterPanel.tsx @@ -18,25 +18,19 @@ import React, { useState } from 'react'; import styled from 'styled-components'; -import { ButtonBorder, ButtonPrimary, ButtonSecondary } from 'design/Button'; -import { SortDir } from 'design/DataTable/types'; +import { ButtonBorder, ButtonSecondary } from 'design/Button'; import { Text, Flex, Toggle } from 'design'; -import Menu, { MenuItem } from 'design/Menu'; +import Menu from 'design/Menu'; import { CheckboxInput } from 'design/Checkbox'; -import { - ArrowUp, - ArrowDown, - ChevronDown, - SquaresFour, - Rows, - ArrowsIn, - ArrowsOut, - Refresh, -} from 'design/Icon'; +import { ChevronDown, ArrowsIn, ArrowsOut, Refresh } from 'design/Icon'; import { ViewMode } from 'gen-proto-ts/teleport/userpreferences/v1/unified_resource_preferences_pb'; -import { HoverTooltip } from 'design/Tooltip'; +import { HoverTooltip } from 'shared/components/ToolTip'; +import { SortMenu } from 'shared/components/Controls/SortMenu'; +import { ViewModeSwitch } from 'shared/components/Controls/ViewModeSwitch'; + +import { MultiselectMenu } from 'shared/components/Controls/MultiselectMenu'; import { ResourceAvailabilityFilter, FilterKind } from './UnifiedResources'; import { @@ -136,11 +130,17 @@ export function FilterPanel({ data-testid="select_all" /> - - ({ + value: kind, + label: kindToLabel[kind], + disabled: disabled, + }))} + selected={kinds || []} onChange={onKindsChanged} - availableKinds={availableKinds} - kindsFromParams={kinds || []} + label="Types" + tooltip="Filter by resource type" + buffered /> {ClusterDropdown} {availabilityFilter && ( @@ -197,10 +197,19 @@ export function FilterPanel({ )} { + if (newSort.dir !== sort.dir) { + onSortOrderButtonClicked(); + } + if (newSort.fieldName !== activeSortFieldOption.value) { + onSortFieldChange(newSort.fieldName); + } + }} /> @@ -221,303 +230,6 @@ function oppositeSort( } } -type FilterTypesMenuProps = { - availableKinds: FilterKind[]; - kindsFromParams: string[]; - onChange: (kinds: string[]) => void; -}; - -const FilterTypesMenu = ({ - onChange, - availableKinds, - kindsFromParams, -}: FilterTypesMenuProps) => { - const kindOptions = availableKinds.map(({ kind, disabled }) => ({ - value: kind, - label: kindToLabel[kind], - disabled: disabled, - })); - - const [anchorEl, setAnchorEl] = useState(null); - // we have a separate state in the filter so we can select a few different things and then click "apply" - const [kinds, setKinds] = useState(kindsFromParams || []); - const handleOpen = event => { - setKinds(kindsFromParams); - setAnchorEl(event.currentTarget); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - // if we cancel, we reset the kinds to what is already selected in the params - const cancelUpdate = () => { - setKinds(kindsFromParams); - handleClose(); - }; - - const handleSelect = (value: string) => { - let newKinds = [...kinds]; - if (newKinds.includes(value)) { - newKinds = newKinds.filter(v => v !== value); - } else { - newKinds.push(value); - } - setKinds(newKinds); - }; - - const handleSelectAll = () => { - setKinds(kindOptions.filter(k => !k.disabled).map(k => k.value)); - }; - - const handleClearAll = () => { - setKinds([]); - }; - - const applyFilters = () => { - onChange(kinds); - handleClose(); - }; - - return ( - - - - Types{' '} - {kindsFromParams.length > 0 ? `(${kindsFromParams.length})` : ''} - - {kindsFromParams.length > 0 && } - - - `margin-top: 36px;`} - transformOrigin={{ - vertical: 'top', - horizontal: 'left', - }} - anchorOrigin={{ - vertical: 'bottom', - horizontal: 'left', - }} - anchorEl={anchorEl} - open={Boolean(anchorEl)} - onClose={cancelUpdate} - > - - - Select All - - - Clear All - - - {kindOptions.map(kind => { - const $checkbox = ( - <> - { - handleSelect(kind.value); - }} - id={kind.value} - checked={kinds.includes(kind.value)} - /> - - {kind.label} - - - ); - return ( - (!kind.disabled ? handleSelect(kind.value) : null)} - > - {kind.disabled ? ( - - {$checkbox} - - ) : ( - $checkbox - )} - - ); - })} - - - Apply Filters - - - Cancel - - - - - ); -}; - -type SortMenuProps = { - transformOrigin?: any; - anchorOrigin?: any; - sortType: string; - sortDir: SortDir; - onChange: (value: string) => void; - onDirChange: () => void; -}; - -const SortMenu: React.FC = props => { - const { sortType, onChange, onDirChange, sortDir } = props; - const [anchorEl, setAnchorEl] = React.useState(null); - - const handleOpen = event => { - setAnchorEl(event.currentTarget); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - const handleSelect = (value: string) => { - handleClose(); - onChange(value); - }; - - return ( - - - props.theme.colors.spotBackground[2]}; - `} - textTransform="none" - size="small" - px={2} - onClick={handleOpen} - > - {sortType} - - - `margin-top: 36px;`} - transformOrigin={{ - vertical: 'top', - horizontal: 'center', - }} - anchorOrigin={{ - vertical: 'bottom', - horizontal: 'center', - }} - anchorEl={anchorEl} - open={Boolean(anchorEl)} - onClose={handleClose} - > - handleSelect('name')}>Name - handleSelect('kind')}>Type - - - props.theme.colors.spotBackground[2]}; - `} - size="small" - > - {sortDir === 'ASC' ? : } - - - - ); -}; - -function kindArraysEqual(arr1: string[], arr2: string[]) { - if (arr1.length !== arr2.length) { - return false; - } - - const sortedArr1 = arr1.slice().sort(); - const sortedArr2 = arr2.slice().sort(); - - for (let i = 0; i < sortedArr1.length; i++) { - if (sortedArr1[i] !== sortedArr2[i]) { - return false; - } - } - - return true; -} - -function ViewModeSwitch({ - currentViewMode, - setCurrentViewMode, -}: { - currentViewMode: ViewMode; - setCurrentViewMode: (viewMode: ViewMode) => void; -}) { - return ( - - setCurrentViewMode(ViewMode.CARD)} - css={` - border-right: 1px solid - ${props => props.theme.colors.spotBackground[2]}; - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; - `} - > - - - setCurrentViewMode(ViewMode.LIST)} - css={` - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; - `} - > - - - - ); -} - const IncludedResourcesSelector = ({ onChange, availabilityFilter, @@ -580,7 +292,9 @@ const IncludedResourcesSelector = ({ onClose={handleClose} > - Show requestable resources + + Show requestable resources + props.theme.colors.spotBackground[2]}; - border-radius: 4px; - display: flex; - - .selected { - background-color: ${props => props.theme.colors.spotBackground[1]}; - - &:hover { - background-color: ${props => props.theme.colors.spotBackground[1]}; - } - } -`; - -const ViewModeSwitchButton = styled.button` - height: 100%; - width: 50%; - overflow: hidden; - border: none; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - - background-color: transparent; - - &:hover { - background-color: ${props => props.theme.colors.spotBackground[0]}; - } -`; - const FiltersExistIndicator = styled.div` position: absolute; top: -4px; @@ -641,8 +322,9 @@ const FiltersExistIndicator = styled.div` const AccessRequestsToggleItem = styled.div` min-height: 40px; box-sizing: border-box; - padding-left: ${props => props.theme.space[2]}px; - padding-right: ${props => props.theme.space[2]}px; + padding-top: 2px; + padding-left: ${props => props.theme.space[3]}px; + padding-right: ${props => props.theme.space[3]}px; display: flex; justify-content: flex-start; align-items: center;