7415 serverless functions update environment variables in a dedicated tab in settings functions not a env file (#7939)
   
This commit is contained in:
@ -72,23 +72,25 @@ export const SettingsServerlessFunctionCodeEditor = ({
|
||||
);
|
||||
|
||||
const environmentDefinition = `
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
${Object.keys(environmentVariables)
|
||||
.map((key) => `${key}: string;`)
|
||||
.join('\n')}
|
||||
declare namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
${Object.keys(environmentVariables)
|
||||
.map((key) => `${key}: string;`)
|
||||
.join('\n')}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare const process: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
};
|
||||
`;
|
||||
|
||||
declare const process: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
};
|
||||
`;
|
||||
|
||||
monaco.languages.typescript.typescriptDefaults.addExtraLib(
|
||||
environmentDefinition,
|
||||
'ts:process-env.d.ts',
|
||||
);
|
||||
monaco.languages.typescript.typescriptDefaults.setExtraLibs([
|
||||
{
|
||||
content: environmentDefinition,
|
||||
filePath: 'ts:process-env.d.ts',
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
await AutoTypings.create(editor, {
|
||||
|
||||
@ -80,9 +80,11 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
|
||||
const HeaderTabList = (
|
||||
<StyledTabList
|
||||
tabListId={SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID}
|
||||
tabs={files.map((file) => {
|
||||
return { id: file.path, title: file.path.split('/').at(-1) || '' };
|
||||
})}
|
||||
tabs={files
|
||||
.filter((file) => file.path !== '.env')
|
||||
.map((file) => {
|
||||
return { id: file.path, title: file.path.split('/').at(-1) || '' };
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@ -13,15 +13,18 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { H2Title } from 'twenty-ui';
|
||||
import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount';
|
||||
import { SettingsServerlessFunctionTabEnvironmentVariablesSection } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection';
|
||||
|
||||
export const SettingsServerlessFunctionSettingsTab = ({
|
||||
formValues,
|
||||
serverlessFunctionId,
|
||||
onChange,
|
||||
onCodeChange,
|
||||
}: {
|
||||
formValues: ServerlessFunctionFormValues;
|
||||
serverlessFunctionId: string;
|
||||
onChange: (key: string) => (value: string) => void;
|
||||
onCodeChange: (filePath: string, value: string) => void;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const [isDeleteFunctionModalOpen, setIsDeleteFunctionModalOpen] =
|
||||
@ -58,6 +61,10 @@ export const SettingsServerlessFunctionSettingsTab = ({
|
||||
formValues={formValues}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<SettingsServerlessFunctionTabEnvironmentVariablesSection
|
||||
formValues={formValues}
|
||||
onCodeChange={onCodeChange}
|
||||
/>
|
||||
<Section>
|
||||
<H2Title title="Danger zone" description="Delete this function" />
|
||||
<Button
|
||||
|
||||
@ -0,0 +1,142 @@
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||
import { LightIconButton } from '@/ui/input/button/components/LightIconButton';
|
||||
import {
|
||||
IconCheck,
|
||||
IconDotsVertical,
|
||||
IconPencil,
|
||||
IconTrash,
|
||||
IconX,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
|
||||
import React, { useState } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
|
||||
import { EnvironmentVariable } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection';
|
||||
|
||||
const StyledEditModeTableRow = styled(TableRow)`
|
||||
grid-template-columns: 180px auto 56px;
|
||||
`;
|
||||
|
||||
const StyledTableRow = styled(TableRow)`
|
||||
grid-template-columns: 180px 300px 32px;
|
||||
`;
|
||||
|
||||
export const SettingsServerlessFunctionTabEnvironmentVariableTableRow = ({
|
||||
envVariable,
|
||||
onChange,
|
||||
onDelete,
|
||||
initialEditMode = false,
|
||||
}: {
|
||||
envVariable: EnvironmentVariable;
|
||||
onChange: (newEnvVariable: EnvironmentVariable) => void;
|
||||
onDelete: () => void;
|
||||
initialEditMode?: boolean;
|
||||
}) => {
|
||||
const [editedEnvVariable, setEditedEnvVariable] = useState(envVariable);
|
||||
const [editMode, setEditMode] = useState(initialEditMode);
|
||||
const dropDownId = `settings-environment-variable-dropdown-${envVariable.id}`;
|
||||
const { closeDropdown } = useDropdown(dropDownId);
|
||||
|
||||
return editMode ? (
|
||||
<StyledEditModeTableRow>
|
||||
<TableCell>
|
||||
<TextInputV2
|
||||
autoFocus
|
||||
value={editedEnvVariable.key}
|
||||
onChange={(newKey) =>
|
||||
setEditedEnvVariable({ ...editedEnvVariable, key: newKey })
|
||||
}
|
||||
placeholder="Name"
|
||||
fullWidth
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<TextInputV2
|
||||
value={editedEnvVariable.value}
|
||||
onChange={(newValue) =>
|
||||
setEditedEnvVariable({ ...editedEnvVariable, value: newValue })
|
||||
}
|
||||
placeholder="Value"
|
||||
fullWidth
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<LightIconButton
|
||||
accent={'tertiary'}
|
||||
Icon={IconX}
|
||||
onClick={() => {
|
||||
if (envVariable.key === '' && envVariable.value === '') {
|
||||
onDelete();
|
||||
}
|
||||
setEditedEnvVariable(envVariable);
|
||||
setEditMode(false);
|
||||
}}
|
||||
/>
|
||||
<LightIconButton
|
||||
accent={'tertiary'}
|
||||
Icon={IconCheck}
|
||||
disabled={
|
||||
editedEnvVariable.key === '' || editedEnvVariable.value === ''
|
||||
}
|
||||
onClick={() => {
|
||||
onChange(editedEnvVariable);
|
||||
setEditMode(false);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</StyledEditModeTableRow>
|
||||
) : (
|
||||
<StyledTableRow onClick={() => setEditMode(true)}>
|
||||
<TableCell>
|
||||
<OverflowingTextWithTooltip text={envVariable.key} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<OverflowingTextWithTooltip text={envVariable.value} />
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Dropdown
|
||||
dropdownMenuWidth="100px"
|
||||
dropdownId={dropDownId}
|
||||
clickableComponent={
|
||||
<LightIconButton
|
||||
aria-label="Env Variable Options"
|
||||
Icon={IconDotsVertical}
|
||||
accent="tertiary"
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownMenu disableBlur disableBorder width="auto">
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
text={'Edit'}
|
||||
LeftIcon={IconPencil}
|
||||
onClick={() => {
|
||||
setEditMode(true);
|
||||
closeDropdown();
|
||||
}}
|
||||
/>
|
||||
<MenuItem
|
||||
text={'Delete'}
|
||||
LeftIcon={IconTrash}
|
||||
onClick={() => {
|
||||
onDelete();
|
||||
closeDropdown();
|
||||
}}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownMenu>
|
||||
}
|
||||
dropdownHotkeyScope={{
|
||||
scope: dropDownId,
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</StyledTableRow>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,151 @@
|
||||
import dotenv from 'dotenv';
|
||||
import { H2Title, IconPlus, IconSearch, MOBILE_VIEWPORT } from 'twenty-ui';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import styled from '@emotion/styled';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { SettingsServerlessFunctionTabEnvironmentVariableTableRow } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariableTableRow';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
const StyledSearchInput = styled(TextInput)`
|
||||
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 = ({
|
||||
formValues,
|
||||
onCodeChange,
|
||||
}: {
|
||||
formValues: ServerlessFunctionFormValues;
|
||||
onCodeChange: (filePath: string, value: string) => void;
|
||||
}) => {
|
||||
const environmentVariables = formValues.code?.['.env']
|
||||
? dotenv.parse(formValues.code['.env'])
|
||||
: {};
|
||||
const environmentVariablesList = Object.entries(environmentVariables).map(
|
||||
([key, value]) => ({ id: v4(), key, value }),
|
||||
);
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [newEnvVarAdded, setNewEnvVarAdded] = useState(false);
|
||||
const [envVariables, setEnvVariables] = useState<EnvironmentVariable[]>(
|
||||
environmentVariablesList,
|
||||
);
|
||||
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.reduce(
|
||||
(acc, { key, value }) =>
|
||||
key.length > 0 && value.length > 0 ? `${acc}\n${key}=${value}` : acc,
|
||||
'',
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Environment variables"
|
||||
description="Set your function environment variables"
|
||||
/>
|
||||
<StyledSearchInput
|
||||
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={(newEnvVariable) => {
|
||||
const newEnvVariables = envVariables.reduce(
|
||||
(acc, { id, key }) => {
|
||||
if (id === newEnvVariable.id) {
|
||||
acc.push(newEnvVariable);
|
||||
} else if (key !== newEnvVariable.key) {
|
||||
acc.push(envVariable);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[] as EnvironmentVariable[],
|
||||
);
|
||||
setEnvVariables(newEnvVariables);
|
||||
onCodeChange(
|
||||
'.env',
|
||||
getFormattedEnvironmentVariables(newEnvVariables),
|
||||
);
|
||||
}}
|
||||
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