1635 extensibilitytwenty cli app vars (#15143)

- Update twenty-cli to support application env variable definition
- Update twenty-server to create a new `core.applicationVariable` entity
to store env variables and provide env var when executing serverless
function
- Update twenty-front to support application environment variable value
setting

<img width="1044" height="660" alt="image"
src="https://github.com/user-attachments/assets/24c3d323-5370-4a80-8174-fc4653cc3c22"
/>

<img width="1178" height="662" alt="image"
src="https://github.com/user-attachments/assets/c124f423-8ed8-4246-ae5b-a9bd6672c7dc"
/>

<img width="1163" height="823" alt="image"
src="https://github.com/user-attachments/assets/fb7425a3-facc-4895-a5eb-8a8e278e0951"
/>

<img width="1087" height="696" alt="image"
src="https://github.com/user-attachments/assets/113da8a2-5590-433c-b1b3-5ed3137f24ca"
/>

<img width="1512" height="715" alt="image"
src="https://github.com/user-attachments/assets/1d2110b7-301d-4f21-a45c-ddd54d6e3391"
/>

<img width="1287" height="581" alt="image"
src="https://github.com/user-attachments/assets/353b16c6-0527-444c-87d6-51447a96cbc7"
/>
This commit is contained in:
martmull
2025-10-17 10:54:38 +02:00
committed by GitHub
parent 54baa47fbb
commit d2e7f2a910
58 changed files with 1305 additions and 359 deletions
@@ -1,6 +1,5 @@
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
import { type EditorProps, type Monaco } from '@monaco-editor/react';
import dotenv from 'dotenv';
import { type editor } from 'monaco-editor';
import { AutoTypings } from 'monaco-editor-auto-typings';
import { useParams } from 'react-router-dom';
@@ -35,7 +34,6 @@ export const SettingsServerlessFunctionCodeEditor = ({
});
const currentFile = files.find((file) => file.path === currentFilePath);
const environmentVariablesFile = files.find((file) => file.path === '.env');
const handleEditorDidMount = async (
editor: editor.IStandaloneCodeEditor,
@@ -67,11 +65,10 @@ export const SettingsServerlessFunctionCodeEditor = ({
target: monaco.languages.typescript.ScriptTarget.ESNext,
});
if (isDefined(environmentVariablesFile)) {
const environmentVariables = dotenv.parse(
environmentVariablesFile.content,
);
// TODO load that with proper env variables
const environmentVariables = {};
if (isDefined(environmentVariables)) {
const environmentDefinition = `
declare namespace NodeJS {
interface ProcessEnv {
@@ -25,7 +25,7 @@ export const SettingsServerlessFunctionNewForm = ({
return (
<Section>
<H2Title title="About" description="Name and set your function" />
<H2Title title="About" description="Name and describe your function" />
<StyledInputsContainer>
<SettingsTextInput
instanceId={nameTextInputId}
@@ -0,0 +1,33 @@
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { SettingsPath } from 'twenty-shared/types';
import { LinkChip } from 'twenty-ui/components';
import { getSettingsPath } from 'twenty-shared/utils';
import { useParams } from 'react-router-dom';
import { t } from '@lingui/core/macro';
export const SettingsServerlessFunctionTabEnvironmentVariablesSection = () => {
const { applicationId = '' } = useParams<{ applicationId: string }>();
return (
<Section>
<H2Title
title={t`Environment Variables`}
description="Accessible in your function via process.env.KEY"
/>
Environment variables are defined at application level for all functions.
Please check{' '}
<LinkChip
label={'application detail page'}
to={getSettingsPath(
SettingsPath.ApplicationDetail,
{
applicationId,
},
undefined,
'settings',
)}
/>
.
</Section>
);
};
@@ -44,11 +44,9 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
const HeaderTabList = (
<StyledTabList
tabs={files
.filter((file) => file.path !== '.env')
.map((file) => {
return { id: file.path, title: file.path.split('/').at(-1) || '' };
})}
tabs={files.map((file) => {
return { id: file.path, title: file.path.split('/').at(-1) || '' };
})}
componentInstanceId={SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID}
/>
);
@@ -1,17 +1,13 @@
import { SettingsServerlessFunctionNewForm } from '@/settings/serverless-functions/components/SettingsServerlessFunctionNewForm';
import { SettingsServerlessFunctionTabEnvironmentVariablesSection } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection';
import { type ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
import { SettingsServerlessFunctionTabEnvironmentVariablesSection } from '@/settings/serverless-functions/components/SettingsServerlessFunctionTabEnvironmentVariablesSection';
export const SettingsServerlessFunctionSettingsTab = ({
formValues,
onChange,
onCodeChange,
serverlessFunctionId,
}: {
formValues: ServerlessFunctionFormValues;
serverlessFunctionId: string;
onChange: (key: string) => (value: string) => void;
onCodeChange: (filePath: string, value: string) => void;
}) => {
return (
<>
@@ -20,10 +16,7 @@ export const SettingsServerlessFunctionSettingsTab = ({
onChange={onChange}
readonly
/>
<SettingsServerlessFunctionTabEnvironmentVariablesSection
onCodeChange={onCodeChange}
serverlessFunctionId={serverlessFunctionId}
/>
<SettingsServerlessFunctionTabEnvironmentVariablesSection />
</>
);
};
@@ -1,4 +1,4 @@
import { type EnvironmentVariable } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection';
import { type EnvironmentVariable } from '~/pages/settings/applications/tabs/SettingsApplicationDetailEnvironmentVariablesTableRow';
import { TextInput } from '@/ui/input/components/TextInput';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
@@ -1,147 +0,0 @@
import { SettingsServerlessFunctionTabEnvironmentVariableTableRow } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariableTableRow';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { Table } from '@/ui/layout/table/components/Table';
import { TableBody } from '@/ui/layout/table/components/TableBody';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled';
import { useMemo, useState } from 'react';
import { H2Title, IconPlus, IconSearch } from 'twenty-ui/display';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { MOBILE_VIEWPORT } from 'twenty-ui/theme';
import { v4 } from 'uuid';
import { serverlessFunctionEnvVarFamilyState } from '@/settings/serverless-functions/states/serverlessFunctionEnvVarFamilyState';
import { useRecoilState } from 'recoil';
const StyledSearchInput = styled(SettingsTextInput)`
padding-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledButtonContainer = styled.div`
display: flex;
justify-content: flex-end;
padding-top: ${({ theme }) => theme.spacing(2)};
@media (max-width: ${MOBILE_VIEWPORT}px) {
padding-top: ${({ theme }) => theme.spacing(5)};
}
`;
const StyledTableBody = styled(TableBody)`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
`;
const StyledTableRow = styled(TableRow)`
grid-template-columns: 180px auto 32px;
`;
export type EnvironmentVariable = { id: string; key: string; value: string };
export const SettingsServerlessFunctionTabEnvironmentVariablesSection = ({
onCodeChange,
serverlessFunctionId,
}: {
serverlessFunctionId: string;
onCodeChange: (filePath: string, value: string) => void;
}) => {
const [searchTerm, setSearchTerm] = useState('');
const [newEnvVarAdded, setNewEnvVarAdded] = useState(false);
const [envVariables, setEnvVariables] = useRecoilState(
serverlessFunctionEnvVarFamilyState(serverlessFunctionId),
);
const filteredEnvVariable = useMemo(() => {
return envVariables.filter(
({ key, value }) =>
key.toLowerCase().includes(searchTerm.toLowerCase()) ||
value.toLowerCase().includes(searchTerm.toLowerCase()),
);
}, [envVariables, searchTerm]);
const getFormattedEnvironmentVariables = (
newEnvVariables: EnvironmentVariable[],
) => {
return [...newEnvVariables]
.reverse()
.reduce(
(acc, { key, value }) =>
key.length > 0 && value.length > 0 ? `${key}=${value}\n${acc}` : acc,
'',
);
};
const onEnvVarChange = (newEnvVariable: EnvironmentVariable) => {
const newEnvVariables: EnvironmentVariable[] = [];
for (const envVariable of envVariables) {
if (envVariable.id === newEnvVariable.id) {
newEnvVariables.push(newEnvVariable);
} else if (envVariable.key !== newEnvVariable.key) {
newEnvVariables.push(envVariable);
}
}
setEnvVariables(newEnvVariables);
onCodeChange('.env', getFormattedEnvironmentVariables(newEnvVariables));
};
return (
<Section>
<H2Title
title="Environment variables"
description="Set your function environment variables"
/>
<StyledSearchInput
instanceId="serverless-function-env-var-search"
LeftIcon={IconSearch}
placeholder="Search a variable"
value={searchTerm}
onChange={setSearchTerm}
/>
<Table>
<StyledTableRow>
<TableHeader>Name</TableHeader>
<TableHeader>Value</TableHeader>
<TableHeader></TableHeader>
</StyledTableRow>
{filteredEnvVariable.length > 0 && (
<StyledTableBody>
{filteredEnvVariable.map((envVariable) => (
<SettingsServerlessFunctionTabEnvironmentVariableTableRow
key={envVariable.id}
envVariable={envVariable}
initialEditMode={newEnvVarAdded && envVariable.value === ''}
onChange={onEnvVarChange}
onDelete={() => {
const newEnvVariables = envVariables.filter(
({ id }) => id !== envVariable.id,
);
setEnvVariables(newEnvVariables);
onCodeChange(
'.env',
getFormattedEnvironmentVariables(newEnvVariables),
);
}}
/>
))}
</StyledTableBody>
)}
</Table>
<StyledButtonContainer>
<Button
Icon={IconPlus}
title="Add Variable"
size="small"
variant="secondary"
onClick={() => {
setEnvVariables((prevState) => {
return [...prevState, { id: v4(), key: '', value: '' }];
});
setNewEnvVarAdded(true);
}}
/>
</StyledButtonContainer>
</Section>
);
};