Admin panel fixes (#10792)

<img width="573" alt="Screenshot 2025-03-12 at 17 36 44"
src="https://github.com/user-attachments/assets/be6c20b0-626d-4a2c-810c-78a49e9f65ee"
/>
<img width="579" alt="Screenshot 2025-03-12 at 17 37 03"
src="https://github.com/user-attachments/assets/23692ff8-ac88-4104-823e-1a06b3074551"
/>
<img width="590" alt="Screenshot 2025-03-12 at 17 37 14"
src="https://github.com/user-attachments/assets/b46de1d3-a312-44cc-a54d-72208224453d"
/>
<img width="556" alt="Screenshot 2025-03-12 at 17 37 37"
src="https://github.com/user-attachments/assets/12176d49-d76d-4fb1-abe6-1f7dc5349d94"
/>
<img width="607" alt="Screenshot 2025-03-12 at 17 37 50"
src="https://github.com/user-attachments/assets/00e2edff-09db-45c5-a4df-6fd9ead830b6"
/>
This commit is contained in:
nitin
2025-03-13 01:25:38 +05:30
committed by GitHub
parent 75da64876a
commit c61748cd6e
20 changed files with 670 additions and 440 deletions

View File

@ -0,0 +1,63 @@
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { IconCopy, OverflowingTextWithTooltip } from 'twenty-ui';
import { useDebouncedCallback } from 'use-debounce';
const StyledEllipsisLabel = styled.div`
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
`;
const StyledExpandedEllipsisLabel = styled.div`
white-space: normal;
word-break: break-all;
`;
const StyledCopyContainer = styled.span`
cursor: pointer;
`;
export const SettingsAdminEnvCopyableText = ({
text,
displayText,
multiline = false,
maxRows,
}: {
text: string;
displayText?: React.ReactNode;
multiline?: boolean;
maxRows?: number;
}) => {
const { enqueueSnackBar } = useSnackBar();
const theme = useTheme();
const { t } = useLingui();
const copyToClipboardDebounced = useDebouncedCallback((value: string) => {
navigator.clipboard.writeText(value);
enqueueSnackBar(t`Copied to clipboard!`, {
variant: SnackBarVariant.Success,
icon: <IconCopy size={theme.icon.size.md} />,
});
}, 200);
return (
<StyledCopyContainer onClick={() => copyToClipboardDebounced(text)}>
{maxRows ? (
<OverflowingTextWithTooltip
text={displayText?.toString() || text}
displayedMaxRows={maxRows}
isTooltipMultiline={multiline}
/>
) : multiline ? (
<StyledExpandedEllipsisLabel>
{displayText || text}
</StyledExpandedEllipsisLabel>
) : (
<StyledEllipsisLabel>{displayText || text}</StyledEllipsisLabel>
)}
</StyledCopyContainer>
);
};

View File

@ -10,14 +10,16 @@ import { Card, H2Title, IconHeartRateMonitor, Section } from 'twenty-ui';
import { useGetEnvironmentVariablesGroupedQuery } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const StyledGroupContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(6)};
`;
const StyledGroupContainer = styled.div``;
const StyledInfoText = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
`;
const StyledCard = styled(Card)`
margin-bottom: ${({ theme }) => theme.spacing(8)};
`;
export const SettingsAdminEnvVariables = () => {
const theme = useTheme();
const { data: environmentVariables, loading: environmentVariablesLoading } =
@ -38,22 +40,20 @@ export const SettingsAdminEnvVariables = () => {
<>
<Section>
<StyledInfoText>
{t` These are only the server values. Ensure your worker environment has the
same variables and values, this is required for asynchronous tasks like
email sync.`}
{t`These are only the server values. Ensure your worker environment has the same variables and values, this is required for asynchronous tasks like email sync.`}
</StyledInfoText>
</Section>
<Section>
{visibleGroups.map((group) => (
<StyledGroupContainer key={group.name}>
<H2Title title={group.name} description={group.description} />
{group.variables.length > 0 && (
<SettingsAdminEnvVariablesTable variables={group.variables} />
)}
</StyledGroupContainer>
))}
{visibleGroups.map((group) => (
<StyledGroupContainer key={group.name}>
<H2Title title={group.name} description={group.description} />
{group.variables.length > 0 && (
<SettingsAdminEnvVariablesTable variables={group.variables} />
)}
</StyledGroupContainer>
))}
<Card rounded>
<Section>
<StyledCard rounded>
<SettingsListItemCardContent
label={t`Other Variables`}
to={getSettingsPath(SettingsPath.AdminPanelOtherEnvVariables)}
@ -61,7 +61,7 @@ export const SettingsAdminEnvVariables = () => {
LeftIcon={IconHeartRateMonitor}
LeftIconColor={theme.font.color.tertiary}
/>
</Card>
</StyledCard>
</Section>
</>
);

View File

@ -1,3 +1,5 @@
import { SettingsAdminEnvCopyableText } from '@/settings/admin-panel/components/SettingsAdminEnvCopyableText';
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useTheme } from '@emotion/react';
@ -19,6 +21,8 @@ type SettingsAdminEnvVariablesRowProps = {
value: string;
sensitive: boolean;
};
isExpanded: boolean;
onExpandToggle: (name: string) => void;
};
const StyledTruncatedCell = styled(TableCell)`
@ -43,53 +47,36 @@ const StyledButton = styled(motion.button)`
const MotionIconChevronDown = motion(IconChevronRight);
const StyledExpandedDetails = styled.div`
background-color: ${({ theme }) => theme.background.secondary};
border-radius: ${({ theme }) => theme.border.radius.sm};
margin: ${({ theme }) => theme.spacing(2)} 0;
padding: ${({ theme }) => theme.spacing(2)};
border: 1px solid ${({ theme }) => theme.border.color.medium};
display: grid;
grid-template-columns: auto 1fr;
gap: ${({ theme }) => theme.spacing(1)};
height: fit-content;
min-height: min-content;
`;
const StyledDetailLabel = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
font-weight: ${({ theme }) => theme.font.weight.regular};
padding-right: ${({ theme }) => theme.spacing(4)};
`;
const StyledEllipsisLabel = styled.div`
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
`;
const StyledExpandedLabel = styled.div`
word-break: break-word;
white-space: normal;
overflow: visible;
`;
const StyledValueContainer = styled.div`
display: flex;
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
justify-content: space-between;
width: 100%;
`;
const StyledTableRow = styled(TableRow)<{ isExpanded: boolean }>`
background-color: ${({ isExpanded, theme }) =>
isExpanded ? theme.background.transparent.light : 'transparent'};
margin-bottom: ${({ theme }) => theme.spacing(0.5)};
`;
const StyledExpandableContainer = styled.div`
width: 100%;
padding-top: ${({ theme }) => theme.spacing(2)};
padding-bottom: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsAdminEnvVariablesRow = ({
variable,
isExpanded,
onExpandToggle,
}: SettingsAdminEnvVariablesRowProps) => {
const [isExpanded, setIsExpanded] = useState(false);
const [showSensitiveValue, setShowSensitiveValue] = useState(false);
const theme = useTheme();
@ -105,10 +92,47 @@ export const SettingsAdminEnvVariablesRow = ({
setShowSensitiveValue(!showSensitiveValue);
};
const environmentVariablesDetails = [
{
label: 'Name',
value: <SettingsAdminEnvCopyableText text={variable.name} />,
},
{
label: 'Description',
value: (
<SettingsAdminEnvCopyableText
text={variable.description}
maxRows={1}
multiline={true}
/>
),
},
{
label: 'Value',
value: (
<StyledValueContainer>
<SettingsAdminEnvCopyableText
text={variable.value}
displayText={displayValue}
multiline={true}
/>
{variable.sensitive && variable.value !== '' && (
<LightIconButton
Icon={showSensitiveValue ? IconEyeOff : IconEye}
size="small"
accent="secondary"
onClick={handleToggleVisibility}
/>
)}
</StyledValueContainer>
),
},
];
return (
<>
<StyledTableRow
onClick={() => setIsExpanded(!isExpanded)}
onClick={() => onExpandToggle(variable.name)}
gridAutoColumns="5fr 4fr 3fr 1fr"
isExpanded={isExpanded}
>
@ -122,7 +146,12 @@ export const SettingsAdminEnvVariablesRow = ({
<StyledEllipsisLabel>{displayValue}</StyledEllipsisLabel>
</StyledTruncatedCell>
<TableCell align="right">
<StyledButton onClick={() => setIsExpanded(!isExpanded)}>
<StyledButton
onClick={(e) => {
e.stopPropagation();
onExpandToggle(variable.name);
}}
>
<MotionIconChevronDown
size={theme.icon.size.md}
color={theme.font.color.tertiary}
@ -133,26 +162,12 @@ export const SettingsAdminEnvVariablesRow = ({
</TableCell>
</StyledTableRow>
<AnimatedExpandableContainer isExpanded={isExpanded} mode="fit-content">
<StyledExpandedDetails>
<StyledDetailLabel>Name</StyledDetailLabel>
<StyledEllipsisLabel>{variable.name}</StyledEllipsisLabel>
<StyledDetailLabel>Description</StyledDetailLabel>
<StyledExpandedLabel>{variable.description}</StyledExpandedLabel>
<StyledDetailLabel>Value</StyledDetailLabel>
<StyledExpandedLabel>
<StyledValueContainer>
{displayValue}
{variable.sensitive && variable.value !== '' && (
<LightIconButton
Icon={showSensitiveValue ? IconEyeOff : IconEye}
size="small"
accent="secondary"
onClick={handleToggleVisibility}
/>
)}
</StyledValueContainer>
</StyledExpandedLabel>
</StyledExpandedDetails>
<StyledExpandableContainer>
<SettingsAdminTableCard
items={environmentVariablesDetails}
gridAutoColumns="1fr 4fr"
/>
</StyledExpandableContainer>
</AnimatedExpandableContainer>
</>
);

View File

@ -1,11 +1,13 @@
import { SettingsAdminEnvVariablesRow } from '@/settings/admin-panel/components/SettingsAdminEnvVariablesRow';
import { Table } from '@/ui/layout/table/components/Table';
import { TableBody } from '@/ui/layout/table/components/TableBody';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled';
import { useState } from 'react';
const StyledTable = styled(Table)`
margin-top: ${({ theme }) => theme.spacing(3)};
const StyledTableBody = styled(TableBody)`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
`;
type SettingsAdminEnvVariablesTableProps = {
@ -19,16 +21,31 @@ type SettingsAdminEnvVariablesTableProps = {
export const SettingsAdminEnvVariablesTable = ({
variables,
}: SettingsAdminEnvVariablesTableProps) => (
<StyledTable>
<TableRow gridAutoColumns="5fr 4fr 3fr 1fr">
<TableHeader>Name</TableHeader>
<TableHeader>Description</TableHeader>
<TableHeader align="right">Value</TableHeader>
<TableHeader align="right"></TableHeader>
</TableRow>
{variables.map((variable) => (
<SettingsAdminEnvVariablesRow key={variable.name} variable={variable} />
))}
</StyledTable>
);
}: SettingsAdminEnvVariablesTableProps) => {
const [expandedRowName, setExpandedRowName] = useState<string | null>(null);
const handleExpandToggle = (name: string) => {
setExpandedRowName(expandedRowName === name ? null : name);
};
return (
<Table>
<TableRow gridAutoColumns="5fr 4fr 3fr 1fr">
<TableHeader>Name</TableHeader>
<TableHeader>Description</TableHeader>
<TableHeader align="right">Value</TableHeader>
<TableHeader align="right"></TableHeader>
</TableRow>
<StyledTableBody>
{variables.map((variable) => (
<SettingsAdminEnvVariablesRow
key={variable.name}
variable={variable}
isExpanded={expandedRowName === variable.name}
onExpandToggle={handleExpandToggle}
/>
))}
</StyledTableBody>
</Table>
);
};

View File

@ -16,16 +16,18 @@ import { useRecoilState, useRecoilValue } from 'recoil';
import { getImageAbsoluteURI, isDefined } from 'twenty-shared';
import {
Button,
H1Title,
H1TitleFontColor,
H2Title,
IconId,
IconMail,
IconSearch,
IconUser,
Section,
} from 'twenty-ui';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useUserLookupAdminPanelMutation } from '~/generated/graphql';
import { currentUserState } from '@/auth/states/currentUserState';
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
import { SettingsAdminVersionContainer } from '@/settings/admin-panel/components/SettingsAdminVersionContainer';
const StyledContainer = styled.div`
@ -35,10 +37,6 @@ const StyledContainer = styled.div`
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledUserInfo = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(5)};
`;
const StyledTabListContainer = styled.div`
align-items: center;
border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
@ -47,12 +45,6 @@ const StyledTabListContainer = styled.div`
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledContentContainer = styled.div`
flex: 1;
width: 100%;
padding: ${({ theme }) => theme.spacing(4)} 0;
`;
export const SettingsAdminGeneral = () => {
const [userIdentifier, setUserIdentifier] = useState('');
const { enqueueSnackBar } = useSnackBar();
@ -124,6 +116,24 @@ export const SettingsAdminGeneral = () => {
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,
},
];
return (
<>
{canAccessFullAdminPanel && (
@ -173,36 +183,31 @@ export const SettingsAdminGeneral = () => {
)}
{isDefined(userLookupResult) && (
<Section>
<StyledUserInfo>
<H1Title
title={t`User Info`}
fontColor={H1TitleFontColor.Primary}
<>
<Section>
<H2Title title={t`User Info`} description={t`About this user`} />
<SettingsAdminTableCard
items={userInfoItems}
rounded
gridAutoColumns="1fr 4fr"
/>
<H2Title title={userFullName} description={t`User Name`} />
</Section>
<Section>
<H2Title
title={userLookupResult.user.email}
description={t`User Email`}
title={t`Workspaces`}
description={t`All workspaces this user is a member of`}
/>
<H2Title
title={userLookupResult.user.id}
description={t`User ID`}
/>
</StyledUserInfo>
<StyledTabListContainer>
<TabList
tabs={tabs}
tabListInstanceId={SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID}
behaveAsLinks={false}
/>
</StyledTabListContainer>
<H1Title title={t`Workspaces`} fontColor={H1TitleFontColor.Primary} />
<StyledTabListContainer>
<TabList
tabs={tabs}
tabListInstanceId={SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID}
behaveAsLinks={false}
/>
</StyledTabListContainer>
<StyledContentContainer>
<SettingsAdminWorkspaceContent activeWorkspace={activeWorkspace} />
</StyledContentContainer>
</Section>
</Section>
</>
)}
</>
);

View File

@ -0,0 +1,93 @@
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 { TableRow } from '@/ui/layout/table/components/TableRow';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { Card, IconComponent } from 'twenty-ui';
const StyledCard = styled(Card)`
background-color: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
`;
const StyledTableRow = styled(TableRow)`
height: ${({ theme }) => theme.spacing(6)};
`;
const StyledTableCellLabel = styled(TableCell)<{
align?: 'left' | 'center' | 'right';
}>`
color: ${({ theme }) => theme.font.color.tertiary};
height: ${({ theme }) => theme.spacing(6)};
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
justify-content: ${({ align }) =>
align === 'right'
? 'flex-end'
: align === 'center'
? 'center'
: 'flex-start'};
`;
const StyledTableCellValue = styled(TableCell)<{
align?: 'left' | 'center' | 'right';
}>`
color: ${({ theme }) => theme.font.color.primary};
height: ${({ theme }) => theme.spacing(6)};
justify-content: ${({ align }) =>
align === 'left'
? 'flex-start'
: align === 'center'
? 'center'
: 'flex-end'};
`;
type TableItem = {
Icon?: IconComponent;
label: string;
value: string | number | React.ReactNode;
};
type SettingsAdminTableCardProps = {
items: TableItem[];
rounded?: boolean;
gridAutoColumns?: string;
labelAlign?: 'left' | 'center' | 'right';
valueAlign?: 'left' | 'center' | 'right';
className?: string;
};
export const SettingsAdminTableCard = ({
items,
rounded = false,
gridAutoColumns,
labelAlign = 'left',
valueAlign = 'left',
className,
}: SettingsAdminTableCardProps) => {
const theme = useTheme();
return (
<StyledCard rounded={rounded} className={className}>
<Table>
<TableBody>
{items.map((item, index) => (
<StyledTableRow
key={index + item.label}
gridAutoColumns={gridAutoColumns}
>
<StyledTableCellLabel align={labelAlign}>
{item.Icon && <item.Icon size={theme.icon.size.md} />}
<span>{item.label}</span>
</StyledTableCellLabel>
<StyledTableCellValue align={valueAlign}>
{item.value}
</StyledTableCellValue>
</StyledTableRow>
))}
</TableBody>
</Table>
</StyledCard>
);
};

View File

@ -1,46 +1,20 @@
import { IconCircleDot, IconComponent, IconStatusChange } from 'twenty-ui';
import { GITHUB_LINK } from '@ui/navigation/link/constants/GithubLink';
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
import { checkTwentyVersionExists } from '@/settings/admin-panel/utils/checkTwentyVersionExists';
import { fetchLatestTwentyRelease } from '@/settings/admin-panel/utils/fetchLatestTwentyRelease';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { GITHUB_LINK } from '@ui/navigation/link/constants/GithubLink';
import { useEffect, useState } from 'react';
import { IconCircleDot, IconStatusChange } from 'twenty-ui';
import packageJson from '../../../../../package.json';
const StyledVersionContainer = styled.div`
background: ${({ theme }) => theme.background.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(3)};
display: grid;
`;
const StyledVersionDetails = styled.div`
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.regular};
display: flex;
flex-direction: row;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledVersionText = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.regular};
`;
const StyledActionLink = styled.a`
align-items: center;
color: ${({ theme }) => theme.font.color.primary};
display: flex;
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
font-weight: ${({ theme }) => theme.font.weight.regular};
gap: ${({ theme }) => theme.spacing(1)};
padding: 0 ${({ theme }) => theme.spacing(1)};
text-decoration: none;
:hover {
@ -52,19 +26,10 @@ const StyledActionLink = styled.a`
const StyledSpan = styled.span`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
font-weight: ${({ theme }) => theme.font.weight.regular};
`;
type VersionDetail = {
Icon: IconComponent;
text: string;
version: string | null;
link: string;
type: 'current' | 'latest';
};
export const SettingsAdminVersionContainer = () => {
const theme = useTheme();
const [latestVersion, setLatestVersion] = useState<string | null>(null);
const [currentVersionExists, setCurrentVersionExists] = useState(false);
@ -73,46 +38,44 @@ export const SettingsAdminVersionContainer = () => {
checkTwentyVersionExists(packageJson.version).then(setCurrentVersionExists);
}, []);
const VERSION_DETAILS: VersionDetail[] = [
const versionItems = [
{
Icon: IconCircleDot,
text: t`Current version:`,
version: packageJson.version,
link: `${GITHUB_LINK}/releases/tag/v${packageJson.version}`,
type: 'current',
label: t`Current version`,
value: currentVersionExists ? (
<StyledActionLink
href={`${GITHUB_LINK}/releases/tag/v${packageJson.version}`}
target="_blank"
rel="noreferrer"
>
{packageJson.version}
</StyledActionLink>
) : (
<StyledSpan>{packageJson.version}</StyledSpan>
),
},
{
Icon: IconStatusChange,
text: t`Latest version:`,
version: latestVersion,
link: `${GITHUB_LINK}/releases/tag/v${latestVersion}`,
type: 'latest',
label: t`Latest version`,
value: latestVersion ? (
<StyledActionLink
href={`${GITHUB_LINK}/releases/tag/v${latestVersion}`}
target="_blank"
rel="noreferrer"
>
{latestVersion}
</StyledActionLink>
) : (
<StyledSpan>{latestVersion ?? 'Loading...'}</StyledSpan>
),
},
];
return (
<StyledVersionContainer>
{VERSION_DETAILS.map((versionDetail, index) => (
<StyledVersionDetails key={index}>
<versionDetail.Icon
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
<StyledVersionText>{versionDetail.text}</StyledVersionText>
{versionDetail.version &&
(versionDetail.type === 'current' ? currentVersionExists : true) ? (
<StyledActionLink
href={versionDetail.link}
target="_blank"
rel="noreferrer"
>
{versionDetail.version}
</StyledActionLink>
) : (
<StyledSpan>{versionDetail.version}</StyledSpan>
)}
</StyledVersionDetails>
))}
</StyledVersionContainer>
<SettingsAdminTableCard
rounded
items={versionItems}
gridAutoColumns="3fr 8fr"
/>
);
};

View File

@ -1,8 +1,19 @@
import { Button, H2Title, IconUser, Toggle } from 'twenty-ui';
import {
AvatarChip,
Button,
H2Title,
IconEyeShare,
IconHome,
IconId,
IconUser,
Section,
Toggle,
} from 'twenty-ui';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
import { useFeatureFlagState } from '@/settings/admin-panel/hooks/useFeatureFlagState';
import { useImpersonationAuth } from '@/settings/admin-panel/hooks/useImpersonationAuth';
import { useImpersonationRedirect } from '@/settings/admin-panel/hooks/useImpersonationRedirect';
@ -11,14 +22,18 @@ import { WorkspaceInfo } from '@/settings/admin-panel/types/WorkspaceInfo';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
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 styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared';
import { getImageAbsoluteURI, isDefined } from 'twenty-shared';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import {
FeatureFlagKey,
useImpersonateMutation,
@ -29,7 +44,14 @@ type SettingsAdminWorkspaceContentProps = {
activeWorkspace: WorkspaceInfo | undefined;
};
const StyledTable = styled(Table)`
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(3)};
margin-top: ${({ theme }) => theme.spacing(6)};
`;
const StyledButtonContainer = styled.div`
margin-top: ${({ theme }) => theme.spacing(3)};
`;
@ -115,64 +137,101 @@ export const SettingsAdminWorkspaceContent = ({
});
};
const workspaceInfoItems = [
{
Icon: IconHome,
label: t`Name`,
value: (
<AvatarChip
name={activeWorkspace?.name ?? ''}
avatarUrl={
getImageAbsoluteURI({
imageUrl: isNonEmptyString(activeWorkspace?.logo)
? activeWorkspace?.logo
: DEFAULT_WORKSPACE_LOGO,
baseUrl: REACT_APP_SERVER_BASE_URL,
}) ?? ''
}
/>
),
},
{
Icon: IconId,
label: t`ID`,
value: activeWorkspace?.id,
},
{
Icon: IconUser,
label: t`Members`,
value: activeWorkspace?.totalUsers,
},
];
if (!activeWorkspace) return null;
return (
<>
<H2Title title={activeWorkspace.name} description={t`Workspace Name`} />
<H2Title
title={`${activeWorkspace.totalUsers} ${
activeWorkspace.totalUsers > 1 ? t`Users` : t`User`
}`}
description={t`Total Users`}
/>
{currentUser?.canImpersonate && (
<Button
Icon={IconUser}
variant="primary"
accent="blue"
title={t`Impersonate`}
onClick={() => handleImpersonate(activeWorkspace.id)}
disabled={
isImpersonateLoading || activeWorkspace.allowImpersonation === false
}
dataTestId="impersonate-button"
<StyledContainer>
<Section>
<H2Title
title={t`Workspace Info`}
description={t`About this workspace`}
/>
)}
<SettingsAdminTableCard
items={workspaceInfoItems}
gridAutoColumns="1fr 4fr"
/>
<StyledButtonContainer>
{currentUser?.canImpersonate && (
<Button
Icon={IconEyeShare}
variant="primary"
accent="default"
title={t`Impersonate`}
onClick={() => handleImpersonate(activeWorkspace.id)}
disabled={
isImpersonateLoading ||
activeWorkspace.allowImpersonation === false
}
dataTestId="impersonate-button"
/>
)}
</StyledButtonContainer>
</Section>
{canManageFeatureFlags && (
<StyledTable>
<TableRow
gridAutoColumns="1fr 100px"
mobileGridAutoColumns="1fr 80px"
>
<TableHeader>{t`Feature Flag`}</TableHeader>
<TableHeader align="right">{t`Status`}</TableHeader>
</TableRow>
{activeWorkspace.featureFlags.map((flag) => (
<Table>
<TableBody>
<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>
<TableHeader>{t`Feature Flag`}</TableHeader>
<TableHeader align="right">{t`Status`}</TableHeader>
</TableRow>
))}
</StyledTable>
{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>
);
};