Environment variables in admin panel (read only) - front (#10011)

Frontend for https://github.com/twentyhq/core-team-issues/issues/293

POC - https://github.com/twentyhq/twenty/pull/9903

---------

Co-authored-by: Félix Malfait <felix@twenty.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
nitin
2025-02-06 21:38:44 +05:30
committed by GitHub
parent a85c4f263a
commit 1b150e1da6
43 changed files with 1224 additions and 758 deletions

View File

@ -1,55 +1,9 @@
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
import { SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID } from '@/settings/admin-panel/constants/SettingsAdminFeatureFlagsTabs';
import { useFeatureFlagsManagement } from '@/settings/admin-panel/hooks/useFeatureFlagsManagement';
import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate';
import { TextInput } from '@/ui/input/components/TextInput';
import { SettingsAdminTabContent } from '@/settings/admin-panel/components/SettingsAdminTabContent';
import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs';
import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId';
import { TabList } from '@/ui/layout/tab/components/TabList';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import { Table } from '@/ui/layout/table/components/Table';
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 { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { getImageAbsoluteURI, isDefined } from 'twenty-shared';
import {
Button,
H1Title,
H1TitleFontColor,
H2Title,
IconSearch,
IconUser,
Section,
Toggle,
} from 'twenty-ui';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
const StyledLinkContainer = styled.div`
margin-right: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
`;
const StyledErrorSection = styled.div`
color: ${({ theme }) => theme.font.color.danger};
margin-top: ${({ theme }) => theme.spacing(2)};
`;
const StyledUserInfo = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(5)};
`;
const StyledTable = styled(Table)`
margin-top: ${({ theme }) => theme.spacing(3)};
`;
import { IconSettings2, IconVariable } from 'twenty-ui';
const StyledTabListContainer = styled.div`
align-items: center;
@ -59,210 +13,30 @@ 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 SettingsAdminContent = () => {
const [userIdentifier, setUserIdentifier] = useState('');
const [userId, setUserId] = useState('');
const {
handleImpersonate,
isLoading: isImpersonateLoading,
error: impersonateError,
canImpersonate,
} = useImpersonate();
const { activeTabId, setActiveTabId } = useTabList(
SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID,
);
const {
userLookupResult,
handleUserLookup,
handleFeatureFlagUpdate,
isLoading,
error,
} = useFeatureFlagsManagement();
const canManageFeatureFlags = useRecoilValue(canManageFeatureFlagsState);
const handleSearch = async () => {
setActiveTabId('');
const result = await handleUserLookup(userIdentifier);
if (isDefined(result?.user?.id) && !error) {
setUserId(result.user.id.trim());
}
if (
isDefined(result?.workspaces) &&
result.workspaces.length > 0 &&
!error
) {
setActiveTabId(result.workspaces[0].id);
}
};
const shouldShowUserData = userLookupResult && !error;
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 renderWorkspaceContent = () => {
if (!activeWorkspace) return null;
return (
<>
<H2Title title={activeWorkspace.name} description={'Workspace Name'} />
<H2Title
title={`${activeWorkspace.totalUsers} ${
activeWorkspace.totalUsers > 1 ? 'Users' : 'User'
}`}
description={'Total Users'}
/>
{canImpersonate && (
<Button
Icon={IconUser}
variant="primary"
accent="blue"
title={'Impersonate'}
onClick={() => handleImpersonate(userId, activeWorkspace.id)}
disabled={
isImpersonateLoading ||
activeWorkspace.allowImpersonation === false
}
dataTestId="impersonate-button"
/>
)}
{canManageFeatureFlags && (
<StyledTable>
<TableRow
gridAutoColumns="1fr 100px"
mobileGridAutoColumns="1fr 80px"
>
<TableHeader>Feature Flag</TableHeader>
<TableHeader align="right">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>
))}
</StyledTable>
)}
</>
);
};
const tabs = [
{
id: SETTINGS_ADMIN_TABS.GENERAL,
title: 'General',
Icon: IconSettings2,
},
{
id: SETTINGS_ADMIN_TABS.ENV_VARIABLES,
title: 'Env Variables',
Icon: IconVariable,
},
];
return (
<>
<Section>
<H2Title
title={
canManageFeatureFlags
? 'Feature Flags & Impersonation'
: 'User Impersonation'
}
description={
canManageFeatureFlags
? 'Look up users and manage their workspace feature flags or impersonate them.'
: 'Look up users to impersonate them.'
}
<StyledTabListContainer>
<TabList
tabs={tabs}
tabListInstanceId={SETTINGS_ADMIN_TABS_ID}
behaveAsLinks={true}
/>
<StyledContainer>
<StyledLinkContainer>
<TextInput
value={userIdentifier}
onChange={setUserIdentifier}
onInputEnter={handleSearch}
placeholder="Enter user ID or email address"
fullWidth
disabled={isLoading}
/>
</StyledLinkContainer>
<Button
Icon={IconSearch}
variant="primary"
accent="blue"
title="Search"
onClick={handleSearch}
disabled={!userIdentifier.trim() || isLoading}
/>
</StyledContainer>
{(error || impersonateError) && (
<StyledErrorSection>{error ?? impersonateError}</StyledErrorSection>
)}
</Section>
{shouldShowUserData && (
<Section>
<StyledUserInfo>
<H1Title title="User Info" fontColor={H1TitleFontColor.Primary} />
<H2Title
title={`${userLookupResult.user.firstName || ''} ${
userLookupResult.user.lastName || ''
}`.trim()}
description="User Name"
/>
<H2Title
title={userLookupResult.user.email}
description="User Email"
/>
<H2Title title={userLookupResult.user.id} description="User ID" />
</StyledUserInfo>
<H1Title title="Workspaces" fontColor={H1TitleFontColor.Primary} />
<StyledTabListContainer>
<TabList
tabs={tabs}
tabListInstanceId={SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID}
behaveAsLinks={false}
/>
</StyledTabListContainer>
<StyledContentContainer>
{renderWorkspaceContent()}
</StyledContentContainer>
</Section>
)}
</StyledTabListContainer>
<SettingsAdminTabContent />
</>
);
};

