mirror of
https://github.com/lingble/twenty.git
synced 2025-11-01 21:27:58 +00:00
Serverless function UI (#6388)
https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=36235-120877 Did not do the file manager part. A Function is defined using one unique file at the moment Feature protected by featureFlag `IS_FUNCTION_SETTINGS_ENABLED` ## Demo https://github.com/user-attachments/assets/0acb8291-47b4-4521-a6fa-a88b9198609b
This commit is contained in:
@@ -3,6 +3,7 @@ module.exports = {
|
|||||||
documents: [
|
documents: [
|
||||||
'./src/modules/databases/graphql/**/*.ts',
|
'./src/modules/databases/graphql/**/*.ts',
|
||||||
'./src/modules/object-metadata/graphql/*.ts',
|
'./src/modules/object-metadata/graphql/*.ts',
|
||||||
|
'./src/modules/settings/serverless-functions/graphql/**/*.ts',
|
||||||
'./src/modules/object-record/graphql/*.tsx',
|
'./src/modules/object-record/graphql/*.tsx',
|
||||||
'./src/modules/metadata/graphql/*.ts',
|
'./src/modules/metadata/graphql/*.ts',
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ module.exports = {
|
|||||||
'!./src/modules/databases/**',
|
'!./src/modules/databases/**',
|
||||||
'!./src/modules/object-metadata/**',
|
'!./src/modules/object-metadata/**',
|
||||||
'!./src/modules/object-record/**',
|
'!./src/modules/object-record/**',
|
||||||
|
'!./src/modules/settings/serverless-functions/**',
|
||||||
'./src/modules/**/*.tsx',
|
'./src/modules/**/*.tsx',
|
||||||
'./src/modules/**/*.ts',
|
'./src/modules/**/*.ts',
|
||||||
'!./src/**/*.test.tsx',
|
'!./src/**/*.test.tsx',
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
@@ -82,7 +82,10 @@ import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace';
|
|||||||
import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers';
|
import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMembers';
|
||||||
import { Tasks } from '~/pages/tasks/Tasks';
|
import { Tasks } from '~/pages/tasks/Tasks';
|
||||||
import { getPageTitleFromPath } from '~/utils/title-utils';
|
import { getPageTitleFromPath } from '~/utils/title-utils';
|
||||||
|
import { SettingsServerlessFunctions } from '~/pages/settings/serverless-functions/SettingsServerlessFunctions';
|
||||||
|
import { SettingsServerlessFunctionsNew } from '~/pages/settings/serverless-functions/SettingsServerlessFunctionsNew';
|
||||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||||
|
import { SettingsServerlessFunctionDetailWrapper } from '~/pages/settings/serverless-functions/SettingsServerlessFunctionDetailWrapper';
|
||||||
|
|
||||||
const ProvidersThatNeedRouterContext = () => {
|
const ProvidersThatNeedRouterContext = () => {
|
||||||
const { pathname } = useLocation();
|
const { pathname } = useLocation();
|
||||||
@@ -130,6 +133,7 @@ const ProvidersThatNeedRouterContext = () => {
|
|||||||
const createRouter = (
|
const createRouter = (
|
||||||
isBillingEnabled?: boolean,
|
isBillingEnabled?: boolean,
|
||||||
isCRMMigrationEnabled?: boolean,
|
isCRMMigrationEnabled?: boolean,
|
||||||
|
isServerlessFunctionSettingsEnabled?: boolean,
|
||||||
) =>
|
) =>
|
||||||
createBrowserRouter(
|
createBrowserRouter(
|
||||||
createRoutesFromElements(
|
createRoutesFromElements(
|
||||||
@@ -256,6 +260,22 @@ const createRouter = (
|
|||||||
</Routes>
|
</Routes>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{isServerlessFunctionSettingsEnabled && (
|
||||||
|
<>
|
||||||
|
<Route
|
||||||
|
path={SettingsPath.ServerlessFunctions}
|
||||||
|
element={<SettingsServerlessFunctions />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={SettingsPath.NewServerlessFunction}
|
||||||
|
element={<SettingsServerlessFunctionsNew />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path={SettingsPath.ServerlessFunctionDetail}
|
||||||
|
element={<SettingsServerlessFunctionDetailWrapper />}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<Route
|
<Route
|
||||||
path={SettingsPath.Integrations}
|
path={SettingsPath.Integrations}
|
||||||
element={<SettingsIntegrations />}
|
element={<SettingsIntegrations />}
|
||||||
@@ -304,10 +324,17 @@ const createRouter = (
|
|||||||
export const App = () => {
|
export const App = () => {
|
||||||
const billing = useRecoilValue(billingState);
|
const billing = useRecoilValue(billingState);
|
||||||
const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED');
|
const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED');
|
||||||
|
const isServerlessFunctionSettingsEnabled = useIsFeatureEnabled(
|
||||||
|
'IS_FUNCTION_SETTINGS_ENABLED',
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouterProvider
|
<RouterProvider
|
||||||
router={createRouter(billing?.isBillingEnabled, isCRMMigrationEnabled)}
|
router={createRouter(
|
||||||
|
billing?.isBillingEnabled,
|
||||||
|
isCRMMigrationEnabled,
|
||||||
|
isServerlessFunctionSettingsEnabled,
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -692,6 +692,19 @@ export type Sentry = {
|
|||||||
release?: Maybe<Scalars['String']>;
|
release?: Maybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ServerlessFunction = {
|
||||||
|
__typename?: 'ServerlessFunction';
|
||||||
|
createdAt: Scalars['DateTime'];
|
||||||
|
description: Scalars['String'];
|
||||||
|
id: Scalars['UUID'];
|
||||||
|
name: Scalars['String'];
|
||||||
|
runtime: Scalars['String'];
|
||||||
|
sourceCodeFullPath: Scalars['String'];
|
||||||
|
sourceCodeHash: Scalars['String'];
|
||||||
|
syncStatus: ServerlessFunctionSyncStatus;
|
||||||
|
updatedAt: Scalars['DateTime'];
|
||||||
|
};
|
||||||
|
|
||||||
export type ServerlessFunctionConnection = {
|
export type ServerlessFunctionConnection = {
|
||||||
__typename?: 'ServerlessFunctionConnection';
|
__typename?: 'ServerlessFunctionConnection';
|
||||||
/** Array of edges. */
|
/** Array of edges. */
|
||||||
@@ -700,6 +713,14 @@ export type ServerlessFunctionConnection = {
|
|||||||
pageInfo: PageInfo;
|
pageInfo: PageInfo;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ServerlessFunctionEdge = {
|
||||||
|
__typename?: 'ServerlessFunctionEdge';
|
||||||
|
/** Cursor for this node. */
|
||||||
|
cursor: Scalars['ConnectionCursor'];
|
||||||
|
/** The node containing the ServerlessFunction */
|
||||||
|
node: ServerlessFunction;
|
||||||
|
};
|
||||||
|
|
||||||
export type ServerlessFunctionExecutionResult = {
|
export type ServerlessFunctionExecutionResult = {
|
||||||
__typename?: 'ServerlessFunctionExecutionResult';
|
__typename?: 'ServerlessFunctionExecutionResult';
|
||||||
/** Execution result in JSON format */
|
/** Execution result in JSON format */
|
||||||
@@ -1090,24 +1111,6 @@ export type RelationEdge = {
|
|||||||
node: Relation;
|
node: Relation;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ServerlessFunction = {
|
|
||||||
__typename?: 'serverlessFunction';
|
|
||||||
createdAt: Scalars['DateTime'];
|
|
||||||
id: Scalars['UUID'];
|
|
||||||
name: Scalars['String'];
|
|
||||||
sourceCodeHash: Scalars['String'];
|
|
||||||
syncStatus: ServerlessFunctionSyncStatus;
|
|
||||||
updatedAt: Scalars['DateTime'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ServerlessFunctionEdge = {
|
|
||||||
__typename?: 'serverlessFunctionEdge';
|
|
||||||
/** Cursor for this node. */
|
|
||||||
cursor: Scalars['ConnectionCursor'];
|
|
||||||
/** The node containing the serverlessFunction */
|
|
||||||
node: ServerlessFunction;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type TimelineCalendarEventFragmentFragment = { __typename?: 'TimelineCalendarEvent', id: any, title: string, description: string, location: string, startsAt: string, endsAt: string, isFullDay: boolean, visibility: CalendarChannelVisibility, participants: Array<{ __typename?: 'TimelineCalendarEventParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> };
|
export type TimelineCalendarEventFragmentFragment = { __typename?: 'TimelineCalendarEvent', id: any, title: string, description: string, location: string, startsAt: string, endsAt: string, isFullDay: boolean, visibility: CalendarChannelVisibility, participants: Array<{ __typename?: 'TimelineCalendarEventParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> };
|
||||||
|
|
||||||
export type TimelineCalendarEventParticipantFragmentFragment = { __typename?: 'TimelineCalendarEventParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string };
|
export type TimelineCalendarEventParticipantFragmentFragment = { __typename?: 'TimelineCalendarEventParticipant', personId?: any | null, workspaceMemberId?: any | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string };
|
||||||
|
|||||||
@@ -23,13 +23,13 @@ import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDraw
|
|||||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
|
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
|
||||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
|
||||||
import { FileFolder, useUploadFileMutation } from '~/generated/graphql';
|
import { FileFolder, useUploadFileMutation } from '~/generated/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||||
|
|
||||||
import { getFileType } from '../files/utils/getFileType';
|
import { getFileType } from '../files/utils/getFileType';
|
||||||
|
|
||||||
|
import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI';
|
||||||
import '@blocknote/core/fonts/inter.css';
|
import '@blocknote/core/fonts/inter.css';
|
||||||
import '@blocknote/mantine/style.css';
|
import '@blocknote/mantine/style.css';
|
||||||
|
|
||||||
@@ -127,9 +127,7 @@ export const ActivityBodyEditor = ({
|
|||||||
if (!result?.data?.uploadFile) {
|
if (!result?.data?.uploadFile) {
|
||||||
throw new Error("Couldn't upload Image");
|
throw new Error("Couldn't upload Image");
|
||||||
}
|
}
|
||||||
const imageUrl =
|
return getFileAbsoluteURI(result.data.uploadFile);
|
||||||
REACT_APP_SERVER_BASE_URL + '/files/' + result?.data?.uploadFile;
|
|
||||||
return imageUrl;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePersistBody = useCallback(
|
const handlePersistBody = useCallback(
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ import {
|
|||||||
FieldContext,
|
FieldContext,
|
||||||
GenericFieldContextType,
|
GenericFieldContextType,
|
||||||
} from '@/object-record/record-field/contexts/FieldContext';
|
} from '@/object-record/record-field/contexts/FieldContext';
|
||||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
|
||||||
import { formatToHumanReadableDate } from '~/utils/date-utils';
|
import { formatToHumanReadableDate } from '~/utils/date-utils';
|
||||||
|
import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI';
|
||||||
|
|
||||||
const StyledRow = styled.div`
|
const StyledRow = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -76,7 +76,7 @@ export const AttachmentRow = ({ attachment }: { attachment: Attachment }) => {
|
|||||||
<StyledLeftContent>
|
<StyledLeftContent>
|
||||||
<AttachmentIcon attachmentType={attachment.type} />
|
<AttachmentIcon attachmentType={attachment.type} />
|
||||||
<StyledLink
|
<StyledLink
|
||||||
href={REACT_APP_SERVER_BASE_URL + '/files/' + attachment.fullPath}
|
href={getFileAbsoluteURI(attachment.fullPath)}
|
||||||
target="__blank"
|
target="__blank"
|
||||||
>
|
>
|
||||||
{attachment.name}
|
{attachment.name}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI';
|
||||||
|
|
||||||
export const downloadFile = (fullPath: string, fileName: string) => {
|
export const downloadFile = (fullPath: string, fileName: string) => {
|
||||||
fetch(REACT_APP_SERVER_BASE_URL + '/files/' + fullPath)
|
fetch(getFileAbsoluteURI(fullPath))
|
||||||
.then((resp) =>
|
.then((resp) =>
|
||||||
resp.status === 200
|
resp.status === 200
|
||||||
? resp.blob()
|
? resp.blob()
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
IconSettings,
|
IconSettings,
|
||||||
IconUserCircle,
|
IconUserCircle,
|
||||||
IconUsers,
|
IconUsers,
|
||||||
|
IconFunction,
|
||||||
} from 'twenty-ui';
|
} from 'twenty-ui';
|
||||||
|
|
||||||
import { useAuth } from '@/auth/hooks/useAuth';
|
import { useAuth } from '@/auth/hooks/useAuth';
|
||||||
@@ -29,6 +30,9 @@ export const SettingsNavigationDrawerItems = () => {
|
|||||||
const { signOut } = useAuth();
|
const { signOut } = useAuth();
|
||||||
|
|
||||||
const billing = useRecoilValue(billingState);
|
const billing = useRecoilValue(billingState);
|
||||||
|
const isFunctionSettingsEnabled = useIsFeatureEnabled(
|
||||||
|
'IS_FUNCTION_SETTINGS_ENABLED',
|
||||||
|
);
|
||||||
const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED');
|
const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -99,6 +103,13 @@ export const SettingsNavigationDrawerItems = () => {
|
|||||||
path={SettingsPath.Developers}
|
path={SettingsPath.Developers}
|
||||||
Icon={IconCode}
|
Icon={IconCode}
|
||||||
/>
|
/>
|
||||||
|
{isFunctionSettingsEnabled && (
|
||||||
|
<SettingsNavigationDrawerItem
|
||||||
|
label="Functions"
|
||||||
|
path={SettingsPath.ServerlessFunctions}
|
||||||
|
Icon={IconFunction}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<SettingsNavigationDrawerItem
|
<SettingsNavigationDrawerItem
|
||||||
label="Integrations"
|
label="Integrations"
|
||||||
path={SettingsPath.Integrations}
|
path={SettingsPath.Integrations}
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { H2Title } from 'twenty-ui';
|
||||||
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
import { TextArea } from '@/ui/input/components/TextArea';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { ServerlessFunctionNewFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||||
|
|
||||||
|
const StyledInputsContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(4)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsServerlessFunctionNewForm = ({
|
||||||
|
formValues,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
formValues: ServerlessFunctionNewFormValues;
|
||||||
|
onChange: (key: string) => (value: string) => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Section>
|
||||||
|
<H2Title title="About" description="Name and set your function" />
|
||||||
|
<StyledInputsContainer>
|
||||||
|
<TextInput
|
||||||
|
placeholder="Name"
|
||||||
|
fullWidth
|
||||||
|
focused
|
||||||
|
value={formValues.name}
|
||||||
|
onChange={onChange('name')}
|
||||||
|
/>
|
||||||
|
<TextArea
|
||||||
|
placeholder="Description"
|
||||||
|
minRows={4}
|
||||||
|
value={formValues.description}
|
||||||
|
onChange={onChange('description')}
|
||||||
|
/>
|
||||||
|
</StyledInputsContainer>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||||
|
import { ServerlessFunction } from '~/generated-metadata/graphql';
|
||||||
|
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||||
|
import { IconChevronRight } from 'twenty-ui';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
|
||||||
|
export const StyledApisFieldTableRow = styled(TableRow)`
|
||||||
|
grid-template-columns: 312px 132px 68px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledNameTableCell = styled(TableCell)`
|
||||||
|
color: ${({ theme }) => theme.font.color.primary};
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIconTableCell = styled(TableCell)`
|
||||||
|
justify-content: center;
|
||||||
|
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledIconChevronRight = styled(IconChevronRight)`
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsServerlessFunctionsFieldItemTableRow = ({
|
||||||
|
serverlessFunction,
|
||||||
|
to,
|
||||||
|
}: {
|
||||||
|
serverlessFunction: ServerlessFunction;
|
||||||
|
to: string;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<StyledApisFieldTableRow to={to}>
|
||||||
|
<StyledNameTableCell>{serverlessFunction.name}</StyledNameTableCell>
|
||||||
|
<StyledNameTableCell>{serverlessFunction.runtime}</StyledNameTableCell>
|
||||||
|
<StyledIconTableCell>
|
||||||
|
<StyledIconChevronRight
|
||||||
|
size={theme.icon.size.md}
|
||||||
|
stroke={theme.icon.stroke.sm}
|
||||||
|
/>
|
||||||
|
</StyledIconTableCell>
|
||||||
|
</StyledApisFieldTableRow>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||||
|
import { Table } from '@/ui/layout/table/components/Table';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||||
|
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||||
|
import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions';
|
||||||
|
import { SettingsServerlessFunctionsFieldItemTableRow } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsFieldItemTableRow';
|
||||||
|
import { ServerlessFunction } from '~/generated-metadata/graphql';
|
||||||
|
import { SettingsServerlessFunctionsTableEmpty } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsTableEmpty';
|
||||||
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
|
||||||
|
const StyledTableRow = styled(TableRow)`
|
||||||
|
grid-template-columns: 312px 132px 68px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledTableBody = styled(TableBody)`
|
||||||
|
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsServerlessFunctionsTable = () => {
|
||||||
|
const { serverlessFunctions } = useGetManyServerlessFunctions();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{serverlessFunctions.length ? (
|
||||||
|
<Table>
|
||||||
|
<StyledTableRow>
|
||||||
|
<TableHeader>Name</TableHeader>
|
||||||
|
<TableHeader>Runtime</TableHeader>
|
||||||
|
<TableHeader></TableHeader>
|
||||||
|
</StyledTableRow>
|
||||||
|
<StyledTableBody>
|
||||||
|
{serverlessFunctions.map(
|
||||||
|
(serverlessFunction: ServerlessFunction) => (
|
||||||
|
<SettingsServerlessFunctionsFieldItemTableRow
|
||||||
|
key={serverlessFunction.id}
|
||||||
|
serverlessFunction={serverlessFunction}
|
||||||
|
to={getSettingsPagePath(SettingsPath.ServerlessFunctions, {
|
||||||
|
id: serverlessFunction.id,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</StyledTableBody>
|
||||||
|
</Table>
|
||||||
|
) : (
|
||||||
|
<SettingsServerlessFunctionsTableEmpty />
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import {
|
||||||
|
AnimatedPlaceholderEmptyContainer,
|
||||||
|
AnimatedPlaceholderEmptySubTitle,
|
||||||
|
AnimatedPlaceholderEmptyTextContainer,
|
||||||
|
AnimatedPlaceholderEmptyTitle,
|
||||||
|
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
|
||||||
|
} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled';
|
||||||
|
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
|
||||||
|
import { IconPlus } from 'twenty-ui';
|
||||||
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
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={getSettingsPagePath(SettingsPath.NewServerlessFunction)}
|
||||||
|
/>
|
||||||
|
</AnimatedPlaceholderEmptyContainer>
|
||||||
|
</StyledEmptyFunctionsContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { H2Title, IconPlayerPlay } from 'twenty-ui';
|
||||||
|
import { CodeEditor } from '@/ui/input/code-editor/components/CodeEditor';
|
||||||
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||||
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
|
import { CoreEditorHeader } from '@/ui/input/code-editor/components/CodeEditorHeader';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||||
|
|
||||||
|
const StyledTabList = styled(TabList)`
|
||||||
|
border-bottom: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsServerlessFunctionCodeEditorTab = ({
|
||||||
|
formValues,
|
||||||
|
handleExecute,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
formValues: ServerlessFunctionFormValues;
|
||||||
|
handleExecute: () => void;
|
||||||
|
onChange: (key: string) => (value: string) => void;
|
||||||
|
}) => {
|
||||||
|
const HeaderButton = (
|
||||||
|
<Button
|
||||||
|
title="Test"
|
||||||
|
variant="primary"
|
||||||
|
accent="blue"
|
||||||
|
size="small"
|
||||||
|
Icon={IconPlayerPlay}
|
||||||
|
onClick={handleExecute}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const TAB_LIST_COMPONENT_ID = 'serverless-function-editor';
|
||||||
|
|
||||||
|
const HeaderTabList = (
|
||||||
|
<StyledTabList
|
||||||
|
tabListId={TAB_LIST_COMPONENT_ID}
|
||||||
|
tabs={[{ id: 'index.ts', title: 'index.ts' }]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Header = (
|
||||||
|
<CoreEditorHeader leftNodes={[HeaderTabList]} rightNodes={[HeaderButton]} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section>
|
||||||
|
<H2Title
|
||||||
|
title="Code your function"
|
||||||
|
description="Write your function (in typescript) below"
|
||||||
|
/>
|
||||||
|
<CodeEditor
|
||||||
|
value={formValues.code}
|
||||||
|
onChange={onChange('code')}
|
||||||
|
header={Header}
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { H2Title } from 'twenty-ui';
|
||||||
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||||
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||||
|
import { SettingsServerlessFunctionNewForm } from '@/settings/serverless-functions/components/SettingsServerlessFunctionNewForm';
|
||||||
|
import { useDeleteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useDeleteOneServerlessFunction';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const SettingsServerlessFunctionSettingsTab = ({
|
||||||
|
formValues,
|
||||||
|
serverlessFunctionId,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
formValues: ServerlessFunctionFormValues;
|
||||||
|
serverlessFunctionId: string;
|
||||||
|
onChange: (key: string) => (value: string) => void;
|
||||||
|
}) => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isDeleteFunctionModalOpen, setIsDeleteFunctionModalOpen] =
|
||||||
|
useState(false);
|
||||||
|
const { deleteOneServerlessFunction } = useDeleteOneServerlessFunction();
|
||||||
|
|
||||||
|
const deleteFunction = async () => {
|
||||||
|
await deleteOneServerlessFunction({ id: serverlessFunctionId });
|
||||||
|
navigate('/settings/functions');
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SettingsServerlessFunctionNewForm
|
||||||
|
formValues={formValues}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
<Section>
|
||||||
|
<H2Title title="Danger zone" description="Delete this function" />
|
||||||
|
<Button
|
||||||
|
accent="danger"
|
||||||
|
onClick={() => setIsDeleteFunctionModalOpen(true)}
|
||||||
|
variant="secondary"
|
||||||
|
size="small"
|
||||||
|
title="Delete function"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
<ConfirmationModal
|
||||||
|
confirmationValue={formValues.name}
|
||||||
|
confirmationPlaceholder={formValues.name}
|
||||||
|
isOpen={isDeleteFunctionModalOpen}
|
||||||
|
setIsOpen={setIsDeleteFunctionModalOpen}
|
||||||
|
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}
|
||||||
|
deleteButtonText="Delete function"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { H2Title, IconPlayerPlay } from 'twenty-ui';
|
||||||
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
|
||||||
|
import { CodeEditor } from '@/ui/input/code-editor/components/CodeEditor';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { CoreEditorHeader } from '@/ui/input/code-editor/components/CodeEditorHeader';
|
||||||
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
|
import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton';
|
||||||
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
|
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
||||||
|
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
||||||
|
import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState';
|
||||||
|
|
||||||
|
const StyledInputsContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: ${({ theme }) => theme.spacing(4)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const SettingsServerlessFunctionTestTab = ({
|
||||||
|
handleExecute,
|
||||||
|
}: {
|
||||||
|
handleExecute: () => void;
|
||||||
|
}) => {
|
||||||
|
const settingsServerlessFunctionCodeEditorOutputParams = useRecoilValue(
|
||||||
|
settingsServerlessFunctionCodeEditorOutputParamsState,
|
||||||
|
);
|
||||||
|
const settingsServerlessFunctionOutput = useRecoilValue(
|
||||||
|
settingsServerlessFunctionOutputState,
|
||||||
|
);
|
||||||
|
const [settingsServerlessFunctionInput, setSettingsServerlessFunctionInput] =
|
||||||
|
useRecoilState(settingsServerlessFunctionInputState);
|
||||||
|
|
||||||
|
const InputHeaderButton = (
|
||||||
|
<Button
|
||||||
|
title="Run Function"
|
||||||
|
variant="primary"
|
||||||
|
accent="blue"
|
||||||
|
size="small"
|
||||||
|
Icon={IconPlayerPlay}
|
||||||
|
onClick={handleExecute}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const InputHeader = (
|
||||||
|
<CoreEditorHeader title={'Input'} rightNodes={[InputHeaderButton]} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const OutputHeaderButton = (
|
||||||
|
<LightCopyIconButton copyText={settingsServerlessFunctionOutput} />
|
||||||
|
);
|
||||||
|
|
||||||
|
const OutputHeader = (
|
||||||
|
<CoreEditorHeader title={'Output'} rightNodes={[OutputHeaderButton]} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Section>
|
||||||
|
<H2Title
|
||||||
|
title="Test your function"
|
||||||
|
description='Insert a JSON input, then press "Run" to test your function.'
|
||||||
|
/>
|
||||||
|
<StyledInputsContainer>
|
||||||
|
<CodeEditor
|
||||||
|
value={settingsServerlessFunctionInput}
|
||||||
|
height={200}
|
||||||
|
onChange={setSettingsServerlessFunctionInput}
|
||||||
|
language={'json'}
|
||||||
|
header={InputHeader}
|
||||||
|
/>
|
||||||
|
<CodeEditor
|
||||||
|
value={settingsServerlessFunctionOutput}
|
||||||
|
height={settingsServerlessFunctionCodeEditorOutputParams.height}
|
||||||
|
language={settingsServerlessFunctionCodeEditorOutputParams.language}
|
||||||
|
options={{ readOnly: true, domReadOnly: true }}
|
||||||
|
header={OutputHeader}
|
||||||
|
/>
|
||||||
|
</StyledInputsContainer>
|
||||||
|
</Section>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
||||||
|
import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState';
|
||||||
|
|
||||||
|
export const SettingsServerlessFunctionTestTabEffect = () => {
|
||||||
|
const settingsServerlessFunctionOutput = useRecoilValue(
|
||||||
|
settingsServerlessFunctionOutputState,
|
||||||
|
);
|
||||||
|
const setSettingsServerlessFunctionCodeEditorOutputParams = useSetRecoilState(
|
||||||
|
settingsServerlessFunctionCodeEditorOutputParamsState,
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
JSON.parse(settingsServerlessFunctionOutput);
|
||||||
|
setSettingsServerlessFunctionCodeEditorOutputParams({
|
||||||
|
language: 'json',
|
||||||
|
height: 300,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const SERVERLESS_FUNCTION_FRAGMENT = gql`
|
||||||
|
fragment ServerlessFunctionFields on ServerlessFunction {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
description
|
||||||
|
sourceCodeHash
|
||||||
|
sourceCodeFullPath
|
||||||
|
runtime
|
||||||
|
syncStatus
|
||||||
|
createdAt
|
||||||
|
updatedAt
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
|
||||||
|
|
||||||
|
export const CREATE_ONE_SERVERLESS_FUNCTION = gql`
|
||||||
|
${SERVERLESS_FUNCTION_FRAGMENT}
|
||||||
|
mutation CreateOneServerlessFunctionItem(
|
||||||
|
$input: CreateServerlessFunctionInput!
|
||||||
|
) {
|
||||||
|
createOneServerlessFunction(input: $input) {
|
||||||
|
...ServerlessFunctionFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
|
||||||
|
|
||||||
|
export const DELETE_ONE_SERVERLESS_FUNCTION = gql`
|
||||||
|
${SERVERLESS_FUNCTION_FRAGMENT}
|
||||||
|
mutation DeleteOneServerlessFunction($input: DeleteServerlessFunctionInput!) {
|
||||||
|
deleteOneServerlessFunction(input: $input) {
|
||||||
|
...ServerlessFunctionFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const EXECUTE_ONE_SERVERLESS_FUNCTION = gql`
|
||||||
|
mutation ExecuteOneServerlessFunction($id: UUID!, $payload: JSON!) {
|
||||||
|
executeOneServerlessFunction(id: $id, payload: $payload) {
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
|
||||||
|
|
||||||
|
export const UPDATE_ONE_SERVERLESS_FUNCTION = gql`
|
||||||
|
${SERVERLESS_FUNCTION_FRAGMENT}
|
||||||
|
mutation UpdateOneServerlessFunction($input: UpdateServerlessFunctionInput!) {
|
||||||
|
updateOneServerlessFunction(input: $input) {
|
||||||
|
...ServerlessFunctionFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
|
||||||
|
|
||||||
|
export const FIND_MANY_SERVERLESS_FUNCTIONS = gql`
|
||||||
|
${SERVERLESS_FUNCTION_FRAGMENT}
|
||||||
|
query GetManyServerlessFunctions {
|
||||||
|
serverlessFunctions(paging: { first: 100 }) {
|
||||||
|
edges {
|
||||||
|
node {
|
||||||
|
...ServerlessFunctionFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
|
||||||
|
|
||||||
|
export const FIND_ONE_SERVERLESS_FUNCTION = gql`
|
||||||
|
${SERVERLESS_FUNCTION_FRAGMENT}
|
||||||
|
query GetOneServerlessFunction($id: UUID!) {
|
||||||
|
serverlessFunction(id: $id) {
|
||||||
|
...ServerlessFunctionFields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||||
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
|
jest.mock(
|
||||||
|
'@/settings/serverless-functions/hooks/useGetOneServerlessFunction',
|
||||||
|
() => ({
|
||||||
|
useGetOneServerlessFunction: jest.fn(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
describe('useServerlessFunctionUpdateFormState', () => {
|
||||||
|
test('should return a form', () => {
|
||||||
|
const serverlessFunctionId = 'serverlessFunctionId';
|
||||||
|
const useGetOneServerlessFunctionMock = jest.requireMock(
|
||||||
|
'@/settings/serverless-functions/hooks/useGetOneServerlessFunction',
|
||||||
|
);
|
||||||
|
useGetOneServerlessFunctionMock.useGetOneServerlessFunction.mockReturnValue(
|
||||||
|
{
|
||||||
|
serverlessFunction: { sourceCodeFullPath: undefined },
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const { result } = renderHook(
|
||||||
|
() => useServerlessFunctionUpdateFormState(serverlessFunctionId),
|
||||||
|
{
|
||||||
|
wrapper: RecoilRoot,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const [formValues] = result.current;
|
||||||
|
|
||||||
|
expect(formValues).toEqual({ name: '', description: '', code: '' });
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { ApolloClient, useMutation } from '@apollo/client';
|
||||||
|
|
||||||
|
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||||
|
import {
|
||||||
|
CreateServerlessFunctionInput,
|
||||||
|
CreateOneServerlessFunctionItemMutation,
|
||||||
|
CreateOneServerlessFunctionItemMutationVariables,
|
||||||
|
} from '~/generated-metadata/graphql';
|
||||||
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
|
import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions';
|
||||||
|
import { CREATE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/createOneServerlessFunction';
|
||||||
|
|
||||||
|
export const useCreateOneServerlessFunction = () => {
|
||||||
|
const apolloMetadataClient = useApolloMetadataClient();
|
||||||
|
const [mutate] = useMutation<
|
||||||
|
CreateOneServerlessFunctionItemMutation,
|
||||||
|
CreateOneServerlessFunctionItemMutationVariables
|
||||||
|
>(CREATE_ONE_SERVERLESS_FUNCTION, {
|
||||||
|
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
|
||||||
|
});
|
||||||
|
|
||||||
|
const createOneServerlessFunction = async (
|
||||||
|
input: CreateServerlessFunctionInput,
|
||||||
|
) => {
|
||||||
|
return await mutate({
|
||||||
|
variables: {
|
||||||
|
input,
|
||||||
|
},
|
||||||
|
awaitRefetchQueries: true,
|
||||||
|
refetchQueries: [getOperationName(FIND_MANY_SERVERLESS_FUNCTIONS) ?? ''],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { createOneServerlessFunction };
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||||
|
import { ApolloClient, useMutation } from '@apollo/client';
|
||||||
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
|
import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions';
|
||||||
|
import { DELETE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/deleteOneServerlessFunction';
|
||||||
|
import {
|
||||||
|
DeleteServerlessFunctionInput,
|
||||||
|
DeleteOneServerlessFunctionMutation,
|
||||||
|
DeleteOneServerlessFunctionMutationVariables,
|
||||||
|
} from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
export const useDeleteOneServerlessFunction = () => {
|
||||||
|
const apolloMetadataClient = useApolloMetadataClient();
|
||||||
|
const [mutate] = useMutation<
|
||||||
|
DeleteOneServerlessFunctionMutation,
|
||||||
|
DeleteOneServerlessFunctionMutationVariables
|
||||||
|
>(DELETE_ONE_SERVERLESS_FUNCTION, {
|
||||||
|
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteOneServerlessFunction = async (
|
||||||
|
input: DeleteServerlessFunctionInput,
|
||||||
|
) => {
|
||||||
|
return await mutate({
|
||||||
|
variables: {
|
||||||
|
input,
|
||||||
|
},
|
||||||
|
awaitRefetchQueries: true,
|
||||||
|
refetchQueries: [getOperationName(FIND_MANY_SERVERLESS_FUNCTIONS) ?? ''],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { deleteOneServerlessFunction };
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||||
|
import { ApolloClient, useMutation } from '@apollo/client';
|
||||||
|
import { EXECUTE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/executeOneServerlessFunction';
|
||||||
|
import {
|
||||||
|
ExecuteOneServerlessFunctionMutation,
|
||||||
|
ExecuteOneServerlessFunctionMutationVariables,
|
||||||
|
} from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
export const useExecuteOneServerlessFunction = () => {
|
||||||
|
const apolloMetadataClient = useApolloMetadataClient();
|
||||||
|
const [mutate] = useMutation<
|
||||||
|
ExecuteOneServerlessFunctionMutation,
|
||||||
|
ExecuteOneServerlessFunctionMutationVariables
|
||||||
|
>(EXECUTE_ONE_SERVERLESS_FUNCTION, {
|
||||||
|
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
|
||||||
|
});
|
||||||
|
|
||||||
|
const executeOneServerlessFunction = async (
|
||||||
|
id: string,
|
||||||
|
payload: object = {},
|
||||||
|
) => {
|
||||||
|
return await mutate({
|
||||||
|
variables: {
|
||||||
|
id,
|
||||||
|
payload,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return { executeOneServerlessFunction };
|
||||||
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { useQuery } from '@apollo/client';
|
||||||
|
import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions';
|
||||||
|
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||||
|
import {
|
||||||
|
GetManyServerlessFunctionsQuery,
|
||||||
|
GetManyServerlessFunctionsQueryVariables,
|
||||||
|
} from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
export const useGetManyServerlessFunctions = () => {
|
||||||
|
const apolloMetadataClient = useApolloMetadataClient();
|
||||||
|
const { data } = useQuery<
|
||||||
|
GetManyServerlessFunctionsQuery,
|
||||||
|
GetManyServerlessFunctionsQueryVariables
|
||||||
|
>(FIND_MANY_SERVERLESS_FUNCTIONS, {
|
||||||
|
client: apolloMetadataClient ?? undefined,
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
serverlessFunctions:
|
||||||
|
data?.serverlessFunctions?.edges.map(({ node }) => node) || [],
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { useQuery } from '@apollo/client';
|
||||||
|
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||||
|
import { FIND_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunction';
|
||||||
|
import {
|
||||||
|
GetOneServerlessFunctionQuery,
|
||||||
|
GetOneServerlessFunctionQueryVariables,
|
||||||
|
} from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
export const useGetOneServerlessFunction = (id: string) => {
|
||||||
|
const apolloMetadataClient = useApolloMetadataClient();
|
||||||
|
const { data } = useQuery<
|
||||||
|
GetOneServerlessFunctionQuery,
|
||||||
|
GetOneServerlessFunctionQueryVariables
|
||||||
|
>(FIND_ONE_SERVERLESS_FUNCTION, {
|
||||||
|
client: apolloMetadataClient ?? undefined,
|
||||||
|
variables: {
|
||||||
|
id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
serverlessFunction: data?.serverlessFunction || null,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||||
|
import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { useGetOneServerlessFunction } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunction';
|
||||||
|
|
||||||
|
export type ServerlessFunctionNewFormValues = {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ServerlessFunctionFormValues = ServerlessFunctionNewFormValues & {
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SetServerlessFunctionFormValues = Dispatch<
|
||||||
|
SetStateAction<ServerlessFunctionFormValues>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const useServerlessFunctionUpdateFormState = (
|
||||||
|
serverlessFunctionId: string,
|
||||||
|
): [ServerlessFunctionFormValues, SetServerlessFunctionFormValues] => {
|
||||||
|
const [formValues, setFormValues] = useState<ServerlessFunctionFormValues>({
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
code: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { serverlessFunction } =
|
||||||
|
useGetOneServerlessFunction(serverlessFunctionId);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const getFileContent = async () => {
|
||||||
|
const resp = await fetch(
|
||||||
|
getFileAbsoluteURI(serverlessFunction?.sourceCodeFullPath),
|
||||||
|
);
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
throw new Error('Network response was not ok');
|
||||||
|
} else {
|
||||||
|
const result = await resp.text();
|
||||||
|
const newState = {
|
||||||
|
code: result,
|
||||||
|
name: serverlessFunction?.name || '',
|
||||||
|
description: serverlessFunction?.description || '',
|
||||||
|
};
|
||||||
|
setFormValues((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
...newState,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (isDefined(serverlessFunction?.sourceCodeFullPath)) {
|
||||||
|
getFileContent();
|
||||||
|
}
|
||||||
|
}, [serverlessFunction, setFormValues]);
|
||||||
|
|
||||||
|
return [formValues, setFormValues];
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { ApolloClient, useMutation } from '@apollo/client';
|
||||||
|
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||||
|
import { UPDATE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/updateOneServerlessFunction';
|
||||||
|
import {
|
||||||
|
UpdateServerlessFunctionInput,
|
||||||
|
UpdateOneServerlessFunctionMutation,
|
||||||
|
UpdateOneServerlessFunctionMutationVariables,
|
||||||
|
} from '~/generated-metadata/graphql';
|
||||||
|
import { getOperationName } from '@apollo/client/utilities';
|
||||||
|
import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions';
|
||||||
|
|
||||||
|
export const useUpdateOneServerlessFunction = () => {
|
||||||
|
const apolloMetadataClient = useApolloMetadataClient();
|
||||||
|
const [mutate] = useMutation<
|
||||||
|
UpdateOneServerlessFunctionMutation,
|
||||||
|
UpdateOneServerlessFunctionMutationVariables
|
||||||
|
>(UPDATE_ONE_SERVERLESS_FUNCTION, {
|
||||||
|
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateOneServerlessFunction = async (
|
||||||
|
input: UpdateServerlessFunctionInput,
|
||||||
|
) => {
|
||||||
|
return await mutate({
|
||||||
|
variables: {
|
||||||
|
input,
|
||||||
|
},
|
||||||
|
awaitRefetchQueries: true,
|
||||||
|
refetchQueries: [getOperationName(FIND_MANY_SERVERLESS_FUNCTIONS) ?? ''],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return { updateOneServerlessFunction };
|
||||||
|
};
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { createState } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const settingsServerlessFunctionCodeEditorOutputParamsState =
|
||||||
|
createState<{ language: string; height: number }>({
|
||||||
|
key: 'settingsServerlessFunctionCodeEditorOutputParamsState',
|
||||||
|
defaultValue: { language: 'plaintext', height: 64 },
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { createState } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const settingsServerlessFunctionInputState = createState<string>({
|
||||||
|
key: 'settingsServerlessFunctionInputState',
|
||||||
|
defaultValue: '{}',
|
||||||
|
});
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { createState } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const settingsServerlessFunctionOutputState = createState<string>({
|
||||||
|
key: 'settingsServerlessFunctionOutputState',
|
||||||
|
defaultValue: 'Enter an input above then press "run Function"',
|
||||||
|
});
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
|
||||||
|
describe('getSettingsPagePath', () => {
|
||||||
|
test('should compute page path', () => {
|
||||||
|
expect(getSettingsPagePath(SettingsPath.ServerlessFunctions)).toEqual(
|
||||||
|
'/settings/functions',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test('should compute page path with id', () => {
|
||||||
|
expect(
|
||||||
|
getSettingsPagePath(SettingsPath.ServerlessFunctions, { id: 'id' }),
|
||||||
|
).toEqual('/settings/functions/id');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -2,6 +2,7 @@ import { SettingsPath } from '@/types/SettingsPath';
|
|||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
type PathParams = {
|
type PathParams = {
|
||||||
|
id?: string;
|
||||||
objectSlug?: string;
|
objectSlug?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -15,5 +16,9 @@ export const getSettingsPagePath = <Path extends SettingsPath>(
|
|||||||
resultPath = resultPath.replace(':objectSlug', params.objectSlug);
|
resultPath = resultPath.replace(':objectSlug', params.objectSlug);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDefined(params?.id)) {
|
||||||
|
resultPath = `${resultPath}/${params?.id}`;
|
||||||
|
}
|
||||||
|
|
||||||
return resultPath;
|
return resultPath;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,10 +16,13 @@ export enum SettingsPath {
|
|||||||
ObjectNewFieldStep2 = 'objects/:objectSlug/new-field/step-2',
|
ObjectNewFieldStep2 = 'objects/:objectSlug/new-field/step-2',
|
||||||
ObjectFieldEdit = 'objects/:objectSlug/:fieldSlug',
|
ObjectFieldEdit = 'objects/:objectSlug/:fieldSlug',
|
||||||
NewObject = 'objects/new',
|
NewObject = 'objects/new',
|
||||||
|
NewServerlessFunction = 'functions/new',
|
||||||
|
ServerlessFunctionDetail = 'functions/:serverlessFunctionId',
|
||||||
WorkspaceMembersPage = 'workspace-members',
|
WorkspaceMembersPage = 'workspace-members',
|
||||||
Workspace = 'workspace',
|
Workspace = 'workspace',
|
||||||
CRMMigration = 'crm-migration',
|
CRMMigration = 'crm-migration',
|
||||||
Developers = 'developers',
|
Developers = 'developers',
|
||||||
|
ServerlessFunctions = 'functions',
|
||||||
DevelopersNewApiKey = 'api-keys/new',
|
DevelopersNewApiKey = 'api-keys/new',
|
||||||
DevelopersApiKeyDetail = 'api-keys/:apiKeyId',
|
DevelopersApiKeyDetail = 'api-keys/:apiKeyId',
|
||||||
Integrations = 'integrations',
|
Integrations = 'integrations',
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import Editor, { Monaco, EditorProps } from '@monaco-editor/react';
|
||||||
|
import { editor } from 'monaco-editor';
|
||||||
|
import { codeEditorTheme } from '@/ui/input/code-editor/theme/CodeEditorTheme';
|
||||||
|
import { useTheme } from '@emotion/react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export const DEFAULT_CODE = `export const handler = async (
|
||||||
|
event: object,
|
||||||
|
context: object
|
||||||
|
): Promise<object> => {
|
||||||
|
// Your code here
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledEditor = styled(Editor)`
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
border-top: none;
|
||||||
|
border-radius: 0 0 ${({ theme }) => theme.border.radius.sm}
|
||||||
|
${({ theme }) => theme.border.radius.sm};
|
||||||
|
`;
|
||||||
|
|
||||||
|
type CodeEditorProps = Omit<EditorProps, 'onChange'> & {
|
||||||
|
header: React.ReactNode;
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CodeEditor = ({
|
||||||
|
value = DEFAULT_CODE,
|
||||||
|
onChange,
|
||||||
|
language = 'typescript',
|
||||||
|
height = 500,
|
||||||
|
options = undefined,
|
||||||
|
header,
|
||||||
|
}: CodeEditorProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const handleEditorDidMount = (
|
||||||
|
editor: editor.IStandaloneCodeEditor,
|
||||||
|
monaco: Monaco,
|
||||||
|
) => {
|
||||||
|
monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme));
|
||||||
|
monaco.editor.setTheme('codeEditorTheme');
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
const style = document.createElement('style');
|
||||||
|
style.innerHTML = `
|
||||||
|
.monaco-editor .margin .line-numbers {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
document.head.appendChild(style);
|
||||||
|
return () => {
|
||||||
|
document.head.removeChild(style);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{header}
|
||||||
|
<StyledEditor
|
||||||
|
height={height}
|
||||||
|
language={language}
|
||||||
|
value={value}
|
||||||
|
onMount={handleEditorDidMount}
|
||||||
|
onChange={(value?: string) => value && onChange?.(value)}
|
||||||
|
options={{
|
||||||
|
...options,
|
||||||
|
overviewRulerLanes: 0,
|
||||||
|
scrollbar: {
|
||||||
|
vertical: 'hidden',
|
||||||
|
horizontal: 'hidden',
|
||||||
|
},
|
||||||
|
minimap: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
const StyledEditorHeader = styled.div`
|
||||||
|
align-items: center;
|
||||||
|
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
|
display: flex;
|
||||||
|
height: ${({ theme }) => theme.spacing(10)};
|
||||||
|
padding: ${({ theme }) => `0 ${theme.spacing(2)}`};
|
||||||
|
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||||
|
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
border-top-right-radius: ${({ theme }) => theme.border.radius.sm};
|
||||||
|
justify-content: space-between;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledElementContainer = styled.div`
|
||||||
|
align-content: flex-end;
|
||||||
|
display: flex;
|
||||||
|
gap: ${({ theme }) => theme.spacing(2)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
export type CoreEditorHeaderProps = {
|
||||||
|
title?: string;
|
||||||
|
leftNodes?: React.ReactNode[];
|
||||||
|
rightNodes?: React.ReactNode[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CoreEditorHeader = ({
|
||||||
|
title,
|
||||||
|
leftNodes,
|
||||||
|
rightNodes,
|
||||||
|
}: CoreEditorHeaderProps) => {
|
||||||
|
return (
|
||||||
|
<StyledEditorHeader>
|
||||||
|
<StyledElementContainer>
|
||||||
|
{leftNodes &&
|
||||||
|
leftNodes.map((leftButton, index) => {
|
||||||
|
return <div key={`left-${index}`}>{leftButton}</div>;
|
||||||
|
})}
|
||||||
|
{title}
|
||||||
|
</StyledElementContainer>
|
||||||
|
<StyledElementContainer>
|
||||||
|
{rightNodes &&
|
||||||
|
rightNodes.map((rightButton, index) => {
|
||||||
|
return <div key={`right-${index}`}>{rightButton}</div>;
|
||||||
|
})}
|
||||||
|
</StyledElementContainer>
|
||||||
|
</StyledEditorHeader>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { editor } from 'monaco-editor';
|
||||||
|
import { ThemeType } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const codeEditorTheme = (theme: ThemeType) => {
|
||||||
|
return {
|
||||||
|
base: 'vs' as editor.BuiltinTheme,
|
||||||
|
inherit: true,
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
token: '',
|
||||||
|
foreground: theme.code.text.gray,
|
||||||
|
fontStyle: 'bold',
|
||||||
|
},
|
||||||
|
{ token: 'keyword', foreground: theme.code.text.sky },
|
||||||
|
{
|
||||||
|
token: 'delimiter',
|
||||||
|
foreground: theme.code.text.gray,
|
||||||
|
},
|
||||||
|
{ token: 'string', foreground: theme.code.text.pink },
|
||||||
|
{
|
||||||
|
token: 'comment',
|
||||||
|
foreground: theme.code.text.orange,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
colors: {
|
||||||
|
'editor.background': theme.background.secondary,
|
||||||
|
'editorCursor.foreground': theme.font.color.primary,
|
||||||
|
'editorLineNumber.foreground': theme.font.color.extraLight,
|
||||||
|
'editorLineNumber.activeForeground': theme.font.color.light,
|
||||||
|
'editor.lineHighlightBackground': theme.background.tertiary,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -29,7 +29,6 @@ const StyledTextArea = styled(TextareaAutosize)`
|
|||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
padding: ${({ theme }) => theme.spacing(2)};
|
padding: ${({ theme }) => theme.spacing(2)};
|
||||||
padding-top: ${({ theme }) => theme.spacing(3)};
|
|
||||||
resize: none;
|
resize: none;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const BACKGROUND: Record<string, string> = {
|
|||||||
emptyTimeline: '/images/placeholders/background/empty_timeline_bg.png',
|
emptyTimeline: '/images/placeholders/background/empty_timeline_bg.png',
|
||||||
loadingMessages: '/images/placeholders/background/loading_messages_bg.png',
|
loadingMessages: '/images/placeholders/background/loading_messages_bg.png',
|
||||||
loadingAccounts: '/images/placeholders/background/loading_accounts_bg.png',
|
loadingAccounts: '/images/placeholders/background/loading_accounts_bg.png',
|
||||||
|
emptyFunctions: '/images/placeholders/background/empty_functions_bg.png',
|
||||||
emptyInbox: '/images/placeholders/background/empty_inbox_bg.png',
|
emptyInbox: '/images/placeholders/background/empty_inbox_bg.png',
|
||||||
error404: '/images/placeholders/background/404_bg.png',
|
error404: '/images/placeholders/background/404_bg.png',
|
||||||
error500: '/images/placeholders/background/500_bg.png',
|
error500: '/images/placeholders/background/500_bg.png',
|
||||||
|
|||||||
@@ -11,4 +11,5 @@ export const DARK_BACKGROUND: Record<string, string> = {
|
|||||||
error500: '/images/placeholders/dark-background/500_bg.png',
|
error500: '/images/placeholders/dark-background/500_bg.png',
|
||||||
loadingMessages: '/images/placeholders/background/loading_messages_bg.png',
|
loadingMessages: '/images/placeholders/background/loading_messages_bg.png',
|
||||||
loadingAccounts: '/images/placeholders/background/loading_accounts_bg.png',
|
loadingAccounts: '/images/placeholders/background/loading_accounts_bg.png',
|
||||||
|
emptyFunctions: '/images/placeholders/dark-background/empty_functions_bg.png',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,4 +11,5 @@ export const DARK_MOVING_IMAGE: Record<string, string> = {
|
|||||||
error500: '/images/placeholders/dark-moving-image/500.png',
|
error500: '/images/placeholders/dark-moving-image/500.png',
|
||||||
loadingMessages: '/images/placeholders/moving-image/loading_messages.png',
|
loadingMessages: '/images/placeholders/moving-image/loading_messages.png',
|
||||||
loadingAccounts: '/images/placeholders/moving-image/loading_accounts.png',
|
loadingAccounts: '/images/placeholders/moving-image/loading_accounts.png',
|
||||||
|
emptyFunctions: '/images/placeholders/dark-moving-image/empty_functions.png',
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export const MOVING_IMAGE: Record<string, string> = {
|
|||||||
emptyTimeline: '/images/placeholders/moving-image/empty_timeline.png',
|
emptyTimeline: '/images/placeholders/moving-image/empty_timeline.png',
|
||||||
loadingMessages: '/images/placeholders/moving-image/loading_messages.png',
|
loadingMessages: '/images/placeholders/moving-image/loading_messages.png',
|
||||||
loadingAccounts: '/images/placeholders/moving-image/loading_accounts.png',
|
loadingAccounts: '/images/placeholders/moving-image/loading_accounts.png',
|
||||||
|
emptyFunctions: '/images/placeholders/moving-image/empty_functions.png',
|
||||||
emptyInbox: '/images/placeholders/moving-image/empty_inbox.png',
|
emptyInbox: '/images/placeholders/moving-image/empty_inbox.png',
|
||||||
error404: '/images/placeholders/moving-image/404.png',
|
error404: '/images/placeholders/moving-image/404.png',
|
||||||
error500: '/images/placeholders/moving-image/500.png',
|
error500: '/images/placeholders/moving-image/500.png',
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type TabListProps = {
|
|||||||
tabListId: string;
|
tabListId: string;
|
||||||
tabs: SingleTabProps[];
|
tabs: SingleTabProps[];
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@@ -34,7 +35,12 @@ const StyledContainer = styled.div`
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const TabList = ({ tabs, tabListId, loading }: TabListProps) => {
|
export const TabList = ({
|
||||||
|
tabs,
|
||||||
|
tabListId,
|
||||||
|
loading,
|
||||||
|
className,
|
||||||
|
}: TabListProps) => {
|
||||||
const initialActiveTabId = tabs.find((tab) => !tab.hide)?.id || '';
|
const initialActiveTabId = tabs.find((tab) => !tab.hide)?.id || '';
|
||||||
|
|
||||||
const { activeTabIdState, setActiveTabId } = useTabList(tabListId);
|
const { activeTabIdState, setActiveTabId } = useTabList(tabListId);
|
||||||
@@ -48,7 +54,7 @@ export const TabList = ({ tabs, tabListId, loading }: TabListProps) => {
|
|||||||
return (
|
return (
|
||||||
<TabListScope tabListScopeId={tabListId}>
|
<TabListScope tabListScopeId={tabListId}>
|
||||||
<ScrollWrapper hideY>
|
<ScrollWrapper hideY>
|
||||||
<StyledContainer>
|
<StyledContainer className={className}>
|
||||||
{tabs
|
{tabs
|
||||||
.filter((tab) => !tab.hide)
|
.filter((tab) => !tab.hide)
|
||||||
.map((tab) => (
|
.map((tab) => (
|
||||||
|
|||||||
@@ -5,5 +5,6 @@ export type FeatureFlagKey =
|
|||||||
| 'IS_AIRTABLE_INTEGRATION_ENABLED'
|
| 'IS_AIRTABLE_INTEGRATION_ENABLED'
|
||||||
| 'IS_POSTGRESQL_INTEGRATION_ENABLED'
|
| 'IS_POSTGRESQL_INTEGRATION_ENABLED'
|
||||||
| 'IS_STRIPE_INTEGRATION_ENABLED'
|
| 'IS_STRIPE_INTEGRATION_ENABLED'
|
||||||
|
| 'IS_FUNCTION_SETTINGS_ENABLED'
|
||||||
| 'IS_COPILOT_ENABLED'
|
| 'IS_COPILOT_ENABLED'
|
||||||
| 'IS_CRM_MIGRATION_ENABLED';
|
| 'IS_CRM_MIGRATION_ENABLED';
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { Section } from '@/ui/layout/section/components/Section';
|
|||||||
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
import { useGenerateApiKeyTokenMutation } from '~/generated/graphql';
|
import { useGenerateApiKeyTokenMutation } from '~/generated/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
export const SettingsDevelopersApiKeysNew = () => {
|
export const SettingsDevelopersApiKeysNew = () => {
|
||||||
const [generateOneApiKeyToken] = useGenerateApiKeyTokenMutation();
|
const [generateOneApiKeyToken] = useGenerateApiKeyTokenMutation();
|
||||||
@@ -85,7 +86,7 @@ export const SettingsDevelopersApiKeysNew = () => {
|
|||||||
placeholder="E.g. backoffice integration"
|
placeholder="E.g. backoffice integration"
|
||||||
value={formValues.name}
|
value={formValues.name}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === Key.Enter) {
|
||||||
handleSave();
|
handleSave();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { useResetRecoilState } from 'recoil';
|
||||||
|
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
||||||
|
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
||||||
|
import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState';
|
||||||
|
|
||||||
|
export const ResetServerlessFunctionStatesEffect = () => {
|
||||||
|
const resetSettingsServerlessFunctionInput = useResetRecoilState(
|
||||||
|
settingsServerlessFunctionInputState,
|
||||||
|
);
|
||||||
|
const resetSettingsServerlessFunctionOutput = useResetRecoilState(
|
||||||
|
settingsServerlessFunctionOutputState,
|
||||||
|
);
|
||||||
|
const resetSettingsServerlessFunctionCodeEditorOutputParamsState =
|
||||||
|
useResetRecoilState(settingsServerlessFunctionCodeEditorOutputParamsState);
|
||||||
|
|
||||||
|
resetSettingsServerlessFunctionInput();
|
||||||
|
resetSettingsServerlessFunctionOutput();
|
||||||
|
resetSettingsServerlessFunctionCodeEditorOutputParamsState();
|
||||||
|
return <></>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
import { IconCode, IconSettings, IconTestPipe } from 'twenty-ui';
|
||||||
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
|
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||||
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||||
|
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||||
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
import { SettingsServerlessFunctionCodeEditorTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab';
|
||||||
|
import { SettingsServerlessFunctionSettingsTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab';
|
||||||
|
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||||
|
import { SettingsServerlessFunctionTestTab } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab';
|
||||||
|
import { useExecuteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useExecuteOneServerlessFunction';
|
||||||
|
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
|
import { useUpdateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useUpdateOneServerlessFunction';
|
||||||
|
import { useDebouncedCallback } from 'use-debounce';
|
||||||
|
import { SettingsServerlessFunctionTestTabEffect } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTabEffect';
|
||||||
|
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
||||||
|
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
||||||
|
|
||||||
|
const TAB_LIST_COMPONENT_ID = 'serverless-function-detail';
|
||||||
|
|
||||||
|
export const SettingsServerlessFunctionDetail = () => {
|
||||||
|
const { serverlessFunctionId = '' } = useParams();
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
const { activeTabIdState, setActiveTabId } = useTabList(
|
||||||
|
TAB_LIST_COMPONENT_ID,
|
||||||
|
);
|
||||||
|
const activeTabId = useRecoilValue(activeTabIdState);
|
||||||
|
const { executeOneServerlessFunction } = useExecuteOneServerlessFunction();
|
||||||
|
const { updateOneServerlessFunction } = useUpdateOneServerlessFunction();
|
||||||
|
const [formValues, setFormValues] =
|
||||||
|
useServerlessFunctionUpdateFormState(serverlessFunctionId);
|
||||||
|
const setSettingsServerlessFunctionOutput = useSetRecoilState(
|
||||||
|
settingsServerlessFunctionOutputState,
|
||||||
|
);
|
||||||
|
const settingsServerlessFunctionInput = useRecoilValue(
|
||||||
|
settingsServerlessFunctionInputState,
|
||||||
|
);
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
try {
|
||||||
|
await updateOneServerlessFunction({
|
||||||
|
id: serverlessFunctionId,
|
||||||
|
name: formValues.name,
|
||||||
|
description: formValues.description,
|
||||||
|
code: formValues.code,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
enqueueSnackBar(
|
||||||
|
(err as Error)?.message || 'An error occurred while updating function',
|
||||||
|
{
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = useDebouncedCallback(save, 500);
|
||||||
|
|
||||||
|
const onChange = (key: string) => {
|
||||||
|
return async (value: string | undefined) => {
|
||||||
|
setFormValues((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
[key]: value,
|
||||||
|
}));
|
||||||
|
await handleSave();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleExecute = async () => {
|
||||||
|
await handleSave();
|
||||||
|
try {
|
||||||
|
const result = await executeOneServerlessFunction(
|
||||||
|
serverlessFunctionId,
|
||||||
|
JSON.parse(settingsServerlessFunctionInput),
|
||||||
|
);
|
||||||
|
setSettingsServerlessFunctionOutput(
|
||||||
|
JSON.stringify(
|
||||||
|
result?.data?.executeOneServerlessFunction?.result,
|
||||||
|
null,
|
||||||
|
4,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
enqueueSnackBar(
|
||||||
|
(err as Error)?.message || 'An error occurred while executing function',
|
||||||
|
{
|
||||||
|
variant: SnackBarVariant.Error,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
setSettingsServerlessFunctionOutput(JSON.stringify(err, null, 4));
|
||||||
|
}
|
||||||
|
setActiveTabId('test');
|
||||||
|
};
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: 'editor', title: 'Editor', Icon: IconCode },
|
||||||
|
{ id: 'test', title: 'Test', Icon: IconTestPipe },
|
||||||
|
{ id: 'settings', title: 'Settings', Icon: IconSettings },
|
||||||
|
];
|
||||||
|
|
||||||
|
const renderActiveTabContent = () => {
|
||||||
|
switch (activeTabId) {
|
||||||
|
case 'editor':
|
||||||
|
return (
|
||||||
|
<SettingsServerlessFunctionCodeEditorTab
|
||||||
|
formValues={formValues}
|
||||||
|
handleExecute={handleExecute}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'test':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SettingsServerlessFunctionTestTabEffect />
|
||||||
|
<SettingsServerlessFunctionTestTab handleExecute={handleExecute} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
case 'settings':
|
||||||
|
return (
|
||||||
|
<SettingsServerlessFunctionSettingsTab
|
||||||
|
formValues={formValues}
|
||||||
|
serverlessFunctionId={serverlessFunctionId}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
formValues.name && (
|
||||||
|
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||||
|
<SettingsPageContainer>
|
||||||
|
<SettingsHeaderContainer>
|
||||||
|
<Breadcrumb
|
||||||
|
links={[
|
||||||
|
{ children: 'Functions', href: '/settings/functions' },
|
||||||
|
{ children: `${formValues.name}` },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</SettingsHeaderContainer>
|
||||||
|
<Section>
|
||||||
|
<TabList tabListId={TAB_LIST_COMPONENT_ID} tabs={tabs} />
|
||||||
|
</Section>
|
||||||
|
{renderActiveTabContent()}
|
||||||
|
</SettingsPageContainer>
|
||||||
|
</SubMenuTopBarContainer>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { ResetServerlessFunctionStatesEffect } from '~/pages/settings/serverless-functions/ResetServerlessFunctionStatesEffect';
|
||||||
|
import { SettingsServerlessFunctionDetail } from '~/pages/settings/serverless-functions/SettingsServerlessFunctionDetail';
|
||||||
|
|
||||||
|
export const SettingsServerlessFunctionDetailWrapper = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<ResetServerlessFunctionStatesEffect />
|
||||||
|
<SettingsServerlessFunctionDetail />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { IconPlus, IconSettings } from 'twenty-ui';
|
||||||
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
|
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||||
|
import { Section } from '@/ui/layout/section/components/Section';
|
||||||
|
import { SettingsServerlessFunctionsTable } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsTable';
|
||||||
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
|
import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink';
|
||||||
|
|
||||||
|
export const SettingsServerlessFunctions = () => {
|
||||||
|
return (
|
||||||
|
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||||
|
<SettingsPageContainer>
|
||||||
|
<SettingsHeaderContainer>
|
||||||
|
<Breadcrumb links={[{ children: 'Functions' }]} />
|
||||||
|
<UndecoratedLink
|
||||||
|
to={getSettingsPagePath(SettingsPath.NewServerlessFunction)}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
Icon={IconPlus}
|
||||||
|
title="New Function"
|
||||||
|
accent="blue"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</UndecoratedLink>
|
||||||
|
</SettingsHeaderContainer>
|
||||||
|
<Section>
|
||||||
|
<SettingsServerlessFunctionsTable />
|
||||||
|
</Section>
|
||||||
|
</SettingsPageContainer>
|
||||||
|
</SubMenuTopBarContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import { IconSettings } from 'twenty-ui';
|
||||||
|
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||||
|
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
|
||||||
|
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||||
|
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
|
||||||
|
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useCreateOneServerlessFunction } from '@/settings/serverless-functions/hooks/useCreateOneServerlessFunction';
|
||||||
|
import { DEFAULT_CODE } from '@/ui/input/code-editor/components/CodeEditor';
|
||||||
|
import { ServerlessFunctionNewFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||||
|
import { SettingsServerlessFunctionNewForm } from '@/settings/serverless-functions/components/SettingsServerlessFunctionNewForm';
|
||||||
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||||
|
import { SettingsPath } from '@/types/SettingsPath';
|
||||||
|
|
||||||
|
export const SettingsServerlessFunctionsNew = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [formValues, setFormValues] = useState<ServerlessFunctionNewFormValues>(
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const { createOneServerlessFunction } = useCreateOneServerlessFunction();
|
||||||
|
const handleSave = async () => {
|
||||||
|
const newServerlessFunction = await createOneServerlessFunction({
|
||||||
|
name: formValues.name,
|
||||||
|
description: formValues.description,
|
||||||
|
code: DEFAULT_CODE,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isDefined(newServerlessFunction?.data)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigate(
|
||||||
|
getSettingsPagePath(SettingsPath.ServerlessFunctions, {
|
||||||
|
id: newServerlessFunction.data.createOneServerlessFunction.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChange = (key: string) => {
|
||||||
|
return (value: string | undefined) => {
|
||||||
|
setFormValues((prevState) => ({
|
||||||
|
...prevState,
|
||||||
|
[key]: value,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const canSave = !!formValues.name && createOneServerlessFunction;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||||
|
<SettingsPageContainer>
|
||||||
|
<SettingsHeaderContainer>
|
||||||
|
<Breadcrumb
|
||||||
|
links={[
|
||||||
|
{ children: 'Functions', href: '/settings/functions' },
|
||||||
|
{ children: 'New' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<SaveAndCancelButtons
|
||||||
|
isSaveDisabled={!canSave}
|
||||||
|
onCancel={() => {
|
||||||
|
navigate('/settings/functions');
|
||||||
|
}}
|
||||||
|
onSave={handleSave}
|
||||||
|
/>
|
||||||
|
</SettingsHeaderContainer>
|
||||||
|
<SettingsServerlessFunctionNewForm
|
||||||
|
formValues={formValues}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
</SettingsPageContainer>
|
||||||
|
</SubMenuTopBarContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { SettingsServerlessFunctionDetail } from '~/pages/settings/serverless-functions/SettingsServerlessFunctionDetail';
|
||||||
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import {
|
||||||
|
PageDecorator,
|
||||||
|
PageDecoratorArgs,
|
||||||
|
} from '~/testing/decorators/PageDecorator';
|
||||||
|
import { graphql, http, HttpResponse } from 'msw';
|
||||||
|
import { getImageAbsoluteURIOrBase64 } from '~/utils/image/getImageAbsoluteURIOrBase64';
|
||||||
|
import { DEFAULT_CODE } from '@/ui/input/code-editor/components/CodeEditor';
|
||||||
|
import { within } from '@storybook/test';
|
||||||
|
import { sleep } from '~/utils/sleep';
|
||||||
|
|
||||||
|
const SOURCE_CODE_FULL_PATH =
|
||||||
|
'serverless-function/20202020-1c25-4d02-bf25-6aeccf7ea419/adb4bd21-7670-4c81-9f74-1fc196fe87ea/source.ts';
|
||||||
|
|
||||||
|
const meta: Meta<PageDecoratorArgs> = {
|
||||||
|
title: 'Pages/Settings/ServerlessFunctions/SettingsServerlessFunctionDetail',
|
||||||
|
component: SettingsServerlessFunctionDetail,
|
||||||
|
decorators: [PageDecorator],
|
||||||
|
args: {
|
||||||
|
routePath: '/settings/function/',
|
||||||
|
routeParams: {
|
||||||
|
':serverlessFunctionId': 'adb4bd21-7670-4c81-9f74-1fc196fe87ea',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
parameters: {
|
||||||
|
msw: {
|
||||||
|
handlers: [
|
||||||
|
...graphqlMocks.handlers,
|
||||||
|
graphql.query('GetOneServerlessFunction', () => {
|
||||||
|
return HttpResponse.json({
|
||||||
|
data: {
|
||||||
|
serverlessFunction: {
|
||||||
|
__typename: 'ServerlessFunction',
|
||||||
|
id: 'adb4bd21-7670-4c81-9f74-1fc196fe87ea',
|
||||||
|
name: 'Serverless Function Name',
|
||||||
|
description: '',
|
||||||
|
syncStatus: 'READY',
|
||||||
|
runtime: 'nodejs18.x',
|
||||||
|
sourceCodeFullPath: SOURCE_CODE_FULL_PATH,
|
||||||
|
sourceCodeHash: '42d2734b3dc8a7b45a16803ed7f417bc',
|
||||||
|
updatedAt: '2024-02-24T10:23:10.673Z',
|
||||||
|
createdAt: '2024-02-24T10:23:10.673Z',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
http.get(
|
||||||
|
getImageAbsoluteURIOrBase64(SOURCE_CODE_FULL_PATH) || '',
|
||||||
|
() => {
|
||||||
|
return HttpResponse.text(DEFAULT_CODE);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
export type Story = StoryObj<typeof SettingsServerlessFunctionDetail>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
await sleep(100);
|
||||||
|
await canvas.findByText('Code your function');
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { SettingsServerlessFunctions } from '~/pages/settings/serverless-functions/SettingsServerlessFunctions';
|
||||||
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import {
|
||||||
|
PageDecorator,
|
||||||
|
PageDecoratorArgs,
|
||||||
|
} from '~/testing/decorators/PageDecorator';
|
||||||
|
import { sleep } from '~/utils/sleep';
|
||||||
|
import { within } from '@storybook/test';
|
||||||
|
|
||||||
|
const meta: Meta<PageDecoratorArgs> = {
|
||||||
|
title: 'Pages/Settings/ServerlessFunctions/SettingsServerlessFunctions',
|
||||||
|
component: SettingsServerlessFunctions,
|
||||||
|
decorators: [PageDecorator],
|
||||||
|
args: { routePath: '/settings/functions' },
|
||||||
|
parameters: {
|
||||||
|
msw: graphqlMocks,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
export type Story = StoryObj<typeof SettingsServerlessFunctions>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
await sleep(100);
|
||||||
|
|
||||||
|
await canvas.findByText('Functions');
|
||||||
|
await canvas.findByText('Add your first Function');
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { SettingsServerlessFunctionsNew } from '~/pages/settings/serverless-functions/SettingsServerlessFunctionsNew';
|
||||||
|
import { graphqlMocks } from '~/testing/graphqlMocks';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import {
|
||||||
|
PageDecorator,
|
||||||
|
PageDecoratorArgs,
|
||||||
|
} from '~/testing/decorators/PageDecorator';
|
||||||
|
import { userEvent, within } from '@storybook/test';
|
||||||
|
import { sleep } from '~/utils/sleep';
|
||||||
|
|
||||||
|
const meta: Meta<PageDecoratorArgs> = {
|
||||||
|
title: 'Pages/Settings/ServerlessFunctions/SettingsServerlessFunctionsNew',
|
||||||
|
component: SettingsServerlessFunctionsNew,
|
||||||
|
decorators: [PageDecorator],
|
||||||
|
args: { routePath: '/settings/functions/new' },
|
||||||
|
parameters: {
|
||||||
|
msw: graphqlMocks,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
export type Story = StoryObj<typeof SettingsServerlessFunctionsNew>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
await sleep(100);
|
||||||
|
await canvas.findByText('Functions');
|
||||||
|
await canvas.findByText('New');
|
||||||
|
|
||||||
|
const input = await canvas.findByPlaceholderText('Name');
|
||||||
|
await userEvent.type(input, 'Function Name');
|
||||||
|
const saveButton = await canvas.findByText('Save');
|
||||||
|
|
||||||
|
await userEvent.click(saveButton);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { sortByAscString } from '~/utils/array/sortByAscString';
|
||||||
|
|
||||||
|
describe('sortByAscString', () => {
|
||||||
|
test('should sort properly', () => {
|
||||||
|
expect(sortByAscString('a', 'b')).toEqual(-1);
|
||||||
|
expect(sortByAscString('b', 'a')).toEqual(1);
|
||||||
|
expect(sortByAscString('a', 'a')).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { getFileAbsoluteURI } from '../getFileAbsoluteURI';
|
||||||
|
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||||
|
|
||||||
|
describe('getFileAbsoluteURI', () => {
|
||||||
|
test('should return absolute uri', () => {
|
||||||
|
expect(getFileAbsoluteURI('foo')).toEqual(
|
||||||
|
`${REACT_APP_SERVER_BASE_URL}/files/foo`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||||
|
|
||||||
|
export const getFileAbsoluteURI = (fileUrl?: string) => {
|
||||||
|
return `${REACT_APP_SERVER_BASE_URL}/files/${fileUrl}`;
|
||||||
|
};
|
||||||
@@ -10,6 +10,7 @@ export enum SettingsPageTitles {
|
|||||||
Members = 'Members - Settings',
|
Members = 'Members - Settings',
|
||||||
Developers = 'Developers - Settings',
|
Developers = 'Developers - Settings',
|
||||||
Integration = 'Integrations - Settings',
|
Integration = 'Integrations - Settings',
|
||||||
|
ServerlessFunctions = 'Functions - Settings',
|
||||||
General = 'General - Settings',
|
General = 'General - Settings',
|
||||||
Default = 'Settings',
|
Default = 'Settings',
|
||||||
}
|
}
|
||||||
@@ -21,6 +22,7 @@ enum SettingsPathPrefixes {
|
|||||||
Objects = `${AppBasePath.Settings}/${SettingsPath.Objects}`,
|
Objects = `${AppBasePath.Settings}/${SettingsPath.Objects}`,
|
||||||
Members = `${AppBasePath.Settings}/${SettingsPath.WorkspaceMembersPage}`,
|
Members = `${AppBasePath.Settings}/${SettingsPath.WorkspaceMembersPage}`,
|
||||||
Developers = `${AppBasePath.Settings}/${SettingsPath.Developers}`,
|
Developers = `${AppBasePath.Settings}/${SettingsPath.Developers}`,
|
||||||
|
ServerlessFunctions = `${AppBasePath.Settings}/${SettingsPath.ServerlessFunctions}`,
|
||||||
Integration = `${AppBasePath.Settings}/${SettingsPath.Integrations}`,
|
Integration = `${AppBasePath.Settings}/${SettingsPath.Integrations}`,
|
||||||
General = `${AppBasePath.Settings}/${SettingsPath.Workspace}`,
|
General = `${AppBasePath.Settings}/${SettingsPath.Workspace}`,
|
||||||
}
|
}
|
||||||
@@ -63,6 +65,8 @@ export const getPageTitleFromPath = (pathname: string): string => {
|
|||||||
return SettingsPageTitles.Objects;
|
return SettingsPageTitles.Objects;
|
||||||
case SettingsPathPrefixes.Developers:
|
case SettingsPathPrefixes.Developers:
|
||||||
return SettingsPageTitles.Developers;
|
return SettingsPageTitles.Developers;
|
||||||
|
case SettingsPathPrefixes.ServerlessFunctions:
|
||||||
|
return SettingsPageTitles.ServerlessFunctions;
|
||||||
case SettingsPathPrefixes.Integration:
|
case SettingsPathPrefixes.Integration:
|
||||||
return SettingsPageTitles.Integration;
|
return SettingsPageTitles.Integration;
|
||||||
case SettingsPathPrefixes.General:
|
case SettingsPathPrefixes.General:
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"@graphql-yoga/nestjs": "patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga-nestjs-npm-2.1.0-cb509e6047.patch",
|
"@graphql-yoga/nestjs": "patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga-nestjs-npm-2.1.0-cb509e6047.patch",
|
||||||
"@langchain/mistralai": "^0.0.24",
|
"@langchain/mistralai": "^0.0.24",
|
||||||
"@langchain/openai": "^0.1.3",
|
"@langchain/openai": "^0.1.3",
|
||||||
|
"@monaco-editor/react": "^4.6.0",
|
||||||
"@nestjs/cache-manager": "^2.2.1",
|
"@nestjs/cache-manager": "^2.2.1",
|
||||||
"@nestjs/devtools-integration": "^0.1.6",
|
"@nestjs/devtools-integration": "^0.1.6",
|
||||||
"@nestjs/graphql": "patch:@nestjs/graphql@12.1.1#./patches/@nestjs+graphql+12.1.1.patch",
|
"@nestjs/graphql": "patch:@nestjs/graphql@12.1.1#./patches/@nestjs+graphql+12.1.1.patch",
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
"lodash.omitby": "^4.6.0",
|
"lodash.omitby": "^4.6.0",
|
||||||
"lodash.uniq": "^4.5.0",
|
"lodash.uniq": "^4.5.0",
|
||||||
"lodash.uniqby": "^4.7.0",
|
"lodash.uniqby": "^4.7.0",
|
||||||
|
"monaco-editor": "^0.50.0",
|
||||||
"passport": "^0.7.0",
|
"passport": "^0.7.0",
|
||||||
"psl": "^1.9.0",
|
"psl": "^1.9.0",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ export const seedFeatureFlags = async (
|
|||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
value: true,
|
value: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: FeatureFlagKeys.IsFunctionSettingsEnabled,
|
||||||
|
workspaceId: workspaceId,
|
||||||
|
value: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: FeatureFlagKeys.IsWorkflowEnabled,
|
key: FeatureFlagKeys.IsWorkflowEnabled,
|
||||||
workspaceId: workspaceId,
|
workspaceId: workspaceId,
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class AddRuntimeColumnToServerlessFunction1721309629608
|
||||||
|
implements MigrationInterface
|
||||||
|
{
|
||||||
|
name = 'AddRuntimeColumnToServerlessFunction1721309629608';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."serverlessFunction" ADD "runtime" character varying NOT NULL DEFAULT 'nodejs18.x'`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."serverlessFunction" ADD "description" character varying`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."serverlessFunction" ADD "sourceCodeFullPath" character varying NOT NULL`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "metadata"."serverlessFunction" DROP COLUMN "runtime"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ export enum FeatureFlagKeys {
|
|||||||
IsMessagingAliasFetchingEnabled = 'IS_MESSAGING_ALIAS_FETCHING_ENABLED',
|
IsMessagingAliasFetchingEnabled = 'IS_MESSAGING_ALIAS_FETCHING_ENABLED',
|
||||||
IsGoogleCalendarSyncV2Enabled = 'IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED',
|
IsGoogleCalendarSyncV2Enabled = 'IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED',
|
||||||
IsFreeAccessEnabled = 'IS_FREE_ACCESS_ENABLED',
|
IsFreeAccessEnabled = 'IS_FREE_ACCESS_ENABLED',
|
||||||
|
IsFunctionSettingsEnabled = 'IS_FUNCTION_SETTINGS_ENABLED',
|
||||||
IsWorkflowEnabled = 'IS_WORKFLOW_ENABLED',
|
IsWorkflowEnabled = 'IS_WORKFLOW_ENABLED',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,11 @@ export const checkFilePath = (filePath: string): string => {
|
|||||||
throw new BadRequestException(`Folder ${folder} is not allowed`);
|
throw new BadRequestException(`Folder ${folder} is not allowed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (size && !settings.storage.imageCropSizes[folder]?.includes(size)) {
|
if (
|
||||||
|
folder !== kebabCase(FileFolder.ServerlessFunction) &&
|
||||||
|
size &&
|
||||||
|
!settings.storage.imageCropSizes[folder]?.includes(size)
|
||||||
|
) {
|
||||||
throw new BadRequestException(`Size ${size} is not allowed`);
|
throw new BadRequestException(`Size ${size} is not allowed`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Readable } from 'stream';
|
import { Readable } from 'stream';
|
||||||
|
|
||||||
export interface StorageDriver {
|
export interface StorageDriver {
|
||||||
|
delete(params: { folderPath: string; filename?: string }): Promise<void>;
|
||||||
read(params: { folderPath: string; filename: string }): Promise<Readable>;
|
read(params: { folderPath: string; filename: string }): Promise<Readable>;
|
||||||
write(params: {
|
write(params: {
|
||||||
file: Buffer | Uint8Array | string;
|
file: Buffer | Uint8Array | string;
|
||||||
|
|||||||
@@ -42,6 +42,19 @@ export class LocalDriver implements StorageDriver {
|
|||||||
await fs.writeFile(filePath, params.file);
|
await fs.writeFile(filePath, params.file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async delete(params: {
|
||||||
|
folderPath: string;
|
||||||
|
filename?: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const filePath = join(
|
||||||
|
`${this.options.storagePath}/`,
|
||||||
|
params.folderPath,
|
||||||
|
params.filename || '',
|
||||||
|
);
|
||||||
|
|
||||||
|
await fs.rm(filePath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
async read(params: {
|
async read(params: {
|
||||||
folderPath: string;
|
folderPath: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ import { Readable } from 'stream';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
CreateBucketCommandInput,
|
CreateBucketCommandInput,
|
||||||
|
DeleteObjectCommand,
|
||||||
|
DeleteObjectsCommand,
|
||||||
GetObjectCommand,
|
GetObjectCommand,
|
||||||
HeadBucketCommandInput,
|
HeadBucketCommandInput,
|
||||||
|
ListObjectsV2Command,
|
||||||
NotFound,
|
NotFound,
|
||||||
PutObjectCommand,
|
PutObjectCommand,
|
||||||
S3,
|
S3,
|
||||||
@@ -53,6 +56,57 @@ export class S3Driver implements StorageDriver {
|
|||||||
await this.s3Client.send(command);
|
await this.s3Client.send(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async emptyS3Directory(folderPath) {
|
||||||
|
const listParams = {
|
||||||
|
Bucket: this.bucketName,
|
||||||
|
Prefix: folderPath,
|
||||||
|
};
|
||||||
|
|
||||||
|
const listObjectsCommand = new ListObjectsV2Command(listParams);
|
||||||
|
const listedObjects = await this.s3Client.send(listObjectsCommand);
|
||||||
|
|
||||||
|
if (listedObjects.Contents?.length === 0) return;
|
||||||
|
|
||||||
|
const deleteParams = {
|
||||||
|
Bucket: this.bucketName,
|
||||||
|
Delete: {
|
||||||
|
Objects: listedObjects.Contents?.map(({ Key }) => {
|
||||||
|
return { Key };
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteObjectCommand = new DeleteObjectsCommand(deleteParams);
|
||||||
|
|
||||||
|
await this.s3Client.send(deleteObjectCommand);
|
||||||
|
|
||||||
|
if (listedObjects.IsTruncated) {
|
||||||
|
await this.emptyS3Directory(folderPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete(params: {
|
||||||
|
folderPath: string;
|
||||||
|
filename?: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
if (params.filename) {
|
||||||
|
const deleteCommand = new DeleteObjectCommand({
|
||||||
|
Key: `${params.folderPath}/${params.filename}`,
|
||||||
|
Bucket: this.bucketName,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.s3Client.send(deleteCommand);
|
||||||
|
} else {
|
||||||
|
await this.emptyS3Directory(params.folderPath);
|
||||||
|
const deleteEmptyFolderCommand = new DeleteObjectCommand({
|
||||||
|
Key: `${params.folderPath}`,
|
||||||
|
Bucket: this.bucketName,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.s3Client.send(deleteEmptyFolderCommand);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async read(params: {
|
async read(params: {
|
||||||
folderPath: string;
|
folderPath: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ import { StorageDriver } from './drivers/interfaces/storage-driver.interface';
|
|||||||
export class FileStorageService implements StorageDriver {
|
export class FileStorageService implements StorageDriver {
|
||||||
constructor(@Inject(STORAGE_DRIVER) private driver: StorageDriver) {}
|
constructor(@Inject(STORAGE_DRIVER) private driver: StorageDriver) {}
|
||||||
|
|
||||||
|
delete(params: { folderPath: string; filename?: string }): Promise<void> {
|
||||||
|
return this.driver.delete(params);
|
||||||
|
}
|
||||||
|
|
||||||
write(params: {
|
write(params: {
|
||||||
file: string | Buffer | Uint8Array;
|
file: string | Buffer | Uint8Array;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity';
|
||||||
|
|
||||||
export interface ServerlessDriver {
|
export interface ServerlessDriver {
|
||||||
|
delete(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
|
||||||
build(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
|
build(serverlessFunction: ServerlessFunctionEntity): Promise<void>;
|
||||||
execute(
|
execute(
|
||||||
serverlessFunction: ServerlessFunctionEntity,
|
serverlessFunction: ServerlessFunctionEntity,
|
||||||
|
|||||||
@@ -5,8 +5,12 @@ import {
|
|||||||
Lambda,
|
Lambda,
|
||||||
LambdaClientConfig,
|
LambdaClientConfig,
|
||||||
InvokeCommand,
|
InvokeCommand,
|
||||||
|
GetFunctionCommand,
|
||||||
|
UpdateFunctionCodeCommand,
|
||||||
|
DeleteFunctionCommand,
|
||||||
} from '@aws-sdk/client-lambda';
|
} from '@aws-sdk/client-lambda';
|
||||||
import { CreateFunctionCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/CreateFunctionCommand';
|
import { CreateFunctionCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/CreateFunctionCommand';
|
||||||
|
import { UpdateFunctionCodeCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/UpdateFunctionCodeCommand';
|
||||||
|
|
||||||
import { ServerlessDriver } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
|
import { ServerlessDriver } from 'src/engine/integrations/serverless/drivers/interfaces/serverless-driver.interface';
|
||||||
|
|
||||||
@@ -42,6 +46,18 @@ export class LambdaDriver
|
|||||||
this.buildDirectoryManagerService = options.buildDirectoryManagerService;
|
this.buildDirectoryManagerService = options.buildDirectoryManagerService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async delete(serverlessFunction: ServerlessFunctionEntity) {
|
||||||
|
try {
|
||||||
|
const deleteFunctionCommand = new DeleteFunctionCommand({
|
||||||
|
FunctionName: serverlessFunction.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.lambdaClient.send(deleteFunctionCommand);
|
||||||
|
} catch {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async build(serverlessFunction: ServerlessFunctionEntity) {
|
async build(serverlessFunction: ServerlessFunctionEntity) {
|
||||||
const javascriptCode = await this.getCompiledCode(
|
const javascriptCode = await this.getCompiledCode(
|
||||||
serverlessFunction,
|
serverlessFunction,
|
||||||
@@ -59,21 +75,44 @@ export class LambdaDriver
|
|||||||
|
|
||||||
await createZipFile(sourceTemporaryDir, lambdaZipPath);
|
await createZipFile(sourceTemporaryDir, lambdaZipPath);
|
||||||
|
|
||||||
const params: CreateFunctionCommandInput = {
|
let existingFunction = true;
|
||||||
Code: {
|
|
||||||
|
try {
|
||||||
|
const getFunctionCommand = new GetFunctionCommand({
|
||||||
|
FunctionName: serverlessFunction.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.lambdaClient.send(getFunctionCommand);
|
||||||
|
} catch {
|
||||||
|
existingFunction = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existingFunction) {
|
||||||
|
const params: CreateFunctionCommandInput = {
|
||||||
|
Code: {
|
||||||
|
ZipFile: await fs.promises.readFile(lambdaZipPath),
|
||||||
|
},
|
||||||
|
FunctionName: serverlessFunction.id,
|
||||||
|
Handler: lambdaHandler,
|
||||||
|
Role: this.lambdaRole,
|
||||||
|
Runtime: serverlessFunction.runtime,
|
||||||
|
Description: 'Lambda function to run user script',
|
||||||
|
Timeout: 900,
|
||||||
|
};
|
||||||
|
|
||||||
|
const command = new CreateFunctionCommand(params);
|
||||||
|
|
||||||
|
await this.lambdaClient.send(command);
|
||||||
|
} else {
|
||||||
|
const params: UpdateFunctionCodeCommandInput = {
|
||||||
ZipFile: await fs.promises.readFile(lambdaZipPath),
|
ZipFile: await fs.promises.readFile(lambdaZipPath),
|
||||||
},
|
FunctionName: serverlessFunction.id,
|
||||||
FunctionName: serverlessFunction.id,
|
};
|
||||||
Handler: lambdaHandler,
|
|
||||||
Role: this.lambdaRole,
|
|
||||||
Runtime: 'nodejs18.x',
|
|
||||||
Description: 'Lambda function to run user script',
|
|
||||||
Timeout: 900,
|
|
||||||
};
|
|
||||||
|
|
||||||
const command = new CreateFunctionCommand(params);
|
const command = new UpdateFunctionCodeCommand(params);
|
||||||
|
|
||||||
await this.lambdaClient.send(command);
|
await this.lambdaClient.send(command);
|
||||||
|
}
|
||||||
|
|
||||||
await this.buildDirectoryManagerService.clean();
|
await this.buildDirectoryManagerService.clean();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ export class LocalDriver
|
|||||||
this.fileStorageService = options.fileStorageService;
|
this.fileStorageService = options.fileStorageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async delete(serverlessFunction: ServerlessFunctionEntity) {
|
||||||
|
await this.fileStorageService.delete({
|
||||||
|
folderPath: this.getFolderPath(serverlessFunction),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async build(serverlessFunction: ServerlessFunctionEntity) {
|
async build(serverlessFunction: ServerlessFunctionEntity) {
|
||||||
const javascriptCode = await this.getCompiledCode(
|
const javascriptCode = await this.getCompiledCode(
|
||||||
serverlessFunction,
|
serverlessFunction,
|
||||||
@@ -57,8 +63,16 @@ export class LocalDriver
|
|||||||
const modifiedContent = `
|
const modifiedContent = `
|
||||||
process.on('message', async (message) => {
|
process.on('message', async (message) => {
|
||||||
const { event, context } = message;
|
const { event, context } = message;
|
||||||
const result = await handler(event, context);
|
try {
|
||||||
process.send(result);
|
const result = await handler(event, context);
|
||||||
|
process.send(result);
|
||||||
|
} catch (error) {
|
||||||
|
process.send({
|
||||||
|
errorType: error.name,
|
||||||
|
errorMessage: error.message,
|
||||||
|
stackTrace: error.stack.split('\\n').filter((line) => line.trim() !== ''),
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
${fileContent}
|
${fileContent}
|
||||||
@@ -67,7 +81,7 @@ export class LocalDriver
|
|||||||
await fs.writeFile(tmpFilePath, modifiedContent);
|
await fs.writeFile(tmpFilePath, modifiedContent);
|
||||||
|
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
const child = fork(tmpFilePath);
|
const child = fork(tmpFilePath, { silent: true });
|
||||||
|
|
||||||
child.on('message', (message: object) => {
|
child.on('message', (message: object) => {
|
||||||
resolve(message);
|
resolve(message);
|
||||||
@@ -75,6 +89,32 @@ export class LocalDriver
|
|||||||
fs.unlink(tmpFilePath);
|
fs.unlink(tmpFilePath);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
child.stderr?.on('data', (data) => {
|
||||||
|
const stackTrace = data
|
||||||
|
.toString()
|
||||||
|
.split('\n')
|
||||||
|
.filter((line) => line.trim() !== '');
|
||||||
|
const errorTrace = stackTrace.filter((line) =>
|
||||||
|
line.includes('Error: '),
|
||||||
|
)?.[0];
|
||||||
|
|
||||||
|
let errorType = 'Unknown';
|
||||||
|
let errorMessage = '';
|
||||||
|
|
||||||
|
if (errorTrace) {
|
||||||
|
errorType = errorTrace.split(':')[0];
|
||||||
|
errorMessage = errorTrace.split(': ')[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve({
|
||||||
|
errorType,
|
||||||
|
errorMessage,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
});
|
||||||
|
child.kill();
|
||||||
|
fs.unlink(tmpFilePath);
|
||||||
|
});
|
||||||
|
|
||||||
child.on('error', (error) => {
|
child.on('error', (error) => {
|
||||||
reject(error);
|
reject(error);
|
||||||
child.kill();
|
child.kill();
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless
|
|||||||
export class ServerlessService implements ServerlessDriver {
|
export class ServerlessService implements ServerlessDriver {
|
||||||
constructor(@Inject(SERVERLESS_DRIVER) private driver: ServerlessDriver) {}
|
constructor(@Inject(SERVERLESS_DRIVER) private driver: ServerlessDriver) {}
|
||||||
|
|
||||||
|
async delete(serverlessFunction: ServerlessFunctionEntity): Promise<void> {
|
||||||
|
return this.driver.delete(serverlessFunction);
|
||||||
|
}
|
||||||
|
|
||||||
async build(serverlessFunction: ServerlessFunctionEntity): Promise<void> {
|
async build(serverlessFunction: ServerlessFunctionEntity): Promise<void> {
|
||||||
return this.driver.build(serverlessFunction);
|
return this.driver.build(serverlessFunction);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class CreateServerlessFunctionFromFileInput {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Field()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsOptional()
|
||||||
|
@Field({ nullable: true })
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
import { CreateServerlessFunctionFromFileInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function-from-file.input';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class CreateServerlessFunctionInput extends CreateServerlessFunctionFromFileInput {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Field()
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { ID, InputType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class DeleteServerlessFunctionInput {
|
||||||
|
@IDField(() => ID, { description: 'The id of the function.' })
|
||||||
|
id!: string;
|
||||||
|
}
|
||||||
@@ -1,14 +1,18 @@
|
|||||||
import { ArgsType, Field } from '@nestjs/graphql';
|
import { ArgsType, Field } from '@nestjs/graphql';
|
||||||
|
|
||||||
import { IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator';
|
import { IsNotEmpty, IsObject, IsOptional, IsUUID } from 'class-validator';
|
||||||
import graphqlTypeJson from 'graphql-type-json';
|
import graphqlTypeJson from 'graphql-type-json';
|
||||||
|
|
||||||
|
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||||
|
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
export class ExecuteServerlessFunctionInput {
|
export class ExecuteServerlessFunctionInput {
|
||||||
@Field({ description: 'Name of the serverless function to execute' })
|
@Field(() => UUIDScalarType, {
|
||||||
|
description: 'Id of the serverless function to execute',
|
||||||
|
})
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsUUID()
|
||||||
name: string;
|
id: string;
|
||||||
|
|
||||||
@Field(() => graphqlTypeJson, {
|
@Field(() => graphqlTypeJson, {
|
||||||
description: 'Payload in JSON format',
|
description: 'Payload in JSON format',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { IsObject } from 'class-validator';
|
|||||||
import graphqlTypeJson from 'graphql-type-json';
|
import graphqlTypeJson from 'graphql-type-json';
|
||||||
|
|
||||||
@ObjectType('ServerlessFunctionExecutionResult')
|
@ObjectType('ServerlessFunctionExecutionResult')
|
||||||
export class ServerlessFunctionExecutionResultDTO {
|
export class ServerlessFunctionExecutionResultDto {
|
||||||
@IsObject()
|
@IsObject()
|
||||||
@Field(() => graphqlTypeJson, {
|
@Field(() => graphqlTypeJson, {
|
||||||
description: 'Execution result in JSON format',
|
description: 'Execution result in JSON format',
|
||||||
@@ -26,7 +26,7 @@ registerEnumType(ServerlessFunctionSyncStatus, {
|
|||||||
description: 'SyncStatus of the serverlessFunction',
|
description: 'SyncStatus of the serverlessFunction',
|
||||||
});
|
});
|
||||||
|
|
||||||
@ObjectType('serverlessFunction')
|
@ObjectType('ServerlessFunction')
|
||||||
@Authorize({
|
@Authorize({
|
||||||
authorize: (context: any) => ({
|
authorize: (context: any) => ({
|
||||||
workspaceId: { eq: context?.req?.user?.workspace?.id },
|
workspaceId: { eq: context?.req?.user?.workspace?.id },
|
||||||
@@ -47,11 +47,25 @@ export class ServerlessFunctionDto {
|
|||||||
@Field()
|
@Field()
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Field()
|
||||||
|
description: string;
|
||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@Field()
|
@Field()
|
||||||
sourceCodeHash: string;
|
sourceCodeHash: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Field()
|
||||||
|
sourceCodeFullPath: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Field()
|
||||||
|
runtime: string;
|
||||||
|
|
||||||
@IsEnum(ServerlessFunctionSyncStatus)
|
@IsEnum(ServerlessFunctionSyncStatus)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@Field(() => ServerlessFunctionSyncStatus)
|
@Field(() => ServerlessFunctionSyncStatus)
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Field, InputType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { IsNotEmpty, IsString, IsUUID } from 'class-validator';
|
||||||
|
|
||||||
|
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class UpdateServerlessFunctionInput {
|
||||||
|
@Field(() => UUIDScalarType, {
|
||||||
|
description: 'Id of the serverless function to execute',
|
||||||
|
})
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsUUID()
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Field()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@Field({ nullable: true })
|
||||||
|
description?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
@Field()
|
||||||
|
code: string;
|
||||||
|
}
|
||||||
@@ -12,6 +12,10 @@ export enum ServerlessFunctionSyncStatus {
|
|||||||
READY = 'READY',
|
READY = 'READY',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum ServerlessFunctionRuntime {
|
||||||
|
NODE18 = 'nodejs18.x',
|
||||||
|
}
|
||||||
|
|
||||||
@Entity('serverlessFunction')
|
@Entity('serverlessFunction')
|
||||||
@Unique('IndexOnNameAndWorkspaceIdUnique', ['name', 'workspaceId'])
|
@Unique('IndexOnNameAndWorkspaceIdUnique', ['name', 'workspaceId'])
|
||||||
export class ServerlessFunctionEntity {
|
export class ServerlessFunctionEntity {
|
||||||
@@ -21,9 +25,18 @@ export class ServerlessFunctionEntity {
|
|||||||
@Column({ nullable: false })
|
@Column({ nullable: false })
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
description: string;
|
||||||
|
|
||||||
@Column({ nullable: false })
|
@Column({ nullable: false })
|
||||||
sourceCodeHash: string;
|
sourceCodeHash: string;
|
||||||
|
|
||||||
|
@Column({ nullable: false })
|
||||||
|
sourceCodeFullPath: string;
|
||||||
|
|
||||||
|
@Column({ nullable: false, default: ServerlessFunctionRuntime.NODE18 })
|
||||||
|
runtime: ServerlessFunctionRuntime;
|
||||||
|
|
||||||
@Column({
|
@Column({
|
||||||
nullable: false,
|
nullable: false,
|
||||||
default: ServerlessFunctionSyncStatus.NOT_READY,
|
default: ServerlessFunctionSyncStatus.NOT_READY,
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export class ServerlessFunctionException extends CustomException {
|
|||||||
|
|
||||||
export enum ServerlessFunctionExceptionCode {
|
export enum ServerlessFunctionExceptionCode {
|
||||||
SERVERLESS_FUNCTION_NOT_FOUND = 'SERVERLESS_FUNCTION_NOT_FOUND',
|
SERVERLESS_FUNCTION_NOT_FOUND = 'SERVERLESS_FUNCTION_NOT_FOUND',
|
||||||
|
FEATURE_FLAG_INVALID = 'FEATURE_FLAG_INVALID',
|
||||||
SERVERLESS_FUNCTION_ALREADY_EXIST = 'SERVERLESS_FUNCTION_ALREADY_EXIST',
|
SERVERLESS_FUNCTION_ALREADY_EXIST = 'SERVERLESS_FUNCTION_ALREADY_EXIST',
|
||||||
SERVERLESS_FUNCTION_NOT_READY = 'SERVERLESS_FUNCTION_NOT_READY',
|
SERVERLESS_FUNCTION_NOT_READY = 'SERVERLESS_FUNCTION_NOT_READY',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
NestjsQueryGraphQLModule,
|
NestjsQueryGraphQLModule,
|
||||||
@@ -14,6 +15,7 @@ import { ServerlessFunctionResolver } from 'src/engine/metadata-modules/serverle
|
|||||||
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
||||||
import { ServerlessFunctionDto } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
|
import { ServerlessFunctionDto } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
|
||||||
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
||||||
|
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -24,6 +26,7 @@ import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-
|
|||||||
[ServerlessFunctionEntity],
|
[ServerlessFunctionEntity],
|
||||||
'metadata',
|
'metadata',
|
||||||
),
|
),
|
||||||
|
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
||||||
],
|
],
|
||||||
services: [ServerlessFunctionService],
|
services: [ServerlessFunctionService],
|
||||||
resolvers: [
|
resolvers: [
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { UseGuards } from '@nestjs/common';
|
import { UseGuards } from '@nestjs/common';
|
||||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
import { FileUpload, GraphQLUpload } from 'graphql-upload';
|
import { FileUpload, GraphQLUpload } from 'graphql-upload';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
||||||
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service';
|
||||||
@@ -9,45 +11,136 @@ import { ExecuteServerlessFunctionInput } from 'src/engine/metadata-modules/serv
|
|||||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { ServerlessFunctionDto } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
|
import { ServerlessFunctionDto } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto';
|
||||||
import { ServerlessFunctionExecutionResultDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result-d-t.o';
|
import { ServerlessFunctionExecutionResultDto } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto';
|
||||||
import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils';
|
import { serverlessFunctionGraphQLApiExceptionHandler } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-graphql-api-exception-handler.utils';
|
||||||
|
import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input';
|
||||||
|
import { CreateServerlessFunctionFromFileInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function-from-file.input';
|
||||||
|
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
|
||||||
|
import { DeleteServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/delete-serverless-function.input';
|
||||||
|
import {
|
||||||
|
FeatureFlagEntity,
|
||||||
|
FeatureFlagKeys,
|
||||||
|
} from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||||
|
import {
|
||||||
|
ServerlessFunctionException,
|
||||||
|
ServerlessFunctionExceptionCode,
|
||||||
|
} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception';
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class ServerlessFunctionResolver {
|
export class ServerlessFunctionResolver {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly serverlessFunctionService: ServerlessFunctionService,
|
private readonly serverlessFunctionService: ServerlessFunctionService,
|
||||||
|
@InjectRepository(FeatureFlagEntity, 'core')
|
||||||
|
private readonly featureFlagRepository: Repository<FeatureFlagEntity>,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
async checkFeatureFlag(workspaceId: string) {
|
||||||
|
const isFunctionSettingsEnabled =
|
||||||
|
await this.featureFlagRepository.findOneBy({
|
||||||
|
workspaceId,
|
||||||
|
key: FeatureFlagKeys.IsFunctionSettingsEnabled,
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isFunctionSettingsEnabled) {
|
||||||
|
throw new ServerlessFunctionException(
|
||||||
|
`IS_FUNCTION_SETTINGS_ENABLED feature flag is not set to true for this workspace`,
|
||||||
|
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Mutation(() => ServerlessFunctionDto)
|
@Mutation(() => ServerlessFunctionDto)
|
||||||
async createOneServerlessFunction(
|
async deleteOneServerlessFunction(
|
||||||
@Args({ name: 'file', type: () => GraphQLUpload })
|
@Args('input') input: DeleteServerlessFunctionInput,
|
||||||
file: FileUpload,
|
|
||||||
@Args('name', { type: () => String }) name: string,
|
|
||||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
return await this.serverlessFunctionService.createOne(
|
await this.checkFeatureFlag(workspaceId);
|
||||||
name,
|
|
||||||
|
return await this.serverlessFunctionService.deleteOneServerlessFunction(
|
||||||
|
input.id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
file,
|
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
serverlessFunctionGraphQLApiExceptionHandler(error);
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Mutation(() => ServerlessFunctionExecutionResultDTO)
|
@Mutation(() => ServerlessFunctionDto)
|
||||||
|
async updateOneServerlessFunction(
|
||||||
|
@Args('input')
|
||||||
|
input: UpdateServerlessFunctionInput,
|
||||||
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await this.checkFeatureFlag(workspaceId);
|
||||||
|
|
||||||
|
return await this.serverlessFunctionService.updateOneServerlessFunction(
|
||||||
|
input,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => ServerlessFunctionDto)
|
||||||
|
async createOneServerlessFunction(
|
||||||
|
@Args('input')
|
||||||
|
input: CreateServerlessFunctionInput,
|
||||||
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await this.checkFeatureFlag(workspaceId);
|
||||||
|
|
||||||
|
return await this.serverlessFunctionService.createOneServerlessFunction(
|
||||||
|
{
|
||||||
|
name: input.name,
|
||||||
|
description: input.description,
|
||||||
|
},
|
||||||
|
input.code,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => ServerlessFunctionDto)
|
||||||
|
async createOneServerlessFunctionFromFile(
|
||||||
|
@Args({ name: 'file', type: () => GraphQLUpload })
|
||||||
|
file: FileUpload,
|
||||||
|
@Args('input')
|
||||||
|
input: CreateServerlessFunctionFromFileInput,
|
||||||
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
await this.checkFeatureFlag(workspaceId);
|
||||||
|
|
||||||
|
return await this.serverlessFunctionService.createOneServerlessFunction(
|
||||||
|
input,
|
||||||
|
file,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
serverlessFunctionGraphQLApiExceptionHandler(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => ServerlessFunctionExecutionResultDto)
|
||||||
async executeOneServerlessFunction(
|
async executeOneServerlessFunction(
|
||||||
@Args() executeServerlessFunctionInput: ExecuteServerlessFunctionInput,
|
@Args() executeServerlessFunctionInput: ExecuteServerlessFunctionInput,
|
||||||
@AuthWorkspace() { id: workspaceId }: Workspace,
|
@AuthWorkspace() { id: workspaceId }: Workspace,
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const { name, payload } = executeServerlessFunctionInput;
|
await this.checkFeatureFlag(workspaceId);
|
||||||
|
const { id, payload } = executeServerlessFunctionInput;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
result: await this.serverlessFunctionService.executeOne(
|
result: await this.serverlessFunctionService.executeOne(
|
||||||
name,
|
id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
payload,
|
payload,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { join } from 'path';
|
|||||||
|
|
||||||
import { FileUpload } from 'graphql-upload';
|
import { FileUpload } from 'graphql-upload';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
|
||||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||||
|
|
||||||
@@ -21,24 +23,28 @@ import { readFileContent } from 'src/engine/integrations/file-storage/utils/read
|
|||||||
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
|
import { FileStorageService } from 'src/engine/integrations/file-storage/file-storage.service';
|
||||||
import { SOURCE_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/source-file-name';
|
import { SOURCE_FILE_NAME } from 'src/engine/integrations/serverless/drivers/constants/source-file-name';
|
||||||
import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils';
|
import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils';
|
||||||
|
import { CreateServerlessFunctionFromFileInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function-from-file.input';
|
||||||
|
import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ServerlessFunctionService {
|
export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFunctionEntity> {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly fileStorageService: FileStorageService,
|
private readonly fileStorageService: FileStorageService,
|
||||||
private readonly serverlessService: ServerlessService,
|
private readonly serverlessService: ServerlessService,
|
||||||
@InjectRepository(ServerlessFunctionEntity, 'metadata')
|
@InjectRepository(ServerlessFunctionEntity, 'metadata')
|
||||||
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
|
private readonly serverlessFunctionRepository: Repository<ServerlessFunctionEntity>,
|
||||||
) {}
|
) {
|
||||||
|
super(serverlessFunctionRepository);
|
||||||
|
}
|
||||||
|
|
||||||
async executeOne(
|
async executeOne(
|
||||||
name: string,
|
id: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
payload: object | undefined = undefined,
|
payload: object | undefined = undefined,
|
||||||
) {
|
) {
|
||||||
const functionToExecute = await this.serverlessFunctionRepository.findOne({
|
const functionToExecute = await this.serverlessFunctionRepository.findOne({
|
||||||
where: {
|
where: {
|
||||||
name,
|
id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -62,14 +68,82 @@ export class ServerlessFunctionService {
|
|||||||
return this.serverlessService.execute(functionToExecute, payload);
|
return this.serverlessService.execute(functionToExecute, payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createOne(
|
async deleteOneServerlessFunction(id: string, workspaceId: string) {
|
||||||
name: string,
|
const existingServerlessFunction =
|
||||||
|
await this.serverlessFunctionRepository.findOne({
|
||||||
|
where: { id, workspaceId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingServerlessFunction) {
|
||||||
|
throw new ServerlessFunctionException(
|
||||||
|
`Function does not exist`,
|
||||||
|
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await super.deleteOne(id);
|
||||||
|
|
||||||
|
await this.serverlessService.delete(existingServerlessFunction);
|
||||||
|
|
||||||
|
return existingServerlessFunction;
|
||||||
|
}
|
||||||
|
|
||||||
|
async updateOneServerlessFunction(
|
||||||
|
serverlessFunctionInput: UpdateServerlessFunctionInput,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
{ createReadStream, mimetype }: FileUpload,
|
|
||||||
) {
|
) {
|
||||||
const existingServerlessFunction =
|
const existingServerlessFunction =
|
||||||
await this.serverlessFunctionRepository.findOne({
|
await this.serverlessFunctionRepository.findOne({
|
||||||
where: { name, workspaceId },
|
where: { id: serverlessFunctionInput.id, workspaceId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingServerlessFunction) {
|
||||||
|
throw new ServerlessFunctionException(
|
||||||
|
`Function does not exist`,
|
||||||
|
ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const codeHasChanged =
|
||||||
|
serverlessFunctionCreateHash(serverlessFunctionInput.code) !==
|
||||||
|
existingServerlessFunction.sourceCodeHash;
|
||||||
|
|
||||||
|
await super.updateOne(existingServerlessFunction.id, {
|
||||||
|
name: serverlessFunctionInput.name,
|
||||||
|
description: serverlessFunctionInput.description,
|
||||||
|
sourceCodeHash: serverlessFunctionCreateHash(
|
||||||
|
serverlessFunctionInput.code,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (codeHasChanged) {
|
||||||
|
const fileFolder = join(
|
||||||
|
FileFolder.ServerlessFunction,
|
||||||
|
workspaceId,
|
||||||
|
existingServerlessFunction.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.fileStorageService.write({
|
||||||
|
file: serverlessFunctionInput.code,
|
||||||
|
name: SOURCE_FILE_NAME,
|
||||||
|
mimeType: undefined,
|
||||||
|
folder: fileFolder,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.serverlessService.build(existingServerlessFunction);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.findById(existingServerlessFunction.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createOneServerlessFunction(
|
||||||
|
serverlessFunctionInput: CreateServerlessFunctionFromFileInput,
|
||||||
|
code: FileUpload | string,
|
||||||
|
workspaceId: string,
|
||||||
|
) {
|
||||||
|
const existingServerlessFunction =
|
||||||
|
await this.serverlessFunctionRepository.findOne({
|
||||||
|
where: { name: serverlessFunctionInput.name, workspaceId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingServerlessFunction) {
|
if (existingServerlessFunction) {
|
||||||
@@ -79,34 +153,44 @@ export class ServerlessFunctionService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const typescriptCode = await readFileContent(createReadStream());
|
let typescriptCode: string;
|
||||||
|
|
||||||
const serverlessFunction = await this.serverlessFunctionRepository.save({
|
if (typeof code === 'string') {
|
||||||
name,
|
typescriptCode = code;
|
||||||
workspaceId,
|
} else {
|
||||||
sourceCodeHash: serverlessFunctionCreateHash(typescriptCode),
|
typescriptCode = await readFileContent(code.createReadStream());
|
||||||
});
|
}
|
||||||
|
|
||||||
|
const serverlessFunctionId = v4();
|
||||||
|
|
||||||
const fileFolder = join(
|
const fileFolder = join(
|
||||||
FileFolder.ServerlessFunction,
|
FileFolder.ServerlessFunction,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
serverlessFunction.id,
|
serverlessFunctionId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const sourceCodeFullPath = fileFolder + '/' + SOURCE_FILE_NAME;
|
||||||
|
|
||||||
|
const serverlessFunction = await super.createOne({
|
||||||
|
...serverlessFunctionInput,
|
||||||
|
id: serverlessFunctionId,
|
||||||
|
workspaceId,
|
||||||
|
sourceCodeHash: serverlessFunctionCreateHash(typescriptCode),
|
||||||
|
sourceCodeFullPath,
|
||||||
|
});
|
||||||
|
|
||||||
await this.fileStorageService.write({
|
await this.fileStorageService.write({
|
||||||
file: typescriptCode,
|
file: typescriptCode,
|
||||||
name: SOURCE_FILE_NAME,
|
name: SOURCE_FILE_NAME,
|
||||||
mimeType: mimetype,
|
mimeType: undefined,
|
||||||
folder: fileFolder,
|
folder: fileFolder,
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.serverlessService.build(serverlessFunction);
|
await this.serverlessService.build(serverlessFunction);
|
||||||
await this.serverlessFunctionRepository.update(serverlessFunction.id, {
|
await super.updateOne(serverlessFunctionId, {
|
||||||
syncStatus: ServerlessFunctionSyncStatus.READY,
|
syncStatus: ServerlessFunctionSyncStatus.READY,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await this.serverlessFunctionRepository.findOneByOrFail({
|
return await this.findById(serverlessFunctionId);
|
||||||
id: serverlessFunction.id,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const serverlessFunctionGraphQLApiExceptionHandler = (error: any) => {
|
|||||||
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_ALREADY_EXIST:
|
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_ALREADY_EXIST:
|
||||||
throw new ConflictError(error.message);
|
throw new ConflictError(error.message);
|
||||||
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_READY:
|
case ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_READY:
|
||||||
|
case ServerlessFunctionExceptionCode.FEATURE_FLAG_INVALID:
|
||||||
throw new ForbiddenError(error.message);
|
throw new ForbiddenError(error.message);
|
||||||
default:
|
default:
|
||||||
throw new InternalServerError(error.message);
|
throw new InternalServerError(error.message);
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ export class AddStandardIdCommand extends CommandRunner {
|
|||||||
IS_MESSAGING_ALIAS_FETCHING_ENABLED: true,
|
IS_MESSAGING_ALIAS_FETCHING_ENABLED: true,
|
||||||
IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED: true,
|
IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED: true,
|
||||||
IS_FREE_ACCESS_ENABLED: false,
|
IS_FREE_ACCESS_ENABLED: false,
|
||||||
|
IS_FUNCTION_SETTINGS_ENABLED: false,
|
||||||
IS_WORKFLOW_ENABLED: false,
|
IS_WORKFLOW_ENABLED: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -81,6 +82,7 @@ export class AddStandardIdCommand extends CommandRunner {
|
|||||||
IS_MESSAGING_ALIAS_FETCHING_ENABLED: true,
|
IS_MESSAGING_ALIAS_FETCHING_ENABLED: true,
|
||||||
IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED: true,
|
IS_GOOGLE_CALENDAR_SYNC_V2_ENABLED: true,
|
||||||
IS_FREE_ACCESS_ENABLED: false,
|
IS_FREE_ACCESS_ENABLED: false,
|
||||||
|
IS_FUNCTION_SETTINGS_ENABLED: false,
|
||||||
IS_WORKFLOW_ENABLED: false,
|
IS_WORKFLOW_ENABLED: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export {
|
|||||||
IconFilter,
|
IconFilter,
|
||||||
IconFilterOff,
|
IconFilterOff,
|
||||||
IconFocusCentered,
|
IconFocusCentered,
|
||||||
|
IconFunction,
|
||||||
IconForbid,
|
IconForbid,
|
||||||
IconGripVertical,
|
IconGripVertical,
|
||||||
IconH1,
|
IconH1,
|
||||||
@@ -127,6 +128,7 @@ export {
|
|||||||
IconPhone,
|
IconPhone,
|
||||||
IconPhoto,
|
IconPhoto,
|
||||||
IconPilcrow,
|
IconPilcrow,
|
||||||
|
IconPlayerPlay,
|
||||||
IconPlug,
|
IconPlug,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconPresentation,
|
IconPresentation,
|
||||||
@@ -153,6 +155,7 @@ export {
|
|||||||
IconTags,
|
IconTags,
|
||||||
IconTarget,
|
IconTarget,
|
||||||
IconTargetArrow,
|
IconTargetArrow,
|
||||||
|
IconTestPipe,
|
||||||
IconTextSize,
|
IconTextSize,
|
||||||
IconTimelineEvent,
|
IconTimelineEvent,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
|
|||||||
10
packages/twenty-ui/src/theme/constants/CodeDark.ts
Normal file
10
packages/twenty-ui/src/theme/constants/CodeDark.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { COLOR } from './Colors';
|
||||||
|
|
||||||
|
export const CODE_DARK = {
|
||||||
|
text: {
|
||||||
|
gray: COLOR.gray50,
|
||||||
|
sky: COLOR.sky50,
|
||||||
|
pink: COLOR.pink50,
|
||||||
|
orange: COLOR.orange40,
|
||||||
|
},
|
||||||
|
};
|
||||||
10
packages/twenty-ui/src/theme/constants/CodeLight.ts
Normal file
10
packages/twenty-ui/src/theme/constants/CodeLight.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { COLOR } from './Colors';
|
||||||
|
|
||||||
|
export const CODE_LIGHT = {
|
||||||
|
text: {
|
||||||
|
gray: COLOR.gray50,
|
||||||
|
sky: COLOR.sky50,
|
||||||
|
pink: COLOR.pink50,
|
||||||
|
orange: COLOR.orange40,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -9,6 +9,7 @@ import { BOX_SHADOW_DARK } from './BoxShadowDark';
|
|||||||
import { FONT_DARK } from './FontDark';
|
import { FONT_DARK } from './FontDark';
|
||||||
import { TAG_DARK } from './TagDark';
|
import { TAG_DARK } from './TagDark';
|
||||||
import { THEME_COMMON } from './ThemeCommon';
|
import { THEME_COMMON } from './ThemeCommon';
|
||||||
|
import { CODE_DARK } from './CodeDark';
|
||||||
|
|
||||||
export const THEME_DARK: ThemeType = {
|
export const THEME_DARK: ThemeType = {
|
||||||
...THEME_COMMON,
|
...THEME_COMMON,
|
||||||
@@ -22,5 +23,6 @@ export const THEME_DARK: ThemeType = {
|
|||||||
name: 'dark',
|
name: 'dark',
|
||||||
snackBar: SNACK_BAR_DARK,
|
snackBar: SNACK_BAR_DARK,
|
||||||
tag: TAG_DARK,
|
tag: TAG_DARK,
|
||||||
|
code: CODE_DARK,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { BOX_SHADOW_LIGHT } from './BoxShadowLight';
|
|||||||
import { FONT_LIGHT } from './FontLight';
|
import { FONT_LIGHT } from './FontLight';
|
||||||
import { TAG_LIGHT } from './TagLight';
|
import { TAG_LIGHT } from './TagLight';
|
||||||
import { THEME_COMMON } from './ThemeCommon';
|
import { THEME_COMMON } from './ThemeCommon';
|
||||||
|
import { CODE_LIGHT } from './CodeLight';
|
||||||
|
|
||||||
export const THEME_LIGHT = {
|
export const THEME_LIGHT = {
|
||||||
...THEME_COMMON,
|
...THEME_COMMON,
|
||||||
@@ -21,5 +22,6 @@ export const THEME_LIGHT = {
|
|||||||
name: 'light',
|
name: 'light',
|
||||||
snackBar: SNACK_BAR_LIGHT,
|
snackBar: SNACK_BAR_LIGHT,
|
||||||
tag: TAG_LIGHT,
|
tag: TAG_LIGHT,
|
||||||
|
code: CODE_LIGHT,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ export * from './constants/BorderDark';
|
|||||||
export * from './constants/BorderLight';
|
export * from './constants/BorderLight';
|
||||||
export * from './constants/BoxShadowDark';
|
export * from './constants/BoxShadowDark';
|
||||||
export * from './constants/BoxShadowLight';
|
export * from './constants/BoxShadowLight';
|
||||||
|
export * from './constants/CodeDark';
|
||||||
|
export * from './constants/CodeLight';
|
||||||
export * from './constants/Colors';
|
export * from './constants/Colors';
|
||||||
export * from './constants/FontCommon';
|
export * from './constants/FontCommon';
|
||||||
export * from './constants/FontDark';
|
export * from './constants/FontDark';
|
||||||
|
|||||||
40
yarn.lock
40
yarn.lock
@@ -8671,6 +8671,30 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@monaco-editor/loader@npm:^1.4.0":
|
||||||
|
version: 1.4.0
|
||||||
|
resolution: "@monaco-editor/loader@npm:1.4.0"
|
||||||
|
dependencies:
|
||||||
|
state-local: "npm:^1.0.6"
|
||||||
|
peerDependencies:
|
||||||
|
monaco-editor: ">= 0.21.0 < 1"
|
||||||
|
checksum: 10c0/68938350adf2f42363a801d87f5d00c87d397d4cba7041141af10a9216bd35c85209b4723a26d56cb32e68eef61471deda2a450f8892891118fbdce7fa1d987d
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"@monaco-editor/react@npm:^4.6.0":
|
||||||
|
version: 4.6.0
|
||||||
|
resolution: "@monaco-editor/react@npm:4.6.0"
|
||||||
|
dependencies:
|
||||||
|
"@monaco-editor/loader": "npm:^1.4.0"
|
||||||
|
peerDependencies:
|
||||||
|
monaco-editor: ">= 0.25.0 < 1"
|
||||||
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
checksum: 10c0/231e9a9b66a530db326f6732de0ebffcce6b79dcfaf4948923d78b9a3d5e2a04b7a06e1f85bbbca45a5ae15c107a124e4c5c46cabadc20a498fb5f2d05f7f379
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@motionone/animation@npm:^10.12.0":
|
"@motionone/animation@npm:^10.12.0":
|
||||||
version: 10.16.3
|
version: 10.16.3
|
||||||
resolution: "@motionone/animation@npm:10.16.3"
|
resolution: "@motionone/animation@npm:10.16.3"
|
||||||
@@ -39375,6 +39399,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"monaco-editor@npm:^0.50.0":
|
||||||
|
version: 0.50.0
|
||||||
|
resolution: "monaco-editor@npm:0.50.0"
|
||||||
|
checksum: 10c0/79189c926c2fc1e3a3b9118e80911599bf18108018fe176c7b47a27b4856b544129f9a59c9a5c321d154d6a30a8d9c231684246e9382f4f18329a548d11cb4d6
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"mri@npm:^1.1.0":
|
"mri@npm:^1.1.0":
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
resolution: "mri@npm:1.2.0"
|
resolution: "mri@npm:1.2.0"
|
||||||
@@ -47161,6 +47192,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"state-local@npm:^1.0.6":
|
||||||
|
version: 1.0.7
|
||||||
|
resolution: "state-local@npm:1.0.7"
|
||||||
|
checksum: 10c0/8dc7daeac71844452fafb514a6d6b6f40d7e2b33df398309ea1c7b3948d6110c57f112b7196500a10c54fdde40291488c52c875575670fb5c819602deca48bd9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"static-browser-server@npm:1.0.3":
|
"static-browser-server@npm:1.0.3":
|
||||||
version: 1.0.3
|
version: 1.0.3
|
||||||
resolution: "static-browser-server@npm:1.0.3"
|
resolution: "static-browser-server@npm:1.0.3"
|
||||||
@@ -49095,6 +49133,7 @@ __metadata:
|
|||||||
"@graphql-yoga/nestjs": "patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga-nestjs-npm-2.1.0-cb509e6047.patch"
|
"@graphql-yoga/nestjs": "patch:@graphql-yoga/nestjs@2.1.0#./patches/@graphql-yoga-nestjs-npm-2.1.0-cb509e6047.patch"
|
||||||
"@langchain/mistralai": "npm:^0.0.24"
|
"@langchain/mistralai": "npm:^0.0.24"
|
||||||
"@langchain/openai": "npm:^0.1.3"
|
"@langchain/openai": "npm:^0.1.3"
|
||||||
|
"@monaco-editor/react": "npm:^4.6.0"
|
||||||
"@nestjs/cache-manager": "npm:^2.2.1"
|
"@nestjs/cache-manager": "npm:^2.2.1"
|
||||||
"@nestjs/cli": "npm:10.3.0"
|
"@nestjs/cli": "npm:10.3.0"
|
||||||
"@nestjs/devtools-integration": "npm:^0.1.6"
|
"@nestjs/devtools-integration": "npm:^0.1.6"
|
||||||
@@ -49125,6 +49164,7 @@ __metadata:
|
|||||||
lodash.omitby: "npm:^4.6.0"
|
lodash.omitby: "npm:^4.6.0"
|
||||||
lodash.uniq: "npm:^4.5.0"
|
lodash.uniq: "npm:^4.5.0"
|
||||||
lodash.uniqby: "npm:^4.7.0"
|
lodash.uniqby: "npm:^4.7.0"
|
||||||
|
monaco-editor: "npm:^0.50.0"
|
||||||
passport: "npm:^0.7.0"
|
passport: "npm:^0.7.0"
|
||||||
psl: "npm:^1.9.0"
|
psl: "npm:^1.9.0"
|
||||||
rimraf: "npm:^5.0.5"
|
rimraf: "npm:^5.0.5"
|
||||||
|
|||||||
Reference in New Issue
Block a user