Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lab #9667

Merged
merged 40 commits into from
Jan 21, 2025
Merged

Lab #9667

Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
06f119e
wip
ehconitin Jan 16, 2025
9c980bc
lint
ehconitin Jan 16, 2025
e267bc1
local state
ehconitin Jan 16, 2025
e151555
correction
ehconitin Jan 16, 2025
6e1253d
lint
ehconitin Jan 16, 2025
19d4823
Merge remote-tracking branch 'upstream/main' into labs-early-access-i…
ehconitin Jan 16, 2025
1ecabf6
Merge remote-tracking branch 'upstream/main' into labs-early-access-i…
ehconitin Jan 16, 2025
e2d3bec
new exceptions, fix hook and review
ehconitin Jan 16, 2025
9f319e3
test fix
ehconitin Jan 16, 2025
e5f154e
lint
ehconitin Jan 16, 2025
2514cfa
fix
ehconitin Jan 16, 2025
74e8280
Merge remote-tracking branch 'upstream/main' into labs-early-access-i…
ehconitin Jan 16, 2025
a2f5d99
rename labs to lab
ehconitin Jan 16, 2025
ead4cec
metadata on public feature flags
ehconitin Jan 17, 2025
1e69b6a
Merge remote-tracking branch 'upstream/main' into labs-early-access-i…
ehconitin Jan 17, 2025
11b39ab
wip -- use client config to fetch public flag
ehconitin Jan 17, 2025
c817706
Merge remote-tracking branch 'upstream/main' into labs-early-access-i…
ehconitin Jan 17, 2025
7fd74ac
lint
ehconitin Jan 17, 2025
55264db
Merge remote-tracking branch 'upstream/main' into labs-early-access-i…
ehconitin Jan 19, 2025
5cdbc36
conflicts
ehconitin Jan 20, 2025
e083c94
Merge remote-tracking branch 'upstream/main' into labs-early-access-i…
ehconitin Jan 20, 2025
445a8a5
add lab images to twenty-website
ehconitin Jan 20, 2025
3a1b5a7
remove comment
ehconitin Jan 20, 2025
271875c
Merge remote-tracking branch 'upstream/main' into labs-early-access-i…
ehconitin Jan 20, 2025
5ebbccc
remove ugly env variable
ehconitin Jan 20, 2025
7d6b015
change image url
ehconitin Jan 20, 2025
9930575
review
ehconitin Jan 20, 2025
419d803
graphql
ehconitin Jan 20, 2025
08f7179
Merge remote-tracking branch 'upstream/main' into labs-early-access-i…
ehconitin Jan 20, 2025
b3da7e5
fallback div if imagePath is missing or invalid
ehconitin Jan 20, 2025
8113439
Merge branch 'main' into labs-early-access-init-2
ehconitin Jan 20, 2025
1f72d6d
Merge branch 'main' into labs-early-access-init-2
ehconitin Jan 21, 2025
334c259
martin's review -- better code practices
ehconitin Jan 21, 2025
6499670
oops
ehconitin Jan 21, 2025
8cf4eb9
revert and remove id check from handleToggle
ehconitin Jan 21, 2025
a759254
remove temp public flags
ehconitin Jan 21, 2025
639f7f1
Merge branch 'main' into labs-early-access-init-2
ehconitin Jan 21, 2025
ad090d5
make tests conditional
ehconitin Jan 21, 2025
4e9a7a7
Merge remote-tracking branch 'upstream/main' into labs-early-access-i…
ehconitin Jan 21, 2025
2f5766b
mock feature flags, public flags and util
ehconitin Jan 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 73 additions & 2 deletions packages/twenty-front/src/generated/graphql.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ export type ClientConfig = {
frontDomain: Scalars['String'];
isEmailVerificationRequired: Scalars['Boolean'];
isMultiWorkspaceEnabled: Scalars['Boolean'];
publicFeatureFlags: Array<PublicFeatureFlag>;
sentry: Sentry;
signInPrefilled: Scalars['Boolean'];
support: Support;
Expand Down Expand Up @@ -530,6 +531,7 @@ export type Mutation = {
switchWorkspace: PublicWorkspaceDataOutput;
track: Analytics;
updateBillingSubscription: UpdateBillingEntity;
updateLabPublicFeatureFlag: Scalars['Boolean'];
updateOneField: Field;
updateOneObject: Object;
updateOneServerlessFunction: ServerlessFunction;
Expand Down Expand Up @@ -747,6 +749,11 @@ export type MutationTrackArgs = {
};


export type MutationUpdateLabPublicFeatureFlagArgs = {
input: UpdateLabPublicFeatureFlagInput;
};


export type MutationUpdateOneFieldArgs = {
input: UpdateOneFieldMetadataInput;
};
Expand Down Expand Up @@ -890,6 +897,19 @@ export type ProductPricesEntity = {
totalNumberOfPrices: Scalars['Int'];
};

export type PublicFeatureFlag = {
__typename?: 'PublicFeatureFlag';
key: FeatureFlagKey;
metadata: PublicFeatureFlagMetadata;
};

export type PublicFeatureFlagMetadata = {
__typename?: 'PublicFeatureFlagMetadata';
description: Scalars['String'];
imagePath: Scalars['String'];
label: Scalars['String'];
};

export type PublicWorkspaceDataOutput = {
__typename?: 'PublicWorkspaceDataOutput';
authProviders: AuthProviders;
Expand Down Expand Up @@ -1358,6 +1378,11 @@ export type UpdateFieldInput = {
settings?: InputMaybe<Scalars['JSON']>;
};

export type UpdateLabPublicFeatureFlagInput = {
publicFeatureFlag: Scalars['String'];
value: Scalars['Boolean'];
};

export type UpdateObjectPayload = {
description?: InputMaybe<Scalars['String']>;
icon?: InputMaybe<Scalars['String']>;
Expand Down Expand Up @@ -2103,7 +2128,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;


export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, isEmailVerificationRequired: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, canManageFeatureFlags: boolean, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, trialPeriods: Array<{ __typename?: 'TrialPeriodDTO', duration: number, isCreditCardRequired: boolean }> }, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } };
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, isEmailVerificationRequired: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, canManageFeatureFlags: boolean, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, trialPeriods: Array<{ __typename?: 'TrialPeriodDTO', duration: number, isCreditCardRequired: boolean }> }, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number }, publicFeatureFlags: Array<{ __typename?: 'PublicFeatureFlag', key: FeatureFlagKey, metadata: { __typename?: 'PublicFeatureFlagMetadata', label: string, description: string, imagePath: string } }> } };