View File

@ -0,0 +1,113 @@
import { SettingsAdminEnvVariablesTable } from '@/settings/admin-panel/components/SettingsAdminEnvVariablesTable';
import styled from '@emotion/styled';
import { useState } from 'react';
import { Button, H1Title, H1TitleFontColor, Section } from 'twenty-ui';
import { useGetEnvironmentVariablesGroupedQuery } from '~/generated/graphql';
const StyledGroupContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(6)};
`;
const StyledSubGroupContainer = styled.div`
margin-top: ${({ theme }) => theme.spacing(4)};
background-color: ${({ theme }) => theme.background.secondary};
border-radius: ${({ theme }) => theme.border.radius.sm};
border: 1px solid ${({ theme }) => theme.border.color.medium};
padding: ${({ theme }) => theme.spacing(4)};
`;
const StyledGroupVariablesContainer = styled.div`
background-color: ${({ theme }) => theme.background.secondary};
border-radius: ${({ theme }) => theme.border.radius.sm};
border: 1px solid ${({ theme }) => theme.border.color.medium};
padding-bottom: ${({ theme }) => theme.spacing(2)};
padding-left: ${({ theme }) => theme.spacing(4)};
padding-right: ${({ theme }) => theme.spacing(4)};
`;
const StyledSubGroupTitle = styled.div`
color: ${({ theme }) => theme.font.color.secondary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledSubGroupDescription = styled.div``;
const StyledGroupDescription = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(4)};
`;
const StyledShowMoreButton = styled(Button)``;
export const SettingsAdminEnvVariables = () => {
const { data: environmentVariables } = useGetEnvironmentVariablesGroupedQuery(
{
fetchPolicy: 'network-only',
},
);
const [showHiddenGroups, setShowHiddenGroups] = useState<
Record<string, boolean>
>({});
const toggleGroupVisibility = (groupName: string) => {
setShowHiddenGroups((prev) => ({
...prev,
[groupName]: !prev[groupName],
}));
};
return (
<Section>
{environmentVariables?.getEnvironmentVariablesGrouped.groups.map(
(group) => {
const isHidden =
group.isHiddenOnLoad && !showHiddenGroups[group.name];
if (isHidden === true) {
return (
<StyledShowMoreButton
key={group.name}
onClick={() => toggleGroupVisibility(group.name)}
title={
showHiddenGroups[group.name] ? 'Show Less' : 'Show More...'
}
></StyledShowMoreButton>
);
}
return (
<StyledGroupContainer key={group.name}>
<H1Title
title={group.name}
fontColor={H1TitleFontColor.Primary}
/>
{group.description !== '' && (
<StyledGroupDescription>
{group.description}
</StyledGroupDescription>
)}
{group.variables.length > 0 && (
<StyledGroupVariablesContainer>
<SettingsAdminEnvVariablesTable variables={group.variables} />
</StyledGroupVariablesContainer>
)}
{group.subgroups.map((subgroup) => (
<StyledSubGroupContainer key={subgroup.name}>
<StyledSubGroupTitle>{subgroup.name}</StyledSubGroupTitle>
{subgroup.description !== '' && (
<StyledSubGroupDescription>
{subgroup.description}
</StyledSubGroupDescription>
)}
<SettingsAdminEnvVariablesTable
variables={subgroup.variables}
/>
</StyledSubGroupContainer>
))}
</StyledGroupContainer>
);
},
)}
</Section>
);
};

View File

@ -0,0 +1,130 @@
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 { useState } from 'react';
import {
AnimatedExpandableContainer,
IconChevronRight,
IconEye,
IconEyeOff,
LightIconButton,
} from 'twenty-ui';
type SettingsAdminEnvVariablesRowProps = {
variable: {
name: string;
description: string;
value: string;
sensitive: boolean;
};
};
const StyledTruncatedCell = styled(TableCell)`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
cursor: pointer;
`;
const StyledExpandedDetails = styled.div`
background-color: ${({ theme }) => theme.background.tertiary};
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`
font-weight: ${({ theme }) => theme.font.weight.medium};
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 StyledTransitionedIconChevronRight = styled(IconChevronRight)`
cursor: pointer;
transform: ${({ $isExpanded }: { $isExpanded: boolean }) =>
$isExpanded ? 'rotate(90deg)' : 'rotate(0deg)'};
transition: ${({ theme }) =>
`transform ${theme.animation.duration.normal}s ease`};
`;
export const SettingsAdminEnvVariablesRow = ({
variable,
}: SettingsAdminEnvVariablesRowProps) => {
const [isExpanded, setIsExpanded] = useState(false);
const [showSensitiveValue, setShowSensitiveValue] = useState(false);
const theme = useTheme();
const displayValue =
variable.value === ''
? 'null'
: variable.sensitive && !showSensitiveValue
? '••••••'
: variable.value;
const handleToggleVisibility = (event: React.MouseEvent) => {
event.stopPropagation();
setShowSensitiveValue(!showSensitiveValue);
};
return (
<>
<TableRow
onClick={() => setIsExpanded(!isExpanded)}
gridAutoColumns="4fr 3fr 2fr 1fr 1fr"
>
<StyledTruncatedCell color="primary">
<StyledEllipsisLabel>{variable.name}</StyledEllipsisLabel>
</StyledTruncatedCell>
<StyledTruncatedCell>
<StyledEllipsisLabel>{variable.description}</StyledEllipsisLabel>
</StyledTruncatedCell>
<StyledTruncatedCell>
<StyledEllipsisLabel>{displayValue}</StyledEllipsisLabel>
</StyledTruncatedCell>
<TableCell align="right">
{variable.sensitive && variable.value !== '' && (
<LightIconButton
Icon={showSensitiveValue ? IconEyeOff : IconEye}
size="small"
accent="secondary"
onClick={handleToggleVisibility}
/>
)}
</TableCell>
<TableCell align="right">
<StyledTransitionedIconChevronRight
$isExpanded={isExpanded}
size={theme.icon.size.sm}
/>
</TableCell>
</TableRow>
<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>{displayValue}</StyledExpandedLabel>
</StyledExpandedDetails>
</AnimatedExpandableContainer>
</>
);
};

View File

@ -0,0 +1,35 @@
import { SettingsAdminEnvVariablesRow } from '@/settings/admin-panel/components/SettingsAdminEnvVariablesRow';
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled';
const StyledTable = styled(Table)`
margin-top: ${({ theme }) => theme.spacing(3)};
`;
type SettingsAdminEnvVariablesTableProps = {
variables: Array<{
name: string;
description: string;
value: string;
sensitive: boolean;
}>;
};
export const SettingsAdminEnvVariablesTable = ({
variables,
}: SettingsAdminEnvVariablesTableProps) => (
<StyledTable>
<TableRow gridAutoColumns="4fr 3fr 2fr 1fr 1fr">
<TableHeader>Name</TableHeader>
<TableHeader>Description</TableHeader>
<TableHeader>Value</TableHeader>
<TableHeader align="right"></TableHeader>
<TableHeader align="right"></TableHeader>
</TableRow>
{variables.map((variable) => (
<SettingsAdminEnvVariablesRow key={variable.name} variable={variable} />
))}
</StyledTable>
);

View File

@ -0,0 +1,192 @@
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
import { SettingsAdminWorkspaceContent } from '@/settings/admin-panel/components/SettingsAdminWorkspaceContent';
import { SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminUserLookupWorkspaceTabsId';
import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate';
import { useUserLookup } from '@/settings/admin-panel/hooks/useUserLookup';
import { adminPanelErrorState } from '@/settings/admin-panel/states/adminPanelErrorState';
import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState';
import { TextInput } from '@/ui/input/components/TextInput';
import { TabList } from '@/ui/layout/tab/components/TabList';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { getImageAbsoluteURI, isDefined } from 'twenty-shared';
import {
Button,
H1Title,
H1TitleFontColor,
H2Title,
IconSearch,
Section,
} from 'twenty-ui';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
const StyledLinkContainer = styled.div`
margin-right: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
`;
const StyledErrorSection = styled.div`
color: ${({ theme }) => theme.font.color.danger};
margin-top: ${({ 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}`};
box-sizing: border-box;
display: flex;
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 [userId, setUserId] = useState('');
const { error: impersonateError } = useImpersonate();
const { activeTabId, setActiveTabId } = useTabList(
SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID,
);
const userLookupResult = useRecoilValue(userLookupResultState);
const adminPanelError = useRecoilValue(adminPanelErrorState);
const { handleUserLookup, isLoading } = useUserLookup();
const canManageFeatureFlags = useRecoilValue(canManageFeatureFlagsState);
const handleSearch = async () => {
setActiveTabId('');
const result = await handleUserLookup(userIdentifier);
if (isDefined(result?.user?.id) && !adminPanelError) {
setUserId(result.user.id.trim());
}
if (
isDefined(result?.workspaces) &&
result.workspaces.length > 0 &&
!adminPanelError
) {
setActiveTabId(result.workspaces[0].id);
}
};
const shouldShowUserData = userLookupResult && !adminPanelError;
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();
return (
<>
<Section>
<H2Title
title={
canManageFeatureFlags
? 'Feature Flags & Impersonation'
: 'User Impersonation'
}
description={
canManageFeatureFlags
? 'Look up users and manage their workspace feature flags or impersonate them.'
: 'Look up users to impersonate them.'
}
/>
<StyledContainer>
<StyledLinkContainer>
<TextInput
value={userIdentifier}
onChange={setUserIdentifier}
onInputEnter={handleSearch}
placeholder="Enter user ID or email address"
fullWidth
disabled={isLoading}
/>
</StyledLinkContainer>
<Button
Icon={IconSearch}
variant="primary"
accent="blue"
title="Search"
onClick={handleSearch}
disabled={!userIdentifier.trim() || isLoading}
/>
</StyledContainer>
{(adminPanelError || impersonateError) && (
<StyledErrorSection>
{adminPanelError ?? impersonateError}
</StyledErrorSection>
)}
</Section>
{shouldShowUserData && (
<Section>
<StyledUserInfo>
<H1Title title="User Info" fontColor={H1TitleFontColor.Primary} />
<H2Title title={userFullName} description="User Name" />
<H2Title
title={userLookupResult.user.email}
description="User Email"
/>
<H2Title title={userLookupResult.user.id} description="User ID" />
</StyledUserInfo>
<H1Title title="Workspaces" fontColor={H1TitleFontColor.Primary} />
<StyledTabListContainer>
<TabList
tabs={tabs}
tabListInstanceId={SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID}
behaveAsLinks={false}
/>
</StyledTabListContainer>
<StyledContentContainer>
<SettingsAdminWorkspaceContent
activeWorkspace={activeWorkspace}
userId={userId}
/>
</StyledContentContainer>
</Section>
)}
</>
);
};

View File

@ -0,0 +1,18 @@
import { SettingsAdminEnvVariables } from '@/settings/admin-panel/components/SettingsAdminEnvVariables';
import { SettingsAdminGeneral } from '@/settings/admin-panel/components/SettingsAdminGeneral';
import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs';
import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
export const SettingsAdminTabContent = () => {
const { activeTabId } = useTabList(SETTINGS_ADMIN_TABS_ID);
switch (activeTabId) {
case SETTINGS_ADMIN_TABS.GENERAL:
return <SettingsAdminGeneral />;
case SETTINGS_ADMIN_TABS.ENV_VARIABLES:
return <SettingsAdminEnvVariables />;
default:
return null;
}
};

View File

@ -0,0 +1,97 @@
import { Button, H2Title, IconUser, Toggle } from 'twenty-ui';
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
import { useFeatureFlag } from '@/settings/admin-panel/hooks/useFeatureFlag';
import { useImpersonate } from '@/settings/admin-panel/hooks/useImpersonate';
import { WorkspaceInfo } from '@/settings/admin-panel/types/WorkspaceInfo';
import { Table } from '@/ui/layout/table/components/Table';
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 styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
type SettingsAdminWorkspaceContentProps = {
activeWorkspace: WorkspaceInfo | undefined;
userId: string;
};
const StyledTable = styled(Table)`
margin-top: ${({ theme }) => theme.spacing(3)};
`;
export const SettingsAdminWorkspaceContent = ({
activeWorkspace,
userId,
}: SettingsAdminWorkspaceContentProps) => {
const canManageFeatureFlags = useRecoilValue(canManageFeatureFlagsState);
const {
handleImpersonate,
isLoading: isImpersonateLoading,
canImpersonate,
} = useImpersonate();
const { handleFeatureFlagUpdate } = useFeatureFlag();
if (!activeWorkspace) return null;
return (
<>
<H2Title title={activeWorkspace.name} description={'Workspace Name'} />
<H2Title
title={`${activeWorkspace.totalUsers} ${
activeWorkspace.totalUsers > 1 ? 'Users' : 'User'
}`}
description={'Total Users'}
/>
{canImpersonate && (
<Button
Icon={IconUser}
variant="primary"
accent="blue"
title={'Impersonate'}
onClick={() => handleImpersonate(userId, activeWorkspace.id)}
disabled={
isImpersonateLoading || activeWorkspace.allowImpersonation === false
}
dataTestId="impersonate-button"
/>
)}
{canManageFeatureFlags && (
<StyledTable>
<TableRow
gridAutoColumns="1fr 100px"
mobileGridAutoColumns="1fr 80px"
>
<TableHeader>Feature Flag</TableHeader>
<TableHeader align="right">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>
))}
</StyledTable>
)}
</>
);
};

View File

@ -1,2 +0,0 @@
export const SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID =
'settings-admin-feature-flags-tab-id';

View File

@ -0,0 +1,4 @@
export const SETTINGS_ADMIN_TABS = {
GENERAL: 'general',
ENV_VARIABLES: 'env-variables',
};

View File

@ -0,0 +1 @@
export const SETTINGS_ADMIN_TABS_ID = 'settings-admin-tabs-id';

View File

@ -0,0 +1,2 @@
export const SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID =
'settings-admin-user-lookup-workspace-tabs-id';

View File

@ -4,7 +4,9 @@ export const GET_ENVIRONMENT_VARIABLES_GROUPED = gql`
query GetEnvironmentVariablesGrouped {
getEnvironmentVariablesGrouped {
groups {
groupName
name
description
isHiddenOnLoad
variables {
name
description
@ -12,7 +14,8 @@ export const GET_ENVIRONMENT_VARIABLES_GROUPED = gql`
sensitive
}
subgroups {
subgroupName
name
description
variables {
name
description

View File

@ -1,46 +1,21 @@
import { UserLookup } from '@/settings/admin-panel/types/UserLookup';
import { useState } from 'react';
import { adminPanelErrorState } from '@/settings/admin-panel/states/adminPanelErrorState';
import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
import {
FeatureFlagKey,
useUpdateWorkspaceFeatureFlagMutation,
useUserLookupAdminPanelMutation,
} from '~/generated/graphql';
export const useFeatureFlagsManagement = () => {
const [userLookupResult, setUserLookupResult] = useState<UserLookup | null>(
null,
export const useFeatureFlag = () => {
const [userLookupResult, setUserLookupResult] = useRecoilState(
userLookupResultState,
);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [userLookup] = useUserLookupAdminPanelMutation({
onCompleted: (data) => {
setIsLoading(false);
if (isDefined(data?.userLookupAdminPanel)) {
setUserLookupResult(data.userLookupAdminPanel);
}
},
onError: (error) => {
setIsLoading(false);
setError(error.message);
},
});
const setError = useSetRecoilState(adminPanelErrorState);
const [updateFeatureFlag] = useUpdateWorkspaceFeatureFlagMutation();
const handleUserLookup = async (userIdentifier: string) => {
setError(null);
setIsLoading(true);
setUserLookupResult(null);
const response = await userLookup({
variables: { userIdentifier },
});
return response.data?.userLookupAdminPanel;
};
const handleFeatureFlagUpdate = async (
workspaceId: string,
featureFlag: FeatureFlagKey,
@ -83,10 +58,6 @@ export const useFeatureFlagsManagement = () => {
};
return {
userLookupResult,
handleUserLookup,
handleFeatureFlagUpdate,
isLoading,
error,
};
};

View File

@ -0,0 +1,42 @@
import { adminPanelErrorState } from '@/settings/admin-panel/states/adminPanelErrorState';
import { userLookupResultState } from '@/settings/admin-panel/states/userLookupResultState';
import { useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
import { useUserLookupAdminPanelMutation } from '~/generated/graphql';
export const useUserLookup = () => {
const setUserLookupResult = useSetRecoilState(userLookupResultState);
const setError = useSetRecoilState(adminPanelErrorState);
const [isLoading, setIsLoading] = useState(false);
const [userLookup] = useUserLookupAdminPanelMutation({
onCompleted: (data) => {
setIsLoading(false);
if (isDefined(data?.userLookupAdminPanel)) {
setUserLookupResult(data.userLookupAdminPanel);
}
},
onError: (error) => {
setIsLoading(false);
setError(error.message);
},
});
const handleUserLookup = async (userIdentifier: string) => {
setError(null);
setIsLoading(true);
setUserLookupResult(null);
const response = await userLookup({
variables: { userIdentifier },
});
return response.data?.userLookupAdminPanel;
};
return {
handleUserLookup,
isLoading,
};
};

View File

@ -0,0 +1,6 @@
import { atom } from 'recoil';
export const adminPanelErrorState = atom<string | null>({
key: 'adminPanelErrorState',
default: null,
});

View File

@ -0,0 +1,7 @@
import { UserLookup } from '@/settings/admin-panel/types/UserLookup';
import { atom } from 'recoil';
export const userLookupResultState = atom<UserLookup | null>({
key: 'userLookupResultState',
default: null,
});