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:
+3
-6
@@ -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 {
|
||||
|
||||
+1
-1
@@ -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}
|
||||
|
||||
+33
@@ -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>
|
||||
);
|
||||
};
|
||||
+3
-5
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
+2
-9
@@ -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
-1
@@ -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';
|
||||
|
||||
-147
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user