nitin
2025-01-21 19:00:59 +05:30
committed by GitHub
parent 86b0a7952b
commit 50f36e345e
31 changed files with 710 additions and 6 deletions

View File

@ -253,6 +253,12 @@ const SettingsAdminContent = lazy(() =>
),
);
const SettingsLab = lazy(() =>
import('~/pages/settings/lab/SettingsLab').then((module) => ({
default: module.SettingsLab,
})),
);
type SettingsRoutesProps = {
isBillingEnabled?: boolean;
isServerlessFunctionSettingsEnabled?: boolean;
@ -379,6 +385,7 @@ export const SettingsRoutes = ({
/>
</>
)}
<Route path={SettingsPath.Lab} element={<SettingsLab />} />
</Routes>
</Suspense>
);

View File

@ -10,6 +10,7 @@ import { isDebugModeState } from '@/client-config/states/isDebugModeState';
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { labPublicFeatureFlagsState } from '@/client-config/states/labPublicFeatureFlagsState';
import { sentryConfigState } from '@/client-config/states/sentryConfigState';
import { supportChatState } from '@/client-config/states/supportChatState';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
@ -52,6 +53,10 @@ export const ClientConfigProviderEffect = () => {
canManageFeatureFlagsState,
);
const setLabPublicFeatureFlags = useSetRecoilState(
labPublicFeatureFlagsState,
);
const { data, loading, error } = useGetClientConfigQuery({
skip: clientConfigApiStatus.isLoaded,
});
@ -117,6 +122,7 @@ export const ClientConfigProviderEffect = () => {
frontDomain: data?.clientConfig?.frontDomain,
});
setCanManageFeatureFlags(data?.clientConfig?.canManageFeatureFlags);
setLabPublicFeatureFlags(data?.clientConfig?.publicFeatureFlags);
}, [
data,
setIsDebugMode,
@ -136,6 +142,7 @@ export const ClientConfigProviderEffect = () => {
setDomainConfiguration,
setAuthProviders,
setCanManageFeatureFlags,
setLabPublicFeatureFlags,
]);
return <></>;

View File

@ -48,6 +48,14 @@ export const GET_CLIENT_CONFIG = gql`
}
chromeExtensionId
canManageFeatureFlags
publicFeatureFlags {
key
metadata {
label
description
imagePath
}
}
}
}
`;

View File

@ -0,0 +1,7 @@
import { atom } from 'recoil';
import { PublicFeatureFlag } from '~/generated/graphql';
export const labPublicFeatureFlagsState = atom<PublicFeatureFlag[]>({
key: 'labPublicFeatureFlagsState',
default: [],
});

View File

