Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions web/packages/shared/components/ClusterDropdown/ClusterDropdown.tsx
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we should add an abort controller here on option change. in the unified resource changing filter mid way cancels previous requests, we should do the same for cluster filter.

when one cluster is loading, changing to another one will briefly flash the previous results before the new one (in my case my other cluster was down, so it briefly flashed me a red error banner before loading the new response)

Copy link
Copy Markdown
Contributor Author

@avatus avatus Jan 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry i hsould've been more clear on access request, the user groups looks like you can change cluster too?? but new cluster dropdown is missing:

This branch is a bit out of date, but we don't have a user groups dropdown anymore. It's no longer an option to select from the user groups table on master/v14

i think we should add an abort controller here on option change.

sure!

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i'm so dumb. so the abort controller didn't fix what i was seeing (b/c duh it's cached..) the flash i'm getting it's from the non-fetch-cluster api call that fails (eg: get sessions with a cluster that's down). that would mean we'd have to add aborters everywhere but i don't think it's that a big deal to change atm

Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* 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 <http://www.gnu.org/licenses/>.
*/

import React, { useState, useEffect } from 'react';
import { useHistory } from 'react-router';
import { ButtonSecondary, Flex, Menu, MenuItem, Text } from 'design';
import { ChevronDown } from 'design/Icon';
import cfg from 'teleport/config';
import { Cluster } from 'teleport/services/clusters';

import { HoverTooltip } from 'shared/components/ToolTip';

export interface ClusterDropdownProps {
clusterLoader: ClusterLoader;
clusterId: string;
/*
* onChange is an optional prop. If onChange is not passed, it will use the built in "changeCluster" function
*/
onChange?: (newValue: string) => void;
/*
* onError is required because this dropdown can be placed on any page, it does not display its own error
* messages. Even if using the internal "loadClusters", we will pass the error back to be consumed by the parent.
*/
onError: (errorMessage: string) => void;
}

interface ClusterLoader {
fetchClusters: (
signal?: AbortSignal,
fromCache?: boolean
) => Promise<Cluster[]>;
clusters: Cluster[];
}

function createOptions(clusters: Cluster[]) {
return clusters.map(cluster => ({
value: cluster.clusterId,
label: cluster.clusterId,
}));
}

export function ClusterDropdown({
clusterLoader,
clusterId,
onChange,
onError,
}: ClusterDropdownProps) {
const initialClusters = clusterLoader.clusters;
const [options, setOptions] = React.useState<Option[]>(
createOptions(initialClusters)
);
const history = useHistory();
const [anchorEl, setAnchorEl] = useState(null);

const selectedOption = {
value: clusterId,
label: clusterId,
};

function loadClusters(signal: AbortSignal) {
onError('');
try {
return clusterLoader.fetchClusters(signal);
} catch (err) {
onError(err.message);
}
}

function changeCluster(clusterId: string) {
const newPathName = cfg.getClusterRoute(clusterId);

const oldPathName = cfg.getClusterRoute(selectedOption.value);

const newPath = history.location.pathname.replace(oldPathName, newPathName);

// keep current view just change the clusterId
history.push(newPath);
}

function onChangeOption(clusterId: string) {
if (onChange) {
onChange(clusterId);
} else {
changeCluster(clusterId);
}
handleClose();
}

useEffect(() => {
const signal = new AbortController();
async function getOptions() {
try {
const res = await loadClusters(signal.signal);
setOptions(createOptions(res));
} catch (err) {
onError(err.message);
}
}

getOptions();
return () => {
signal.abort();
};
}, []);

const handleOpen = event => {
setAnchorEl(event.currentTarget);
};

const handleClose = () => {
setAnchorEl(null);
};

if (options.length < 1) {
return null;
}

return (
<Flex textAlign="center" alignItems="center">
<HoverTooltip tipContent={'Select cluster'}>
<ButtonSecondary
px={2}
css={`
border-color: ${props => props.theme.colors.spotBackground[0]};
`}
textTransform="none"
size="small"
onClick={handleOpen}
>
{selectedOption.label}
<ChevronDown ml={2} size="small" color="text.slightlyMuted" />
</ButtonSecondary>
</HoverTooltip>
<Menu
popoverCss={() => `margin-top: 36px;`}
transformOrigin={{
vertical: 'top',
horizontal: 'left',
}}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleClose}
>
{options.map(cluster => (
<MenuItem
px={2}
key={cluster.value}
onClick={() => onChangeOption(cluster.value)}
>
<Text ml={2} fontWeight={300} fontSize={2}>
{cluster.label}
</Text>
</MenuItem>
))}
</Menu>
</Flex>
);
}

type Option = { value: string; label: string };
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ interface FilterPanelProps {
expandAllLabels: boolean;
setExpandAllLabels: (expandAllLabels: boolean) => void;
hideViewModeOptions: boolean;
/*
* ClusterDropdown is an optional prop to add a ClusterDropdown to the
* FilterPanel component. This is useful to turn off in Connect and use on web only
*/
ClusterDropdown?: JSX.Element;
}

