Add admin panel workspace detail page with chat viewer (#19579)

## Overview
Adds comprehensive admin panel functionality for viewing workspace
details and AI chat threads.

## Changes

### Frontend
- **New Routes**: Added `AdminPanelWorkspaceDetail` and
`AdminPanelWorkspaceChatThread` pages with lazy loading
- **New Queries**: 
  - `getAdminWorkspaceChatThreads` - fetch chat threads for a workspace
  - `getAdminChatThreadMessages` - fetch messages for a specific thread
  - `workspaceLookupAdminPanel` - lookup workspace info and users
- **New Components**:
- `SettingsAdminWorkspaceDetail` - displays workspace info and chat
sessions tabs
- `SettingsAdminWorkspaceChatThread` - renders chat conversation with
message bubbles
- **Navigation**: Updated AI admin panel to link to workspace detail
pages
- **Settings Paths**: Added `AdminPanelWorkspaceDetail` and
`AdminPanelWorkspaceChatThread` paths

### Backend
- **New DTOs**:
  - `AdminWorkspaceChatThreadDTO` - workspace chat thread data
  - `AdminChatThreadMessagesDTO` - thread with messages
  - `AdminChatMessageDTO` - individual message with parts
- **New Resolvers**: Added three queries to `AdminPanelResolver`
- **New Service Methods**:
  - `workspaceLookup()` - fetch workspace info
  - `getWorkspaceChatThreads()` - list chat threads
  - `getChatThreadMessages()` - fetch thread messages with validation
- **Module Updates**: Added entity imports for workspace, user, AI chat,
and feature flag data

### Security
- Added `allowImpersonation` check before accessing chat data
- Validates workspace ownership and access permissions

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Félix Malfait
2026-04-13 09:47:34 +02:00
committed by GitHub
parent 150c326369
commit 09806d7d8c
49 changed files with 3456 additions and 1592 deletions
@@ -0,0 +1,127 @@
import { t } from '@lingui/core/macro';
import { styled } from '@linaria/react';
import { LazyMarkdownRenderer } from '@/ai/components/LazyMarkdownRenderer';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { Card } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import {
AgentMessageRole,
type GetAdminChatThreadMessagesQuery,
} from '~/generated-metadata/graphql';
type ChatMessage = NonNullable<
GetAdminChatThreadMessagesQuery['getAdminChatThreadMessages']
>['messages'][number];
type SettingsAdminChatThreadMessageListProps = {
messages: ChatMessage[];
};
const StyledMessagesContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${themeCssVariables.spacing[4]};
`;
const StyledMessageBubble = styled.div<{ isUser?: boolean }>`
align-items: ${({ isUser }) => (isUser ? 'flex-end' : 'flex-start')};
display: flex;
flex-direction: column;
width: 100%;
`;
const StyledMessageContent = styled.div<{ isUser?: boolean }>`
background: ${({ isUser }) =>
isUser ? themeCssVariables.background.tertiary : 'transparent'};
border-radius: ${themeCssVariables.border.radius.sm};
color: ${({ isUser }) =>
isUser
? themeCssVariables.font.color.secondary
: themeCssVariables.font.color.primary};
font-weight: ${({ isUser }) => (isUser ? 500 : 400)};
line-height: 1.4em;
max-width: 100%;
overflow-wrap: break-word;
padding: ${({ isUser }) =>
isUser ? `0 ${themeCssVariables.spacing[2]}` : '0'};
white-space: ${({ isUser }) => (isUser ? 'pre-wrap' : 'normal')};
width: ${({ isUser }) => (isUser ? 'fit-content' : '100%')};
`;
const StyledRoleLabel = styled.span`
color: ${themeCssVariables.font.color.light};
font-size: ${themeCssVariables.font.size.xs};
font-weight: ${themeCssVariables.font.weight.medium};
margin-bottom: ${themeCssVariables.spacing[1]};
text-transform: capitalize;
`;
const StyledTimestamp = styled.span`
color: ${themeCssVariables.font.color.light};
font-size: ${themeCssVariables.font.size.xs};
margin-top: ${themeCssVariables.spacing[1]};
`;
export const SettingsAdminChatThreadMessageList = ({
messages,
}: SettingsAdminChatThreadMessageListProps) => {
const visibleMessages = messages.filter(
(message) => message.role !== AgentMessageRole.SYSTEM,
);
if (visibleMessages.length === 0) {
return (
<Card rounded>
<TableRow gridTemplateColumns="1fr">
<TableCell
color={themeCssVariables.font.color.tertiary}
align="center"
>
{t`No messages found.`}
</TableCell>
</TableRow>
</Card>
);
}
return (
<StyledMessagesContainer>
{visibleMessages.map((message) => {
const isUser = message.role === AgentMessageRole.USER;
const textParts = message.parts
.filter((part) => part.type === 'text' && part.textContent !== null)
.map((part) => part.textContent)
.join('\n');
const toolParts = message.parts.filter(
(part) => part.type === 'tool-call' && part.toolName !== null,
);
if (textParts.length === 0 && toolParts.length === 0) {
return null;
}
return (
<StyledMessageBubble key={message.id} isUser={isUser}>
<StyledRoleLabel>{message.role}</StyledRoleLabel>
{textParts.length > 0 && (
<StyledMessageContent isUser={isUser}>
{isUser ? textParts : <LazyMarkdownRenderer text={textParts} />}
</StyledMessageContent>
)}
{toolParts.map((part, index) => (
<StyledMessageContent key={index} isUser={false}>
{t`Tool call: ${part.toolName}`}
</StyledMessageContent>
))}
<StyledTimestamp>
{new Date(message.createdAt).toLocaleString()}
</StyledTimestamp>
</StyledMessageBubble>
);
})}
</StyledMessagesContainer>
);
};
@@ -1,134 +1,74 @@
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
import { SettingsAdminWorkspaceContent } from '@/settings/admin-panel/components/SettingsAdminWorkspaceContent';
import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SettingsAdminVersionContainer } from '@/settings/admin-panel/components/SettingsAdminVersionContainer';
import { ADMIN_PANEL_RECENT_USERS } from '@/settings/admin-panel/graphql/queries/adminPanelRecentUsers';
import { ADMIN_PANEL_TOP_WORKSPACES } from '@/settings/admin-panel/graphql/queries/adminPanelTopWorkspaces';
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { Table } from '@/ui/layout/table/components/Table';
import { TableBody } from '@/ui/layout/table/components/TableBody';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useQuery } from '@apollo/client/react';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useState } from 'react';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useMutation } from '@apollo/client/react';
import { UserLookupAdminPanelDocument } from '~/generated-metadata/graphql';
import { useDebounce } from 'use-debounce';
import { SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { currentUserState } from '@/auth/states/currentUserState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
import { SettingsAdminVersionContainer } from '@/settings/admin-panel/components/SettingsAdminVersionContainer';
import { SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminUserLookupWorkspaceTabsId';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentState';
import { getImageAbsoluteURI, isDefined } from 'twenty-shared/utils';
import {
H2Title,
IconId,
IconMail,
IconSearch,
IconUser,
} from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
gap: ${themeCssVariables.spacing[2]};
const StyledEmptyState = styled.div`
color: ${themeCssVariables.font.color.tertiary};
padding: ${themeCssVariables.spacing[4]} 0;
`;
export const SettingsAdminGeneral = () => {
const [userIdentifier, setUserIdentifier] = useState('');
const { enqueueErrorSnackBar } = useSnackBar();
const [userSearchTerm, setUserSearchTerm] = useState('');
const [debouncedUserSearchTerm] = useDebounce(userSearchTerm, 300);
const [activeTabId, setActiveTabId] = useAtomComponentState(
activeTabIdComponentState,
SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID,
);
const [userLookupResult, setUserLookupResult] = useAtomState(
userLookupResultState,
);
const [isUserLookupLoading, setIsUserLookupLoading] = useState(false);
const [userLookup] = useMutation(UserLookupAdminPanelDocument);
const [workspaceSearchTerm, setWorkspaceSearchTerm] = useState('');
const [debouncedWorkspaceSearchTerm] = useDebounce(workspaceSearchTerm, 300);
const currentUser = useAtomStateValue(currentUserState);
const canAccessFullAdminPanel = currentUser?.canAccessFullAdminPanel;
const canImpersonate = currentUser?.canImpersonate;
const canManageFeatureFlags = useAtomStateValue(canManageFeatureFlagsState);
const handleSearch = async () => {
setActiveTabId('');
setIsUserLookupLoading(true);
setUserLookupResult(null);
const { data: recentUsersData, loading: isLoadingUsers } = useQuery<{
adminPanelRecentUsers: {
id: string;
email: string;
firstName?: string | null;
lastName?: string | null;
createdAt: string;
workspaceName?: string | null;
workspaceId?: string | null;
}[];
}>(ADMIN_PANEL_RECENT_USERS, {
variables: { searchTerm: debouncedUserSearchTerm },
skip: !canImpersonate,
});
const response = await userLookup({
variables: { userIdentifier },
onCompleted: (data) => {
setIsUserLookupLoading(false);
if (isDefined(data?.userLookupAdminPanel)) {
setUserLookupResult(data.userLookupAdminPanel);
}
},
onError: (error) => {
setIsUserLookupLoading(false);
enqueueErrorSnackBar({
apolloError: error,
});
},
});
const { data: topWorkspacesData, loading: isLoadingWorkspaces } = useQuery<{
adminPanelTopWorkspaces: {
id: string;
name: string;
totalUsers: number;
subdomain: string;
}[];
}>(ADMIN_PANEL_TOP_WORKSPACES, {
variables: { searchTerm: debouncedWorkspaceSearchTerm },
skip: !canImpersonate,
});
const result = response.data?.userLookupAdminPanel;
if (isDefined(result?.workspaces) && result.workspaces.length > 0) {
setActiveTabId(result.workspaces[0].id);
}
};
const activeWorkspace = userLookupResult?.workspaces.find(
(workspace) => workspace.id === activeTabId,
);
const tabs =
userLookupResult?.workspaces.map((workspace) => ({
id: workspace.id,
title: workspace.name,
logo:
getImageAbsoluteURI({
imageUrl: isNonEmptyString(workspace.logo)
? workspace.logo
: DEFAULT_WORKSPACE_LOGO,
baseUrl: REACT_APP_SERVER_BASE_URL,
}) ?? '',
})) ?? [];
const userFullName = `${userLookupResult?.user.firstName || ''} ${
userLookupResult?.user.lastName || ''
}`.trim();
const userInfoItems = [
{
Icon: IconUser,
label: t`Name`,
value: userFullName,
},
{
Icon: IconMail,
label: t`Email`,
value: userLookupResult?.user.email,
},
{
Icon: IconId,
label: t`ID`,
value: userLookupResult?.user.id,
},
];
const recentUsers = recentUsersData?.adminPanelRecentUsers ?? [];
const topWorkspaces = topWorkspacesData?.adminPanelTopWorkspaces ?? [];
return (
<>
@@ -143,63 +83,105 @@ export const SettingsAdminGeneral = () => {
)}
{canImpersonate && (
<Section>
<H2Title
title={
canManageFeatureFlags
? t`Feature Flags & Impersonation`
: t`User Impersonation`
}
description={
canManageFeatureFlags
? t`Look up users and manage their workspace feature flags or impersonate them.`
: t`Look up users to impersonate them.`
}
/>
<StyledContainer>
<SettingsTextInput
instanceId="admin-user-lookup"
value={userIdentifier}
onChange={setUserIdentifier}
onInputEnter={handleSearch}
placeholder={t`Enter user ID or email address`}
fullWidth
disabled={isUserLookupLoading}
/>
<Button
Icon={IconSearch}
variant="primary"
accent="blue"
title={t`Search`}
onClick={handleSearch}
disabled={!userIdentifier.trim() || isUserLookupLoading}
/>
</StyledContainer>
</Section>
)}
{isDefined(userLookupResult) && (
<>
<Section>
<H2Title title={t`User Info`} description={t`About this user`} />
<SettingsTableCard
items={userInfoItems}
rounded
gridAutoColumns="1fr 4fr"
<H2Title
title={t`Recent Users`}
description={
canManageFeatureFlags
? t`Last 10 users created. Click to manage feature flags or impersonate.`
: t`Last 10 users created. Click to impersonate.`
}
/>
<SettingsTextInput
instanceId="admin-panel-user-search"
value={userSearchTerm}
onChange={setUserSearchTerm}
placeholder={t`Search by name, email, or user ID...`}
fullWidth
/>
{isLoadingUsers ? (
<SettingsSkeletonLoader />
) : recentUsers.length === 0 ? (
<StyledEmptyState>
{t`No users found matching your search criteria.`}
</StyledEmptyState>
) : (
<Table>
<TableBody>
<TableRow gridTemplateColumns="1fr 2fr 1fr">
<TableHeader>{t`Name`}</TableHeader>
<TableHeader>{t`Email`}</TableHeader>
<TableHeader align="right">{t`Workspace`}</TableHeader>
</TableRow>
{recentUsers.map((user) => (
<TableRow
key={user.id}
gridTemplateColumns="1fr 2fr 1fr"
to={getSettingsPath(SettingsPath.AdminPanelUserDetail, {
userId: user.id,
})}
>
<TableCell color={themeCssVariables.font.color.primary}>
{`${user.firstName || ''} ${user.lastName || ''}`.trim() ||
'\u2014'}
</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell align="right">
{user.workspaceName || '\u2014'}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</Section>
<Section>
<H2Title
title={t`Workspaces`}
description={t`All workspaces this user is a member of`}
title={t`Top Workspaces`}
description={t`Top 10 workspaces by number of users`}
/>
<TabList
tabs={tabs}
behaveAsLinks={false}
componentInstanceId={SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID}
<SettingsTextInput
instanceId="admin-panel-workspace-search"
value={workspaceSearchTerm}
onChange={setWorkspaceSearchTerm}
placeholder={t`Search by workspace name, subdomain, or ID...`}
fullWidth
/>
<SettingsAdminWorkspaceContent activeWorkspace={activeWorkspace} />
{isLoadingWorkspaces ? (
<SettingsSkeletonLoader />
) : topWorkspaces.length === 0 ? (
<StyledEmptyState>
{t`No workspaces found matching your search criteria.`}
</StyledEmptyState>
) : (
<Table>
<TableBody>
<TableRow gridTemplateColumns="2fr 1fr">
<TableHeader>{t`Workspace`}</TableHeader>
<TableHeader align="right">{t`Users`}</TableHeader>
</TableRow>
{topWorkspaces.map((workspace) => (
<TableRow
key={workspace.id}
gridTemplateColumns="2fr 1fr"
to={getSettingsPath(
SettingsPath.AdminPanelWorkspaceDetail,
{ workspaceId: workspace.id },
)}
>
<TableCell color={themeCssVariables.font.color.primary}>
{workspace.name || '\u2014'}
</TableCell>
<TableCell align="right">
{workspace.totalUsers}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</Section>
</>
)}
@@ -1,46 +1,29 @@
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
import { useFeatureFlagState } from '@/settings/admin-panel/hooks/useFeatureFlagState';
import { useImpersonationAuth } from '@/settings/admin-panel/hooks/useImpersonationAuth';
import { useImpersonationRedirect } from '@/settings/admin-panel/hooks/useImpersonationRedirect';
import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState';
import { type WorkspaceInfo } from '@/settings/admin-panel/types/WorkspaceInfo';
import { getWorkspaceSchemaName } from '@/settings/admin-panel/utils/getWorkspaceSchemaName';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Table } from '@/ui/layout/table/components/Table';
import { TableBody } from '@/ui/layout/table/components/TableBody';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useMutation } from '@apollo/client/react';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { getImageAbsoluteURI, isDefined } from 'twenty-shared/utils';
import { AvatarOrIcon, Chip } from 'twenty-ui/components';
import { SettingsPath } from 'twenty-shared/types';
import {
getImageAbsoluteURI,
getSettingsPath,
isDefined,
} from 'twenty-shared/utils';
import { AvatarOrIcon, LinkChip } from 'twenty-ui/components';
import {
H2Title,
IconEyeShare,
IconCalendar,
IconHome,
IconId,
IconLink,
IconStatusChange,
IconUser,
} from 'twenty-ui/display';
import { Button, Toggle } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import {
type FeatureFlagKey,
ImpersonateDocument,
UpdateWorkspaceFeatureFlagDocument,
} from '~/generated-metadata/graphql';
type SettingsAdminWorkspaceContentProps = {
activeWorkspace: WorkspaceInfo | undefined;
@@ -53,93 +36,11 @@ const StyledContainer = styled.div`
margin-top: ${themeCssVariables.spacing[6]};
`;
const StyledButtonContainer = styled.div`
margin-top: ${themeCssVariables.spacing[3]};
`;
export const SettingsAdminWorkspaceContent = ({
activeWorkspace,
}: SettingsAdminWorkspaceContentProps) => {
const canManageFeatureFlags = useAtomStateValue(canManageFeatureFlagsState);
const { enqueueErrorSnackBar } = useSnackBar();
const [currentUser] = useAtomState(currentUserState);
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const [updateFeatureFlag] = useMutation(UpdateWorkspaceFeatureFlagDocument);
const [isImpersonateLoading, setIsImpersonationLoading] = useState(false);
const { executeImpersonationAuth } = useImpersonationAuth();
const { executeImpersonationRedirect } = useImpersonationRedirect();
const [impersonate] = useMutation(ImpersonateDocument);
const { updateFeatureFlagState } = useFeatureFlagState();
const userLookupResult = useAtomStateValue(userLookupResultState);
const { t } = useLingui();
const handleImpersonate = async (workspaceId: string) => {
if (!userLookupResult?.user.id) {
enqueueErrorSnackBar({ message: t`Please search for a user first` });
return;
}
setIsImpersonationLoading(true);
await impersonate({
variables: { userId: userLookupResult.user.id, workspaceId },
onCompleted: async (data) => {
const { loginToken, workspace } = data.impersonate;
const isCurrentWorkspace = workspace.id === currentWorkspace?.id;
if (isCurrentWorkspace) {
await executeImpersonationAuth(loginToken.token);
return;
}
return executeImpersonationRedirect(
workspace.workspaceUrls,
loginToken.token,
'_blank',
);
},
onError: (error) => {
const errorMessage = error.message;
enqueueErrorSnackBar({
message: t`Failed to impersonate user. ${errorMessage}`,
});
},
}).finally(() => {
setIsImpersonationLoading(false);
});
};
const handleFeatureFlagUpdate = async (
workspaceId: string,
featureFlag: FeatureFlagKey,
value: boolean,
) => {
const previousValue = userLookupResult?.workspaces
.find((workspace) => workspace.id === workspaceId)
?.featureFlags.find((flag) => flag.key === featureFlag)?.value;
updateFeatureFlagState(workspaceId, featureFlag, value);
await updateFeatureFlag({
variables: {
workspaceId,
featureFlag,
value,
},
onError: (error) => {
if (isDefined(previousValue)) {
updateFeatureFlagState(workspaceId, featureFlag, previousValue);
}
const errorMessage = error.message;
enqueueErrorSnackBar({
message: t`Failed to update feature flag. ${errorMessage}`,
});
},
});
};
const getWorkspaceUrl = (workspaceUrls: WorkspaceInfo['workspaceUrls']) => {
return workspaceUrls.customUrl ?? workspaceUrls.subdomainUrl;
};
@@ -148,10 +49,13 @@ export const SettingsAdminWorkspaceContent = ({
{
Icon: IconHome,
label: t`Name`,
value: (
<Chip
value: activeWorkspace?.id ? (
<LinkChip
label={activeWorkspace?.name ?? ''}
emptyLabel={t`Untitled`}
to={getSettingsPath(SettingsPath.AdminPanelWorkspaceDetail, {
workspaceId: activeWorkspace.id,
})}
leftComponent={
<AvatarOrIcon
avatarUrl={
@@ -165,6 +69,8 @@ export const SettingsAdminWorkspaceContent = ({
/>
}
/>
) : (
(activeWorkspace?.name ?? '')
),
},
{
@@ -191,6 +97,18 @@ export const SettingsAdminWorkspaceContent = ({
label: t`Members`,
value: activeWorkspace?.totalUsers,
},
{
Icon: IconStatusChange,
label: t`Status`,
value: activeWorkspace?.activationStatus,
},
{
Icon: IconCalendar,
label: t`Created`,
value: activeWorkspace?.createdAt
? new Date(activeWorkspace.createdAt).toLocaleDateString()
: '',
},
];
if (!activeWorkspace) return null;
@@ -206,62 +124,7 @@ export const SettingsAdminWorkspaceContent = ({
items={workspaceInfoItems}
gridAutoColumns="1fr 4fr"
/>
<StyledButtonContainer>
{currentUser?.canImpersonate && (
<Button
Icon={IconEyeShare}
variant="primary"
accent="default"
title={
activeWorkspace.allowImpersonation === false
? t`Impersonation is disabled for this workspace`
: t`Impersonate`
}
onClick={() => handleImpersonate(activeWorkspace.id)}
disabled={
isImpersonateLoading ||
activeWorkspace.allowImpersonation === false
}
dataTestId="impersonate-button"
/>
)}
</StyledButtonContainer>
</Section>
{canManageFeatureFlags && (
<Table>
<TableBody>
<TableRow
gridAutoColumns="1fr 100px"
mobileGridAutoColumns="1fr 80px"
>
<TableHeader>{t`Feature Flag`}</TableHeader>
<TableHeader align="right">{t`Status`}</TableHeader>
</TableRow>
{activeWorkspace.featureFlags.map((flag) => (
<TableRow
gridAutoColumns="1fr 100px"
mobileGridAutoColumns="1fr 80px"
key={flag.key}
>
<TableCell>{flag.key}</TableCell>
<TableCell align="right">
<Toggle
value={flag.value}
onChange={(newValue) =>
handleFeatureFlagUpdate(
activeWorkspace.id,
flag.key,
newValue,
)
}
/>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</StyledContainer>
);
};