mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-11 22:49:54 +00:00
Show roles in expandable list (#65577)
This commit is contained in:
@@ -40,8 +40,27 @@ interface StoryProps {
|
||||
activeClusterExpired: boolean;
|
||||
deviceTrust: 'enrolled' | 'required-not-enrolled' | 'not-enrolled';
|
||||
showProfileErrors: boolean;
|
||||
roles: string[];
|
||||
}
|
||||
|
||||
function withUuidSuffix(roles: string[]): string[] {
|
||||
return roles.map(role => `${role}-${crypto.randomUUID().split('-').at(0)}`);
|
||||
}
|
||||
|
||||
const baseRoles = [
|
||||
'circle-mark-app-access',
|
||||
'grafana-lite-app-access',
|
||||
'grafana-gold-app-access',
|
||||
'release-lion-app-access',
|
||||
'release-fox-app-access',
|
||||
'sales-center-lorem-app-access',
|
||||
'sales-center-ipsum-db-access',
|
||||
'sales-center-shop-app-access',
|
||||
'sales-center-floor-db-access',
|
||||
];
|
||||
|
||||
const roles = withUuidSuffix(baseRoles);
|
||||
|
||||
const meta: Meta<StoryProps> = {
|
||||
title: 'Teleterm/Identity',
|
||||
component: props => {
|
||||
@@ -76,6 +95,7 @@ const meta: Meta<StoryProps> = {
|
||||
? new Date()
|
||||
: new Date(Date.now() + 24 * 60 * 60 * 1000)
|
||||
),
|
||||
roles: props.roles || clusters[0].loggedInUser.roles,
|
||||
isDeviceTrusted: props.deviceTrust === 'enrolled',
|
||||
trustedDeviceRequirement:
|
||||
props.deviceTrust === 'required-not-enrolled'
|
||||
@@ -130,17 +150,7 @@ const clusterOrange = makeRootCluster({
|
||||
name: 'orange-psv-eindhoven-eredivisie-production-lorem-ipsum',
|
||||
loggedInUser: makeLoggedInUser({
|
||||
name: 'ruud-van-nistelrooy-van-der-sar',
|
||||
roles: [
|
||||
'circle-mark-app-access',
|
||||
'grafana-lite-app-access',
|
||||
'grafana-gold-app-access',
|
||||
'release-lion-app-access',
|
||||
'release-fox-app-access',
|
||||
'sales-center-lorem-app-access',
|
||||
'sales-center-ipsum-db-access',
|
||||
'sales-center-shop-app-access',
|
||||
'sales-center-floor-db-access',
|
||||
],
|
||||
roles,
|
||||
}),
|
||||
uri: '/clusters/orange',
|
||||
});
|
||||
@@ -230,6 +240,13 @@ export const OneClusterWithActiveCluster: StoryObj<StoryProps> = {
|
||||
},
|
||||
};
|
||||
|
||||
export const ManyRoles: StoryObj<StoryProps> = {
|
||||
args: {
|
||||
clusters: ['violet'],
|
||||
roles: Array.from({ length: 30 }, () => withUuidSuffix(baseRoles)).flat(),
|
||||
},
|
||||
};
|
||||
|
||||
export const ManyClustersWithNoActiveCluster: StoryObj<StoryProps> = {
|
||||
args: {
|
||||
clusters: ['orange', 'green', 'violet'],
|
||||
|
||||
@@ -90,3 +90,37 @@ test.each([
|
||||
|
||||
await testCase.expect();
|
||||
});
|
||||
|
||||
test('roles list remembers it was expanded', async () => {
|
||||
const appContext = new MockAppContext();
|
||||
appContext.addRootCluster(
|
||||
makeRootCluster({
|
||||
loggedInUser: makeLoggedInUser({ roles: ['requests-reviewer'] }),
|
||||
})
|
||||
);
|
||||
|
||||
render(
|
||||
<MockAppContextProvider appContext={appContext}>
|
||||
<IdentityContainer />
|
||||
</MockAppContextProvider>
|
||||
);
|
||||
|
||||
await toggleProfiles();
|
||||
|
||||
const roleItemBeforeExpand = await screen.findByText('requests-reviewer');
|
||||
expect(roleItemBeforeExpand).not.toBeVisible();
|
||||
|
||||
await userEvent.click(await screen.findByText(/Roles/));
|
||||
expect(await screen.findByText('requests-reviewer')).toBeVisible();
|
||||
|
||||
// Close and open again.
|
||||
await toggleProfiles();
|
||||
expect(screen.queryByText('requests-reviewer')).not.toBeInTheDocument();
|
||||
|
||||
await toggleProfiles();
|
||||
expect(await screen.findByText('requests-reviewer')).toBeVisible();
|
||||
});
|
||||
|
||||
async function toggleProfiles() {
|
||||
await userEvent.click(await screen.findByTitle(/Open Profiles/));
|
||||
}
|
||||
|
||||
@@ -109,6 +109,7 @@ export function IdentityContainer() {
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
onClose={() => setOpen(false)}
|
||||
popoverCss={() => `max-width: min(450px, 90%)`}
|
||||
updatePositionOnChildResize
|
||||
>
|
||||
<Container>
|
||||
{activeRootCluster && (
|
||||
|
||||
@@ -20,7 +20,7 @@ import { formatDistanceToNowStrict, isPast } from 'date-fns';
|
||||
import { JSX } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { ButtonText, Flex, Label, P3, Stack } from 'design';
|
||||
import { ButtonText, Flex, P3, Stack } from 'design';
|
||||
import {
|
||||
Clock,
|
||||
Logout,
|
||||
@@ -33,6 +33,7 @@ import { Timestamp } from 'gen-proto-ts/google/protobuf/timestamp_pb';
|
||||
import { Cluster } from 'gen-proto-ts/teleport/lib/teleterm/v1/cluster_pb';
|
||||
|
||||
import { ProfileStatusError } from 'teleterm/ui/components/ProfileStatusError';
|
||||
import { usePersistedState } from 'teleterm/ui/hooks/usePersistedState';
|
||||
import { WorkspaceColor } from 'teleterm/ui/services/workspacesService';
|
||||
import { DeviceTrustStatus } from 'teleterm/ui/TopBar/Identity/Identity';
|
||||
import { RootClusterUri, routing } from 'teleterm/ui/uri';
|
||||
@@ -44,6 +45,7 @@ import {
|
||||
IdentityListItem,
|
||||
TitleAndSubtitle,
|
||||
} from './IdentityListItem';
|
||||
import { Roles } from './Roles';
|
||||
|
||||
export function ActiveCluster(props: {
|
||||
activeCluster: Cluster | undefined;
|
||||
@@ -57,6 +59,11 @@ export function ActiveCluster(props: {
|
||||
const validUntil =
|
||||
props.activeCluster.loggedInUser?.validUntil &&
|
||||
Timestamp.toDate(props.activeCluster.loggedInUser.validUntil);
|
||||
const roles = props.activeCluster.loggedInUser?.roles || [];
|
||||
const [isRolesExpanded, setIsRolesExpanded] = usePersistedState(
|
||||
'showRolesExpanded',
|
||||
false
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -102,20 +109,12 @@ export function ActiveCluster(props: {
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
<Flex flexWrap="wrap" gap={1} mt={1}>
|
||||
{props.activeCluster.loggedInUser?.roles.map(role => (
|
||||
<Label
|
||||
css={`
|
||||
line-height: 20px;
|
||||
`}
|
||||
key={role}
|
||||
kind="secondary"
|
||||
>
|
||||
{role}
|
||||
</Label>
|
||||
))}
|
||||
</Flex>
|
||||
<Stack gap={0}>
|
||||
<Roles
|
||||
roles={roles}
|
||||
expanded={isRolesExpanded}
|
||||
setExpanded={setIsRolesExpanded}
|
||||
/>
|
||||
{validUntil && (
|
||||
<Flex gap={1} color="text.slightlyMuted">
|
||||
<Clock size="small" />
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Teleport
|
||||
* Copyright (C) 2026 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 styled from 'styled-components';
|
||||
|
||||
import Flex, { Stack } from 'design/Flex';
|
||||
import { ChevronDown } from 'design/Icon';
|
||||
import { P3 } from 'design/Text';
|
||||
|
||||
export function Roles(props: {
|
||||
roles: string[];
|
||||
expanded: boolean;
|
||||
setExpanded(expanded: boolean): void;
|
||||
}) {
|
||||
return (
|
||||
<Details
|
||||
open={props.expanded}
|
||||
onToggle={event => props.setExpanded(event.currentTarget.open)}
|
||||
>
|
||||
<Summary>
|
||||
<Flex gap={1}>
|
||||
<Chevron size="small" />
|
||||
Roles ({props.roles.length})
|
||||
</Flex>
|
||||
</Summary>
|
||||
<Expandable>
|
||||
{props.roles.length ? (
|
||||
<List>
|
||||
{props.roles.map(role => (
|
||||
<li key={role}>{role}</li>
|
||||
))}
|
||||
</List>
|
||||
) : (
|
||||
<P3 ml={1}>No roles.</P3>
|
||||
)}
|
||||
</Expandable>
|
||||
</Details>
|
||||
);
|
||||
}
|
||||
|
||||
const Details = styled.details`
|
||||
${props => props.theme.typography.body3};
|
||||
color: ${props => props.theme.colors.text.slightlyMuted};
|
||||
width: 100%;
|
||||
|
||||
&::details-content {
|
||||
transition:
|
||||
height 0.2s ease,
|
||||
content-visibility 0.2s ease allow-discrete;
|
||||
height: 0;
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
interpolate-size: allow-keywords;
|
||||
|
||||
&[open]::details-content {
|
||||
height: auto;
|
||||
}
|
||||
`;
|
||||
|
||||
const Chevron = styled(ChevronDown)`
|
||||
transition: transform 0.2s ease;
|
||||
|
||||
${Details}[open] & {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const Summary = styled.summary`
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
width: 100%;
|
||||
list-style: none;
|
||||
border-radius: ${props => props.theme.space[1]}px;
|
||||
|
||||
&:hover {
|
||||
color: ${props => props.theme.colors.text.main};
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid ${props => props.theme.colors.text.slightlyMuted};
|
||||
}
|
||||
`;
|
||||
|
||||
const Expandable = styled(Stack).attrs({ pl: 3 })`
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: ${props => props.theme.space[2]}px;
|
||||
top: ${props => props.theme.space[1]}px;
|
||||
bottom: ${props => props.theme.space[1]}px;
|
||||
width: 2px;
|
||||
background: ${({ theme }) => theme.colors.interactive.tonal.neutral[0]};
|
||||
}
|
||||
`;
|
||||
|
||||
const List = styled.ul`
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0 0 0 ${props => props.theme.space[3]}px;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
max-height: 30vh;
|
||||
`;
|
||||
@@ -66,6 +66,8 @@ export interface StatePersistenceState {
|
||||
};
|
||||
/** Shows a banner above the cluster list to notify that a new tsh home dir is used. */
|
||||
showTshHomeMigrationBanner: boolean;
|
||||
/** Remembers whether the roles section in the identity popover is expanded. */
|
||||
showRolesExpanded?: boolean;
|
||||
}
|
||||
|
||||
// Before adding new methods to this service, consider using usePersistedState instead.
|
||||
|
||||
Reference in New Issue
Block a user