export function FilterPanel({
Expand All @@ -79,6 +84,7 @@ export function FilterPanel({
expandAllLabels,
setExpandAllLabels,
hideViewModeOptions,
ClusterDropdown = null,
}: FilterPanelProps) {
const { sort, kinds } = params;

Expand Down Expand Up @@ -120,6 +126,7 @@ export function FilterPanel({
availableKinds={availableKinds}
kindsFromParams={kinds || []}
/>
{ClusterDropdown}
</Flex>
<Flex gap={2} alignItems="center">
<Flex mr={1}>{BulkActions}</Flex>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ export interface UnifiedResourcesProps {
*/
pinning: UnifiedResourcesPinning;
availableKinds: FilterKind[];
/*
* ClusterDropdown is an optional prop to add a ClusterDropdown to the
* FilterPanel component. This is useful to turn off in Connect and use on web only
*/
ClusterDropdown?: JSX.Element;
setParams(params: UnifiedResourcesQueryParams): void;
/** A list of actions that can be performed on the selected items. */
bulkActions?: BulkAction[];
Expand Down Expand Up @@ -175,6 +180,7 @@ export function UnifiedResources(props: UnifiedResourcesProps) {
unifiedResourcePreferencesAttempt,
updateUnifiedResourcesPreferences,
unifiedResourcePreferences,
ClusterDropdown,
bulkActions = [],
} = props;

Expand Down Expand Up @@ -439,6 +445,7 @@ export function UnifiedResources(props: UnifiedResourcesProps) {
currentViewMode={unifiedResourcePreferences.viewMode}
setCurrentViewMode={selectViewMode}
expandAllLabels={expandAllLabels}
ClusterDropdown={ClusterDropdown}
setExpandAllLabels={expandAllLabels => {
setLabelsViewMode(
expandAllLabels
Expand Down
15 changes: 14 additions & 1 deletion web/packages/teleport/src/Audit/Audit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import React from 'react';
import React, { useState } from 'react';

import { Danger } from 'design/Alert';
import { Indicator, Box } from 'design';
import { ClusterDropdown } from 'shared/components/ClusterDropdown/ClusterDropdown';

import RangePicker from 'teleport/components/EventRangePicker';
import {
Expand Down Expand Up @@ -53,7 +54,9 @@ export function Audit(props: State) {
clusterId,
fetchMore,
fetchStatus,
ctx,
} = props;
const [errorMessage, setErrorMessage] = useState('');

return (
<FeatureBox>
Expand All @@ -68,6 +71,16 @@ export function Audit(props: State) {
</FeatureHeader>
<ExternalAuditStorageCta />
{attempt.status === 'failed' && <Danger> {attempt.statusText} </Danger>}
{!errorMessage && (
<Box mb={4}>
<ClusterDropdown
clusterLoader={ctx.clusterService}
clusterId={clusterId}
onError={setErrorMessage}
/>
</Box>
)}
{errorMessage && <Danger>{errorMessage}</Danger>}
{attempt.status === 'processing' && (
<Box textAlign="center" m={10}>
<Indicator />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12852,6 +12852,9 @@ exports[`loaded audit log screen 1`] = `
</div>
</div>
</div>
<div
class="c3"
/>
<div
class="c10"
>
Expand Down
1 change: 1 addition & 0 deletions web/packages/teleport/src/Audit/useAuditEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export default function useAuditEvents(
range,
setRange,
rangeOptions,
ctx,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ import { Document, createContext } from './DocumentNodes.story';

test('render DocumentNodes', async () => {
const ctx = createContext();
jest.spyOn(ctx, 'fetchClusters');
jest.spyOn(ctx.clustersService, 'fetchClusters');
jest.spyOn(ctx.nodesService, 'fetchNodes');

const { container } = render(<Document value={ctx} />);
await waitFor(() => expect(ctx.fetchClusters).toHaveBeenCalledTimes(1));
await waitFor(() =>
expect(ctx.clustersService.fetchClusters).toHaveBeenCalledTimes(1)
);
await waitFor(() =>
expect(ctx.nodesService.fetchNodes).toHaveBeenCalledTimes(1)
);
Expand Down
25 changes: 15 additions & 10 deletions web/packages/teleport/src/Console/DocumentNodes/DocumentNodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,19 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

import React from 'react';
import React, { useState } from 'react';
import styled from 'styled-components';
import { Indicator, Flex, Box } from 'design';
import { Indicator, Box, Text } from 'design';
import { Danger } from 'design/Alert';

import { ClusterDropdown } from 'shared/components/ClusterDropdown/ClusterDropdown';

import NodeList from 'teleport/components/NodeList';
import ErrorMessage from 'teleport/components/AgentErrorMessage';
import Document from 'teleport/Console/Document';

import * as stores from 'teleport/Console/stores/types';

import ClusterSelector from './ClusterSelector';
import useNodes from './useNodes';

type Props = {
Expand All @@ -36,6 +38,7 @@ type Props = {

export default function DocumentNodes(props: Props) {
const { doc, visible } = props;
const [clusterDropdownError, setClusterDropdownError] = useState('');
const {
fetchedData,
fetchNext,
Expand All @@ -53,6 +56,7 @@ export default function DocumentNodes(props: Props) {
getNodeSshLogins,
onLabelClick,
pageIndicators,
consoleCtx,
} = useNodes(doc);

function onLoginMenuSelect(
Expand All @@ -79,15 +83,16 @@ export default function DocumentNodes(props: Props) {
return (
<Document visible={visible}>
<Container mx="auto" mt="4" px="5">
<Flex justifyContent="space-between" mb="4" alignItems="end">
<ClusterSelector
value={doc.clusterId}
width="336px"
maxMenuHeight={200}
mr="20px"
<Box justifyContent="space-between" mb="2" alignItems="end">
<Text fontSize={1}>Clusters:</Text>
<ClusterDropdown
clusterLoader={consoleCtx.clustersService}
onChange={onChangeCluster}
clusterId={doc.clusterId}
onError={setClusterDropdownError}
/>
</Flex>
</Box>
{clusterDropdownError && <Danger>{clusterDropdownError}</Danger>}
{attempt.status === 'processing' && (
<Box textAlign="center" m={10}>
<Indicator />
Expand Down
Loading