martmull
2024-10-22 14:51:03 +02:00
committed by GitHub
parent 7fc844ea8f
commit e767f16dbe
26 changed files with 547 additions and 242 deletions

View File

@ -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, {

View File

@ -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) || '' };
})}
/>
);

View File

@ -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

View File

@ -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>
);
};

View File

@ -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>
);
};