nitin
2025-03-10 19:02:40 +05:30
committed by GitHub
parent a1e0d7b7d7
commit 77574594f2
29 changed files with 496 additions and 212 deletions

View File

@ -274,14 +274,6 @@ const SettingsAdmin = lazy(() =>
})), })),
); );
const SettingsAdminContent = lazy(() =>
import('@/settings/admin-panel/components/SettingsAdminContent').then(
(module) => ({
default: module.SettingsAdminContent,
}),
),
);
const SettingsAdminIndicatorHealthStatus = lazy(() => const SettingsAdminIndicatorHealthStatus = lazy(() =>
import( import(
'~/pages/settings/admin-panel/SettingsAdminIndicatorHealthStatus' '~/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(() => const SettingsLab = lazy(() =>
import('~/pages/settings/lab/SettingsLab').then((module) => ({ import('~/pages/settings/lab/SettingsLab').then((module) => ({
default: module.SettingsLab, default: module.SettingsLab,
@ -482,14 +482,14 @@ export const SettingsRoutes = ({
{isAdminPageEnabled && ( {isAdminPageEnabled && (
<> <>
<Route path={SettingsPath.ServerAdmin} element={<SettingsAdmin />} /> <Route path={SettingsPath.AdminPanel} element={<SettingsAdmin />} />
<Route <Route
path={SettingsPath.FeatureFlags} path={SettingsPath.AdminPanelIndicatorHealthStatus}
element={<SettingsAdminContent />} element={<SettingsAdminIndicatorHealthStatus />}
/> />
<Route <Route
path={SettingsPath.ServerAdminIndicatorHealthStatus} path={SettingsPath.AdminPanelOtherEnvVariables}
element={<SettingsAdminIndicatorHealthStatus />} element={<SettingsAdminSecondaryEnvVariables />}
/> />
</> </>
)} )}

View File

@ -1,9 +1,11 @@
import { currentUserState } from '@/auth/states/currentUserState';
import { SettingsAdminTabContent } from '@/settings/admin-panel/components/SettingsAdminTabContent'; import { SettingsAdminTabContent } from '@/settings/admin-panel/components/SettingsAdminTabContent';
import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs'; import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs';
import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId'; import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId';
import { TabList } from '@/ui/layout/tab/components/TabList'; import { TabList } from '@/ui/layout/tab/components/TabList';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { t } from '@lingui/core/macro'; import { t } from '@lingui/core/macro';
import { useRecoilValue } from 'recoil';
import { IconHeart, IconSettings2, IconVariable } from 'twenty-ui'; import { IconHeart, IconSettings2, IconVariable } from 'twenty-ui';
const StyledTabListContainer = styled.div` const StyledTabListContainer = styled.div`
@ -15,21 +17,28 @@ const StyledTabListContainer = styled.div`
`; `;
export const SettingsAdminContent = () => { export const SettingsAdminContent = () => {
const currentUser = useRecoilValue(currentUserState);
const canAccessFullAdminPanel = currentUser?.canAccessFullAdminPanel;
const canImpersonate = currentUser?.canImpersonate;
const tabs = [ const tabs = [
{ {
id: SETTINGS_ADMIN_TABS.GENERAL, id: SETTINGS_ADMIN_TABS.GENERAL,
title: t`General`, title: t`General`,
Icon: IconSettings2, Icon: IconSettings2,
disabled: !canAccessFullAdminPanel && !canImpersonate,
}, },
{ {
id: SETTINGS_ADMIN_TABS.ENV_VARIABLES, id: SETTINGS_ADMIN_TABS.ENV_VARIABLES,
title: t`Env Variables`, title: t`Env Variables`,
Icon: IconVariable, Icon: IconVariable,
disabled: !canAccessFullAdminPanel,
}, },
{ {
id: SETTINGS_ADMIN_TABS.HEALTH_STATUS, id: SETTINGS_ADMIN_TABS.HEALTH_STATUS,
title: t`Health Status`, title: t`Health Status`,
Icon: IconHeart, Icon: IconHeart,
disabled: !canAccessFullAdminPanel,
}, },
]; ];

View File

@ -1,60 +1,35 @@
import { SettingsAdminEnvVariablesTable } from '@/settings/admin-panel/components/SettingsAdminEnvVariablesTable'; import { SettingsAdminEnvVariablesTable } from '@/settings/admin-panel/components/SettingsAdminEnvVariablesTable';
import { SettingsAdminTabSkeletonLoader } from '@/settings/admin-panel/components/SettingsAdminTabSkeletonLoader'; 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 styled from '@emotion/styled';
import { useState } from 'react'; import { t } from '@lingui/core/macro';
import { Button, H1Title, H1TitleFontColor, H2Title, Section } from 'twenty-ui';
import { Card, H2Title, IconHeartRateMonitor, Section } from 'twenty-ui';
import { useGetEnvironmentVariablesGroupedQuery } from '~/generated/graphql'; import { useGetEnvironmentVariablesGroupedQuery } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
const StyledGroupContainer = styled.div` const StyledGroupContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(6)}; margin-bottom: ${({ theme }) => theme.spacing(6)};
`; `;
const StyledGroupDescription = styled.div` const StyledInfoText = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(4)}; color: ${({ theme }) => theme.font.color.secondary};
`;
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};
`}
`; `;
export const SettingsAdminEnvVariables = () => { export const SettingsAdminEnvVariables = () => {
const theme = useTheme();
const { data: environmentVariables, loading: environmentVariablesLoading } = const { data: environmentVariables, loading: environmentVariablesLoading } =
useGetEnvironmentVariablesGroupedQuery({ useGetEnvironmentVariablesGroupedQuery({
fetchPolicy: 'network-only', fetchPolicy: 'network-only',
}); });
const [selectedGroup, setSelectedGroup] = useState<string | null>(null);
const toggleGroupVisibility = (groupName: string) => {
setSelectedGroup(selectedGroup === groupName ? null : groupName);
};
const hiddenGroups =
environmentVariables?.getEnvironmentVariablesGrouped.groups.filter(
(group) => group.isHiddenOnLoad,
) ?? [];
const visibleGroups = const visibleGroups =
environmentVariables?.getEnvironmentVariablesGrouped.groups.filter( environmentVariables?.getEnvironmentVariablesGrouped.groups.filter(
(group) => !group.isHiddenOnLoad, (group) => !group.isHiddenOnLoad,
) ?? []; ) ?? [];
const selectedGroupData =
environmentVariables?.getEnvironmentVariablesGrouped.groups.find(
(group) => group.name === selectedGroup,
);
if (environmentVariablesLoading) { if (environmentVariablesLoading) {
return <SettingsAdminTabSkeletonLoader />; return <SettingsAdminTabSkeletonLoader />;
} }
@ -62,9 +37,11 @@ export const SettingsAdminEnvVariables = () => {
return ( return (
<> <>
<Section> <Section>
These are only the server values. Ensure your worker environment has the <StyledInfoText>
{t` These are only the server values. Ensure your worker environment has the
same variables and values, this is required for asynchronous tasks like same variables and values, this is required for asynchronous tasks like
email sync. email sync.`}
</StyledInfoText>
</Section> </Section>
<Section> <Section>
{visibleGroups.map((group) => ( {visibleGroups.map((group) => (
@ -76,42 +53,15 @@ export const SettingsAdminEnvVariables = () => {
</StyledGroupContainer> </StyledGroupContainer>
))} ))}
{hiddenGroups.length > 0 && ( <Card rounded>
<> <SettingsListItemCardContent
<StyledButtonsRow> label={t`Other Variables`}
{hiddenGroups.map((group) => ( to={getSettingsPath(SettingsPath.AdminPanelOtherEnvVariables)}
<StyledShowMoreButton rightComponent={null}
key={group.name} LeftIcon={IconHeartRateMonitor}
onClick={() => toggleGroupVisibility(group.name)} LeftIconColor={theme.font.color.tertiary}
title={group.name} />
variant="secondary" </Card>
isSelected={selectedGroup === group.name}
>
{group.name} variables
</StyledShowMoreButton>
))}
</StyledButtonsRow>
{selectedGroupData && (
<StyledGroupContainer>
<H1Title
title={selectedGroupData.name}
fontColor={H1TitleFontColor.Primary}
/>
{selectedGroupData.description !== '' && (
<StyledGroupDescription>
{selectedGroupData.description}
</StyledGroupDescription>
)}
{selectedGroupData.variables.length > 0 && (
<SettingsAdminEnvVariablesTable
variables={selectedGroupData.variables}
/>
)}
</StyledGroupContainer>
)}
</>
)}
</Section> </Section>
</> </>
); );

View File

@ -16,7 +16,6 @@ import { useRecoilState, useRecoilValue } from 'recoil';
import { getImageAbsoluteURI, isDefined } from 'twenty-shared'; import { getImageAbsoluteURI, isDefined } from 'twenty-shared';
import { import {
Button, Button,
GithubVersionLink,
H1Title, H1Title,
H1TitleFontColor, H1TitleFontColor,
H2Title, H2Title,
@ -27,7 +26,7 @@ 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 packageJson from '../../../../../package.json'; import { SettingsAdminVersionContainer } from '@/settings/admin-panel/components/SettingsAdminVersionContainer';
const StyledContainer = styled.div` const StyledContainer = styled.div`
align-items: center; align-items: center;
@ -54,11 +53,6 @@ const StyledContentContainer = styled.div`
padding: ${({ theme }) => theme.spacing(4)} 0; padding: ${({ theme }) => theme.spacing(4)} 0;
`; `;
const StyledErrorMessage = styled.div`
color: ${({ theme }) => theme.color.red};
margin-top: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsAdminGeneral = () => { export const SettingsAdminGeneral = () => {
const [userIdentifier, setUserIdentifier] = useState(''); const [userIdentifier, setUserIdentifier] = useState('');
const { enqueueSnackBar } = useSnackBar(); const { enqueueSnackBar } = useSnackBar();
@ -75,6 +69,8 @@ export const SettingsAdminGeneral = () => {
const currentUser = useRecoilValue(currentUserState); const currentUser = useRecoilValue(currentUserState);
const canAccessFullAdminPanel = currentUser?.canAccessFullAdminPanel;
const canImpersonate = currentUser?.canImpersonate; const canImpersonate = currentUser?.canImpersonate;
const canManageFeatureFlags = useRecoilValue(canManageFeatureFlagsState); const canManageFeatureFlags = useRecoilValue(canManageFeatureFlagsState);
@ -130,51 +126,51 @@ export const SettingsAdminGeneral = () => {
return ( return (
<> <>
<Section> {canAccessFullAdminPanel && (
<H2Title title={t`About`} description={t`Version of the application`} /> <Section>
<GithubVersionLink version={packageJson.version} /> <H2Title
</Section> title={t`About`}
description={t`Version of the application`}
<Section>
<H2Title
title={
canManageFeatureFlags
? t`Feature Flags & Impersonation`
: t`User Impersonation`
}
description={
canManageFeatureFlags
? t`Look up users and manage their workspace feature flags or impersonate them.`
: t`Look up users to impersonate them.`
}
/>
<StyledContainer>
<TextInput
value={userIdentifier}
onChange={setUserIdentifier}
onInputEnter={handleSearch}
placeholder={t`Enter user ID or email address`}
fullWidth
disabled={isUserLookupLoading}
/> />
<Button <SettingsAdminVersionContainer />
Icon={IconSearch} </Section>
variant="primary" )}
accent="blue"
title={t`Search`} {canImpersonate && (
onClick={handleSearch} <Section>
disabled={ <H2Title
!userIdentifier.trim() || isUserLookupLoading || !canImpersonate title={
canManageFeatureFlags
? t`Feature Flags & Impersonation`
: t`User Impersonation`
}
description={
canManageFeatureFlags
? t`Look up users and manage their workspace feature flags or impersonate them.`
: t`Look up users to impersonate them.`
} }
/> />
</StyledContainer>
{!canImpersonate && ( <StyledContainer>
<StyledErrorMessage> <TextInput
{t`You do not have access to impersonate users.`} value={userIdentifier}
</StyledErrorMessage> onChange={setUserIdentifier}
)} onInputEnter={handleSearch}
</Section> placeholder={t`Enter user ID or email address`}
fullWidth
disabled={isUserLookupLoading}
/>
<Button
Icon={IconSearch}
variant="primary"
accent="blue"
title={t`Search`}
onClick={handleSearch}
disabled={!userIdentifier.trim() || isUserLookupLoading}
/>
</StyledContainer>
</Section>
)}
{isDefined(userLookupResult) && ( {isDefined(userLookupResult) && (
<Section> <Section>

View File

@ -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<string | null>(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 (
<StyledVersionContainer>
{VERSION_DETAILS.map((versionDetail, index) => (
<StyledVersionDetails key={index}>
<versionDetail.Icon
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
<StyledVersionText>{versionDetail.text}</StyledVersionText>
{versionDetail.version &&
(versionDetail.type === 'current' ? currentVersionExists : true) ? (
<StyledActionLink
href={versionDetail.link}
target="_blank"
rel="noreferrer"
>
{versionDetail.version}
</StyledActionLink>
) : (
<StyledSpan>{versionDetail.version}</StyledSpan>
)}
</StyledVersionDetails>
))}
</StyledVersionContainer>
);
};

View File

@ -1,6 +1,7 @@
import { SettingsAdminHealthAccountSyncCountersTable } from '@/settings/admin-panel/health-status/components/SettingsAdminHealthAccountSyncCountersTable'; import { SettingsAdminHealthAccountSyncCountersTable } from '@/settings/admin-panel/health-status/components/SettingsAdminHealthAccountSyncCountersTable';
import { SettingsAdminIndicatorHealthContext } from '@/settings/admin-panel/health-status/contexts/SettingsAdminIndicatorHealthContext'; import { SettingsAdminIndicatorHealthContext } from '@/settings/admin-panel/health-status/contexts/SettingsAdminIndicatorHealthContext';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useContext } from 'react'; import { useContext } from 'react';
import { AdminPanelHealthServiceStatus } from '~/generated/graphql'; import { AdminPanelHealthServiceStatus } from '~/generated/graphql';
@ -44,14 +45,14 @@ export const ConnectedAccountHealthStatus = () => {
{!isMessageSyncDown && serviceDetails.messageSync?.details && ( {!isMessageSyncDown && serviceDetails.messageSync?.details && (
<SettingsAdminHealthAccountSyncCountersTable <SettingsAdminHealthAccountSyncCountersTable
details={serviceDetails.messageSync.details} details={serviceDetails.messageSync.details}
title="Message Sync Status" title={t`Message Sync Status`}
/> />
)} )}
{!isCalendarSyncDown && serviceDetails.calendarSync?.details && ( {!isCalendarSyncDown && serviceDetails.calendarSync?.details && (
<SettingsAdminHealthAccountSyncCountersTable <SettingsAdminHealthAccountSyncCountersTable
details={serviceDetails.calendarSync.details} details={serviceDetails.calendarSync.details}
title="Calendar Sync Status" title={t`Calendar Sync Status`}
/> />
)} )}
</> </>

View File

@ -1,5 +1,6 @@
import { SettingsAdminTabSkeletonLoader } from '@/settings/admin-panel/components/SettingsAdminTabSkeletonLoader'; import { SettingsAdminTabSkeletonLoader } from '@/settings/admin-panel/components/SettingsAdminTabSkeletonLoader';
import { SettingsHealthStatusListCard } from '@/settings/admin-panel/health-status/components/SettingsHealthStatusListCard'; import { SettingsHealthStatusListCard } from '@/settings/admin-panel/health-status/components/SettingsHealthStatusListCard';
import { t } from '@lingui/core/macro';
import { H2Title, Section } from 'twenty-ui'; import { H2Title, Section } from 'twenty-ui';
import { useGetSystemHealthStatusQuery } from '~/generated/graphql'; import { useGetSystemHealthStatusQuery } from '~/generated/graphql';
@ -17,7 +18,10 @@ export const SettingsAdminHealthStatus = () => {
return ( return (
<> <>
<Section> <Section>
<H2Title title="Health Status" description="How your system is doing" /> <H2Title
title={t`Health Status`}
description={t`How your system is doing`}
/>
<SettingsHealthStatusListCard <SettingsHealthStatusListCard
services={services} services={services}
loading={loadingHealthStatus} loading={loadingHealthStatus}

View File

@ -1,9 +1,26 @@
import { SettingsListCard } from '@/settings/components/SettingsListCard'; import { SettingsListCard } from '@/settings/components/SettingsListCard';
import { SettingsPath } from '@/types/SettingsPath'; import { SettingsPath } from '@/types/SettingsPath';
import { SystemHealthService } from '~/generated/graphql'; import { useTheme } from '@emotion/react';
import {
IconAppWindow,
IconComponent,
IconDatabase,
IconServer2,
IconTool,
IconUserCircle,
} from 'twenty-ui';
import { HealthIndicatorId, SystemHealthService } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { SettingsAdminHealthStatusRightContainer } from './SettingsAdminHealthStatusRightContainer'; import { SettingsAdminHealthStatusRightContainer } from './SettingsAdminHealthStatusRightContainer';
const HealthStatusIcons: { [k in HealthIndicatorId]: IconComponent } = {
[HealthIndicatorId.database]: IconDatabase,
[HealthIndicatorId.redis]: IconServer2,
[HealthIndicatorId.worker]: IconTool,
[HealthIndicatorId.connectedAccount]: IconUserCircle,
[HealthIndicatorId.app]: IconAppWindow,
};
export const SettingsHealthStatusListCard = ({ export const SettingsHealthStatusListCard = ({
services, services,
loading, loading,
@ -11,16 +28,21 @@ export const SettingsHealthStatusListCard = ({
services: Array<SystemHealthService>; services: Array<SystemHealthService>;
loading?: boolean; loading?: boolean;
}) => { }) => {
const theme = useTheme();
return ( return (
<SettingsListCard <SettingsListCard
items={services} items={services}
rounded={true}
RowIconFn={(row) => HealthStatusIcons[row.id]}
RowIconColor={theme.font.color.tertiary}
getItemLabel={(service) => service.label} getItemLabel={(service) => service.label}
isLoading={loading} isLoading={loading}
RowRightComponent={({ item: service }) => ( RowRightComponent={({ item: service }) => (
<SettingsAdminHealthStatusRightContainer status={service.status} /> <SettingsAdminHealthStatusRightContainer status={service.status} />
)} )}
to={(service) => to={(service) =>
getSettingsPath(SettingsPath.ServerAdminIndicatorHealthStatus, { getSettingsPath(SettingsPath.AdminPanelIndicatorHealthStatus, {
indicatorId: service.id, indicatorId: service.id,
}) })
} }

View File

@ -1,5 +1,6 @@
import { WorkerQueueMetricsSection } from '@/settings/admin-panel/health-status/components/WorkerQueueMetricsSection'; import { WorkerQueueMetricsSection } from '@/settings/admin-panel/health-status/components/WorkerQueueMetricsSection';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { useContext } from 'react'; import { useContext } from 'react';
import { AdminPanelHealthServiceStatus } from '~/generated/graphql'; import { AdminPanelHealthServiceStatus } from '~/generated/graphql';
import { SettingsAdminIndicatorHealthContext } from '../contexts/SettingsAdminIndicatorHealthContext'; import { SettingsAdminIndicatorHealthContext } from '../contexts/SettingsAdminIndicatorHealthContext';
@ -20,7 +21,7 @@ export const WorkerHealthStatus = () => {
<> <>
{isWorkerDown ? ( {isWorkerDown ? (
<StyledErrorMessage> <StyledErrorMessage>
Queue information is not available because the worker is down {t`Queue information is not available because the worker is down`}
</StyledErrorMessage> </StyledErrorMessage>
) : ( ) : (
(indicatorHealth.queues ?? []).map((queue) => ( (indicatorHealth.queues ?? []).map((queue) => (

View File

@ -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 { 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 { Select } from '@/ui/input/components/Select';
import { Table } from '@/ui/layout/table/components/Table'; import { Table } from '@/ui/layout/table/components/Table';
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 styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { ResponsiveLine } from '@nivo/line';
import { import {
QueueMetricsTimeRange, QueueMetricsTimeRange,
useGetQueueMetricsQuery, useGetQueueMetricsQuery,
@ -142,17 +142,17 @@ export const WorkerMetricsGraph = ({
const getAxisLabel = () => { const getAxisLabel = () => {
switch (timeRange) { switch (timeRange) {
case QueueMetricsTimeRange.OneHour: case QueueMetricsTimeRange.OneHour:
return 'Last 1 Hour (oldest → newest)'; return t`Last 1 Hour (oldest → newest)`;
case QueueMetricsTimeRange.FourHours: case QueueMetricsTimeRange.FourHours:
return 'Last 4 Hours (oldest → newest)'; return t`Last 4 Hours (oldest → newest)`;
case QueueMetricsTimeRange.TwelveHours: case QueueMetricsTimeRange.TwelveHours:
return 'Last 12 Hours (oldest → newest)'; return t`Last 12 Hours (oldest → newest)`;
case QueueMetricsTimeRange.OneDay: case QueueMetricsTimeRange.OneDay:
return 'Last 24 Hours (oldest → newest)'; return t`Last 24 Hours (oldest → newest)`;
case QueueMetricsTimeRange.SevenDays: case QueueMetricsTimeRange.SevenDays:
return 'Last 7 Days (oldest → newest)'; return t`Last 7 Days (oldest → newest)`;
default: default:
return 'Recent Events (oldest → newest)'; return t`Recent Events (oldest → newest)`;
} }
}; };
@ -163,14 +163,14 @@ export const WorkerMetricsGraph = ({
dropdownId={`timerange-${queueName}`} dropdownId={`timerange-${queueName}`}
value={timeRange} value={timeRange}
options={[ options={[
{ value: QueueMetricsTimeRange.SevenDays, label: 'This week' }, { value: QueueMetricsTimeRange.SevenDays, label: t`This week` },
{ value: QueueMetricsTimeRange.OneDay, label: 'Today' }, { value: QueueMetricsTimeRange.OneDay, label: t`Today` },
{ {
value: QueueMetricsTimeRange.TwelveHours, value: QueueMetricsTimeRange.TwelveHours,
label: 'Last 12 hours', label: t`Last 12 hours`,
}, },
{ value: QueueMetricsTimeRange.FourHours, label: 'Last 4 hours' }, { value: QueueMetricsTimeRange.FourHours, label: t`Last 4 hours` },
{ value: QueueMetricsTimeRange.OneHour, label: 'Last 1 hour' }, { value: QueueMetricsTimeRange.OneHour, label: t`Last 1 hour` },
]} ]}
onChange={onTimeRangeChange} onChange={onTimeRangeChange}
needIconCheck needIconCheck
@ -179,7 +179,7 @@ export const WorkerMetricsGraph = ({
<StyledGraphContainer> <StyledGraphContainer>
{loading ? ( {loading ? (
<StyledNoDataMessage>Loading metrics data...</StyledNoDataMessage> <StyledNoDataMessage>{t`Loading metrics data...`}</StyledNoDataMessage>
) : hasData ? ( ) : hasData ? (
<ResponsiveLine <ResponsiveLine
data={metricsData} data={metricsData}
@ -282,7 +282,7 @@ export const WorkerMetricsGraph = ({
]} ]}
/> />
) : ( ) : (
<StyledNoDataMessage>No metrics data available</StyledNoDataMessage> <StyledNoDataMessage>{t`No metrics data available`}</StyledNoDataMessage>
)} )}
</StyledGraphContainer> </StyledGraphContainer>
{metricsDetails && ( {metricsDetails && (

View File

@ -1,3 +1,4 @@
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';
import { import {
@ -17,7 +18,7 @@ export const WorkerQueueMetricsSection = ({
return ( return (
<Section> <Section>
<H2Title title={queue.queueName} description="Queue performance" /> <H2Title title={queue.queueName} description={t`Queue performance`} />
<WorkerMetricsGraph <WorkerMetricsGraph
queueName={queue.queueName} queueName={queue.queueName}
timeRange={timeRange} timeRange={timeRange}

View File

@ -0,0 +1,12 @@
export const checkTwentyVersionExists = async (
version: string,
): Promise<boolean> => {
try {
const response = await fetch(
`https://api.github.com/repos/twentyhq/twenty/releases/tags/v${version}`,
);
return response.status === 200;
} catch (error) {
return false;
}
};

View File

@ -0,0 +1,11 @@
export const fetchLatestTwentyRelease = async (): Promise<string> => {
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';
}
};

View File

@ -42,10 +42,12 @@ type SettingsListCardProps<ListItem extends { id: string }> = {
onRowClick?: (item: ListItem) => void; onRowClick?: (item: ListItem) => void;
RowIcon?: IconComponent; RowIcon?: IconComponent;
RowIconFn?: (item: ListItem) => IconComponent; RowIconFn?: (item: ListItem) => IconComponent;
RowIconColor?: string;
RowRightComponent: ComponentType<{ item: ListItem }>; RowRightComponent: ComponentType<{ item: ListItem }>;
footerButtonLabel?: string; footerButtonLabel?: string;
onFooterButtonClick?: () => void; onFooterButtonClick?: () => void;
to?: (item: ListItem) => string; to?: (item: ListItem) => string;
rounded?: boolean;
}; };
export const SettingsListCard = < export const SettingsListCard = <
@ -61,21 +63,24 @@ export const SettingsListCard = <
onRowClick, onRowClick,
RowIcon, RowIcon,
RowIconFn, RowIconFn,
RowIconColor,
RowRightComponent, RowRightComponent,
onFooterButtonClick, onFooterButtonClick,
footerButtonLabel, footerButtonLabel,
to, to,
rounded,
}: SettingsListCardProps<ListItem>) => { }: SettingsListCardProps<ListItem>) => {
const theme = useTheme(); const theme = useTheme();
if (isLoading === true) return <SettingsListSkeletonCard />; if (isLoading === true) return <SettingsListSkeletonCard />;
return ( return (
<Card> <Card rounded={rounded}>
{items.map((item, index) => ( {items.map((item, index) => (
<SettingsListItemCardContent <SettingsListItemCardContent
key={item.id} key={item.id}
LeftIcon={RowIconFn ? RowIconFn(item) : RowIcon} LeftIcon={RowIconFn ? RowIconFn(item) : RowIcon}
LeftIconColor={RowIconColor}
label={getItemLabel(item)} label={getItemLabel(item)}
description={getItemDescription?.(item)} description={getItemDescription?.(item)}
rightComponent={<RowRightComponent item={item} />} rightComponent={<RowRightComponent item={item} />}

View File

@ -3,7 +3,7 @@ import styled from '@emotion/styled';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { isDefined } from 'twenty-shared'; import { isDefined } from 'twenty-shared';
import { CardContent, IconComponent } from 'twenty-ui'; import { CardContent, IconChevronRight, IconComponent } from 'twenty-ui';
const StyledRow = styled(CardContent)` const StyledRow = styled(CardContent)`
align-items: center; align-items: center;
@ -17,6 +17,12 @@ const StyledRow = styled(CardContent)`
min-height: ${({ theme }) => theme.spacing(6)}; min-height: ${({ theme }) => theme.spacing(6)};
`; `;
const StyledRightContainer = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledContent = styled.div` const StyledContent = styled.div`
flex: 1 0 auto; flex: 1 0 auto;
display: flex; display: flex;
@ -43,6 +49,7 @@ type SettingsListItemCardContentProps = {
description?: string; description?: string;
divider?: boolean; divider?: boolean;
LeftIcon?: IconComponent; LeftIcon?: IconComponent;
LeftIconColor?: string;
onClick?: () => void; onClick?: () => void;
rightComponent: ReactNode; rightComponent: ReactNode;
to?: string; to?: string;
@ -53,6 +60,7 @@ export const SettingsListItemCardContent = ({
description, description,
divider, divider,
LeftIcon, LeftIcon,
LeftIconColor,
onClick, onClick,
rightComponent, rightComponent,
to, to,
@ -61,12 +69,25 @@ export const SettingsListItemCardContent = ({
const content = ( const content = (
<StyledRow onClick={onClick} divider={divider}> <StyledRow onClick={onClick} divider={divider}>
{!!LeftIcon && <LeftIcon size={theme.icon.size.md} />} {!!LeftIcon && (
<LeftIcon
size={theme.icon.size.md}
color={LeftIconColor ?? 'currentColor'}
/>
)}
<StyledContent> <StyledContent>
{label} {label}
{!!description && <StyledDescription>{description}</StyledDescription>} {!!description && <StyledDescription>{description}</StyledDescription>}
</StyledContent> </StyledContent>
{rightComponent} <StyledRightContainer>
{rightComponent}
{!!to && (
<IconChevronRight
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
)}
</StyledRightContainer>
</StyledRow> </StyledRow>
); );

View File

@ -186,8 +186,8 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
label: t`Other`, label: t`Other`,
items: [ items: [
{ {
label: t`Server Admin`, label: t`Admin Panel`,
path: SettingsPath.ServerAdmin, path: SettingsPath.AdminPanel,
Icon: IconServer, Icon: IconServer,
isHidden: !isAdminEnabled, isHidden: !isAdminEnabled,
}, },

View File

@ -36,10 +36,10 @@ export enum SettingsPath {
DevelopersNewWebhook = 'developers/webhooks/new', DevelopersNewWebhook = 'developers/webhooks/new',
DevelopersNewWebhookDetail = 'developers/webhooks/:webhookId', DevelopersNewWebhookDetail = 'developers/webhooks/:webhookId',
Releases = 'releases', Releases = 'releases',
ServerAdmin = 'server-admin', AdminPanel = 'admin-panel',
FeatureFlags = 'server-admin/feature-flags', AdminPanelHealthStatus = 'admin-panel#health-status',
ServerAdminHealthStatus = 'server-admin#health-status', AdminPanelIndicatorHealthStatus = 'admin-panel/health-status/:indicatorId',
ServerAdminIndicatorHealthStatus = 'server-admin/health-status/:indicatorId', AdminPanelOtherEnvVariables = 'admin-panel/other-env-variables',
Lab = 'lab', Lab = 'lab',
Roles = 'roles', Roles = 'roles',
RoleDetail = 'roles/:roleId', RoleDetail = 'roles/:roleId',

View File

@ -10,13 +10,13 @@ export const SettingsAdmin = () => {
return ( return (
<SubMenuTopBarContainer <SubMenuTopBarContainer
title={t`Server Admin`} title={t`Admin Panel`}
links={[ links={[
{ {
children: t`Other`, children: t`Other`,
href: getSettingsPath(SettingsPath.ServerAdmin), href: getSettingsPath(SettingsPath.AdminPanel),
}, },
{ children: t`Server Admin` }, { children: t`Admin Panel` },
]} ]}
> >
<SettingsPageContainer> <SettingsPageContainer>

View File

@ -8,7 +8,7 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBa
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro'; import { useLingui } from '@lingui/react/macro';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { H2Title, Section } from 'twenty-ui'; import { H3Title, Section } from 'twenty-ui';
import { import {
AdminPanelHealthServiceStatus, AdminPanelHealthServiceStatus,
HealthIndicatorId, HealthIndicatorId,
@ -16,19 +16,24 @@ import {
} from '~/generated/graphql'; } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath'; 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)}; margin-top: ${({ theme }) => theme.spacing(2)};
`; `;
const StyledTitleContainer = styled.div` const StyledTitleContainer = styled.div`
display: flex; display: flex;
align-items: center; gap: ${({ theme }) => theme.spacing(4)};
gap: ${({ theme }) => theme.spacing(2)};
`; `;
const StyledHealthStatusContainer = styled.div` const StyledHealthStatusContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(4)}; margin-top: ${({ theme }) => theme.spacing(2)};
margin-top: ${({ theme }) => theme.spacing(1)};
`; `;
export const SettingsAdminIndicatorHealthStatus = () => { export const SettingsAdminIndicatorHealthStatus = () => {
@ -51,15 +56,15 @@ export const SettingsAdminIndicatorHealthStatus = () => {
links={[ links={[
{ {
children: t`Other`, children: t`Other`,
href: getSettingsPath(SettingsPath.ServerAdmin), href: getSettingsPath(SettingsPath.AdminPanel),
}, },
{ {
children: t`Server Admin`, children: t`Admin Panel`,
href: getSettingsPath(SettingsPath.ServerAdmin), href: getSettingsPath(SettingsPath.AdminPanel),
}, },
{ {
children: t`Health Status`, children: t`Health Status`,
href: getSettingsPath(SettingsPath.ServerAdminHealthStatus), href: getSettingsPath(SettingsPath.AdminPanelHealthStatus),
}, },
{ children: `${data?.getIndicatorHealthStatus?.label}` }, { children: `${data?.getIndicatorHealthStatus?.label}` },
]} ]}
@ -82,19 +87,20 @@ export const SettingsAdminIndicatorHealthStatus = () => {
> >
<Section> <Section>
<StyledTitleContainer> <StyledTitleContainer>
<StyledH2Title <StyledH3Title
title={`${data?.getIndicatorHealthStatus?.label}`} title={`${data?.getIndicatorHealthStatus?.label}`}
description={data?.getIndicatorHealthStatus?.description}
/> />
{indicatorId !== HealthIndicatorId.connectedAccount && {data?.getIndicatorHealthStatus?.status && (
data?.getIndicatorHealthStatus?.status && ( <StyledHealthStatusContainer>
<StyledHealthStatusContainer> <SettingsAdminHealthStatusRightContainer
<SettingsAdminHealthStatusRightContainer status={data?.getIndicatorHealthStatus.status}
status={data?.getIndicatorHealthStatus.status} />
/> </StyledHealthStatusContainer>
</StyledHealthStatusContainer> )}
)}
</StyledTitleContainer> </StyledTitleContainer>
<StyledDescription>
{data?.getIndicatorHealthStatus?.description}
</StyledDescription>
</Section> </Section>
<SettingsAdminIndicatorHealthStatusContent /> <SettingsAdminIndicatorHealthStatusContent />

View File

@ -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 <SettingsSkeletonLoader />;
}
return (
<SubMenuTopBarContainer
links={[
{
children: t`Other`,
href: getSettingsPath(SettingsPath.AdminPanel),
},
{
children: t`Admin Panel`,
href: getSettingsPath(SettingsPath.AdminPanel),
},
{
children: t`Other Environment Variables`,
},
]}
>
<SettingsPageContainer>
<Section>
{hiddenGroups.map((group) => (
<StyledGroupContainer key={group.name}>
<H2Title title={group.name} description={group.description} />
{group.variables.length > 0 && (
<SettingsAdminEnvVariablesTable variables={group.variables} />
)}
</StyledGroupContainer>
))}
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};

View File

@ -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 { HealthIndicatorId } from 'src/engine/core-modules/health/enums/health-indicator-id.enum';
import { WorkerHealthIndicator } from 'src/engine/core-modules/health/indicators/worker.health'; import { WorkerHealthIndicator } from 'src/engine/core-modules/health/indicators/worker.health';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; 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 { ImpersonateGuard } from 'src/engine/guards/impersonate-guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { AdminPanelHealthServiceData } from './dtos/admin-panel-health-service-data.dto'; import { AdminPanelHealthServiceData } from './dtos/admin-panel-health-service-data.dto';
import { QueueMetricsData } from './dtos/queue-metrics-data.dto'; import { QueueMetricsData } from './dtos/queue-metrics-data.dto';
@Resolver() @Resolver()
@UseFilters(AuthGraphqlApiExceptionFilter) @UseFilters(AuthGraphqlApiExceptionFilter)
export class AdminPanelResolver { export class AdminPanelResolver {
@ -60,19 +62,19 @@ export class AdminPanelResolver {
return true; return true;
} }
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard) @UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => EnvironmentVariablesOutput) @Query(() => EnvironmentVariablesOutput)
async getEnvironmentVariablesGrouped(): Promise<EnvironmentVariablesOutput> { async getEnvironmentVariablesGrouped(): Promise<EnvironmentVariablesOutput> {
return this.adminService.getEnvironmentVariablesGrouped(); return this.adminService.getEnvironmentVariablesGrouped();
} }
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard) @UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => SystemHealth) @Query(() => SystemHealth)
async getSystemHealthStatus(): Promise<SystemHealth> { async getSystemHealthStatus(): Promise<SystemHealth> {
return this.adminPanelHealthService.getSystemHealthStatus(); return this.adminPanelHealthService.getSystemHealthStatus();
} }
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard) @UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => AdminPanelHealthServiceData) @Query(() => AdminPanelHealthServiceData)
async getIndicatorHealthStatus( async getIndicatorHealthStatus(
@Args('indicatorId', { @Args('indicatorId', {
@ -83,7 +85,7 @@ export class AdminPanelResolver {
return this.adminPanelHealthService.getIndicatorHealthStatus(indicatorId); return this.adminPanelHealthService.getIndicatorHealthStatus(indicatorId);
} }
@UseGuards(WorkspaceAuthGuard, UserAuthGuard, ImpersonateGuard) @UseGuards(WorkspaceAuthGuard, UserAuthGuard, AdminPanelGuard)
@Query(() => QueueMetricsData) @Query(() => QueueMetricsData)
async getQueueMetrics( async getQueueMetrics(
@Args('queueName', { type: () => String }) @Args('queueName', { type: () => String })

View File

@ -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 { RedisClientModule } from 'src/engine/core-modules/redis-client/redis-client.module';
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.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 { 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'; import { HealthCacheService } from './health-cache.service';
@ -19,7 +18,6 @@ import { WorkerHealthIndicator } from './indicators/worker.health';
imports: [ imports: [
TerminusModule, TerminusModule,
RedisClientModule, RedisClientModule,
WorkspaceHealthModule,
ObjectMetadataModule, ObjectMetadataModule,
WorkspaceMigrationModule, WorkspaceMigrationModule,
], ],

View File

@ -4,12 +4,10 @@ import { Test, TestingModule } from '@nestjs/testing';
import { AppHealthIndicator } from 'src/engine/core-modules/health/indicators/app.health'; import { AppHealthIndicator } from 'src/engine/core-modules/health/indicators/app.health';
import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; 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 { 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', () => { describe('AppHealthIndicator', () => {
let service: AppHealthIndicator; let service: AppHealthIndicator;
let objectMetadataService: jest.Mocked<ObjectMetadataService>; let objectMetadataService: jest.Mocked<ObjectMetadataService>;
let workspaceHealthService: jest.Mocked<WorkspaceHealthService>;
let workspaceMigrationService: jest.Mocked<WorkspaceMigrationService>; let workspaceMigrationService: jest.Mocked<WorkspaceMigrationService>;
let healthIndicatorService: jest.Mocked<HealthIndicatorService>; let healthIndicatorService: jest.Mocked<HealthIndicatorService>;
@ -18,10 +16,6 @@ describe('AppHealthIndicator', () => {
findMany: jest.fn(), findMany: jest.fn(),
} as any; } as any;
workspaceHealthService = {
healthCheck: jest.fn(),
} as any;
workspaceMigrationService = { workspaceMigrationService = {
getPendingMigrations: jest.fn(), getPendingMigrations: jest.fn(),
} as any; } as any;
@ -44,10 +38,7 @@ describe('AppHealthIndicator', () => {
provide: ObjectMetadataService, provide: ObjectMetadataService,
useValue: objectMetadataService, useValue: objectMetadataService,
}, },
{
provide: WorkspaceHealthService,
useValue: workspaceHealthService,
},
{ {
provide: WorkspaceMigrationService, provide: WorkspaceMigrationService,
useValue: workspaceMigrationService, useValue: workspaceMigrationService,

View File

@ -7,7 +7,6 @@ import {
import { HealthStateManager } from 'src/engine/core-modules/health/utils/health-state-manager.util'; 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 { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service';
import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.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() @Injectable()
export class AppHealthIndicator { export class AppHealthIndicator {
@ -15,7 +14,6 @@ export class AppHealthIndicator {
constructor( constructor(
private readonly healthIndicatorService: HealthIndicatorService, private readonly healthIndicatorService: HealthIndicatorService,
private readonly workspaceHealthService: WorkspaceHealthService,
private readonly objectMetadataService: ObjectMetadataService, private readonly objectMetadataService: ObjectMetadataService,
private readonly workspaceMigrationService: WorkspaceMigrationService, private readonly workspaceMigrationService: WorkspaceMigrationService,
) {} ) {}

View File

@ -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);
});
});

View File

@ -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<boolean> | Observable<boolean> {
const ctx = GqlExecutionContext.create(context);
const request = ctx.getContext().req;
return request.user.canAccessFullAdminPanel === true;
}
}

View File

@ -6,6 +6,7 @@ export {
IconAlertTriangle, IconAlertTriangle,
IconApi, IconApi,
IconApps, IconApps,
IconAppWindow,
IconArchive, IconArchive,
IconArchiveOff, IconArchiveOff,
IconArrowBackUp, IconArrowBackUp,
@ -158,6 +159,7 @@ export {
IconHeadphones, IconHeadphones,
IconHeart, IconHeart,
IconHeartOff, IconHeartOff,
IconHeartRateMonitor,
IconHelpCircle, IconHelpCircle,
IconHierarchy, IconHierarchy,
IconHierarchy2, IconHierarchy2,
@ -241,6 +243,7 @@ export {
IconSearch, IconSearch,
IconSend, IconSend,
IconServer, IconServer,
IconServer2,
IconSettings, IconSettings,
IconSettings2, IconSettings2,
IconSettingsAutomation, IconSettingsAutomation,
@ -254,6 +257,7 @@ export {
IconSquareKey, IconSquareKey,
IconSquareRoundedCheck, IconSquareRoundedCheck,
IconSquareRoundedX, IconSquareRoundedX,
IconStatusChange,
IconStepInto, IconStepInto,
IconTable, IconTable,
IconTag, IconTag,
@ -266,6 +270,7 @@ export {
IconTimeDuration30, IconTimeDuration30,
IconTimeDuration60, IconTimeDuration60,
IconTimelineEvent, IconTimelineEvent,
IconTool,
IconTrash, IconTrash,
IconTrashX, IconTrashX,
IconTypography, IconTypography,

View File

@ -1,5 +1,5 @@
import React from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import React from 'react';
const StyledButtonLink = styled.a` const StyledButtonLink = styled.a`
align-items: center; 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 ( return (
<StyledButtonLink <StyledButtonLink
className={props.className}
href={props.href} href={props.href}
onClick={props.onClick} onClick={props.onClick}
target={props.target} target={props.target}