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:
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -10,6 +10,12 @@ const StyledErrorMessage = styled.div`
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(8)};
|
||||
`;
|
||||
|
||||
export const ConnectedAccountHealthStatus = () => {
|
||||
const { indicatorHealth } = useContext(SettingsAdminIndicatorHealthContext);
|
||||
const details = indicatorHealth.details;
|
||||
@ -35,7 +41,7 @@ export const ConnectedAccountHealthStatus = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContainer>
|
||||
{errorMessages.length > 0 && (
|
||||
<StyledErrorMessage>
|
||||
{`${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`}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -10,8 +10,8 @@ const StyledDetailsContainer = styled.div`
|
||||
padding: ${({ theme }) => theme.spacing(4)};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
white-space: pre-wrap;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
overflow-x: auto;
|
||||
`;
|
||||
|
||||
const StyledErrorMessage = styled.div`
|
||||
|
||||
@ -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 { 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 styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { ResponsiveLine } from '@nivo/line';
|
||||
import { isNumber } from '@tiptap/core';
|
||||
import {
|
||||
QueueMetricsTimeRange,
|
||||
useGetQueueMetricsQuery,
|
||||
} 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`
|
||||
background-color: ${({ theme }) => theme.background.tertiary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
height: 230px;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
padding-top: ${({ theme }) => theme.spacing(4)};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledQueueMetricsContainer = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
height: 240px;
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
padding-top: ${({ theme }) => theme.spacing(1)};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(3)};
|
||||
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)};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
padding-top: ${({ theme }) => theme.spacing(2.5)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledNoDataMessage = styled.div`
|
||||
@ -60,31 +30,9 @@ const StyledNoDataMessage = styled.div`
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledTooltipContainer = styled.div`
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
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};
|
||||
const StyledSettingsAdminTableCard = styled(SettingsAdminTableCard)`
|
||||
padding-left: ${({ theme }) => theme.spacing(2)};
|
||||
padding-right: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
type WorkerMetricsGraphProps = {
|
||||
@ -96,7 +44,6 @@ type WorkerMetricsGraphProps = {
|
||||
export const WorkerMetricsGraph = ({
|
||||
queueName,
|
||||
timeRange,
|
||||
onTimeRangeChange,
|
||||
}: WorkerMetricsGraphProps) => {
|
||||
const theme = useTheme();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
@ -158,25 +105,6 @@ export const WorkerMetricsGraph = ({
|
||||
|
||||
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>
|
||||
{loading ? (
|
||||
<StyledNoDataMessage>{t`Loading metrics data...`}</StyledNoDataMessage>
|
||||
@ -185,7 +113,7 @@ export const WorkerMetricsGraph = ({
|
||||
data={metricsData}
|
||||
curve="monotoneX"
|
||||
enableArea={true}
|
||||
colors={[theme.color.green, theme.color.red]}
|
||||
colors={[theme.color.blue, theme.color.red]}
|
||||
theme={{
|
||||
text: {
|
||||
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={{
|
||||
type: 'linear',
|
||||
min: 0,
|
||||
@ -230,7 +158,7 @@ export const WorkerMetricsGraph = ({
|
||||
}}
|
||||
axisBottom={{
|
||||
legend: getAxisLabel(),
|
||||
legendOffset: 30,
|
||||
legendOffset: 20,
|
||||
legendPosition: 'middle',
|
||||
tickSize: 5,
|
||||
tickPadding: 5,
|
||||
@ -241,29 +169,12 @@ export const WorkerMetricsGraph = ({
|
||||
tickSize: 6,
|
||||
tickPadding: 5,
|
||||
tickValues: 4,
|
||||
legend: 'Count',
|
||||
legendOffset: -40,
|
||||
legendPosition: 'middle',
|
||||
}}
|
||||
enableGridX={false}
|
||||
gridYValues={4}
|
||||
pointSize={0}
|
||||
enableSlices="x"
|
||||
sliceTooltip={({ 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>
|
||||
)}
|
||||
sliceTooltip={({ slice }) => <WorkerMetricsTooltip slice={slice} />}
|
||||
useMesh={true}
|
||||
legends={[
|
||||
{
|
||||
@ -276,8 +187,9 @@ export const WorkerMetricsGraph = ({
|
||||
itemDirection: 'left-to-right',
|
||||
itemWidth: 100,
|
||||
itemHeight: 20,
|
||||
symbolSize: 12,
|
||||
symbolShape: 'square',
|
||||
itemTextColor: theme.font.color.secondary,
|
||||
symbolSize: 4,
|
||||
symbolShape: 'circle',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@ -286,29 +198,22 @@ export const WorkerMetricsGraph = ({
|
||||
)}
|
||||
</StyledGraphContainer>
|
||||
{metricsDetails && (
|
||||
<>
|
||||
<StyledQueueMetricsTitle>Metrics:</StyledQueueMetricsTitle>
|
||||
<StyledQueueMetricsContainer>
|
||||
<Table>
|
||||
{Object.entries(metricsDetails)
|
||||
.filter(([key]) => key !== '__typename')
|
||||
.map(([key, value]) => (
|
||||
<StyledTableRow key={key}>
|
||||
<TableCell align="left">
|
||||
{key.charAt(0).toUpperCase() + key.slice(1)}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{typeof value === 'number'
|
||||
? value
|
||||
: Array.isArray(value)
|
||||
? value.length
|
||||
: String(value)}
|
||||
</TableCell>
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</Table>
|
||||
</StyledQueueMetricsContainer>
|
||||
</>
|
||||
<StyledSettingsAdminTableCard
|
||||
rounded
|
||||
items={Object.entries(metricsDetails)
|
||||
.filter(([key]) => key !== '__typename')
|
||||
.map(([key, value]) => ({
|
||||
label: key.charAt(0).toUpperCase() + key.slice(1),
|
||||
value: isNumber(value)
|
||||
? value
|
||||
: Array.isArray(value)
|
||||
? value.length
|
||||
: String(value),
|
||||
}))}
|
||||
gridAutoColumns="1fr 1fr"
|
||||
labelAlign="left"
|
||||
valueAlign="right"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -1,3 +1,5 @@
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
import { H2Title, Section } from 'twenty-ui';
|
||||
@ -11,19 +13,55 @@ type WorkerQueueMetricsSectionProps = {
|
||||
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 = ({
|
||||
queue,
|
||||
}: WorkerQueueMetricsSectionProps) => {
|
||||
const [timeRange, setTimeRange] = useState(QueueMetricsTimeRange.OneHour);
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title title={queue.queueName} description={t`Queue performance`} />
|
||||
<StyledContainer>
|
||||
<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
|
||||
queueName={queue.queueName}
|
||||
timeRange={timeRange}
|
||||
onTimeRangeChange={setTimeRange}
|
||||
/>
|
||||
</Section>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,9 +5,9 @@ import { Link } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared';
|
||||
import { CardContent, IconChevronRight, IconComponent } from 'twenty-ui';
|
||||
|
||||
const StyledRow = styled(CardContent)`
|
||||
const StyledRow = styled(CardContent)<{ to?: boolean }>`
|
||||
align-items: center;
|
||||
cursor: ${({ onClick }) => (onClick ? 'pointer' : 'default')};
|
||||
cursor: ${({ onClick, to }) => (onClick || to ? 'pointer' : 'default')};
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
@ -15,6 +15,11 @@ const StyledRow = styled(CardContent)`
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
padding-left: ${({ theme }) => theme.spacing(3)};
|
||||
min-height: ${({ theme }) => theme.spacing(6)};
|
||||
|
||||
&:hover {
|
||||
${({ to, theme }) =>
|
||||
to && `background: ${theme.background.transparent.light};`}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledRightContainer = styled.div`
|
||||
@ -36,12 +41,8 @@ const StyledDescription = styled.span`
|
||||
`;
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
text-decoration: none;
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
|
||||
&:hover {
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
}
|
||||
text-decoration: none;
|
||||
`;
|
||||
|
||||
type SettingsListItemCardContentProps = {
|
||||
@ -68,7 +69,7 @@ export const SettingsListItemCardContent = ({
|
||||
const theme = useTheme();
|
||||
|
||||
const content = (
|
||||
<StyledRow onClick={onClick} divider={divider}>
|
||||
<StyledRow onClick={onClick} divider={divider} to={!!to}>
|
||||
{!!LeftIcon && (
|
||||
<LeftIcon
|
||||
size={theme.icon.size.md}
|
||||
|
||||
@ -8,7 +8,7 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBa
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { H3Title, Section } from 'twenty-ui';
|
||||
import { H2Title, H3Title, Section } from 'twenty-ui';
|
||||
import {
|
||||
AdminPanelHealthServiceStatus,
|
||||
HealthIndicatorId,
|
||||
@ -16,23 +16,10 @@ import {
|
||||
} from '~/generated/graphql';
|
||||
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`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledHealthStatusContainer = styled.div`
|
||||
margin-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
@ -87,23 +74,25 @@ export const SettingsAdminIndicatorHealthStatus = () => {
|
||||
>
|
||||
<Section>
|
||||
<StyledTitleContainer>
|
||||
<StyledH3Title
|
||||
title={`${data?.getIndicatorHealthStatus?.label}`}
|
||||
/>
|
||||
<H3Title title={data?.getIndicatorHealthStatus?.label} />
|
||||
{data?.getIndicatorHealthStatus?.status && (
|
||||
<StyledHealthStatusContainer>
|
||||
<SettingsAdminHealthStatusRightContainer
|
||||
status={data?.getIndicatorHealthStatus.status}
|
||||
/>
|
||||
</StyledHealthStatusContainer>
|
||||
<SettingsAdminHealthStatusRightContainer
|
||||
status={data?.getIndicatorHealthStatus.status}
|
||||
/>
|
||||
)}
|
||||
</StyledTitleContainer>
|
||||
<StyledDescription>
|
||||
{data?.getIndicatorHealthStatus?.description}
|
||||
</StyledDescription>
|
||||
</Section>
|
||||
|
||||
<SettingsAdminIndicatorHealthStatusContent />
|
||||
<Section>
|
||||
{data?.getIndicatorHealthStatus?.id !== HealthIndicatorId.worker &&
|
||||
data?.getIndicatorHealthStatus?.id !==
|
||||
HealthIndicatorId.connectedAccount && (
|
||||
<H2Title
|
||||
title={t`Status`}
|
||||
description={data?.getIndicatorHealthStatus?.description}
|
||||
/>
|
||||
)}
|
||||
<SettingsAdminIndicatorHealthStatusContent />
|
||||
</Section>
|
||||
</SettingsAdminIndicatorHealthContext.Provider>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
|
||||
@ -5,13 +5,11 @@ import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { H2Title, Section } from 'twenty-ui';
|
||||
import { H2Title } 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``;
|
||||
|
||||
export const SettingsAdminSecondaryEnvVariables = () => {
|
||||
const { data: environmentVariables, loading: environmentVariablesLoading } =
|
||||
@ -30,6 +28,7 @@ export const SettingsAdminSecondaryEnvVariables = () => {
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={t`Other Environment Variables`}
|
||||
links={[
|
||||
{
|
||||
children: t`Other`,
|
||||
@ -45,16 +44,14 @@ export const SettingsAdminSecondaryEnvVariables = () => {
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
{hiddenGroups.map((group) => (
|
||||
<StyledGroupContainer key={group.name}>
|
||||
<H2Title title={group.name} description={group.description} />
|
||||
{group.variables.length > 0 && (
|
||||
<SettingsAdminEnvVariablesTable variables={group.variables} />
|
||||
)}
|
||||
</StyledGroupContainer>
|
||||
))}
|
||||
</Section>
|
||||
{hiddenGroups.map((group) => (
|
||||
<StyledGroupContainer key={group.name}>
|
||||
<H2Title title={group.name} description={group.description} />
|
||||
{group.variables.length > 0 && (
|
||||
<SettingsAdminEnvVariablesTable variables={group.variables} />
|
||||
)}
|
||||
</StyledGroupContainer>
|
||||
))}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
|
||||
@ -10,27 +10,27 @@ export const HEALTH_INDICATORS: Record<HealthIndicatorId, HealthIndicatorInfo> =
|
||||
{
|
||||
[HealthIndicatorId.database]: {
|
||||
id: HealthIndicatorId.database,
|
||||
label: 'Database Status',
|
||||
label: 'Database',
|
||||
description: 'PostgreSQL database connection status',
|
||||
},
|
||||
[HealthIndicatorId.redis]: {
|
||||
id: HealthIndicatorId.redis,
|
||||
label: 'Redis Status',
|
||||
label: 'Redis',
|
||||
description: 'Redis connection status',
|
||||
},
|
||||
[HealthIndicatorId.worker]: {
|
||||
id: HealthIndicatorId.worker,
|
||||
label: 'Worker Status',
|
||||
description: 'Background job worker status',
|
||||
label: 'Worker',
|
||||
description: 'Background job worker health status',
|
||||
},
|
||||
[HealthIndicatorId.connectedAccount]: {
|
||||
id: HealthIndicatorId.connectedAccount,
|
||||
label: 'Connected Account Status',
|
||||
description: 'Connected accounts status',
|
||||
label: 'Connected Accounts',
|
||||
description: 'Connected accounts health status',
|
||||
},
|
||||
[HealthIndicatorId.app]: {
|
||||
id: HealthIndicatorId.app,
|
||||
label: 'App Status',
|
||||
label: 'App',
|
||||
description: 'Workspace metadata migration status check',
|
||||
},
|
||||
};
|
||||
|
||||
@ -4,18 +4,18 @@ export {
|
||||
IconAlertCircle,
|
||||
IconAlertTriangle,
|
||||
IconApi,
|
||||
IconAppWindow,
|
||||
IconApps,
|
||||
IconAppWindow,
|
||||
IconArchive,
|
||||
IconArchiveOff,
|
||||
IconArrowBackUp,
|
||||
IconArrowDown,
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
IconArrowUp,
|
||||
IconArrowUpRight,
|
||||
IconArrowsDiagonal,
|
||||
IconArrowsVertical,
|
||||
IconArrowUp,
|
||||
IconArrowUpRight,
|
||||
IconAt,
|
||||
IconBaselineDensitySmall,
|
||||
IconBell,
|
||||
@ -47,8 +47,8 @@ export {
|
||||
IconChevronDown,
|
||||
IconChevronLeft,
|
||||
IconChevronRight,
|
||||
IconChevronUp,
|
||||
IconChevronsRight,
|
||||
IconChevronUp,
|
||||
IconCircleDot,
|
||||
IconCircleOff,
|
||||
IconCirclePlus,
|
||||
@ -128,6 +128,7 @@ export {
|
||||
IconExternalLink,
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
IconEyeShare,
|
||||
IconFile,
|
||||
IconFileCheck,
|
||||
IconFileExport,
|
||||
@ -167,6 +168,7 @@ export {
|
||||
IconHistoryToggle,
|
||||
IconHome,
|
||||
IconHours24,
|
||||
IconId,
|
||||
IconInbox,
|
||||
IconInfoCircle,
|
||||
IconJson,
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { OverflowingTextWithTooltip } from '@ui/display/tooltip/OverflowingTextWithTooltip';
|
||||
|
||||
type H2TitleProps = {
|
||||
title: string;
|
||||
@ -45,6 +46,14 @@ export const H2Title = ({
|
||||
<StyledTitle>{title}</StyledTitle>
|
||||
{adornment}
|
||||
</StyledTitleContainer>
|
||||
{description && <StyledDescription>{description}</StyledDescription>}
|
||||
{description && (
|
||||
<StyledDescription>
|
||||
<OverflowingTextWithTooltip
|
||||
text={description}
|
||||
displayedMaxRows={2}
|
||||
isTooltipMultiline={true}
|
||||
/>
|
||||
</StyledDescription>
|
||||
)}
|
||||
</StyledContainer>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user