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 && }
+
+
+
+
+ );
+};
+
+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}
+
+
+
+
+
+ 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 && }
-
-
-
-
- );
-};
-
-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}
-
-
-
-
- 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;