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 { 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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@ -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>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 { 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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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`
|
||||||
|
|||||||
@ -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>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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 { 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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user