admin panel fast follows (#10723)
fast follows: - https://discord.com/channels/1130383047699738754/1346433965451382845 - https://discord.com/channels/1130383047699738754/1346434512757981264 - https://discord.com/channels/1130383047699738754/1346453484911853610 --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
@ -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 />}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) => (
|
||||||
|
|||||||
@ -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 && (
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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';
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -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} />}
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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',
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 />
|
||||||
|
|||||||
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -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 })
|
||||||
|
|||||||
@ -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,
|
||||||
],
|
],
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
) {}
|
) {}
|
||||||
|
|||||||
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user