1518 extensibility front add an application section in settings (#15056)
Protected by IS_APPLICATION_ENABLED featureFlag Add `Application` section in settings <img width="301" height="137" alt="image" src="https://github.com/user-attachments/assets/ee53bdd2-36f6-45c6-8646-17b1e08abf00" /> A `settings/applications` route listing all installed applications <img width="661" height="428" alt="image" src="https://github.com/user-attachments/assets/69d534c4-4e9e-452a-a3d9-ded0223bb457" /> Introduce a new Tag for application managed items <img width="885" height="759" alt="image" src="https://github.com/user-attachments/assets/19767be5-61e5-4bd2-a51d-54ed9bfb1923" /> A `settings/applications/<application_id>` details setting page listing all objects, serverlessFunctions and agents created by the application: <img width="917" height="778" alt="image" src="https://github.com/user-attachments/assets/7fc056a6-1d73-4242-b2eb-6f8955d8597d" /> A `settings/applications/<application_id>/<serverless_function_id>` <img width="905" height="652" alt="image" src="https://github.com/user-attachments/assets/56ca0021-26bf-42cb-9abf-34879f16050a" /> Add trigger tab in serverless function details (readonly for now) <img width="899" height="724" alt="image" src="https://github.com/user-attachments/assets/5eeefa35-f2a4-4fd8-a640-7b5c5891f226" /> Set object, serverless and agent setting detail pages readonly for managed items <img width="1075" height="859" alt="image" src="https://github.com/user-attachments/assets/57c73d69-4980-47a2-b752-8dc5ab494530" /> <img width="648" height="582" alt="image" src="https://github.com/user-attachments/assets/5ad5f3f7-3bc3-4e40-870a-4981c6492524" /> <img width="982" height="692" alt="image" src="https://github.com/user-attachments/assets/7ad756c4-5d33-4a0a-9eb8-416c040362b9" /> <img width="1077" height="647" alt="image" src="https://github.com/user-attachments/assets/e086b9f5-4062-4d10-82a9-4023de3cad3f" />
This commit is contained in:
+1
-14
@@ -1,7 +1,7 @@
|
||||
import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages';
|
||||
import { type EditorProps, type Monaco } from '@monaco-editor/react';
|
||||
import dotenv from 'dotenv';
|
||||
import { type editor, MarkerSeverity } from 'monaco-editor';
|
||||
import { type editor } from 'monaco-editor';
|
||||
import { AutoTypings } from 'monaco-editor-auto-typings';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -20,14 +20,12 @@ type SettingsServerlessFunctionCodeEditorProps = Omit<
|
||||
currentFilePath: string;
|
||||
files: File[];
|
||||
onChange: (value: string) => void;
|
||||
setIsCodeValid: (isCodeValid: boolean) => void;
|
||||
};
|
||||
|
||||
export const SettingsServerlessFunctionCodeEditor = ({
|
||||
currentFilePath,
|
||||
files,
|
||||
onChange,
|
||||
setIsCodeValid,
|
||||
height = 450,
|
||||
options = undefined,
|
||||
}: SettingsServerlessFunctionCodeEditorProps) => {
|
||||
@@ -106,16 +104,6 @@ export const SettingsServerlessFunctionCodeEditor = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditorValidation = (markers: editor.IMarker[]) => {
|
||||
for (const marker of markers) {
|
||||
if (marker.severity === MarkerSeverity.Error) {
|
||||
setIsCodeValid?.(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
setIsCodeValid?.(true);
|
||||
};
|
||||
|
||||
return (
|
||||
isDefined(currentFile) &&
|
||||
isDefined(availablePackages) && (
|
||||
@@ -125,7 +113,6 @@ export const SettingsServerlessFunctionCodeEditor = ({
|
||||
language={currentFile.language}
|
||||
onMount={handleEditorDidMount}
|
||||
onChange={onChange}
|
||||
onValidate={handleEditorValidation}
|
||||
options={options}
|
||||
variant="with-header"
|
||||
/>
|
||||
|
||||
+4
@@ -14,9 +14,11 @@ const StyledInputsContainer = styled.div`
|
||||
export const SettingsServerlessFunctionNewForm = ({
|
||||
formValues,
|
||||
onChange,
|
||||
readonly = false,
|
||||
}: {
|
||||
formValues: ServerlessFunctionNewFormValues;
|
||||
onChange: (key: string) => (value: string) => void;
|
||||
readonly?: boolean;
|
||||
}) => {
|
||||
const descriptionTextAreaId = `${formValues.name}-description`;
|
||||
const nameTextInputId = `${formValues.name}-name`;
|
||||
@@ -32,6 +34,7 @@ export const SettingsServerlessFunctionNewForm = ({
|
||||
autoFocusOnMount
|
||||
value={formValues.name}
|
||||
onChange={onChange('name')}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
<TextArea
|
||||
textAreaId={descriptionTextAreaId}
|
||||
@@ -39,6 +42,7 @@ export const SettingsServerlessFunctionNewForm = ({
|
||||
minRows={4}
|
||||
value={formValues.description}
|
||||
onChange={onChange('description')}
|
||||
readOnly={readonly}
|
||||
/>
|
||||
</StyledInputsContainer>
|
||||
</Section>
|
||||
|
||||
+36
-33
@@ -1,7 +1,4 @@
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsServerlessFunctionsFieldItemTableRow } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsFieldItemTableRow';
|
||||
import { SettingsServerlessFunctionsTableEmpty } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsTableEmpty';
|
||||
import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
@@ -10,6 +7,8 @@ import styled from '@emotion/styled';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { type ServerlessFunction } from '~/generated-metadata/graphql';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
const StyledTableRow = styled(TableRow)`
|
||||
grid-template-columns: 312px 132px 68px;
|
||||
@@ -19,37 +18,41 @@ const StyledTableBody = styled(TableBody)`
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
`;
|
||||
|
||||
export const SettingsServerlessFunctionsTable = () => {
|
||||
const { serverlessFunctions } = useGetManyServerlessFunctions();
|
||||
export const SettingsServerlessFunctionsTable = ({
|
||||
serverlessFunctions,
|
||||
}: {
|
||||
serverlessFunctions: ServerlessFunction[];
|
||||
}) => {
|
||||
const { applicationId = '' } = useParams();
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
if (serverlessFunctions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{serverlessFunctions.length ? (
|
||||
<SettingsPageContainer>
|
||||
<Table>
|
||||
<StyledTableRow>
|
||||
<TableHeader>Name</TableHeader>
|
||||
<TableHeader>Runtime</TableHeader>
|
||||
<TableHeader></TableHeader>
|
||||
</StyledTableRow>
|
||||
<StyledTableBody>
|
||||
{serverlessFunctions.map(
|
||||
(serverlessFunction: ServerlessFunction) => (
|
||||
<SettingsServerlessFunctionsFieldItemTableRow
|
||||
key={serverlessFunction.id}
|
||||
serverlessFunction={serverlessFunction}
|
||||
to={getSettingsPath(SettingsPath.ServerlessFunctions, {
|
||||
id: serverlessFunction.id,
|
||||
})}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</StyledTableBody>
|
||||
</Table>
|
||||
</SettingsPageContainer>
|
||||
) : (
|
||||
<SettingsServerlessFunctionsTableEmpty />
|
||||
)}
|
||||
</>
|
||||
<Table>
|
||||
<StyledTableRow>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader>Runtime</TableHeader>
|
||||
<TableHeader></TableHeader>
|
||||
</StyledTableRow>
|
||||
<StyledTableBody>
|
||||
{serverlessFunctions.map((serverlessFunction: ServerlessFunction) => (
|
||||
<SettingsServerlessFunctionsFieldItemTableRow
|
||||
key={serverlessFunction.id}
|
||||
serverlessFunction={serverlessFunction}
|
||||
to={getSettingsPath(
|
||||
SettingsPath.ApplicationServerlessFunctionDetail,
|
||||
{
|
||||
applicationId,
|
||||
serverlessFunctionId: serverlessFunction.id,
|
||||
},
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
</StyledTableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
||||
-43
@@ -1,43 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { IconPlus } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import {
|
||||
AnimatedPlaceholder,
|
||||
AnimatedPlaceholderEmptyContainer,
|
||||
AnimatedPlaceholderEmptySubTitle,
|
||||
AnimatedPlaceholderEmptyTextContainer,
|
||||
AnimatedPlaceholderEmptyTitle,
|
||||
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
|
||||
} from 'twenty-ui/layout';
|
||||
|
||||
const StyledEmptyFunctionsContainer = styled.div`
|
||||
height: 60vh;
|
||||
`;
|
||||
|
||||
export const SettingsServerlessFunctionsTableEmpty = () => {
|
||||
return (
|
||||
<StyledEmptyFunctionsContainer>
|
||||
<AnimatedPlaceholderEmptyContainer
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...EMPTY_PLACEHOLDER_TRANSITION_PROPS}
|
||||
>
|
||||
<AnimatedPlaceholder type="emptyFunctions" />
|
||||
<AnimatedPlaceholderEmptyTextContainer>
|
||||
<AnimatedPlaceholderEmptyTitle>
|
||||
Add your first Function
|
||||
</AnimatedPlaceholderEmptyTitle>
|
||||
<AnimatedPlaceholderEmptySubTitle>
|
||||
Add your first Function to get started
|
||||
</AnimatedPlaceholderEmptySubTitle>
|
||||
</AnimatedPlaceholderEmptyTextContainer>
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title="New function"
|
||||
to={getSettingsPath(SettingsPath.NewServerlessFunction)}
|
||||
/>
|
||||
</AnimatedPlaceholderEmptyContainer>
|
||||
</StyledEmptyFunctionsContainer>
|
||||
);
|
||||
};
|
||||
+11
-41
@@ -7,12 +7,7 @@ import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
|
||||
import styled from '@emotion/styled';
|
||||
import {
|
||||
H2Title,
|
||||
IconGitCommit,
|
||||
IconPlayerPlay,
|
||||
IconRestore,
|
||||
} from 'twenty-ui/display';
|
||||
import { H2Title, IconPlayerPlay } from 'twenty-ui/display';
|
||||
import { Button, CoreEditorHeader } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
|
||||
@@ -23,21 +18,13 @@ const StyledTabList = styled(TabList)`
|
||||
export const SettingsServerlessFunctionCodeEditorTab = ({
|
||||
files,
|
||||
handleExecute,
|
||||
handlePublish,
|
||||
handleReset,
|
||||
resetDisabled,
|
||||
publishDisabled,
|
||||
onChange,
|
||||
setIsCodeValid,
|
||||
isTesting = false,
|
||||
}: {
|
||||
files: File[];
|
||||
handleExecute: () => void;
|
||||
handlePublish: () => void;
|
||||
handleReset: () => void;
|
||||
resetDisabled: boolean;
|
||||
publishDisabled: boolean;
|
||||
onChange: (filePath: string, value: string) => void;
|
||||
setIsCodeValid: (isCodeValid: boolean) => void;
|
||||
isTesting?: boolean;
|
||||
}) => {
|
||||
const activeTabId = useRecoilComponentValue(
|
||||
activeTabIdComponentState,
|
||||
@@ -50,29 +37,10 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
|
||||
accent="blue"
|
||||
size="small"
|
||||
Icon={IconPlayerPlay}
|
||||
disabled={isTesting}
|
||||
onClick={handleExecute}
|
||||
/>
|
||||
);
|
||||
const PublishButton = (
|
||||
<Button
|
||||
title="Publish"
|
||||
variant="secondary"
|
||||
size="small"
|
||||
Icon={IconGitCommit}
|
||||
onClick={handlePublish}
|
||||
disabled={publishDisabled}
|
||||
/>
|
||||
);
|
||||
const ResetButton = (
|
||||
<Button
|
||||
title="Reset"
|
||||
variant="secondary"
|
||||
size="small"
|
||||
Icon={IconRestore}
|
||||
onClick={handleReset}
|
||||
disabled={resetDisabled}
|
||||
/>
|
||||
);
|
||||
|
||||
const HeaderTabList = (
|
||||
<StyledTabList
|
||||
@@ -91,16 +59,18 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
|
||||
title="Code your function"
|
||||
description="Write your function (in typescript) below"
|
||||
/>
|
||||
<CoreEditorHeader
|
||||
leftNodes={[HeaderTabList]}
|
||||
rightNodes={[ResetButton, PublishButton, TestButton]}
|
||||
/>
|
||||
<CoreEditorHeader leftNodes={[HeaderTabList]} rightNodes={[TestButton]} />
|
||||
{activeTabId && (
|
||||
<SettingsServerlessFunctionCodeEditor
|
||||
files={files}
|
||||
currentFilePath={activeTabId}
|
||||
onChange={(newCodeValue) => onChange(activeTabId, newCodeValue)}
|
||||
setIsCodeValid={setIsCodeValid}
|
||||
options={{
|
||||
readOnly: true,
|
||||
readOnlyMessage: {
|
||||
value: 'Managed serverless functions are not editable',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
+3
-45
@@ -1,70 +1,28 @@
|
||||
import { SettingsServerlessFunctionNewForm } from '@/settings/serverless-functions/components/SettingsServerlessFunctionNewForm';
|
||||
import { SettingsServerlessFunctionTabEnvironmentVariablesSection } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection';
|
||||
import { useDeleteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useDeleteOneServerlessFunction';
|
||||
import { type ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
|
||||
const DELETE_FUNCTION_MODAL_ID = 'delete-function-modal';
|
||||
|
||||
export const SettingsServerlessFunctionSettingsTab = ({
|
||||
formValues,
|
||||
serverlessFunctionId,
|
||||
onChange,
|
||||
onCodeChange,
|
||||
serverlessFunctionId,
|
||||
}: {
|
||||
formValues: ServerlessFunctionFormValues;
|
||||
serverlessFunctionId: string;
|
||||
onChange: (key: string) => (value: string) => void;
|
||||
onCodeChange: (filePath: string, value: string) => void;
|
||||
}) => {
|
||||
const navigate = useNavigateSettings();
|
||||
const { openModal } = useModal();
|
||||
const { deleteOneServerlessFunction } = useDeleteOneServerlessFunction();
|
||||
|
||||
const deleteFunction = async () => {
|
||||
await deleteOneServerlessFunction({ id: serverlessFunctionId });
|
||||
navigate(SettingsPath.ServerlessFunctions);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsServerlessFunctionNewForm
|
||||
formValues={formValues}
|
||||
onChange={onChange}
|
||||
readonly
|
||||
/>
|
||||
<SettingsServerlessFunctionTabEnvironmentVariablesSection
|
||||
formValues={formValues}
|
||||
onCodeChange={onCodeChange}
|
||||
/>
|
||||
<Section>
|
||||
<H2Title title="Danger zone" description="Delete this function" />
|
||||
<Button
|
||||
accent="danger"
|
||||
onClick={() => openModal(DELETE_FUNCTION_MODAL_ID)}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
title="Delete function"
|
||||
/>
|
||||
</Section>
|
||||
<ConfirmationModal
|
||||
confirmationValue={formValues.name}
|
||||
confirmationPlaceholder={formValues.name}
|
||||
modalId={DELETE_FUNCTION_MODAL_ID}
|
||||
title="Function Deletion"
|
||||
subtitle={
|
||||
<>
|
||||
This action cannot be undone. This will permanently delete your
|
||||
function. <br /> Please type in the function name to confirm.
|
||||
</>
|
||||
}
|
||||
onConfirmClick={deleteFunction}
|
||||
confirmButtonText="Delete function"
|
||||
serverlessFunctionId={serverlessFunctionId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+30
-36
@@ -1,18 +1,18 @@
|
||||
import { SettingsServerlessFunctionTabEnvironmentVariableTableRow } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariableTableRow';
|
||||
import { type ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
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 dotenv from 'dotenv';
|
||||
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)};
|
||||
@@ -39,24 +39,18 @@ const StyledTableRow = styled(TableRow)`
|
||||
export type EnvironmentVariable = { id: string; key: string; value: string };
|
||||
|
||||
export const SettingsServerlessFunctionTabEnvironmentVariablesSection = ({
|
||||
formValues,
|
||||
onCodeChange,
|
||||
serverlessFunctionId,
|
||||
}: {
|
||||
formValues: ServerlessFunctionFormValues;
|
||||
serverlessFunctionId: string;
|
||||
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 [envVariables, setEnvVariables] = useRecoilState(
|
||||
serverlessFunctionEnvVarFamilyState(serverlessFunctionId),
|
||||
);
|
||||
|
||||
const filteredEnvVariable = useMemo(() => {
|
||||
return envVariables.filter(
|
||||
({ key, value }) =>
|
||||
@@ -68,11 +62,28 @@ export const SettingsServerlessFunctionTabEnvironmentVariablesSection = ({
|
||||
const getFormattedEnvironmentVariables = (
|
||||
newEnvVariables: EnvironmentVariable[],
|
||||
) => {
|
||||
return newEnvVariables.reduce(
|
||||
(acc, { key, value }) =>
|
||||
key.length > 0 && value.length > 0 ? `${acc}\n${key}=${value}` : acc,
|
||||
'',
|
||||
);
|
||||
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 (
|
||||
@@ -101,24 +112,7 @@ export const SettingsServerlessFunctionTabEnvironmentVariablesSection = ({
|
||||
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),
|
||||
);
|
||||
}}
|
||||
onChange={onEnvVarChange}
|
||||
onDelete={() => {
|
||||
const newEnvVariables = envVariables.filter(
|
||||
({ id }) => id !== envVariable.id,
|
||||
|
||||
+4
@@ -21,9 +21,11 @@ const StyledCodeEditorContainer = styled.div`
|
||||
export const SettingsServerlessFunctionTestTab = ({
|
||||
handleExecute,
|
||||
serverlessFunctionId,
|
||||
isTesting = false,
|
||||
}: {
|
||||
handleExecute: () => void;
|
||||
serverlessFunctionId: string;
|
||||
isTesting?: boolean;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
const [serverlessFunctionTestData, setServerlessFunctionTestData] =
|
||||
@@ -54,6 +56,7 @@ export const SettingsServerlessFunctionTestTab = ({
|
||||
size="small"
|
||||
Icon={IconPlayerPlay}
|
||||
onClick={handleExecute}
|
||||
disabled={isTesting}
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
@@ -67,6 +70,7 @@ export const SettingsServerlessFunctionTestTab = ({
|
||||
</StyledCodeEditorContainer>
|
||||
<ServerlessFunctionExecutionResult
|
||||
serverlessFunctionTestData={serverlessFunctionTestData}
|
||||
isTesting={isTesting}
|
||||
/>
|
||||
</StyledInputsContainer>
|
||||
</Section>
|
||||
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import { H2Title, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { type ServerlessFunction } from '~/generated/graphql';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { SettingsDatabaseEventsForm } from '@/settings/components/SettingsDatabaseEventsForm';
|
||||
import { FormTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormTextFieldInput';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import styled from '@emotion/styled';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { Tag } from 'twenty-ui/components';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
|
||||
export const StyledRouteTriggerTableRow = styled(TableRow)`
|
||||
grid-template-columns: 1fr 120px 120px;
|
||||
`;
|
||||
|
||||
const StyledTableCell = styled(TableCell)`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledRouteTriggerTableHeaderRow = styled(StyledRouteTriggerTableRow)`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const SettingsServerlessFunctionTriggersTab = ({
|
||||
serverlessFunction,
|
||||
}: {
|
||||
serverlessFunction: ServerlessFunction;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const databaseEventTriggers = serverlessFunction.databaseEventTriggers ?? [];
|
||||
|
||||
const cronTriggers = serverlessFunction.cronTriggers ?? [];
|
||||
|
||||
const routeTriggers = serverlessFunction.routeTriggers ?? [];
|
||||
|
||||
const databaseEvents = databaseEventTriggers?.map((event) => {
|
||||
const [object, action]: [string, string] =
|
||||
event.settings.eventName.split('.');
|
||||
|
||||
return { object, action };
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{databaseEvents.length > 0 && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Database event`}
|
||||
description={t`Select the events that should trigger the function`}
|
||||
/>
|
||||
<SettingsDatabaseEventsForm events={databaseEvents} disabled />
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{cronTriggers.length > 0 && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Cron`}
|
||||
description={t`Triggers the function at regular intervals`}
|
||||
/>
|
||||
{cronTriggers.map((cronTrigger, index) => (
|
||||
<FormTextFieldInput
|
||||
key={index}
|
||||
label={t`Expression`}
|
||||
placeholder="0 */1 * * *"
|
||||
hint={t`Format: [Minute] [Hour] [Day of Month] [Month] [Day of Week]`}
|
||||
onChange={() => {}}
|
||||
readonly
|
||||
defaultValue={cronTrigger.settings.pattern}
|
||||
/>
|
||||
))}
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{routeTriggers.length > 0 && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Http`}
|
||||
description={t`Triggers the function with Http request`}
|
||||
/>
|
||||
<Table>
|
||||
<StyledRouteTriggerTableHeaderRow>
|
||||
<TableHeader>{t`Path`}</TableHeader>
|
||||
<TableHeader>{t`Method`}</TableHeader>
|
||||
<TableHeader>{t`Auth Required`}</TableHeader>
|
||||
</StyledRouteTriggerTableHeaderRow>
|
||||
{routeTriggers.map((routeTrigger, index) => (
|
||||
<StyledRouteTriggerTableRow key={index}>
|
||||
<StyledTableCell>
|
||||
<OverflowingTextWithTooltip text={routeTrigger.path} />
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>{routeTrigger.httpMethod}</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Tag
|
||||
text={routeTrigger.isAuthRequired ? 'True' : 'False'}
|
||||
color={routeTrigger.isAuthRequired ? 'green' : 'orange'}
|
||||
weight="medium"
|
||||
/>
|
||||
</StyledTableCell>
|
||||
</StyledRouteTriggerTableRow>
|
||||
))}
|
||||
</Table>
|
||||
</Section>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user