export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>;

Expand All @@ -2126,6 +2151,13 @@ export type UserLookupAdminPanelMutationVariables = Exact<{

export type UserLookupAdminPanelMutation = { __typename?: 'Mutation', userLookupAdminPanel: { __typename?: 'UserLookup', user: { __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }, workspaces: Array<{ __typename?: 'WorkspaceInfo', id: string, name: string, logo?: string | null, totalUsers: number, allowImpersonation: boolean, users: Array<{ __typename?: 'UserInfo', id: string, email: string, firstName?: string | null, lastName?: string | null }>, featureFlags: Array<{ __typename?: 'FeatureFlag', key: FeatureFlagKey, value: boolean }> }> } };

export type UpdateLabPublicFeatureFlagMutationVariables = Exact<{
input: UpdateLabPublicFeatureFlagInput;
}>;


export type UpdateLabPublicFeatureFlagMutation = { __typename?: 'Mutation', updateLabPublicFeatureFlag: boolean };

export type CreateOidcIdentityProviderMutationVariables = Exact<{
input: SetupOidcSsoInput;
}>;
Expand Down Expand Up @@ -3615,6 +3647,14 @@ export const GetClientConfigDocument = gql`
}
chromeExtensionId
canManageFeatureFlags
publicFeatureFlags {
key
metadata {
label
description
imagePath
}
}
}
}
`;
Expand Down Expand Up @@ -3769,6 +3809,37 @@ export function useUserLookupAdminPanelMutation(baseOptions?: Apollo.MutationHoo
export type UserLookupAdminPanelMutationHookResult = ReturnType<typeof useUserLookupAdminPanelMutation>;
export type UserLookupAdminPanelMutationResult = Apollo.MutationResult<UserLookupAdminPanelMutation>;
export type UserLookupAdminPanelMutationOptions = Apollo.BaseMutationOptions<UserLookupAdminPanelMutation, UserLookupAdminPanelMutationVariables>;
export const UpdateLabPublicFeatureFlagDocument = gql`
mutation UpdateLabPublicFeatureFlag($input: UpdateLabPublicFeatureFlagInput!) {
updateLabPublicFeatureFlag(input: $input)
}
`;
export type UpdateLabPublicFeatureFlagMutationFn = Apollo.MutationFunction<UpdateLabPublicFeatureFlagMutation, UpdateLabPublicFeatureFlagMutationVariables>;

/**
* __useUpdateLabPublicFeatureFlagMutation__
*
* To run a mutation, you first call `useUpdateLabPublicFeatureFlagMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useUpdateLabPublicFeatureFlagMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [updateLabPublicFeatureFlagMutation, { data, loading, error }] = useUpdateLabPublicFeatureFlagMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useUpdateLabPublicFeatureFlagMutation(baseOptions?: Apollo.MutationHookOptions<UpdateLabPublicFeatureFlagMutation, UpdateLabPublicFeatureFlagMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<UpdateLabPublicFeatureFlagMutation, UpdateLabPublicFeatureFlagMutationVariables>(UpdateLabPublicFeatureFlagDocument, options);
}
export type UpdateLabPublicFeatureFlagMutationHookResult = ReturnType<typeof useUpdateLabPublicFeatureFlagMutation>;
export type UpdateLabPublicFeatureFlagMutationResult = Apollo.MutationResult<UpdateLabPublicFeatureFlagMutation>;
export type UpdateLabPublicFeatureFlagMutationOptions = Apollo.BaseMutationOptions<UpdateLabPublicFeatureFlagMutation, UpdateLabPublicFeatureFlagMutationVariables>;
export const CreateOidcIdentityProviderDocument = gql`
mutation CreateOIDCIdentityProvider($input: SetupOIDCSsoInput!) {
createOIDCIdentityProvider(input: $input) {
Expand Down Expand Up @@ -4707,4 +4778,4 @@ export function useGetWorkspaceFromInviteHashLazyQuery(baseOptions?: Apollo.Lazy
}
export type GetWorkspaceFromInviteHashQueryHookResult = ReturnType<typeof useGetWorkspaceFromInviteHashQuery>;
export type GetWorkspaceFromInviteHashLazyQueryHookResult = ReturnType<typeof useGetWorkspaceFromInviteHashLazyQuery>;
export type GetWorkspaceFromInviteHashQueryResult = Apollo.QueryResult<GetWorkspaceFromInviteHashQuery, GetWorkspaceFromInviteHashQueryVariables>;
export type GetWorkspaceFromInviteHashQueryResult = Apollo.QueryResult<GetWorkspaceFromInviteHashQuery, GetWorkspaceFromInviteHashQueryVariables>;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -379,6 +385,7 @@ export const SettingsRoutes = ({
/>
</>
)}
<Route path={SettingsPath.Lab} element={<SettingsLab />} />
</Routes>
</Suspense>
);
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -52,6 +53,10 @@ export const ClientConfigProviderEffect = () => {
canManageFeatureFlagsState,
);

const setLabPublicFeatureFlags = useSetRecoilState(
labPublicFeatureFlagsState,
);

const { data, loading, error } = useGetClientConfigQuery({
skip: clientConfigApiStatus.isLoaded,
});
Expand Down Expand Up @@ -117,6 +122,7 @@ export const ClientConfigProviderEffect = () => {
frontDomain: data?.clientConfig?.frontDomain,
});
setCanManageFeatureFlags(data?.clientConfig?.canManageFeatureFlags);
setLabPublicFeatureFlags(data?.clientConfig?.publicFeatureFlags);
}, [
data,
setIsDebugMode,
Expand All @@ -136,6 +142,7 @@ export const ClientConfigProviderEffect = () => {
setDomainConfiguration,
setAuthProviders,
setCanManageFeatureFlags,
setLabPublicFeatureFlags,
]);

return <></>;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ export const GET_CLIENT_CONFIG = gql`
}
chromeExtensionId
canManageFeatureFlags
publicFeatureFlags {
key
metadata {
label
description
imagePath
}
}
}
}
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { atom } from 'recoil';
import { PublicFeatureFlag } from '~/generated/graphql';

export const labPublicFeatureFlagsState = atom<PublicFeatureFlag[]>({
key: 'labPublicFeatureFlagsState',
default: [],
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
IconComponent,
IconCurrencyDollar,
IconDoorEnter,
IconFlask,
IconFunction,
IconHierarchy2,
IconKey,
Expand All @@ -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';
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
`;

Expand All @@ -40,6 +44,7 @@ type SettingsOptionCardContentToggleProps = {
divider?: boolean;
disabled?: boolean;
advancedMode?: boolean;
toggleCentered?: boolean;
checked: boolean;
onChange: (checked: boolean) => void;
};
Expand All @@ -51,6 +56,7 @@ export const SettingsOptionCardContentToggle = ({
divider,
disabled = false,
advancedMode = false,
toggleCentered = true,
checked,
onChange,
}: SettingsOptionCardContentToggleProps) => {
Expand Down Expand Up @@ -83,6 +89,7 @@ export const SettingsOptionCardContentToggle = ({
disabled={disabled}
toggleSize="small"
color={advancedMode ? theme.color.yellow : theme.color.blue}
toggleCentered={toggleCentered}
/>
</StyledSettingsOptionCardToggleContent>
{divider && <Separator />}
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
)
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { gql } from '@apollo/client';

export const UPDATE_LAB_PUBLIC_FEATURE_FLAG = gql`
mutation UpdateLabPublicFeatureFlag(
$input: UpdateLabPublicFeatureFlagInput!
) {
updateLabPublicFeatureFlag(input: $input)
}
`;
Loading
Loading