@ -8,6 +8,7 @@ import {
IconComponent,
IconCurrencyDollar,
IconDoorEnter,
IconFlask,
IconFunction,
IconHierarchy2,
IconKey,
@ -22,6 +23,7 @@ import {
import { useAuth } from '@/auth/hooks/useAuth';
import { currentUserState } from '@/auth/states/currentUserState';
import { billingState } from '@/client-config/states/billingState';
import { labPublicFeatureFlagsState } from '@/client-config/states/labPublicFeatureFlagsState';
import { AdvancedSettingsWrapper } from '@/settings/components/AdvancedSettingsWrapper';
import { SettingsNavigationDrawerItem } from '@/settings/components/SettingsNavigationDrawerItem';
import { SettingsPath } from '@/types/SettingsPath';
@ -64,6 +66,7 @@ export const SettingsNavigationDrawerItems = () => {
const currentUser = useRecoilValue(currentUserState);
const isAdminPageEnabled = currentUser?.canImpersonate;
const labPublicFeatureFlags = useRecoilValue(labPublicFeatureFlagsState);
// TODO: Refactor this part to only have arrays of navigation items
const currentPathName = useLocation().pathname;
@ -200,6 +203,13 @@ export const SettingsNavigationDrawerItems = () => {
Icon={IconServer}
/>
)}
{labPublicFeatureFlags?.length > 0 && (
<SettingsNavigationDrawerItem
label={t`Lab`}
path={SettingsPath.Lab}
Icon={IconFlask}
/>
)}
<SettingsNavigationDrawerItem
label={t`Releases`}
path={SettingsPath.Releases}

View File

@ -23,7 +23,11 @@ const StyledSettingsOptionCardToggleContent = styled(
}
`;
const StyledSettingsOptionCardToggleButton = styled(Toggle)`
const StyledSettingsOptionCardToggleButton = styled(Toggle)<{
toggleCentered?: boolean;
}>`
align-self: ${({ toggleCentered }) =>
toggleCentered ? 'center' : 'flex-start'};
margin-left: auto;
`;
@ -40,6 +44,7 @@ type SettingsOptionCardContentToggleProps = {
divider?: boolean;
disabled?: boolean;
advancedMode?: boolean;
toggleCentered?: boolean;
checked: boolean;
onChange: (checked: boolean) => void;
};
@ -51,6 +56,7 @@ export const SettingsOptionCardContentToggle = ({
divider,
disabled = false,
advancedMode = false,
toggleCentered = true,
checked,
onChange,
}: SettingsOptionCardContentToggleProps) => {
@ -83,6 +89,7 @@ export const SettingsOptionCardContentToggle = ({
disabled={disabled}
toggleSize="small"
color={advancedMode ? theme.color.yellow : theme.color.blue}
toggleCentered={toggleCentered}
/>
</StyledSettingsOptionCardToggleContent>
{divider && <Separator />}

View File

@ -0,0 +1,84 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
import { useLabPublicFeatureFlags } from '@/settings/lab/hooks/useLabPublicFeatureFlags';
import styled from '@emotion/styled';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { Card, MOBILE_VIEWPORT } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql';
const StyledCardGrid = styled.div`
display: grid;
gap: ${({ theme }) => theme.spacing(4)};
grid-template-columns: 1fr;
& > *:not(:first-child) {
grid-column: span 1;
}
@media (min-width: ${MOBILE_VIEWPORT}px) {
grid-template-columns: repeat(2, 1fr);
& > *:first-child {
grid-column: 1 / -1;
}
}
`;
const StyledImage = styled.img<{ isFirstCard: boolean }>`
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
height: ${({ isFirstCard }) => (isFirstCard ? '240px' : '120px')};
width: 100%;
`;
const StyledFallbackDiv = styled.div<{ isFirstCard: boolean }>`
background-color: ${({ theme }) => theme.background.tertiary};
border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
height: ${({ isFirstCard }) => (isFirstCard ? '240px' : '120px')};
width: 100%;
`;
export const SettingsLabContent = () => {
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const { labPublicFeatureFlags, handleLabPublicFeatureFlagUpdate } =
useLabPublicFeatureFlags();
const [hasImageLoadingError, setHasImageLoadingError] = useState<
Record<string, boolean>
>({});
const handleToggle = async (key: FeatureFlagKey, value: boolean) => {
await handleLabPublicFeatureFlagUpdate(key, value);
};
const handleImageError = (key: string) => {
setHasImageLoadingError((prev) => ({ ...prev, [key]: true }));
};
return (
currentWorkspace?.id && (
<StyledCardGrid>
{labPublicFeatureFlags.map((flag, index) => (
<Card key={flag.key} rounded>
{flag.metadata.imagePath && !hasImageLoadingError[flag.key] ? (
<StyledImage
src={flag.metadata.imagePath}
alt={flag.metadata.label}
isFirstCard={index === 0}
onError={() => handleImageError(flag.key)}
/>
) : (
<StyledFallbackDiv isFirstCard={index === 0} />
)}
<SettingsOptionCardContentToggle
title={flag.metadata.label}
description={flag.metadata.description}
checked={flag.value}
onChange={(value) => handleToggle(flag.key, value)}
toggleCentered={false}
/>
</Card>
))}
</StyledCardGrid>
)
);
};

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const UPDATE_LAB_PUBLIC_FEATURE_FLAG = gql`
mutation UpdateLabPublicFeatureFlag(
$input: UpdateLabPublicFeatureFlagInput!
) {
updateLabPublicFeatureFlag(input: $input)
}
`;

View File

@ -0,0 +1,66 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { labPublicFeatureFlagsState } from '@/client-config/states/labPublicFeatureFlagsState';
import { useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-ui';
import {
FeatureFlagKey,
useUpdateLabPublicFeatureFlagMutation,
} from '~/generated/graphql';
export const useLabPublicFeatureFlags = () => {
const [error, setError] = useState<string | null>(null);
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
currentWorkspaceState,
);
const labPublicFeatureFlags = useRecoilValue(labPublicFeatureFlagsState);
const [updateLabPublicFeatureFlag] = useUpdateLabPublicFeatureFlagMutation();
const handleLabPublicFeatureFlagUpdate = async (
publicFeatureFlag: FeatureFlagKey,
value: boolean,
) => {
if (!isDefined(currentWorkspace)) {
setError('No workspace selected');
return false;
}
setError(null);
const response = await updateLabPublicFeatureFlag({
variables: {
input: {
publicFeatureFlag,
value,
},
},
onError: (error) => {
setError(error.message);
},
});
if (isDefined(response.data)) {
setCurrentWorkspace({
...currentWorkspace,
featureFlags: currentWorkspace.featureFlags?.map((flag) =>
flag.key === publicFeatureFlag ? { ...flag, value } : flag,
),
});
}
return !!response.data;
};
return {
labPublicFeatureFlags: labPublicFeatureFlags.map((flag) => ({
...flag,
value:
currentWorkspace?.featureFlags?.find(
(workspaceFlag) => workspaceFlag.key === flag.key,
)?.value ?? false,
})),
handleLabPublicFeatureFlagUpdate,
error,
};
};

View File

@ -35,4 +35,5 @@ export enum SettingsPath {
Releases = 'releases',
AdminPanel = 'admin-panel',
FeatureFlags = 'admin-panel/feature-flags',
Lab = 'lab',
}