diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx
index 0e72485df..5b36573bc 100644
--- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx
+++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx
@@ -274,14 +274,6 @@ const SettingsAdmin = lazy(() =>
})),
);
-const SettingsAdminContent = lazy(() =>
- import('@/settings/admin-panel/components/SettingsAdminContent').then(
- (module) => ({
- default: module.SettingsAdminContent,
- }),
- ),
-);
-
const SettingsAdminIndicatorHealthStatus = lazy(() =>
import(
'~/pages/settings/admin-panel/SettingsAdminIndicatorHealthStatus'
@@ -290,6 +282,14 @@ const SettingsAdminIndicatorHealthStatus = lazy(() =>
})),
);
+const SettingsAdminSecondaryEnvVariables = lazy(() =>
+ import(
+ '~/pages/settings/admin-panel/SettingsAdminSecondaryEnvVariables'
+ ).then((module) => ({
+ default: module.SettingsAdminSecondaryEnvVariables,
+ })),
+);
+
const SettingsLab = lazy(() =>
import('~/pages/settings/lab/SettingsLab').then((module) => ({
default: module.SettingsLab,
@@ -482,14 +482,14 @@ export const SettingsRoutes = ({
{isAdminPageEnabled && (
<>
- } />
+ } />
}
+ path={SettingsPath.AdminPanelIndicatorHealthStatus}
+ element={}
/>
}
+ path={SettingsPath.AdminPanelOtherEnvVariables}
+ element={}
/>
>
)}
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx
index 8c7b174df..998a9d0a5 100644
--- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx
+++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminContent.tsx
@@ -1,9 +1,11 @@
+import { currentUserState } from '@/auth/states/currentUserState';
import { SettingsAdminTabContent } from '@/settings/admin-panel/components/SettingsAdminTabContent';
import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs';
import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId';
import { TabList } from '@/ui/layout/tab/components/TabList';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
+import { useRecoilValue } from 'recoil';
import { IconHeart, IconSettings2, IconVariable } from 'twenty-ui';
const StyledTabListContainer = styled.div`
@@ -15,21 +17,28 @@ const StyledTabListContainer = styled.div`
`;
export const SettingsAdminContent = () => {
+ const currentUser = useRecoilValue(currentUserState);
+
+ const canAccessFullAdminPanel = currentUser?.canAccessFullAdminPanel;
+ const canImpersonate = currentUser?.canImpersonate;
const tabs = [
{
id: SETTINGS_ADMIN_TABS.GENERAL,
title: t`General`,
Icon: IconSettings2,
+ disabled: !canAccessFullAdminPanel && !canImpersonate,
},
{
id: SETTINGS_ADMIN_TABS.ENV_VARIABLES,
title: t`Env Variables`,
Icon: IconVariable,
+ disabled: !canAccessFullAdminPanel,
},
{
id: SETTINGS_ADMIN_TABS.HEALTH_STATUS,
title: t`Health Status`,
Icon: IconHeart,
+ disabled: !canAccessFullAdminPanel,
},
];
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminEnvVariables.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminEnvVariables.tsx
index 2856ce0e0..206e33f39 100644
--- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminEnvVariables.tsx
+++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminEnvVariables.tsx
@@ -1,60 +1,35 @@
import { SettingsAdminEnvVariablesTable } from '@/settings/admin-panel/components/SettingsAdminEnvVariablesTable';
import { SettingsAdminTabSkeletonLoader } from '@/settings/admin-panel/components/SettingsAdminTabSkeletonLoader';
+import { SettingsListItemCardContent } from '@/settings/components/SettingsListItemCardContent';
+import { SettingsPath } from '@/types/SettingsPath';
+import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
-import { useState } from 'react';
-import { Button, H1Title, H1TitleFontColor, H2Title, Section } from 'twenty-ui';
+import { t } from '@lingui/core/macro';
+
+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 StyledGroupDescription = styled.div`
- margin-bottom: ${({ theme }) => theme.spacing(4)};
-`;
-
-const StyledButtonsRow = styled.div`
- display: flex;
- flex-wrap: wrap;
- gap: ${({ theme }) => theme.spacing(2)};
- margin-bottom: ${({ theme }) => theme.spacing(6)};
-`;
-
-const StyledShowMoreButton = styled(Button)<{ isSelected?: boolean }>`
- ${({ isSelected, theme }) =>
- isSelected &&
- `
- background-color: ${theme.background.transparent.light};
- `}
+const StyledInfoText = styled.div`
+ color: ${({ theme }) => theme.font.color.secondary};
`;
export const SettingsAdminEnvVariables = () => {
+ const theme = useTheme();
const { data: environmentVariables, loading: environmentVariablesLoading } =
useGetEnvironmentVariablesGroupedQuery({
fetchPolicy: 'network-only',
});
- const [selectedGroup, setSelectedGroup] = useState(null);
-
- const toggleGroupVisibility = (groupName: string) => {
- setSelectedGroup(selectedGroup === groupName ? null : groupName);
- };
-
- const hiddenGroups =
- environmentVariables?.getEnvironmentVariablesGrouped.groups.filter(
- (group) => group.isHiddenOnLoad,
- ) ?? [];
-
const visibleGroups =
environmentVariables?.getEnvironmentVariablesGrouped.groups.filter(
(group) => !group.isHiddenOnLoad,
) ?? [];
- const selectedGroupData =
- environmentVariables?.getEnvironmentVariablesGrouped.groups.find(
- (group) => group.name === selectedGroup,
- );
-
if (environmentVariablesLoading) {
return ;
}
@@ -62,9 +37,11 @@ export const SettingsAdminEnvVariables = () => {
return (
<>
- 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.
+ email sync.`}
+
{visibleGroups.map((group) => (
@@ -76,42 +53,15 @@ export const SettingsAdminEnvVariables = () => {
))}
- {hiddenGroups.length > 0 && (
- <>
-
- {hiddenGroups.map((group) => (
- toggleGroupVisibility(group.name)}
- title={group.name}
- variant="secondary"
- isSelected={selectedGroup === group.name}
- >
- {group.name} variables
-
- ))}
-
-
- {selectedGroupData && (
-
-
- {selectedGroupData.description !== '' && (
-
- {selectedGroupData.description}
-
- )}
- {selectedGroupData.variables.length > 0 && (
-
- )}
-
- )}
- >
- )}
+
+
+
>
);
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminGeneral.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminGeneral.tsx
index 6c422e1ed..f50cfaf90 100644
--- a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminGeneral.tsx
+++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminGeneral.tsx
@@ -16,7 +16,6 @@ import { useRecoilState, useRecoilValue } from 'recoil';
import { getImageAbsoluteURI, isDefined } from 'twenty-shared';
import {
Button,
- GithubVersionLink,
H1Title,
H1TitleFontColor,
H2Title,
@@ -27,7 +26,7 @@ import { REACT_APP_SERVER_BASE_URL } from '~/config';
import { useUserLookupAdminPanelMutation } from '~/generated/graphql';
import { currentUserState } from '@/auth/states/currentUserState';
-import packageJson from '../../../../../package.json';
+import { SettingsAdminVersionContainer } from '@/settings/admin-panel/components/SettingsAdminVersionContainer';
const StyledContainer = styled.div`
align-items: center;
@@ -54,11 +53,6 @@ const StyledContentContainer = styled.div`
padding: ${({ theme }) => theme.spacing(4)} 0;
`;
-const StyledErrorMessage = styled.div`
- color: ${({ theme }) => theme.color.red};
- margin-top: ${({ theme }) => theme.spacing(2)};
-`;
-
export const SettingsAdminGeneral = () => {
const [userIdentifier, setUserIdentifier] = useState('');
const { enqueueSnackBar } = useSnackBar();
@@ -75,6 +69,8 @@ export const SettingsAdminGeneral = () => {
const currentUser = useRecoilValue(currentUserState);
+ const canAccessFullAdminPanel = currentUser?.canAccessFullAdminPanel;
+
const canImpersonate = currentUser?.canImpersonate;
const canManageFeatureFlags = useRecoilValue(canManageFeatureFlagsState);
@@ -130,51 +126,51 @@ export const SettingsAdminGeneral = () => {
return (
<>
-
-
-
+ )}
+
+ {canImpersonate && (
+
+
-
- {!canImpersonate && (
-
- {t`You do not have access to impersonate users.`}
-
- )}
-
+
+
+
+
+
+
+ )}
{isDefined(userLookupResult) && (
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminVersionContainer.tsx b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminVersionContainer.tsx
new file mode 100644
index 000000000..ff8bf72a3
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminVersionContainer.tsx
@@ -0,0 +1,118 @@
+import { IconCircleDot, IconComponent, IconStatusChange } from 'twenty-ui';
+
+import { GITHUB_LINK } from '@ui/navigation/link/constants/GithubLink';
+
+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 { useEffect, useState } from 'react';
+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};
+ gap: ${({ theme }) => theme.spacing(1)};
+ padding: 0 ${({ theme }) => theme.spacing(1)};
+ text-decoration: none;
+
+ :hover {
+ color: ${({ theme }) => theme.font.color.primary};
+ cursor: pointer;
+ }
+`;
+
+const StyledSpan = styled.span`
+ color: ${({ theme }) => theme.font.color.primary};
+ font-size: ${({ theme }) => theme.font.size.sm};
+ font-weight: ${({ theme }) => theme.font.weight.medium};
+`;
+
+type VersionDetail = {
+ Icon: IconComponent;
+ text: string;
+ version: string | null;
+ link: string;
+ type: 'current' | 'latest';
+};
+
+export const SettingsAdminVersionContainer = () => {
+ const theme = useTheme();
+ const [latestVersion, setLatestVersion] = useState(null);
+ const [currentVersionExists, setCurrentVersionExists] = useState(false);
+
+ useEffect(() => {
+ fetchLatestTwentyRelease().then(setLatestVersion);
+ checkTwentyVersionExists(packageJson.version).then(setCurrentVersionExists);
+ }, []);
+
+ const VERSION_DETAILS: VersionDetail[] = [
+ {
+ Icon: IconCircleDot,
+ text: t`Current version:`,
+ version: packageJson.version,
+ link: `${GITHUB_LINK}/releases/tag/v${packageJson.version}`,
+ type: 'current',
+ },
+ {
+ Icon: IconStatusChange,
+ text: t`Latest version:`,
+ version: latestVersion,
+ link: `${GITHUB_LINK}/releases/tag/v${latestVersion}`,
+ type: 'latest',
+ },
+ ];
+
+ return (
+
+ {VERSION_DETAILS.map((versionDetail, index) => (
+
+
+ {versionDetail.text}
+ {versionDetail.version &&
+ (versionDetail.type === 'current' ? currentVersionExists : true) ? (
+
+ {versionDetail.version}
+
+ ) : (
+ {versionDetail.version}
+ )}
+
+ ))}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/ConnectedAccountHealthStatus.tsx b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/ConnectedAccountHealthStatus.tsx
index 145cb9531..ad8907a70 100644
--- a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/ConnectedAccountHealthStatus.tsx
+++ b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/ConnectedAccountHealthStatus.tsx
@@ -1,6 +1,7 @@
import { SettingsAdminHealthAccountSyncCountersTable } from '@/settings/admin-panel/health-status/components/SettingsAdminHealthAccountSyncCountersTable';
import { SettingsAdminIndicatorHealthContext } from '@/settings/admin-panel/health-status/contexts/SettingsAdminIndicatorHealthContext';
import styled from '@emotion/styled';
+import { t } from '@lingui/core/macro';
import { useContext } from 'react';
import { AdminPanelHealthServiceStatus } from '~/generated/graphql';
@@ -44,14 +45,14 @@ export const ConnectedAccountHealthStatus = () => {
{!isMessageSyncDown && serviceDetails.messageSync?.details && (
)}
{!isCalendarSyncDown && serviceDetails.calendarSync?.details && (
)}
>
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminHealthStatus.tsx b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminHealthStatus.tsx
index e7a7c6580..bbeae9ad4 100644
--- a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminHealthStatus.tsx
+++ b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminHealthStatus.tsx
@@ -1,5 +1,6 @@
import { SettingsAdminTabSkeletonLoader } from '@/settings/admin-panel/components/SettingsAdminTabSkeletonLoader';
import { SettingsHealthStatusListCard } from '@/settings/admin-panel/health-status/components/SettingsHealthStatusListCard';
+import { t } from '@lingui/core/macro';
import { H2Title, Section } from 'twenty-ui';
import { useGetSystemHealthStatusQuery } from '~/generated/graphql';
@@ -17,7 +18,10 @@ export const SettingsAdminHealthStatus = () => {
return (
<>
-
+
;
loading?: boolean;
}) => {
+ const theme = useTheme();
+
return (
HealthStatusIcons[row.id]}
+ RowIconColor={theme.font.color.tertiary}
getItemLabel={(service) => service.label}
isLoading={loading}
RowRightComponent={({ item: service }) => (
)}
to={(service) =>
- getSettingsPath(SettingsPath.ServerAdminIndicatorHealthStatus, {
+ getSettingsPath(SettingsPath.AdminPanelIndicatorHealthStatus, {
indicatorId: service.id,
})
}
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerHealthStatus.tsx b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerHealthStatus.tsx
index 7e1224af5..53e8acad0 100644
--- a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerHealthStatus.tsx
+++ b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerHealthStatus.tsx
@@ -1,5 +1,6 @@
import { WorkerQueueMetricsSection } from '@/settings/admin-panel/health-status/components/WorkerQueueMetricsSection';
import styled from '@emotion/styled';
+import { t } from '@lingui/core/macro';
import { useContext } from 'react';
import { AdminPanelHealthServiceStatus } from '~/generated/graphql';
import { SettingsAdminIndicatorHealthContext } from '../contexts/SettingsAdminIndicatorHealthContext';
@@ -20,7 +21,7 @@ export const WorkerHealthStatus = () => {
<>
{isWorkerDown ? (
- Queue information is not available because the worker is down
+ {t`Queue information is not available because the worker is down`}
) : (
(indicatorHealth.queues ?? []).map((queue) => (
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerMetricsGraph.tsx b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerMetricsGraph.tsx
index d505a39d0..3a229f7a0 100644
--- a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerMetricsGraph.tsx
+++ b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerMetricsGraph.tsx
@@ -1,13 +1,13 @@
-import { useTheme } from '@emotion/react';
-import styled from '@emotion/styled';
-import { ResponsiveLine } from '@nivo/line';
-
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 {
QueueMetricsTimeRange,
useGetQueueMetricsQuery,
@@ -142,17 +142,17 @@ export const WorkerMetricsGraph = ({
const getAxisLabel = () => {
switch (timeRange) {
case QueueMetricsTimeRange.OneHour:
- return 'Last 1 Hour (oldest → newest)';
+ return t`Last 1 Hour (oldest → newest)`;
case QueueMetricsTimeRange.FourHours:
- return 'Last 4 Hours (oldest → newest)';
+ return t`Last 4 Hours (oldest → newest)`;
case QueueMetricsTimeRange.TwelveHours:
- return 'Last 12 Hours (oldest → newest)';
+ return t`Last 12 Hours (oldest → newest)`;
case QueueMetricsTimeRange.OneDay:
- return 'Last 24 Hours (oldest → newest)';
+ return t`Last 24 Hours (oldest → newest)`;
case QueueMetricsTimeRange.SevenDays:
- return 'Last 7 Days (oldest → newest)';
+ return t`Last 7 Days (oldest → newest)`;
default:
- return 'Recent Events (oldest → newest)';
+ return t`Recent Events (oldest → newest)`;
}
};
@@ -163,14 +163,14 @@ export const WorkerMetricsGraph = ({
dropdownId={`timerange-${queueName}`}
value={timeRange}
options={[
- { value: QueueMetricsTimeRange.SevenDays, label: 'This week' },
- { value: QueueMetricsTimeRange.OneDay, label: 'Today' },
+ { value: QueueMetricsTimeRange.SevenDays, label: t`This week` },
+ { value: QueueMetricsTimeRange.OneDay, label: t`Today` },
{
value: QueueMetricsTimeRange.TwelveHours,
- label: 'Last 12 hours',
+ label: t`Last 12 hours`,
},
- { value: QueueMetricsTimeRange.FourHours, label: 'Last 4 hours' },
- { value: QueueMetricsTimeRange.OneHour, label: 'Last 1 hour' },
+ { value: QueueMetricsTimeRange.FourHours, label: t`Last 4 hours` },
+ { value: QueueMetricsTimeRange.OneHour, label: t`Last 1 hour` },
]}
onChange={onTimeRangeChange}
needIconCheck
@@ -179,7 +179,7 @@ export const WorkerMetricsGraph = ({
{loading ? (
- Loading metrics data...
+ {t`Loading metrics data...`}
) : hasData ? (
) : (
- No metrics data available
+ {t`No metrics data available`}
)}
{metricsDetails && (
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerQueueMetricsSection.tsx b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerQueueMetricsSection.tsx
index 0efe478b1..06c8fd047 100644
--- a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerQueueMetricsSection.tsx
+++ b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/WorkerQueueMetricsSection.tsx
@@ -1,3 +1,4 @@
+import { t } from '@lingui/core/macro';
import { useState } from 'react';
import { H2Title, Section } from 'twenty-ui';
import {
@@ -17,7 +18,7 @@ export const WorkerQueueMetricsSection = ({
return (
-
+
=> {
+ try {
+ const response = await fetch(
+ `https://api.github.com/repos/twentyhq/twenty/releases/tags/v${version}`,
+ );
+ return response.status === 200;
+ } catch (error) {
+ return false;
+ }
+};
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/utils/fetchLatestTwentyRelease.ts b/packages/twenty-front/src/modules/settings/admin-panel/utils/fetchLatestTwentyRelease.ts
new file mode 100644
index 000000000..ec722601f
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/admin-panel/utils/fetchLatestTwentyRelease.ts
@@ -0,0 +1,11 @@
+export const fetchLatestTwentyRelease = async (): Promise => {
+ try {
+ const response = await fetch(
+ 'https://api.github.com/repos/twentyhq/twenty/releases/latest',
+ );
+ const data = await response.json();
+ return data.tag_name.replace('v', '');
+ } catch (error) {
+ return 'Could not fetch latest release';
+ }
+};
diff --git a/packages/twenty-front/src/modules/settings/components/SettingsListCard.tsx b/packages/twenty-front/src/modules/settings/components/SettingsListCard.tsx
index 189c468d4..c40f35f39 100644
--- a/packages/twenty-front/src/modules/settings/components/SettingsListCard.tsx
+++ b/packages/twenty-front/src/modules/settings/components/SettingsListCard.tsx
@@ -42,10 +42,12 @@ type SettingsListCardProps = {
onRowClick?: (item: ListItem) => void;
RowIcon?: IconComponent;
RowIconFn?: (item: ListItem) => IconComponent;
+ RowIconColor?: string;
RowRightComponent: ComponentType<{ item: ListItem }>;
footerButtonLabel?: string;
onFooterButtonClick?: () => void;
to?: (item: ListItem) => string;
+ rounded?: boolean;
};
export const SettingsListCard = <
@@ -61,21 +63,24 @@ export const SettingsListCard = <
onRowClick,
RowIcon,
RowIconFn,
+ RowIconColor,
RowRightComponent,
onFooterButtonClick,
footerButtonLabel,
to,
+ rounded,
}: SettingsListCardProps) => {
const theme = useTheme();
if (isLoading === true) return ;
return (
-
+
{items.map((item, index) => (
}
diff --git a/packages/twenty-front/src/modules/settings/components/SettingsListItemCardContent.tsx b/packages/twenty-front/src/modules/settings/components/SettingsListItemCardContent.tsx
index 0dfb42211..2aaa8b2f4 100644
--- a/packages/twenty-front/src/modules/settings/components/SettingsListItemCardContent.tsx
+++ b/packages/twenty-front/src/modules/settings/components/SettingsListItemCardContent.tsx
@@ -3,7 +3,7 @@ import styled from '@emotion/styled';
import { ReactNode } from 'react';
import { Link } from 'react-router-dom';
import { isDefined } from 'twenty-shared';
-import { CardContent, IconComponent } from 'twenty-ui';
+import { CardContent, IconChevronRight, IconComponent } from 'twenty-ui';
const StyledRow = styled(CardContent)`
align-items: center;
@@ -17,6 +17,12 @@ const StyledRow = styled(CardContent)`
min-height: ${({ theme }) => theme.spacing(6)};
`;
+const StyledRightContainer = styled.div`
+ align-items: center;
+ display: flex;
+ gap: ${({ theme }) => theme.spacing(1)};
+`;
+
const StyledContent = styled.div`
flex: 1 0 auto;
display: flex;
@@ -43,6 +49,7 @@ type SettingsListItemCardContentProps = {
description?: string;
divider?: boolean;
LeftIcon?: IconComponent;
+ LeftIconColor?: string;
onClick?: () => void;
rightComponent: ReactNode;
to?: string;
@@ -53,6 +60,7 @@ export const SettingsListItemCardContent = ({
description,
divider,
LeftIcon,
+ LeftIconColor,
onClick,
rightComponent,
to,
@@ -61,12 +69,25 @@ export const SettingsListItemCardContent = ({
const content = (
- {!!LeftIcon && }
+ {!!LeftIcon && (
+
+ )}
{label}
{!!description && {description}}
- {rightComponent}
+
+ {rightComponent}
+ {!!to && (
+
+ )}
+
);
diff --git a/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx b/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx
index b03b16634..342f8a482 100644
--- a/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx
+++ b/packages/twenty-front/src/modules/settings/hooks/useSettingsNavigationItems.tsx
@@ -186,8 +186,8 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
label: t`Other`,
items: [
{
- label: t`Server Admin`,
- path: SettingsPath.ServerAdmin,
+ label: t`Admin Panel`,
+ path: SettingsPath.AdminPanel,
Icon: IconServer,
isHidden: !isAdminEnabled,
},
diff --git a/packages/twenty-front/src/modules/types/SettingsPath.ts b/packages/twenty-front/src/modules/types/SettingsPath.ts
index dd074d90c..88d09139d 100644
--- a/packages/twenty-front/src/modules/types/SettingsPath.ts
+++ b/packages/twenty-front/src/modules/types/SettingsPath.ts
@@ -36,10 +36,10 @@ export enum SettingsPath {
DevelopersNewWebhook = 'developers/webhooks/new',
DevelopersNewWebhookDetail = 'developers/webhooks/:webhookId',
Releases = 'releases',
- ServerAdmin = 'server-admin',
- FeatureFlags = 'server-admin/feature-flags',
- ServerAdminHealthStatus = 'server-admin#health-status',
- ServerAdminIndicatorHealthStatus = 'server-admin/health-status/:indicatorId',
+ AdminPanel = 'admin-panel',
+ AdminPanelHealthStatus = 'admin-panel#health-status',
+ AdminPanelIndicatorHealthStatus = 'admin-panel/health-status/:indicatorId',
+ AdminPanelOtherEnvVariables = 'admin-panel/other-env-variables',
Lab = 'lab',
Roles = 'roles',
RoleDetail = 'roles/:roleId',
diff --git a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdmin.tsx b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdmin.tsx
index c21926b03..43b4a92e9 100644
--- a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdmin.tsx
+++ b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdmin.tsx
@@ -10,13 +10,13 @@ export const SettingsAdmin = () => {
return (
diff --git a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminIndicatorHealthStatus.tsx b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminIndicatorHealthStatus.tsx
index b6f67de72..fd2619b10 100644
--- a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminIndicatorHealthStatus.tsx
+++ b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminIndicatorHealthStatus.tsx
@@ -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 { H2Title, Section } from 'twenty-ui';
+import { H3Title, Section } from 'twenty-ui';
import {
AdminPanelHealthServiceStatus,
HealthIndicatorId,
@@ -16,19 +16,24 @@ import {
} from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
-const StyledH2Title = styled(H2Title)`
+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`
display: flex;
- align-items: center;
- gap: ${({ theme }) => theme.spacing(2)};
+ gap: ${({ theme }) => theme.spacing(4)};
`;
const StyledHealthStatusContainer = styled.div`
- margin-bottom: ${({ theme }) => theme.spacing(4)};
- margin-top: ${({ theme }) => theme.spacing(1)};
+ margin-top: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsAdminIndicatorHealthStatus = () => {
@@ -51,15 +56,15 @@ export const SettingsAdminIndicatorHealthStatus = () => {
links={[
{
children: t`Other`,
- href: getSettingsPath(SettingsPath.ServerAdmin),
+ href: getSettingsPath(SettingsPath.AdminPanel),
},
{
- children: t`Server Admin`,
- href: getSettingsPath(SettingsPath.ServerAdmin),
+ children: t`Admin Panel`,
+ href: getSettingsPath(SettingsPath.AdminPanel),
},
{
children: t`Health Status`,
- href: getSettingsPath(SettingsPath.ServerAdminHealthStatus),
+ href: getSettingsPath(SettingsPath.AdminPanelHealthStatus),
},
{ children: `${data?.getIndicatorHealthStatus?.label}` },
]}
@@ -82,19 +87,20 @@ export const SettingsAdminIndicatorHealthStatus = () => {
>
-
- {indicatorId !== HealthIndicatorId.connectedAccount &&
- data?.getIndicatorHealthStatus?.status && (
-
-
-
- )}
+ {data?.getIndicatorHealthStatus?.status && (
+
+
+
+ )}
+
+ {data?.getIndicatorHealthStatus?.description}
+
diff --git a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminSecondaryEnvVariables.tsx b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminSecondaryEnvVariables.tsx
new file mode 100644
index 000000000..217c9ef60
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminSecondaryEnvVariables.tsx
@@ -0,0 +1,61 @@
+import { SettingsAdminEnvVariablesTable } from '@/settings/admin-panel/components/SettingsAdminEnvVariablesTable';
+import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
+import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
+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 { useGetEnvironmentVariablesGroupedQuery } from '~/generated/graphql';
+import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
+
+const StyledGroupContainer = styled.div`
+ margin-bottom: ${({ theme }) => theme.spacing(6)};
+`;
+
+export const SettingsAdminSecondaryEnvVariables = () => {
+ const { data: environmentVariables, loading: environmentVariablesLoading } =
+ useGetEnvironmentVariablesGroupedQuery({
+ fetchPolicy: 'network-only',
+ });
+
+ const hiddenGroups =
+ environmentVariables?.getEnvironmentVariablesGrouped.groups.filter(
+ (group) => group.isHiddenOnLoad,
+ ) ?? [];
+
+ if (environmentVariablesLoading) {
+ return ;
+ }
+
+ return (
+
+
+
+ {hiddenGroups.map((group) => (
+
+
+ {group.variables.length > 0 && (
+
+ )}
+
+ ))}
+
+
+
+ );
+};
diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts
index 3d628adc6..a94a0d90e 100644
--- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts
+++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.resolver.ts
@@ -15,12 +15,14 @@ import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filt
import { HealthIndicatorId } from 'src/engine/core-modules/health/enums/health-indicator-id.enum';
import { WorkerHealthIndicator } from 'src/engine/core-modules/health/indicators/worker.health';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
+import { AdminPanelGuard } from 'src/engine/guards/admin-panel-guard';
import { ImpersonateGuard } from 'src/engine/guards/impersonate-guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { AdminPanelHealthServiceData } from './dtos/admin-panel-health-service-data.dto';
import { QueueMetricsData } from './dtos/queue-metrics-data.dto';
+
@Resolver()
@UseFilters(AuthGraphqlApiExceptionFilter)
export class AdminPanelResolver {
@@ -60,19 +62,19 @@ export class AdminPanelResolver {
return true;
}
- @UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard)
+ @UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => EnvironmentVariablesOutput)
async getEnvironmentVariablesGrouped(): Promise {
return this.adminService.getEnvironmentVariablesGrouped();
}
- @UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard)
+ @UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => SystemHealth)
async getSystemHealthStatus(): Promise {
return this.adminPanelHealthService.getSystemHealthStatus();
}
- @UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard)
+ @UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => AdminPanelHealthServiceData)
async getIndicatorHealthStatus(
@Args('indicatorId', {
@@ -83,7 +85,7 @@ export class AdminPanelResolver {
return this.adminPanelHealthService.getIndicatorHealthStatus(indicatorId);
}
- @UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard)
+ @UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => QueueMetricsData)
async getQueueMetrics(
@Args('queueName', { type: () => String })
diff --git a/packages/twenty-server/src/engine/core-modules/health/health.module.ts b/packages/twenty-server/src/engine/core-modules/health/health.module.ts
index 6bd0b9228..8f3915798 100644
--- a/packages/twenty-server/src/engine/core-modules/health/health.module.ts
+++ b/packages/twenty-server/src/engine/core-modules/health/health.module.ts
@@ -7,7 +7,6 @@ import { AppHealthIndicator } from 'src/engine/core-modules/health/indicators/ap
import { RedisClientModule } from 'src/engine/core-modules/redis-client/redis-client.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
import { WorkspaceMigrationModule } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.module';
-import { WorkspaceHealthModule } from 'src/engine/workspace-manager/workspace-health/workspace-health.module';
import { HealthCacheService } from './health-cache.service';
@@ -19,7 +18,6 @@ import { WorkerHealthIndicator } from './indicators/worker.health';
imports: [
TerminusModule,
RedisClientModule,
- WorkspaceHealthModule,
ObjectMetadataModule,
WorkspaceMigrationModule,
],
diff --git a/packages/twenty-server/src/engine/core-modules/health/indicators/__tests__/app.health.spec.ts b/packages/twenty-server/src/engine/core-modules/health/indicators/__tests__/app.health.spec.ts
index e268eeb18..3b905147b 100644
--- a/packages/twenty-server/src/engine/core-modules/health/indicators/__tests__/app.health.spec.ts
+++ b/packages/twenty-server/src/engine/core-modules/health/indicators/__tests__/app.health.spec.ts
@@ -4,12 +4,10 @@ import { Test, TestingModule } from '@nestjs/testing';
import { AppHealthIndicator } from 'src/engine/core-modules/health/indicators/app.health';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
-import { WorkspaceHealthService } from 'src/engine/workspace-manager/workspace-health/workspace-health.service';
describe('AppHealthIndicator', () => {
let service: AppHealthIndicator;
let objectMetadataService: jest.Mocked;
- let workspaceHealthService: jest.Mocked;
let workspaceMigrationService: jest.Mocked;
let healthIndicatorService: jest.Mocked;
@@ -18,10 +16,6 @@ describe('AppHealthIndicator', () => {
findMany: jest.fn(),
} as any;
- workspaceHealthService = {
- healthCheck: jest.fn(),
- } as any;
-
workspaceMigrationService = {
getPendingMigrations: jest.fn(),
} as any;
@@ -44,10 +38,7 @@ describe('AppHealthIndicator', () => {
provide: ObjectMetadataService,
useValue: objectMetadataService,
},
- {
- provide: WorkspaceHealthService,
- useValue: workspaceHealthService,
- },
+
{
provide: WorkspaceMigrationService,
useValue: workspaceMigrationService,
diff --git a/packages/twenty-server/src/engine/core-modules/health/indicators/app.health.ts b/packages/twenty-server/src/engine/core-modules/health/indicators/app.health.ts
index 379e804fb..5e3f08e22 100644
--- a/packages/twenty-server/src/engine/core-modules/health/indicators/app.health.ts
+++ b/packages/twenty-server/src/engine/core-modules/health/indicators/app.health.ts
@@ -7,7 +7,6 @@ import {
import { HealthStateManager } from 'src/engine/core-modules/health/utils/health-state-manager.util';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service';
-import { WorkspaceHealthService } from 'src/engine/workspace-manager/workspace-health/workspace-health.service';
@Injectable()
export class AppHealthIndicator {
@@ -15,7 +14,6 @@ export class AppHealthIndicator {
constructor(
private readonly healthIndicatorService: HealthIndicatorService,
- private readonly workspaceHealthService: WorkspaceHealthService,
private readonly objectMetadataService: ObjectMetadataService,
private readonly workspaceMigrationService: WorkspaceMigrationService,
) {}
diff --git a/packages/twenty-server/src/engine/guards/__tests__/admin-panel-guard.spec.ts b/packages/twenty-server/src/engine/guards/__tests__/admin-panel-guard.spec.ts
new file mode 100644
index 000000000..76a63422e
--- /dev/null
+++ b/packages/twenty-server/src/engine/guards/__tests__/admin-panel-guard.spec.ts
@@ -0,0 +1,52 @@
+import { ExecutionContext } from '@nestjs/common';
+import { GqlExecutionContext } from '@nestjs/graphql';
+
+import { AdminPanelGuard } from 'src/engine/guards/admin-panel-guard';
+
+describe('AdminPanelGuard', () => {
+ const guard = new AdminPanelGuard();
+
+ it('should return true if user can access full admin panel', async () => {
+ const mockContext = {
+ getContext: jest.fn(() => ({
+ req: {
+ user: {
+ canAccessFullAdminPanel: true,
+ },
+ },
+ })),
+ };
+
+ jest
+ .spyOn(GqlExecutionContext, 'create')
+ .mockReturnValue(mockContext as any);
+
+ const mockExecutionContext = {} as ExecutionContext;
+
+ const result = await guard.canActivate(mockExecutionContext);
+
+ expect(result).toBe(true);
+ });
+
+ it('should return false if user cannot access full admin panel', async () => {
+ const mockContext = {
+ getContext: jest.fn(() => ({
+ req: {
+ user: {
+ canAccessFullAdminPanel: false,
+ },
+ },
+ })),
+ };
+
+ jest
+ .spyOn(GqlExecutionContext, 'create')
+ .mockReturnValue(mockContext as any);
+
+ const mockExecutionContext = {} as ExecutionContext;
+
+ const result = await guard.canActivate(mockExecutionContext);
+
+ expect(result).toBe(false);
+ });
+});
diff --git a/packages/twenty-server/src/engine/guards/impersonate-guard.spec.ts b/packages/twenty-server/src/engine/guards/__tests__/impersonate-guard.spec.ts
similarity index 100%
rename from packages/twenty-server/src/engine/guards/impersonate-guard.spec.ts
rename to packages/twenty-server/src/engine/guards/__tests__/impersonate-guard.spec.ts
diff --git a/packages/twenty-server/src/engine/guards/admin-panel-guard.ts b/packages/twenty-server/src/engine/guards/admin-panel-guard.ts
new file mode 100644
index 000000000..81f613d2c
--- /dev/null
+++ b/packages/twenty-server/src/engine/guards/admin-panel-guard.ts
@@ -0,0 +1,15 @@
+import { CanActivate, ExecutionContext } from '@nestjs/common';
+import { GqlExecutionContext } from '@nestjs/graphql';
+
+import { Observable } from 'rxjs';
+
+export class AdminPanelGuard implements CanActivate {
+ canActivate(
+ context: ExecutionContext,
+ ): boolean | Promise | Observable {
+ const ctx = GqlExecutionContext.create(context);
+ const request = ctx.getContext().req;
+
+ return request.user.canAccessFullAdminPanel === true;
+ }
+}
diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
index 81c263ea3..174d63db9 100644
--- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
+++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts
@@ -6,6 +6,7 @@ export {
IconAlertTriangle,
IconApi,
IconApps,
+ IconAppWindow,
IconArchive,
IconArchiveOff,
IconArrowBackUp,
@@ -158,6 +159,7 @@ export {
IconHeadphones,
IconHeart,
IconHeartOff,
+ IconHeartRateMonitor,
IconHelpCircle,
IconHierarchy,
IconHierarchy2,
@@ -241,6 +243,7 @@ export {
IconSearch,
IconSend,
IconServer,
+ IconServer2,
IconSettings,
IconSettings2,
IconSettingsAutomation,
@@ -254,6 +257,7 @@ export {
IconSquareKey,
IconSquareRoundedCheck,
IconSquareRoundedX,
+ IconStatusChange,
IconStepInto,
IconTable,
IconTag,
@@ -266,6 +270,7 @@ export {
IconTimeDuration30,
IconTimeDuration60,
IconTimelineEvent,
+ IconTool,
IconTrash,
IconTrashX,
IconTypography,
diff --git a/packages/twenty-ui/src/navigation/link/components/ActionLink.tsx b/packages/twenty-ui/src/navigation/link/components/ActionLink.tsx
index 29b6b3bd1..629b34af8 100644
--- a/packages/twenty-ui/src/navigation/link/components/ActionLink.tsx
+++ b/packages/twenty-ui/src/navigation/link/components/ActionLink.tsx
@@ -1,5 +1,5 @@
-import React from 'react';
import styled from '@emotion/styled';
+import React from 'react';
const StyledButtonLink = styled.a`
align-items: center;
@@ -17,9 +17,14 @@ const StyledButtonLink = styled.a`
}
`;
-export const ActionLink = (props: React.ComponentProps<'a'>) => {
+type ActionLinkProps = React.ComponentProps<'a'> & {
+ className?: string;
+};
+
+export const ActionLink = (props: ActionLinkProps) => {
return (