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 { useGetEnvironmentVariablesGroupedQuery } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const StyledGroupContainer = styled.div` const StyledGroupContainer = styled.div``;
margin-bottom: ${({ theme }) => theme.spacing(6)};
`;
const StyledInfoText = styled.div` const StyledInfoText = styled.div`
color: ${({ theme }) => theme.font.color.secondary}; color: ${({ theme }) => theme.font.color.secondary};
`; `;
const StyledCard = styled(Card)`
margin-bottom: ${({ theme }) => theme.spacing(8)};
`;
export const SettingsAdminEnvVariables = () => { export const SettingsAdminEnvVariables = () => {
const theme = useTheme(); const theme = useTheme();
const { data: environmentVariables, loading: environmentVariablesLoading } = const { data: environmentVariables, loading: environmentVariablesLoading } =
@ -38,22 +40,20 @@ export const SettingsAdminEnvVariables = () => {
<> <>
<Section> <Section>
<StyledInfoText> <StyledInfoText>
{t` These are only the server values. Ensure your worker environment has the {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.`}
same variables and values, this is required for asynchronous tasks like
email sync.`}
</StyledInfoText> </StyledInfoText>
</Section> </Section>
<Section> {visibleGroups.map((group) => (
{visibleGroups.map((group) => ( <StyledGroupContainer key={group.name}>
<StyledGroupContainer key={group.name}> <H2Title title={group.name} description={group.description} />
<H2Title title={group.name} description={group.description} /> {group.variables.length > 0 && (
{group.variables.length > 0 && ( <SettingsAdminEnvVariablesTable variables={group.variables} />
<SettingsAdminEnvVariablesTable variables={group.variables} /> )}
)} </StyledGroupContainer>
</StyledGroupContainer> ))}
))}
<Card rounded> <Section>
<StyledCard rounded>
<SettingsListItemCardContent <SettingsListItemCardContent
label={t`Other Variables`} label={t`Other Variables`}
to={getSettingsPath(SettingsPath.AdminPanelOtherEnvVariables)} to={getSettingsPath(SettingsPath.AdminPanelOtherEnvVariables)}
@ -61,7 +61,7 @@ export const SettingsAdminEnvVariables = () => {
LeftIcon={IconHeartRateMonitor} LeftIcon={IconHeartRateMonitor}
LeftIconColor={theme.font.color.tertiary} LeftIconColor={theme.font.color.tertiary}
/> />
</Card> </StyledCard>
</Section> </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 { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow'; import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
@ -19,6 +21,8 @@ type SettingsAdminEnvVariablesRowProps = {
value: string; value: string;
sensitive: boolean; sensitive: boolean;
}; };
isExpanded: boolean;
onExpandToggle: (name: string) => void;
}; };
const StyledTruncatedCell = styled(TableCell)` const StyledTruncatedCell = styled(TableCell)`
@ -43,53 +47,36 @@ const StyledButton = styled(motion.button)`
const MotionIconChevronDown = motion(IconChevronRight); 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` const StyledEllipsisLabel = styled.div`
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
`; `;
const StyledExpandedLabel = styled.div`
word-break: break-word;
white-space: normal;
overflow: visible;
`;
const StyledValueContainer = styled.div` const StyledValueContainer = styled.div`
display: flex;
align-items: center; align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
justify-content: space-between; justify-content: space-between;
width: 100%;
`; `;
const StyledTableRow = styled(TableRow)<{ isExpanded: boolean }>` const StyledTableRow = styled(TableRow)<{ isExpanded: boolean }>`
background-color: ${({ isExpanded, theme }) => background-color: ${({ isExpanded, theme }) =>
isExpanded ? theme.background.transparent.light : 'transparent'}; 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 = ({ export const SettingsAdminEnvVariablesRow = ({
variable, variable,
isExpanded,
onExpandToggle,
}: SettingsAdminEnvVariablesRowProps) => { }: SettingsAdminEnvVariablesRowProps) => {
const [isExpanded, setIsExpanded] = useState(false);
const [showSensitiveValue, setShowSensitiveValue] = useState(false); const [showSensitiveValue, setShowSensitiveValue] = useState(false);
const theme = useTheme(); const theme = useTheme();
@ -105,10 +92,47 @@ export const SettingsAdminEnvVariablesRow = ({
setShowSensitiveValue(!showSensitiveValue); 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 ( return (
<> <>
<StyledTableRow <StyledTableRow
onClick={() => setIsExpanded(!isExpanded)} onClick={() => onExpandToggle(variable.name)}
gridAutoColumns="5fr 4fr 3fr 1fr" gridAutoColumns="5fr 4fr 3fr 1fr"
isExpanded={isExpanded} isExpanded={isExpanded}
> >
@ -122,7 +146,12 @@ export const SettingsAdminEnvVariablesRow = ({
<StyledEllipsisLabel>{displayValue}</StyledEllipsisLabel> <StyledEllipsisLabel>{displayValue}</StyledEllipsisLabel>
</StyledTruncatedCell> </StyledTruncatedCell>
<TableCell align="right"> <TableCell align="right">
<StyledButton onClick={() => setIsExpanded(!isExpanded)}> <StyledButton
onClick={(e) => {
e.stopPropagation();
onExpandToggle(variable.name);
}}
>
<MotionIconChevronDown <MotionIconChevronDown
size={theme.icon.size.md} size={theme.icon.size.md}
color={theme.font.color.tertiary} color={theme.font.color.tertiary}
@ -133,26 +162,12 @@ export const SettingsAdminEnvVariablesRow = ({
</TableCell> </TableCell>
</StyledTableRow> </StyledTableRow>
<AnimatedExpandableContainer isExpanded={isExpanded} mode="fit-content"> <AnimatedExpandableContainer isExpanded={isExpanded} mode="fit-content">
<StyledExpandedDetails> <StyledExpandableContainer>
<StyledDetailLabel>Name</StyledDetailLabel> <SettingsAdminTableCard
<StyledEllipsisLabel>{variable.name}</StyledEllipsisLabel> items={environmentVariablesDetails}
<StyledDetailLabel>Description</StyledDetailLabel> gridAutoColumns="1fr 4fr"
<StyledExpandedLabel>{variable.description}</StyledExpandedLabel> />
<StyledDetailLabel>Value</StyledDetailLabel> </StyledExpandableContainer>
<StyledExpandedLabel>
<StyledValueContainer>
{displayValue}
{variable.sensitive && variable.value !== '' && (
<LightIconButton
Icon={showSensitiveValue ? IconEyeOff : IconEye}
size="small"
accent="secondary"
onClick={handleToggleVisibility}
/>
)}
</StyledValueContainer>
</StyledExpandedLabel>
</StyledExpandedDetails>
</AnimatedExpandableContainer> </AnimatedExpandableContainer>
</> </>
); );

View File

@ -1,11 +1,13 @@
import { SettingsAdminEnvVariablesRow } from '@/settings/admin-panel/components/SettingsAdminEnvVariablesRow'; import { SettingsAdminEnvVariablesRow } from '@/settings/admin-panel/components/SettingsAdminEnvVariablesRow';
import { Table } from '@/ui/layout/table/components/Table'; 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 { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow'; import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useState } from 'react';
const StyledTable = styled(Table)` const StyledTableBody = styled(TableBody)`
margin-top: ${({ theme }) => theme.spacing(3)}; border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
`; `;
type SettingsAdminEnvVariablesTableProps = { type SettingsAdminEnvVariablesTableProps = {
@ -19,16 +21,31 @@ type SettingsAdminEnvVariablesTableProps = {
export const SettingsAdminEnvVariablesTable = ({ export const SettingsAdminEnvVariablesTable = ({
variables, variables,
}: SettingsAdminEnvVariablesTableProps) => ( }: SettingsAdminEnvVariablesTableProps) => {
<StyledTable> const [expandedRowName, setExpandedRowName] = useState<string | null>(null);
<TableRow gridAutoColumns="5fr 4fr 3fr 1fr">
<TableHeader>Name</TableHeader> const handleExpandToggle = (name: string) => {
<TableHeader>Description</TableHeader> setExpandedRowName(expandedRowName === name ? null : name);
<TableHeader align="right">Value</TableHeader> };
<TableHeader align="right"></TableHeader>
</TableRow> return (
{variables.map((variable) => ( <Table>
<SettingsAdminEnvVariablesRow key={variable.name} variable={variable} /> <TableRow gridAutoColumns="5fr 4fr 3fr 1fr">
))} <TableHeader>Name</TableHeader>
</StyledTable> <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 { getImageAbsoluteURI, isDefined } from 'twenty-shared';
import { import {
Button, Button,
H1Title,
H1TitleFontColor,
H2Title, H2Title,
IconId,
IconMail,
IconSearch, IconSearch,
IconUser,
Section, Section,
} from 'twenty-ui'; } from 'twenty-ui';
import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useUserLookupAdminPanelMutation } from '~/generated/graphql'; import { useUserLookupAdminPanelMutation } from '~/generated/graphql';
import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserState } from '@/auth/states/currentUserState';
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
import { SettingsAdminVersionContainer } from '@/settings/admin-panel/components/SettingsAdminVersionContainer'; import { SettingsAdminVersionContainer } from '@/settings/admin-panel/components/SettingsAdminVersionContainer';
const StyledContainer = styled.div` const StyledContainer = styled.div`
@ -35,10 +37,6 @@ const StyledContainer = styled.div`
gap: ${({ theme }) => theme.spacing(2)}; gap: ${({ theme }) => theme.spacing(2)};
`; `;
const StyledUserInfo = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(5)};
`;
const StyledTabListContainer = styled.div` const StyledTabListContainer = styled.div`
align-items: center; align-items: center;
border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`}; border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
@ -47,12 +45,6 @@ const StyledTabListContainer = styled.div`
gap: ${({ theme }) => theme.spacing(2)}; gap: ${({ theme }) => theme.spacing(2)};
`; `;
const StyledContentContainer = styled.div`
flex: 1;
width: 100%;
padding: ${({ theme }) => theme.spacing(4)} 0;
`;
export const SettingsAdminGeneral = () => { export const SettingsAdminGeneral = () => {
const [userIdentifier, setUserIdentifier] = useState(''); const [userIdentifier, setUserIdentifier] = useState('');
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
@ -124,6 +116,24 @@ export const SettingsAdminGeneral = () => {
userLookupResult?.user.lastName || '' userLookupResult?.user.lastName || ''
}`.trim(); }`.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 ( return (
<> <>
{canAccessFullAdminPanel && ( {canAccessFullAdminPanel && (
@ -173,36 +183,31 @@ export const SettingsAdminGeneral = () => {
)} )}
{isDefined(userLookupResult) && ( {isDefined(userLookupResult) && (
<Section> <>
<StyledUserInfo> <Section>
<H1Title <H2Title title={t`User Info`} description={t`About this user`} />
title={t`User Info`} <SettingsAdminTableCard
fontColor={H1TitleFontColor.Primary} items={userInfoItems}
rounded
gridAutoColumns="1fr 4fr"
/> />
<H2Title title={userFullName} description={t`User Name`} /> </Section>
<Section>
<H2Title <H2Title
title={userLookupResult.user.email} title={t`Workspaces`}
description={t`User Email`} description={t`All workspaces this user is a member of`}
/> />
<H2Title <StyledTabListContainer>
title={userLookupResult.user.id} <TabList
description={t`User ID`} tabs={tabs}
/> tabListInstanceId={SETTINGS_ADMIN_USER_LOOKUP_WORKSPACE_TABS_ID}
</StyledUserInfo> 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} /> <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 { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
import { GITHUB_LINK } from '@ui/navigation/link/constants/GithubLink';
import { checkTwentyVersionExists } from '@/settings/admin-panel/utils/checkTwentyVersionExists'; import { checkTwentyVersionExists } from '@/settings/admin-panel/utils/checkTwentyVersionExists';
import { fetchLatestTwentyRelease } from '@/settings/admin-panel/utils/fetchLatestTwentyRelease'; import { fetchLatestTwentyRelease } from '@/settings/admin-panel/utils/fetchLatestTwentyRelease';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { GITHUB_LINK } from '@ui/navigation/link/constants/GithubLink';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { IconCircleDot, IconStatusChange } from 'twenty-ui';
import packageJson from '../../../../../package.json'; 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` const StyledActionLink = styled.a`
align-items: center; align-items: center;
color: ${({ theme }) => theme.font.color.primary}; color: ${({ theme }) => theme.font.color.primary};
display: flex; display: flex;
font-size: ${({ theme }) => theme.font.size.sm}; 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)}; gap: ${({ theme }) => theme.spacing(1)};
padding: 0 ${({ theme }) => theme.spacing(1)};
text-decoration: none; text-decoration: none;
:hover { :hover {
@ -52,19 +26,10 @@ const StyledActionLink = styled.a`
const StyledSpan = styled.span` const StyledSpan = styled.span`
color: ${({ theme }) => theme.font.color.primary}; color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.sm}; 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 = () => { export const SettingsAdminVersionContainer = () => {
const theme = useTheme();
const [latestVersion, setLatestVersion] = useState<string | null>(null); const [latestVersion, setLatestVersion] = useState<string | null>(null);
const [currentVersionExists, setCurrentVersionExists] = useState(false); const [currentVersionExists, setCurrentVersionExists] = useState(false);
@ -73,46 +38,44 @@ export const SettingsAdminVersionContainer = () => {
checkTwentyVersionExists(packageJson.version).then(setCurrentVersionExists); checkTwentyVersionExists(packageJson.version).then(setCurrentVersionExists);
}, []); }, []);
const VERSION_DETAILS: VersionDetail[] = [ const versionItems = [
{ {
Icon: IconCircleDot, Icon: IconCircleDot,
text: t`Current version:`, label: t`Current version`,
version: packageJson.version, value: currentVersionExists ? (
link: `${GITHUB_LINK}/releases/tag/v${packageJson.version}`, <StyledActionLink
type: 'current', href={`${GITHUB_LINK}/releases/tag/v${packageJson.version}`}
target="_blank"
rel="noreferrer"
>
{packageJson.version}
</StyledActionLink>
) : (
<StyledSpan>{packageJson.version}</StyledSpan>
),
}, },
{ {
Icon: IconStatusChange, Icon: IconStatusChange,
text: t`Latest version:`, label: t`Latest version`,
version: latestVersion, value: latestVersion ? (
link: `${GITHUB_LINK}/releases/tag/v${latestVersion}`, <StyledActionLink
type: 'latest', href={`${GITHUB_LINK}/releases/tag/v${latestVersion}`}
target="_blank"
rel="noreferrer"
>
{latestVersion}
</StyledActionLink>
) : (
<StyledSpan>{latestVersion ?? 'Loading...'}</StyledSpan>
),
}, },
]; ];
return ( return (
<StyledVersionContainer> <SettingsAdminTableCard
{VERSION_DETAILS.map((versionDetail, index) => ( rounded
<StyledVersionDetails key={index}> items={versionItems}
<versionDetail.Icon gridAutoColumns="3fr 8fr"
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>
); );
}; };

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 { currentUserState } from '@/auth/states/currentUserState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState'; import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeatureFlagsState';
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
import { useFeatureFlagState } from '@/settings/admin-panel/hooks/useFeatureFlagState'; import { useFeatureFlagState } from '@/settings/admin-panel/hooks/useFeatureFlagState';
import { useImpersonationAuth } from '@/settings/admin-panel/hooks/useImpersonationAuth'; import { useImpersonationAuth } from '@/settings/admin-panel/hooks/useImpersonationAuth';
import { useImpersonationRedirect } from '@/settings/admin-panel/hooks/useImpersonationRedirect'; 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 { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Table } from '@/ui/layout/table/components/Table'; 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 { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow'; 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 styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react'; import { useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil'; 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 { import {
FeatureFlagKey, FeatureFlagKey,
useImpersonateMutation, useImpersonateMutation,
@ -29,7 +44,14 @@ type SettingsAdminWorkspaceContentProps = {
activeWorkspace: WorkspaceInfo | undefined; 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)}; 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; if (!activeWorkspace) return null;
return ( return (
<> <StyledContainer>
<H2Title title={activeWorkspace.name} description={t`Workspace Name`} /> <Section>
<H2Title <H2Title
title={`${activeWorkspace.totalUsers} ${ title={t`Workspace Info`}
activeWorkspace.totalUsers > 1 ? t`Users` : t`User` description={t`About this workspace`}
}`}
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"
/> />
)} <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 && ( {canManageFeatureFlags && (
<StyledTable> <Table>
<TableRow <TableBody>
gridAutoColumns="1fr 100px"
mobileGridAutoColumns="1fr 80px"
>
<TableHeader>{t`Feature Flag`}</TableHeader>
<TableHeader align="right">{t`Status`}</TableHeader>
</TableRow>
{activeWorkspace.featureFlags.map((flag) => (
<TableRow <TableRow
gridAutoColumns="1fr 100px" gridAutoColumns="1fr 100px"
mobileGridAutoColumns="1fr 80px" mobileGridAutoColumns="1fr 80px"
key={flag.key}
> >
<TableCell>{flag.key}</TableCell> <TableHeader>{t`Feature Flag`}</TableHeader>
<TableCell align="right"> <TableHeader align="right">{t`Status`}</TableHeader>
<Toggle
value={flag.value}
onChange={(newValue) =>
handleFeatureFlagUpdate(
activeWorkspace.id,
flag.key,
newValue,
)
}
/>
</TableCell>
</TableRow> </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>
); );
}; };

View File

@ -10,6 +10,12 @@ const StyledErrorMessage = styled.div`
margin-top: ${({ theme }) => theme.spacing(2)}; margin-top: ${({ theme }) => theme.spacing(2)};
`; `;
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
gap: ${({ theme }) => theme.spacing(8)};
`;
export const ConnectedAccountHealthStatus = () => { export const ConnectedAccountHealthStatus = () => {
const { indicatorHealth } = useContext(SettingsAdminIndicatorHealthContext); const { indicatorHealth } = useContext(SettingsAdminIndicatorHealthContext);
const details = indicatorHealth.details; const details = indicatorHealth.details;
@ -35,7 +41,7 @@ export const ConnectedAccountHealthStatus = () => {
} }
return ( return (
<> <StyledContainer>
{errorMessages.length > 0 && ( {errorMessages.length > 0 && (
<StyledErrorMessage> <StyledErrorMessage>
{`${errorMessages.join(' and ')} ${errorMessages.length > 1 ? 'are' : 'is'} not available because the service is down`} {`${errorMessages.join(' and ')} ${errorMessages.length > 1 ? 'are' : 'is'} not available because the service is down`}
@ -55,6 +61,6 @@ export const ConnectedAccountHealthStatus = () => {
title={t`Calendar Sync Status`} title={t`Calendar Sync Status`}
/> />
)} )}
</> </StyledContainer>
); );
}; };

View File

@ -10,8 +10,8 @@ const StyledDetailsContainer = styled.div`
padding: ${({ theme }) => theme.spacing(4)}; padding: ${({ theme }) => theme.spacing(4)};
border-radius: ${({ theme }) => theme.border.radius.md}; border-radius: ${({ theme }) => theme.border.radius.md};
border: 1px solid ${({ theme }) => theme.border.color.medium}; border: 1px solid ${({ theme }) => theme.border.color.medium};
white-space: pre-wrap;
font-size: ${({ theme }) => theme.font.size.sm}; font-size: ${({ theme }) => theme.font.size.sm};
overflow-x: auto;
`; `;
const StyledErrorMessage = styled.div` const StyledErrorMessage = styled.div`

View File

@ -1,55 +1,25 @@
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
import { WorkerMetricsTooltip } from '@/settings/admin-panel/health-status/components/WorkerMetricsTooltip';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Select } from '@/ui/input/components/Select';
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useTheme } from '@emotion/react'; import { useTheme } from '@emotion/react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { ResponsiveLine } from '@nivo/line'; import { ResponsiveLine } from '@nivo/line';
import { isNumber } from '@tiptap/core';
import { import {
QueueMetricsTimeRange, QueueMetricsTimeRange,
useGetQueueMetricsQuery, useGetQueueMetricsQuery,
} from '~/generated/graphql'; } from '~/generated/graphql';
const StyledTableRow = styled(TableRow)`
height: ${({ theme }) => theme.spacing(6)};
`;
const StyledQueueMetricsTitle = styled.div`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
margin-bottom: ${({ theme }) => theme.spacing(3)};
padding-left: ${({ theme }) => theme.spacing(3)};
`;
const StyledGraphContainer = styled.div` const StyledGraphContainer = styled.div`
background-color: ${({ theme }) => theme.background.tertiary}; background-color: ${({ theme }) => theme.background.secondary};
border-radius: ${({ theme }) => theme.border.radius.sm}; border-radius: ${({ theme }) => theme.border.radius.md};
height: 230px; height: 240px;
margin-bottom: ${({ theme }) => theme.spacing(4)};
padding-top: ${({ theme }) => theme.spacing(4)};
padding-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledQueueMetricsContainer = styled.div`
border: 1px solid ${({ theme }) => theme.border.color.medium}; border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.sm}; margin-bottom: ${({ theme }) => theme.spacing(4)};
padding-top: ${({ theme }) => theme.spacing(1)}; padding-top: ${({ theme }) => theme.spacing(2.5)};
padding-bottom: ${({ theme }) => theme.spacing(3)}; width: 100%;
padding-left: ${({ theme }) => theme.spacing(3)};
padding-right: ${({ theme }) => theme.spacing(3)};
`;
const StyledGraphControls = styled.div`
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
justify-content: flex-end;
margin-bottom: ${({ theme }) => theme.spacing(2)};
margin-top: ${({ theme }) => theme.spacing(1)};
`; `;
const StyledNoDataMessage = styled.div` const StyledNoDataMessage = styled.div`
@ -60,31 +30,9 @@ const StyledNoDataMessage = styled.div`
justify-content: center; justify-content: center;
`; `;
const StyledTooltipContainer = styled.div` const StyledSettingsAdminTableCard = styled(SettingsAdminTableCard)`
background-color: ${({ theme }) => theme.background.secondary}; padding-left: ${({ theme }) => theme.spacing(2)};
border: 1px solid ${({ theme }) => theme.border.color.medium}; padding-right: ${({ theme }) => theme.spacing(2)};
border-radius: ${({ theme }) => theme.border.radius.sm};
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: ${({ theme }) => theme.spacing(2)};
font-size: ${({ theme }) => theme.font.size.sm};
`;
const StyledTooltipItem = styled.div<{ color: string }>`
align-items: center;
color: ${({ theme }) => theme.font.color.primary};
display: flex;
padding: ${({ theme }) => theme.spacing(0.5)} 0;
`;
const StyledTooltipColorSquare = styled.div<{ color: string }>`
width: 12px;
height: 12px;
background-color: ${({ color }) => color};
margin-right: ${({ theme }) => theme.spacing(1)};
`;
const StyledTooltipValue = styled.span`
font-weight: ${({ theme }) => theme.font.weight.medium};
`; `;
type WorkerMetricsGraphProps = { type WorkerMetricsGraphProps = {
@ -96,7 +44,6 @@ type WorkerMetricsGraphProps = {
export const WorkerMetricsGraph = ({ export const WorkerMetricsGraph = ({
queueName, queueName,
timeRange, timeRange,
onTimeRangeChange,
}: WorkerMetricsGraphProps) => { }: WorkerMetricsGraphProps) => {
const theme = useTheme(); const theme = useTheme();
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
@ -158,25 +105,6 @@ export const WorkerMetricsGraph = ({
return ( return (
<> <>
<StyledGraphControls>
<Select
dropdownId={`timerange-${queueName}`}
value={timeRange}
options={[
{ value: QueueMetricsTimeRange.SevenDays, label: t`This week` },
{ value: QueueMetricsTimeRange.OneDay, label: t`Today` },
{
value: QueueMetricsTimeRange.TwelveHours,
label: t`Last 12 hours`,
},
{ value: QueueMetricsTimeRange.FourHours, label: t`Last 4 hours` },
{ value: QueueMetricsTimeRange.OneHour, label: t`Last 1 hour` },
]}
onChange={onTimeRangeChange}
needIconCheck
/>
</StyledGraphControls>
<StyledGraphContainer> <StyledGraphContainer>
{loading ? ( {loading ? (
<StyledNoDataMessage>{t`Loading metrics data...`}</StyledNoDataMessage> <StyledNoDataMessage>{t`Loading metrics data...`}</StyledNoDataMessage>
@ -185,7 +113,7 @@ export const WorkerMetricsGraph = ({
data={metricsData} data={metricsData}
curve="monotoneX" curve="monotoneX"
enableArea={true} enableArea={true}
colors={[theme.color.green, theme.color.red]} colors={[theme.color.blue, theme.color.red]}
theme={{ theme={{
text: { text: {
fill: theme.font.color.light, fill: theme.font.color.light,
@ -216,7 +144,7 @@ export const WorkerMetricsGraph = ({
}, },
}, },
}} }}
margin={{ top: 40, right: 30, bottom: 40, left: 50 }} margin={{ top: 40, right: 30, bottom: 40, left: 40 }}
xScale={{ xScale={{
type: 'linear', type: 'linear',
min: 0, min: 0,
@ -230,7 +158,7 @@ export const WorkerMetricsGraph = ({
}} }}
axisBottom={{ axisBottom={{
legend: getAxisLabel(), legend: getAxisLabel(),
legendOffset: 30, legendOffset: 20,
legendPosition: 'middle', legendPosition: 'middle',
tickSize: 5, tickSize: 5,
tickPadding: 5, tickPadding: 5,
@ -241,29 +169,12 @@ export const WorkerMetricsGraph = ({
tickSize: 6, tickSize: 6,
tickPadding: 5, tickPadding: 5,
tickValues: 4, tickValues: 4,
legend: 'Count',
legendOffset: -40,
legendPosition: 'middle',
}} }}
enableGridX={false} enableGridX={false}
gridYValues={4} gridYValues={4}
pointSize={0} pointSize={0}
enableSlices="x" enableSlices="x"
sliceTooltip={({ slice }) => ( sliceTooltip={({ slice }) => <WorkerMetricsTooltip slice={slice} />}
<StyledTooltipContainer>
{slice.points.map((point) => (
<StyledTooltipItem key={point.id} color={point.serieColor}>
<StyledTooltipColorSquare color={point.serieColor} />
<span>
{point.serieId}:{' '}
<StyledTooltipValue>
{String(point.data.y)}
</StyledTooltipValue>
</span>
</StyledTooltipItem>
))}
</StyledTooltipContainer>
)}
useMesh={true} useMesh={true}
legends={[ legends={[
{ {
@ -276,8 +187,9 @@ export const WorkerMetricsGraph = ({
itemDirection: 'left-to-right', itemDirection: 'left-to-right',
itemWidth: 100, itemWidth: 100,
itemHeight: 20, itemHeight: 20,
symbolSize: 12, itemTextColor: theme.font.color.secondary,
symbolShape: 'square', symbolSize: 4,
symbolShape: 'circle',
}, },
]} ]}
/> />
@ -286,29 +198,22 @@ export const WorkerMetricsGraph = ({
)} )}
</StyledGraphContainer> </StyledGraphContainer>
{metricsDetails && ( {metricsDetails && (
<> <StyledSettingsAdminTableCard
<StyledQueueMetricsTitle>Metrics:</StyledQueueMetricsTitle> rounded
<StyledQueueMetricsContainer> items={Object.entries(metricsDetails)
<Table> .filter(([key]) => key !== '__typename')
{Object.entries(metricsDetails) .map(([key, value]) => ({
.filter(([key]) => key !== '__typename') label: key.charAt(0).toUpperCase() + key.slice(1),
.map(([key, value]) => ( value: isNumber(value)
<StyledTableRow key={key}> ? value
<TableCell align="left"> : Array.isArray(value)
{key.charAt(0).toUpperCase() + key.slice(1)} ? value.length
</TableCell> : String(value),
<TableCell align="right"> }))}
{typeof value === 'number' gridAutoColumns="1fr 1fr"
? value labelAlign="left"
: Array.isArray(value) valueAlign="right"
? value.length />
: String(value)}
</TableCell>
</StyledTableRow>
))}
</Table>
</StyledQueueMetricsContainer>
</>
)} )}
</> </>
); );

View File

@ -0,0 +1,68 @@
import styled from '@emotion/styled';
import { Point } from '@nivo/line';
import { ReactElement } from 'react';
const StyledTooltipContainer = styled.div`
backdrop-filter: ${({ theme }) => theme.blur.medium};
background-color: ${({ theme }) => theme.background.transparent.secondary};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-radius: ${({ theme }) => theme.border.radius.md};
box-shadow: ${({ theme }) => theme.boxShadow.light};
display: flex;
flex-direction: column;
font-size: ${({ theme }) => theme.font.size.sm};
gap: ${({ theme }) => theme.spacing(2)};
padding: ${({ theme }) => theme.spacing(2)};
`;
const StyledTooltipItem = styled.div`
align-items: center;
color: ${({ theme }) => theme.font.color.primary};
display: flex;
`;
const StyledTooltipColorCircle = styled.div<{ color: string }>`
background-color: ${({ color }) => color};
border-radius: 50%;
height: 8px;
margin-right: ${({ theme }) => theme.spacing(2)};
width: 8px;
`;
const StyledTooltipDataRow = styled.div`
align-items: center;
display: flex;
justify-content: space-between;
color: ${({ theme }) => theme.font.color.tertiary};
gap: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledTooltipValue = styled.span`
color: ${({ theme }) => theme.font.color.primary};
font-weight: ${({ theme }) => theme.font.weight.medium};
`;
type WorkerMetricsTooltipProps = {
slice: {
points: readonly Point[];
};
};
export const WorkerMetricsTooltip = ({
slice,
}: WorkerMetricsTooltipProps): ReactElement => {
return (
<StyledTooltipContainer>
{slice.points.map((point) => (
<StyledTooltipItem key={point.id} color={point.serieColor}>
<StyledTooltipColorCircle color={point.serieColor} />
<StyledTooltipDataRow>
<span>{point.serieId}</span>
<StyledTooltipValue>{String(point.data.y)}</StyledTooltipValue>
</StyledTooltipDataRow>
</StyledTooltipItem>
))}
</StyledTooltipContainer>
);
};

View File

@ -1,3 +1,5 @@
import { Select } from '@/ui/input/components/Select';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { useState } from 'react'; import { useState } from 'react';
import { H2Title, Section } from 'twenty-ui'; import { H2Title, Section } from 'twenty-ui';
@ -11,19 +13,55 @@ type WorkerQueueMetricsSectionProps = {
queue: AdminPanelWorkerQueueHealth; queue: AdminPanelWorkerQueueHealth;
}; };
const StyledControlsContainer = styled.div`
display: flex;
align-items: flex-start;
justify-content: space-between;
`;
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
margin-bottom: ${({ theme }) => theme.spacing(8)};
`;
export const WorkerQueueMetricsSection = ({ export const WorkerQueueMetricsSection = ({
queue, queue,
}: WorkerQueueMetricsSectionProps) => { }: WorkerQueueMetricsSectionProps) => {
const [timeRange, setTimeRange] = useState(QueueMetricsTimeRange.OneHour); const [timeRange, setTimeRange] = useState(QueueMetricsTimeRange.OneHour);
return ( return (
<Section> <StyledContainer>
<H2Title title={queue.queueName} description={t`Queue performance`} /> <Section>
<StyledControlsContainer>
<H2Title title={queue.queueName} description={t`Queue performance`} />
<Select
dropdownId={`timerange-${queue.queueName}`}
value={timeRange}
options={[
{ value: QueueMetricsTimeRange.SevenDays, label: t`This week` },
{ value: QueueMetricsTimeRange.OneDay, label: t`Today` },
{
value: QueueMetricsTimeRange.TwelveHours,
label: t`Last 12 hours`,
},
{
value: QueueMetricsTimeRange.FourHours,
label: t`Last 4 hours`,
},
{ value: QueueMetricsTimeRange.OneHour, label: t`Last 1 hour` },
]}
onChange={setTimeRange}
needIconCheck
selectSizeVariant="small"
/>
</StyledControlsContainer>
</Section>
<WorkerMetricsGraph <WorkerMetricsGraph
queueName={queue.queueName} queueName={queue.queueName}
timeRange={timeRange} timeRange={timeRange}
onTimeRangeChange={setTimeRange} onTimeRangeChange={setTimeRange}
/> />
</Section> </StyledContainer>
); );
}; };

View File

@ -5,9 +5,9 @@ import { Link } from 'react-router-dom';
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
import { CardContent, IconChevronRight, IconComponent } from 'twenty-ui'; import { CardContent, IconChevronRight, IconComponent } from 'twenty-ui';
const StyledRow = styled(CardContent)` const StyledRow = styled(CardContent)<{ to?: boolean }>`
align-items: center; align-items: center;
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'default')}; cursor: ${({ onClick, to }) => (onClick || to ? 'pointer' : 'default')};
display: flex; display: flex;
font-size: ${({ theme }) => theme.font.size.sm}; font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium}; font-weight: ${({ theme }) => theme.font.weight.medium};
@ -15,6 +15,11 @@ const StyledRow = styled(CardContent)`
padding: ${({ theme }) => theme.spacing(2)}; padding: ${({ theme }) => theme.spacing(2)};
padding-left: ${({ theme }) => theme.spacing(3)}; padding-left: ${({ theme }) => theme.spacing(3)};
min-height: ${({ theme }) => theme.spacing(6)}; min-height: ${({ theme }) => theme.spacing(6)};
&:hover {
${({ to, theme }) =>
to && `background: ${theme.background.transparent.light};`}
}
`; `;
const StyledRightContainer = styled.div` const StyledRightContainer = styled.div`
@ -36,12 +41,8 @@ const StyledDescription = styled.span`
`; `;
const StyledLink = styled(Link)` const StyledLink = styled(Link)`
text-decoration: none;
color: ${({ theme }) => theme.font.color.secondary}; color: ${({ theme }) => theme.font.color.secondary};
text-decoration: none;
&:hover {
color: ${({ theme }) => theme.font.color.secondary};
}
`; `;
type SettingsListItemCardContentProps = { type SettingsListItemCardContentProps = {
@ -68,7 +69,7 @@ export const SettingsListItemCardContent = ({
const theme = useTheme(); const theme = useTheme();
const content = ( const content = (
<StyledRow onClick={onClick} divider={divider}> <StyledRow onClick={onClick} divider={divider} to={!!to}>
{!!LeftIcon && ( {!!LeftIcon && (
<LeftIcon <LeftIcon
size={theme.icon.size.md} size={theme.icon.size.md}

View File

@ -8,7 +8,7 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBa
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { H3Title, Section } from 'twenty-ui'; import { H2Title, H3Title, Section } from 'twenty-ui';
import { import {
AdminPanelHealthServiceStatus, AdminPanelHealthServiceStatus,
HealthIndicatorId, HealthIndicatorId,
@ -16,23 +16,10 @@ import {
} from '~/generated/graphql'; } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const StyledH3Title = styled(H3Title)`
margin-top: ${({ theme }) => theme.spacing(2)};
`;
const StyledDescription = styled.div`
color: ${({ theme }) => theme.font.color.tertiary};
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.regular};
margin-top: ${({ theme }) => theme.spacing(2)};
`;
const StyledTitleContainer = styled.div` const StyledTitleContainer = styled.div`
align-items: center;
display: flex; display: flex;
gap: ${({ theme }) => theme.spacing(4)}; gap: ${({ theme }) => theme.spacing(4)};
`;
const StyledHealthStatusContainer = styled.div`
margin-top: ${({ theme }) => theme.spacing(2)}; margin-top: ${({ theme }) => theme.spacing(2)};
`; `;
@ -87,23 +74,25 @@ export const SettingsAdminIndicatorHealthStatus = () => {
> >
<Section> <Section>
<StyledTitleContainer> <StyledTitleContainer>
<StyledH3Title <H3Title title={data?.getIndicatorHealthStatus?.label} />
title={`${data?.getIndicatorHealthStatus?.label}`}
/>
{data?.getIndicatorHealthStatus?.status && ( {data?.getIndicatorHealthStatus?.status && (
<StyledHealthStatusContainer> <SettingsAdminHealthStatusRightContainer
<SettingsAdminHealthStatusRightContainer status={data?.getIndicatorHealthStatus.status}
status={data?.getIndicatorHealthStatus.status} />
/>
</StyledHealthStatusContainer>
)} )}
</StyledTitleContainer> </StyledTitleContainer>
<StyledDescription>
{data?.getIndicatorHealthStatus?.description}
</StyledDescription>
</Section> </Section>
<Section>
<SettingsAdminIndicatorHealthStatusContent /> {data?.getIndicatorHealthStatus?.id !== HealthIndicatorId.worker &&
data?.getIndicatorHealthStatus?.id !==
HealthIndicatorId.connectedAccount && (
<H2Title
title={t`Status`}
description={data?.getIndicatorHealthStatus?.description}
/>
)}
<SettingsAdminIndicatorHealthStatusContent />
</Section>
</SettingsAdminIndicatorHealthContext.Provider> </SettingsAdminIndicatorHealthContext.Provider>
</SettingsPageContainer> </SettingsPageContainer>
</SubMenuTopBarContainer> </SubMenuTopBarContainer>

View File

@ -5,13 +5,11 @@ import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { H2Title, Section } from 'twenty-ui'; import { H2Title } from 'twenty-ui';
import { useGetEnvironmentVariablesGroupedQuery } from '~/generated/graphql'; import { useGetEnvironmentVariablesGroupedQuery } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const StyledGroupContainer = styled.div` const StyledGroupContainer = styled.div``;
margin-bottom: ${({ theme }) => theme.spacing(6)};
`;
export const SettingsAdminSecondaryEnvVariables = () => { export const SettingsAdminSecondaryEnvVariables = () => {
const { data: environmentVariables, loading: environmentVariablesLoading } = const { data: environmentVariables, loading: environmentVariablesLoading } =
@ -30,6 +28,7 @@ export const SettingsAdminSecondaryEnvVariables = () => {
return ( return (
<SubMenuTopBarContainer <SubMenuTopBarContainer
title={t`Other Environment Variables`}
links={[ links={[
{ {
children: t`Other`, children: t`Other`,
@ -45,16 +44,14 @@ export const SettingsAdminSecondaryEnvVariables = () => {
]} ]}
> >
<SettingsPageContainer> <SettingsPageContainer>
<Section> {hiddenGroups.map((group) => (
{hiddenGroups.map((group) => ( <StyledGroupContainer key={group.name}>
<StyledGroupContainer key={group.name}> <H2Title title={group.name} description={group.description} />
<H2Title title={group.name} description={group.description} /> {group.variables.length > 0 && (
{group.variables.length > 0 && ( <SettingsAdminEnvVariablesTable variables={group.variables} />
<SettingsAdminEnvVariablesTable variables={group.variables} /> )}
)} </StyledGroupContainer>
</StyledGroupContainer> ))}
))}
</Section>
</SettingsPageContainer> </SettingsPageContainer>
</SubMenuTopBarContainer> </SubMenuTopBarContainer>
); );

View File

@ -10,27 +10,27 @@ export const HEALTH_INDICATORS: Record<HealthIndicatorId, HealthIndicatorInfo> =
{ {
[HealthIndicatorId.database]: { [HealthIndicatorId.database]: {
id: HealthIndicatorId.database, id: HealthIndicatorId.database,
label: 'Database Status', label: 'Database',
description: 'PostgreSQL database connection status', description: 'PostgreSQL database connection status',
}, },
[HealthIndicatorId.redis]: { [HealthIndicatorId.redis]: {
id: HealthIndicatorId.redis, id: HealthIndicatorId.redis,
label: 'Redis Status', label: 'Redis',
description: 'Redis connection status', description: 'Redis connection status',
}, },
[HealthIndicatorId.worker]: { [HealthIndicatorId.worker]: {
id: HealthIndicatorId.worker, id: HealthIndicatorId.worker,
label: 'Worker Status', label: 'Worker',
description: 'Background job worker status', description: 'Background job worker health status',
}, },
[HealthIndicatorId.connectedAccount]: { [HealthIndicatorId.connectedAccount]: {
id: HealthIndicatorId.connectedAccount, id: HealthIndicatorId.connectedAccount,
label: 'Connected Account Status', label: 'Connected Accounts',
description: 'Connected accounts status', description: 'Connected accounts health status',
}, },
[HealthIndicatorId.app]: { [HealthIndicatorId.app]: {
id: HealthIndicatorId.app, id: HealthIndicatorId.app,
label: 'App Status', label: 'App',
description: 'Workspace metadata migration status check', description: 'Workspace metadata migration status check',
}, },
}; };

View File

@ -4,18 +4,18 @@ export {
IconAlertCircle, IconAlertCircle,
IconAlertTriangle, IconAlertTriangle,
IconApi, IconApi,
IconAppWindow,
IconApps, IconApps,
IconAppWindow,
IconArchive, IconArchive,
IconArchiveOff, IconArchiveOff,
IconArrowBackUp, IconArrowBackUp,
IconArrowDown, IconArrowDown,
IconArrowLeft, IconArrowLeft,
IconArrowRight, IconArrowRight,
IconArrowUp,
IconArrowUpRight,
IconArrowsDiagonal, IconArrowsDiagonal,
IconArrowsVertical, IconArrowsVertical,
IconArrowUp,
IconArrowUpRight,
IconAt, IconAt,
IconBaselineDensitySmall, IconBaselineDensitySmall,
IconBell, IconBell,
@ -47,8 +47,8 @@ export {
IconChevronDown, IconChevronDown,
IconChevronLeft, IconChevronLeft,
IconChevronRight, IconChevronRight,
IconChevronUp,
IconChevronsRight, IconChevronsRight,
IconChevronUp,
IconCircleDot, IconCircleDot,
IconCircleOff, IconCircleOff,
IconCirclePlus, IconCirclePlus,
@ -128,6 +128,7 @@ export {
IconExternalLink, IconExternalLink,
IconEye, IconEye,
IconEyeOff, IconEyeOff,
IconEyeShare,
IconFile, IconFile,
IconFileCheck, IconFileCheck,
IconFileExport, IconFileExport,
@ -167,6 +168,7 @@ export {
IconHistoryToggle, IconHistoryToggle,
IconHome, IconHome,
IconHours24, IconHours24,
IconId,
IconInbox, IconInbox,
IconInfoCircle, IconInfoCircle,
IconJson, IconJson,

View File

@ -1,4 +1,5 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { OverflowingTextWithTooltip } from '@ui/display/tooltip/OverflowingTextWithTooltip';
type H2TitleProps = { type H2TitleProps = {
title: string; title: string;
@ -45,6 +46,14 @@ export const H2Title = ({
<StyledTitle>{title}</StyledTitle> <StyledTitle>{title}</StyledTitle>
{adornment} {adornment}
</StyledTitleContainer> </StyledTitleContainer>
{description && <StyledDescription>{description}</StyledDescription>} {description && (
<StyledDescription>
<OverflowingTextWithTooltip
text={description}
displayedMaxRows={2}
isTooltipMultiline={true}
/>
</StyledDescription>
)}
</StyledContainer> </StyledContainer>
); );