Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12bd695c59 | ||
|
|
17347736be | ||
|
|
dfe0b5bfd4 | ||
|
|
186d5b8faa | ||
|
|
011afa6011 | ||
|
|
a2fa941ce3 | ||
|
|
898713bd49 | ||
|
|
4658d44d8b | ||
|
|
a596e7d904 | ||
|
|
91f2f08995 |
@@ -4,7 +4,7 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h2 align="center" >The #1 Open-Source CRM</h2>
|
||||
<h2 align="center">The #1 Open-Source CRM</h2>
|
||||
|
||||
<p align="center"><a href="https://twenty.com"><img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="12" height="12"/> Website</a> · <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="12" height="12"/> Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/map-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.webp" width="12" height="12"/> Figma</a></p>
|
||||
|
||||
|
||||
@@ -1434,11 +1434,6 @@ type EnterpriseSubscriptionStatusDTO {
|
||||
isCancellationScheduled: Boolean!
|
||||
}
|
||||
|
||||
type Analytics {
|
||||
"""Boolean that confirms query was dispatched"""
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type VerificationRecord {
|
||||
type: String!
|
||||
key: String!
|
||||
@@ -2621,6 +2616,11 @@ type SendEmailOutput {
|
||||
error: String
|
||||
}
|
||||
|
||||
type Analytics {
|
||||
"""Boolean that confirms query was dispatched"""
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type EventLogRecord {
|
||||
event: String!
|
||||
timestamp: DateTime!
|
||||
@@ -3007,7 +3007,6 @@ type Query {
|
||||
): IndexConnection!
|
||||
findManyAgents: [Agent!]!
|
||||
findOneAgent(input: AgentIdInput!): Agent!
|
||||
myConnectedAccounts: [ConnectedAccountPublicDTO!]!
|
||||
getRoles: [Role!]!
|
||||
getToolIndex: [ToolIndexEntry!]!
|
||||
getToolInputSchema(toolName: String!): JSON
|
||||
@@ -3028,6 +3027,7 @@ type Query {
|
||||
getViewGroup(id: String!): ViewGroup
|
||||
myMessageFolders(messageChannelId: UUID): [MessageFolder!]!
|
||||
myMessageChannels(connectedAccountId: UUID): [MessageChannel!]!
|
||||
myConnectedAccounts: [ConnectedAccountPublicDTO!]!
|
||||
myCalendarChannels(connectedAccountId: UUID): [CalendarChannel!]!
|
||||
minimalMetadata: MinimalMetadata!
|
||||
appConnections(filter: ListAppConnectionsInput): [AppConnection!]!
|
||||
@@ -3200,8 +3200,6 @@ type Mutation {
|
||||
updateApiKey(input: UpdateApiKeyInput!): ApiKey
|
||||
revokeApiKey(input: RevokeApiKeyInput!): ApiKey
|
||||
assignRoleToApiKey(apiKeyId: UUID!, roleId: UUID!): Boolean!
|
||||
createObjectEvent(event: String!, recordId: UUID!, objectMetadataId: UUID!, properties: JSON): Analytics!
|
||||
trackAnalytics(type: AnalyticsType!, name: String, event: String, properties: JSON): Analytics!
|
||||
skipSyncEmailOnboardingStep: OnboardingStepSuccess!
|
||||
skipBookOnboardingStep: OnboardingStepSuccess!
|
||||
checkoutSession(recurringInterval: SubscriptionInterval!, plan: BillingPlanKey! = PRO, requirePaymentMethod: Boolean! = true, successUrlPath: String): BillingSession!
|
||||
@@ -3254,7 +3252,6 @@ type Mutation {
|
||||
createOneAgent(input: CreateAgentInput!): Agent!
|
||||
updateOneAgent(input: UpdateAgentInput!): Agent!
|
||||
deleteOneAgent(input: AgentIdInput!): Agent!
|
||||
deleteConnectedAccount(id: UUID!): ConnectedAccountPublicDTO!
|
||||
updateWorkspaceMemberRole(workspaceMemberId: UUID!, roleId: UUID!): WorkspaceMember!
|
||||
createOneRole(createRoleInput: CreateRoleInput!): Role!
|
||||
updateOneRole(updateRoleInput: UpdateRoleInput!): Role!
|
||||
@@ -3283,6 +3280,7 @@ type Mutation {
|
||||
updateMessageChannel(input: UpdateMessageChannelInput!): MessageChannel!
|
||||
createEmailGroupChannel(input: CreateEmailGroupChannelInput!): CreateEmailGroupChannelOutput!
|
||||
deleteEmailGroupChannel(id: UUID!): MessageChannel!
|
||||
deleteConnectedAccount(id: UUID!): ConnectedAccountPublicDTO!
|
||||
updateCalendarChannel(input: UpdateCalendarChannelInput!): CalendarChannel!
|
||||
createChatThread: AgentChatThread!
|
||||
sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String, fileAttachments: [FileAttachmentInput!]): SendChatMessageResult!
|
||||
@@ -3344,6 +3342,8 @@ type Mutation {
|
||||
createSAMLIdentityProvider(input: SetupSAMLSsoInput!): SetupSso!
|
||||
deleteSSOIdentityProvider(input: DeleteSsoInput!): DeleteSso!
|
||||
editSSOIdentityProvider(input: EditSsoInput!): EditSso!
|
||||
createObjectEvent(event: String!, recordId: UUID!, objectMetadataId: UUID!, properties: JSON): Analytics!
|
||||
trackAnalytics(type: AnalyticsType!, name: String, event: String, properties: JSON): Analytics!
|
||||
duplicateDashboard(id: UUID!): DuplicatedDashboard!
|
||||
impersonate(userId: UUID!, workspaceId: UUID!): Impersonate!
|
||||
sendEmail(input: SendEmailInput!): SendEmailOutput!
|
||||
@@ -3716,11 +3716,6 @@ input RevokeApiKeyInput {
|
||||
id: UUID!
|
||||
}
|
||||
|
||||
enum AnalyticsType {
|
||||
PAGEVIEW
|
||||
TRACK
|
||||
}
|
||||
|
||||
input CreateApprovedAccessDomainInput {
|
||||
domain: String!
|
||||
email: String!
|
||||
@@ -4430,6 +4425,11 @@ input EditSsoInput {
|
||||
status: SSOIdentityProviderStatus!
|
||||
}
|
||||
|
||||
enum AnalyticsType {
|
||||
PAGEVIEW
|
||||
TRACK
|
||||
}
|
||||
|
||||
input SendEmailInput {
|
||||
connectedAccountId: String!
|
||||
to: String!
|
||||
@@ -4498,6 +4498,7 @@ type Subscription {
|
||||
onEventSubscription(eventStreamId: String!): EventSubscription
|
||||
logicFunctionLogs(input: LogicFunctionLogsInput!): LogicFunctionLogs!
|
||||
onAgentChatEvent(threadId: UUID!): AgentChatEvent!
|
||||
eventLogsLive(table: EventLogTable!): [EventLogRecord!]
|
||||
}
|
||||
|
||||
input LogicFunctionLogsInput {
|
||||
|
||||
@@ -1091,12 +1091,6 @@ export interface EnterpriseSubscriptionStatusDTO {
|
||||
__typename: 'EnterpriseSubscriptionStatusDTO'
|
||||
}
|
||||
|
||||
export interface Analytics {
|
||||
/** Boolean that confirms query was dispatched */
|
||||
success: Scalars['Boolean']
|
||||
__typename: 'Analytics'
|
||||
}
|
||||
|
||||
export interface VerificationRecord {
|
||||
type: Scalars['String']
|
||||
key: Scalars['String']
|
||||
@@ -2314,6 +2308,12 @@ export interface SendEmailOutput {
|
||||
__typename: 'SendEmailOutput'
|
||||
}
|
||||
|
||||
export interface Analytics {
|
||||
/** Boolean that confirms query was dispatched */
|
||||
success: Scalars['Boolean']
|
||||
__typename: 'Analytics'
|
||||
}
|
||||
|
||||
export interface EventLogRecord {
|
||||
event: Scalars['String']
|
||||
timestamp: Scalars['DateTime']
|
||||
@@ -2613,7 +2613,6 @@ export interface Query {
|
||||
indexMetadatas: IndexConnection
|
||||
findManyAgents: Agent[]
|
||||
findOneAgent: Agent
|
||||
myConnectedAccounts: ConnectedAccountPublicDTO[]
|
||||
getRoles: Role[]
|
||||
getToolIndex: ToolIndexEntry[]
|
||||
getToolInputSchema?: Scalars['JSON']
|
||||
@@ -2625,6 +2624,7 @@ export interface Query {
|
||||
getViewGroup?: ViewGroup
|
||||
myMessageFolders: MessageFolder[]
|
||||
myMessageChannels: MessageChannel[]
|
||||
myConnectedAccounts: ConnectedAccountPublicDTO[]
|
||||
myCalendarChannels: CalendarChannel[]
|
||||
minimalMetadata: MinimalMetadata
|
||||
appConnections: AppConnection[]
|
||||
@@ -2724,8 +2724,6 @@ export interface Mutation {
|
||||
updateApiKey?: ApiKey
|
||||
revokeApiKey?: ApiKey
|
||||
assignRoleToApiKey: Scalars['Boolean']
|
||||
createObjectEvent: Analytics
|
||||
trackAnalytics: Analytics
|
||||
skipSyncEmailOnboardingStep: OnboardingStepSuccess
|
||||
skipBookOnboardingStep: OnboardingStepSuccess
|
||||
checkoutSession: BillingSession
|
||||
@@ -2778,7 +2776,6 @@ export interface Mutation {
|
||||
createOneAgent: Agent
|
||||
updateOneAgent: Agent
|
||||
deleteOneAgent: Agent
|
||||
deleteConnectedAccount: ConnectedAccountPublicDTO
|
||||
updateWorkspaceMemberRole: WorkspaceMember
|
||||
createOneRole: Role
|
||||
updateOneRole: Role
|
||||
@@ -2807,6 +2804,7 @@ export interface Mutation {
|
||||
updateMessageChannel: MessageChannel
|
||||
createEmailGroupChannel: CreateEmailGroupChannelOutput
|
||||
deleteEmailGroupChannel: MessageChannel
|
||||
deleteConnectedAccount: ConnectedAccountPublicDTO
|
||||
updateCalendarChannel: CalendarChannel
|
||||
createChatThread: AgentChatThread
|
||||
sendChatMessage: SendChatMessageResult
|
||||
@@ -2868,6 +2866,8 @@ export interface Mutation {
|
||||
createSAMLIdentityProvider: SetupSso
|
||||
deleteSSOIdentityProvider: DeleteSso
|
||||
editSSOIdentityProvider: EditSso
|
||||
createObjectEvent: Analytics
|
||||
trackAnalytics: Analytics
|
||||
duplicateDashboard: DuplicatedDashboard
|
||||
impersonate: Impersonate
|
||||
sendEmail: SendEmailOutput
|
||||
@@ -2892,16 +2892,17 @@ export interface Mutation {
|
||||
__typename: 'Mutation'
|
||||
}
|
||||
|
||||
export type AnalyticsType = 'PAGEVIEW' | 'TRACK'
|
||||
|
||||
export type WorkspaceMigrationActionType = 'delete' | 'create' | 'update'
|
||||
|
||||
export type AnalyticsType = 'PAGEVIEW' | 'TRACK'
|
||||
|
||||
export type FileFolder = 'ProfilePicture' | 'WorkspaceLogo' | 'Attachment' | 'PersonPicture' | 'CorePicture' | 'File' | 'AgentChat' | 'BuiltLogicFunction' | 'BuiltFrontComponent' | 'PublicAsset' | 'Source' | 'FilesField' | 'Dependencies' | 'Workflow' | 'EmailAttachment' | 'AppTarball' | 'GeneratedSdkClient'
|
||||
|
||||
export interface Subscription {
|
||||
onEventSubscription?: EventSubscription
|
||||
logicFunctionLogs: LogicFunctionLogs
|
||||
onAgentChatEvent: AgentChatEvent
|
||||
eventLogsLive?: EventLogRecord[]
|
||||
__typename: 'Subscription'
|
||||
}
|
||||
|
||||
@@ -4049,13 +4050,6 @@ export interface EnterpriseSubscriptionStatusDTOGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface AnalyticsGenqlSelection{
|
||||
/** Boolean that confirms query was dispatched */
|
||||
success?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface VerificationRecordGenqlSelection{
|
||||
type?: boolean | number
|
||||
key?: boolean | number
|
||||
@@ -5364,6 +5358,13 @@ export interface SendEmailOutputGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface AnalyticsGenqlSelection{
|
||||
/** Boolean that confirms query was dispatched */
|
||||
success?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface EventLogRecordGenqlSelection{
|
||||
event?: boolean | number
|
||||
timestamp?: boolean | number
|
||||
@@ -5671,7 +5672,6 @@ export interface QueryGenqlSelection{
|
||||
filter: IndexFilter} })
|
||||
findManyAgents?: AgentGenqlSelection
|
||||
findOneAgent?: (AgentGenqlSelection & { __args: {input: AgentIdInput} })
|
||||
myConnectedAccounts?: ConnectedAccountPublicDTOGenqlSelection
|
||||
getRoles?: RoleGenqlSelection
|
||||
getToolIndex?: ToolIndexEntryGenqlSelection
|
||||
getToolInputSchema?: { __args: {toolName: Scalars['String']} }
|
||||
@@ -5689,6 +5689,7 @@ export interface QueryGenqlSelection{
|
||||
getViewGroup?: (ViewGroupGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
myMessageFolders?: (MessageFolderGenqlSelection & { __args?: {messageChannelId?: (Scalars['UUID'] | null)} })
|
||||
myMessageChannels?: (MessageChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
myConnectedAccounts?: ConnectedAccountPublicDTOGenqlSelection
|
||||
myCalendarChannels?: (CalendarChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
minimalMetadata?: MinimalMetadataGenqlSelection
|
||||
appConnections?: (AppConnectionGenqlSelection & { __args?: {filter?: (ListAppConnectionsInput | null)} })
|
||||
@@ -5811,8 +5812,6 @@ export interface MutationGenqlSelection{
|
||||
updateApiKey?: (ApiKeyGenqlSelection & { __args: {input: UpdateApiKeyInput} })
|
||||
revokeApiKey?: (ApiKeyGenqlSelection & { __args: {input: RevokeApiKeyInput} })
|
||||
assignRoleToApiKey?: { __args: {apiKeyId: Scalars['UUID'], roleId: Scalars['UUID']} }
|
||||
createObjectEvent?: (AnalyticsGenqlSelection & { __args: {event: Scalars['String'], recordId: Scalars['UUID'], objectMetadataId: Scalars['UUID'], properties?: (Scalars['JSON'] | null)} })
|
||||
trackAnalytics?: (AnalyticsGenqlSelection & { __args: {type: AnalyticsType, name?: (Scalars['String'] | null), event?: (Scalars['String'] | null), properties?: (Scalars['JSON'] | null)} })
|
||||
skipSyncEmailOnboardingStep?: OnboardingStepSuccessGenqlSelection
|
||||
skipBookOnboardingStep?: OnboardingStepSuccessGenqlSelection
|
||||
checkoutSession?: (BillingSessionGenqlSelection & { __args: {recurringInterval: SubscriptionInterval, plan: BillingPlanKey, requirePaymentMethod: Scalars['Boolean'], successUrlPath?: (Scalars['String'] | null)} })
|
||||
@@ -5865,7 +5864,6 @@ export interface MutationGenqlSelection{
|
||||
createOneAgent?: (AgentGenqlSelection & { __args: {input: CreateAgentInput} })
|
||||
updateOneAgent?: (AgentGenqlSelection & { __args: {input: UpdateAgentInput} })
|
||||
deleteOneAgent?: (AgentGenqlSelection & { __args: {input: AgentIdInput} })
|
||||
deleteConnectedAccount?: (ConnectedAccountPublicDTOGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
updateWorkspaceMemberRole?: (WorkspaceMemberGenqlSelection & { __args: {workspaceMemberId: Scalars['UUID'], roleId: Scalars['UUID']} })
|
||||
createOneRole?: (RoleGenqlSelection & { __args: {createRoleInput: CreateRoleInput} })
|
||||
updateOneRole?: (RoleGenqlSelection & { __args: {updateRoleInput: UpdateRoleInput} })
|
||||
@@ -5894,6 +5892,7 @@ export interface MutationGenqlSelection{
|
||||
updateMessageChannel?: (MessageChannelGenqlSelection & { __args: {input: UpdateMessageChannelInput} })
|
||||
createEmailGroupChannel?: (CreateEmailGroupChannelOutputGenqlSelection & { __args: {input: CreateEmailGroupChannelInput} })
|
||||
deleteEmailGroupChannel?: (MessageChannelGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
deleteConnectedAccount?: (ConnectedAccountPublicDTOGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
updateCalendarChannel?: (CalendarChannelGenqlSelection & { __args: {input: UpdateCalendarChannelInput} })
|
||||
createChatThread?: AgentChatThreadGenqlSelection
|
||||
sendChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], text: Scalars['String'], messageId: Scalars['UUID'], browsingContext?: (Scalars['JSON'] | null), modelId?: (Scalars['String'] | null), fileAttachments?: (FileAttachmentInput[] | null)} })
|
||||
@@ -5955,6 +5954,8 @@ export interface MutationGenqlSelection{
|
||||
createSAMLIdentityProvider?: (SetupSsoGenqlSelection & { __args: {input: SetupSAMLSsoInput} })
|
||||
deleteSSOIdentityProvider?: (DeleteSsoGenqlSelection & { __args: {input: DeleteSsoInput} })
|
||||
editSSOIdentityProvider?: (EditSsoGenqlSelection & { __args: {input: EditSsoInput} })
|
||||
createObjectEvent?: (AnalyticsGenqlSelection & { __args: {event: Scalars['String'], recordId: Scalars['UUID'], objectMetadataId: Scalars['UUID'], properties?: (Scalars['JSON'] | null)} })
|
||||
trackAnalytics?: (AnalyticsGenqlSelection & { __args: {type: AnalyticsType, name?: (Scalars['String'] | null), event?: (Scalars['String'] | null), properties?: (Scalars['JSON'] | null)} })
|
||||
duplicateDashboard?: (DuplicatedDashboardGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
impersonate?: (ImpersonateGenqlSelection & { __args: {userId: Scalars['UUID'], workspaceId: Scalars['UUID']} })
|
||||
sendEmail?: (SendEmailOutputGenqlSelection & { __args: {input: SendEmailInput} })
|
||||
@@ -6356,6 +6357,7 @@ export interface SubscriptionGenqlSelection{
|
||||
onEventSubscription?: (EventSubscriptionGenqlSelection & { __args: {eventStreamId: Scalars['String']} })
|
||||
logicFunctionLogs?: (LogicFunctionLogsGenqlSelection & { __args: {input: LogicFunctionLogsInput} })
|
||||
onAgentChatEvent?: (AgentChatEventGenqlSelection & { __args: {threadId: Scalars['UUID']} })
|
||||
eventLogsLive?: (EventLogRecordGenqlSelection & { __args: {table: EventLogTable} })
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
@@ -7003,14 +7005,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const Analytics_possibleTypes: string[] = ['Analytics']
|
||||
export const isAnalytics = (obj?: { __typename?: any } | null): obj is Analytics => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isAnalytics"')
|
||||
return Analytics_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const VerificationRecord_possibleTypes: string[] = ['VerificationRecord']
|
||||
export const isVerificationRecord = (obj?: { __typename?: any } | null): obj is VerificationRecord => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isVerificationRecord"')
|
||||
@@ -8139,6 +8133,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const Analytics_possibleTypes: string[] = ['Analytics']
|
||||
export const isAnalytics = (obj?: { __typename?: any } | null): obj is Analytics => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isAnalytics"')
|
||||
return Analytics_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const EventLogRecord_possibleTypes: string[] = ['EventLogRecord']
|
||||
export const isEventLogRecord = (obj?: { __typename?: any } | null): obj is EventLogRecord => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isEventLogRecord"')
|
||||
@@ -8990,17 +8992,17 @@ export const enumUsageOperationType = {
|
||||
WEB_SEARCH: 'WEB_SEARCH' as const
|
||||
}
|
||||
|
||||
export const enumAnalyticsType = {
|
||||
PAGEVIEW: 'PAGEVIEW' as const,
|
||||
TRACK: 'TRACK' as const
|
||||
}
|
||||
|
||||
export const enumWorkspaceMigrationActionType = {
|
||||
delete: 'delete' as const,
|
||||
create: 'create' as const,
|
||||
update: 'update' as const
|
||||
}
|
||||
|
||||
export const enumAnalyticsType = {
|
||||
PAGEVIEW: 'PAGEVIEW' as const,
|
||||
TRACK: 'TRACK' as const
|
||||
}
|
||||
|
||||
export const enumFileFolder = {
|
||||
ProfilePicture: 'ProfilePicture' as const,
|
||||
WorkspaceLogo: 'WorkspaceLogo' as const,
|
||||
|
||||
@@ -6,10 +6,10 @@ services:
|
||||
volumes:
|
||||
- server-local-data:/app/packages/twenty-server/.local-storage
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "3010:3000"
|
||||
environment:
|
||||
NODE_PORT: 3000
|
||||
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default
|
||||
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/${PG_DATABASE_NAME}
|
||||
SERVER_URL: ${SERVER_URL}
|
||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
||||
DISABLE_DB_MIGRATIONS: ${DISABLE_DB_MIGRATIONS}
|
||||
@@ -46,11 +46,6 @@ services:
|
||||
# EMAIL_SMTP_USER: ${EMAIL_SMTP_USER:-}
|
||||
# EMAIL_SMTP_PASSWORD: ${EMAIL_SMTP_PASSWORD:-}
|
||||
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: curl --fail http://localhost:3000/healthz
|
||||
interval: 5s
|
||||
@@ -64,7 +59,7 @@ services:
|
||||
- server-local-data:/app/packages/twenty-server/.local-storage
|
||||
command: ["yarn", "worker:prod"]
|
||||
environment:
|
||||
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default
|
||||
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/${PG_DATABASE_NAME}
|
||||
SERVER_URL: ${SERVER_URL}
|
||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
||||
DISABLE_DB_MIGRATIONS: "true" # it already runs on the server
|
||||
@@ -101,38 +96,7 @@ services:
|
||||
# EMAIL_SMTP_USER: ${EMAIL_SMTP_USER:-}
|
||||
# EMAIL_SMTP_PASSWORD: ${EMAIL_SMTP_PASSWORD:-}
|
||||
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
server:
|
||||
condition: service_healthy
|
||||
restart: always
|
||||
|
||||
db:
|
||||
image: postgres:16
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_DB: ${PG_DATABASE_NAME:-default}
|
||||
POSTGRES_PASSWORD: ${PG_DATABASE_PASSWORD:-postgres}
|
||||
POSTGRES_USER: ${PG_DATABASE_USER:-postgres}
|
||||
healthcheck:
|
||||
test: pg_isready -U ${PG_DATABASE_USER:-postgres} -h localhost -d postgres
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
restart: always
|
||||
|
||||
redis:
|
||||
image: redis
|
||||
restart: always
|
||||
command: ["--maxmemory-policy", "noeviction"]
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
server-local-data:
|
||||
|
||||
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 69 KiB |
@@ -4933,12 +4933,18 @@ export type StandardOverrides = {
|
||||
|
||||
export type Subscription = {
|
||||
__typename?: 'Subscription';
|
||||
eventLogsLive?: Maybe<Array<EventLogRecord>>;
|
||||
logicFunctionLogs: LogicFunctionLogs;
|
||||
onAgentChatEvent: AgentChatEvent;
|
||||
onEventSubscription?: Maybe<EventSubscription>;
|
||||
};
|
||||
|
||||
|
||||
export type SubscriptionEventLogsLiveArgs = {
|
||||
table: EventLogTable;
|
||||
};
|
||||
|
||||
|
||||
export type SubscriptionLogicFunctionLogsArgs = {
|
||||
input: LogicFunctionLogsInput;
|
||||
};
|
||||
@@ -7404,7 +7410,14 @@ export type EventLogsQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type EventLogsQuery = { __typename?: 'Query', eventLogs: { __typename?: 'EventLogQueryResult', totalCount: number, records: Array<{ __typename?: 'EventLogRecord', event: string, timestamp: string, userId?: string | null, properties?: any | null, recordId?: string | null, objectMetadataId?: string | null, isCustom?: boolean | null }>, pageInfo: { __typename?: 'EventLogPageInfo', endCursor?: string | null, hasNextPage: boolean } } };
|
||||
export type EventLogsQuery = { __typename?: 'Query', eventLogs: { __typename?: 'EventLogQueryResult', totalCount: number, records: Array<{ __typename?: 'EventLogRecord', event: string, timestamp: string, userId?: string | null, properties?: any | null, recordId?: string | null, objectMetadataId?: string | null }>, pageInfo: { __typename?: 'EventLogPageInfo', endCursor?: string | null, hasNextPage: boolean } } };
|
||||
|
||||
export type EventLogsLiveSubscriptionVariables = Exact<{
|
||||
table: EventLogTable;
|
||||
}>;
|
||||
|
||||
|
||||
export type EventLogsLiveSubscription = { __typename?: 'Subscription', eventLogsLive?: Array<{ __typename?: 'EventLogRecord', event: string, timestamp: string, userId?: string | null, properties?: any | null, recordId?: string | null, objectMetadataId?: string | null }> | null };
|
||||
|
||||
export type UpdateLabPublicFeatureFlagMutationVariables = Exact<{
|
||||
input: UpdateLabPublicFeatureFlagInput;
|
||||
@@ -8255,7 +8268,8 @@ export const SetEnterpriseKeyDocument = {"kind":"Document","definitions":[{"kind
|
||||
export const EnterpriseCheckoutSessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EnterpriseCheckoutSession"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"billingInterval"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enterpriseCheckoutSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"billingInterval"},"value":{"kind":"Variable","name":{"kind":"Name","value":"billingInterval"}}}]}]}}]} as unknown as DocumentNode<EnterpriseCheckoutSessionQuery, EnterpriseCheckoutSessionQueryVariables>;
|
||||
export const EnterprisePortalSessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EnterprisePortalSession"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"returnUrlPath"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enterprisePortalSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"returnUrlPath"},"value":{"kind":"Variable","name":{"kind":"Name","value":"returnUrlPath"}}}]}]}}]} as unknown as DocumentNode<EnterprisePortalSessionQuery, EnterprisePortalSessionQueryVariables>;
|
||||
export const EnterpriseSubscriptionStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EnterpriseSubscriptionStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enterpriseSubscriptionStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"licensee"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}},{"kind":"Field","name":{"kind":"Name","value":"cancelAt"}},{"kind":"Field","name":{"kind":"Name","value":"currentPeriodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"isCancellationScheduled"}}]}}]}}]} as unknown as DocumentNode<EnterpriseSubscriptionStatusQuery, EnterpriseSubscriptionStatusQueryVariables>;
|
||||
export const EventLogsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventLogs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EventLogQueryInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"eventLogs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}},{"kind":"Field","name":{"kind":"Name","value":"properties"}},{"kind":"Field","name":{"kind":"Name","value":"recordId"}},{"kind":"Field","name":{"kind":"Name","value":"objectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}}]}}]}}]}}]} as unknown as DocumentNode<EventLogsQuery, EventLogsQueryVariables>;
|
||||
export const EventLogsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventLogs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EventLogQueryInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"eventLogs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}},{"kind":"Field","name":{"kind":"Name","value":"properties"}},{"kind":"Field","name":{"kind":"Name","value":"recordId"}},{"kind":"Field","name":{"kind":"Name","value":"objectMetadataId"}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}}]}}]}}]}}]} as unknown as DocumentNode<EventLogsQuery, EventLogsQueryVariables>;
|
||||
export const EventLogsLiveDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"EventLogsLive"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"table"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EventLogTable"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"eventLogsLive"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"table"},"value":{"kind":"Variable","name":{"kind":"Name","value":"table"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}},{"kind":"Field","name":{"kind":"Name","value":"properties"}},{"kind":"Field","name":{"kind":"Name","value":"recordId"}},{"kind":"Field","name":{"kind":"Name","value":"objectMetadataId"}}]}}]}}]} as unknown as DocumentNode<EventLogsLiveSubscription, EventLogsLiveSubscriptionVariables>;
|
||||
export const UpdateLabPublicFeatureFlagDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateLabPublicFeatureFlag"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateLabPublicFeatureFlagInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateLabPublicFeatureFlag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]}}]} as unknown as DocumentNode<UpdateLabPublicFeatureFlagMutation, UpdateLabPublicFeatureFlagMutationVariables>;
|
||||
export const UploadWorkspaceMemberProfilePictureDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UploadWorkspaceMemberProfilePicture"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"file"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Upload"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uploadWorkspaceMemberProfilePicture"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"file"},"value":{"kind":"Variable","name":{"kind":"Name","value":"file"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]} as unknown as DocumentNode<UploadWorkspaceMemberProfilePictureMutation, UploadWorkspaceMemberProfilePictureMutationVariables>;
|
||||
export const UpdateUserEmailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateUserEmail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"newEmail"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"verifyEmailRedirectPath"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateUserEmail"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"newEmail"},"value":{"kind":"Variable","name":{"kind":"Name","value":"newEmail"}}},{"kind":"Argument","name":{"kind":"Name","value":"verifyEmailRedirectPath"},"value":{"kind":"Variable","name":{"kind":"Name","value":"verifyEmailRedirectPath"}}}]}]}}]} as unknown as DocumentNode<UpdateUserEmailMutation, UpdateUserEmailMutationVariables>;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { originalDragSelectionComponentState } from '@/object-record/record-drag
|
||||
|
||||
import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-index/constants/RecordIndexRemoveSortingModalId';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { getBoardCardDropBehavior } from '@/object-record/record-board/utils/getBoardCardDropBehavior';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { useAtomComponentSelectorCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorCallbackState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
@@ -82,8 +83,13 @@ export const RecordBoardDragDropContext = ({
|
||||
}
|
||||
|
||||
const existingRecordSorts = store.get(currentRecordSorts);
|
||||
const boardCardDropBehavior = getBoardCardDropBehavior({
|
||||
hasRecordSorts: existingRecordSorts.length > 0,
|
||||
sourceDroppableId: result.source.droppableId,
|
||||
destinationDroppableId: result.destination.droppableId,
|
||||
});
|
||||
|
||||
if (existingRecordSorts.length > 0) {
|
||||
if (boardCardDropBehavior.shouldBlockDrop) {
|
||||
store.set(isRecordBoardDropProcessingCallbackState, false);
|
||||
endRecordDrag();
|
||||
openModal(RECORD_INDEX_REMOVE_SORTING_MODAL_ID);
|
||||
@@ -91,7 +97,9 @@ export const RecordBoardDragDropContext = ({
|
||||
}
|
||||
|
||||
try {
|
||||
processBoardCardDrop(result, originalDragSelection);
|
||||
processBoardCardDrop(result, originalDragSelection, {
|
||||
shouldUpdatePosition: boardCardDropBehavior.shouldUpdatePosition,
|
||||
});
|
||||
} catch (error) {
|
||||
store.set(isRecordBoardDropProcessingCallbackState, false);
|
||||
endRecordDrag();
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { getBoardCardDropBehavior } from '@/object-record/record-board/utils/getBoardCardDropBehavior';
|
||||
|
||||
describe('getBoardCardDropBehavior', () => {
|
||||
it('should block same-column drops when record sorting is active', () => {
|
||||
expect(
|
||||
getBoardCardDropBehavior({
|
||||
hasRecordSorts: true,
|
||||
sourceDroppableId: 'new',
|
||||
destinationDroppableId: 'new',
|
||||
}),
|
||||
).toEqual({
|
||||
shouldBlockDrop: true,
|
||||
shouldUpdatePosition: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow cross-column drops without position updates when record sorting is active', () => {
|
||||
expect(
|
||||
getBoardCardDropBehavior({
|
||||
hasRecordSorts: true,
|
||||
sourceDroppableId: 'new',
|
||||
destinationDroppableId: 'won',
|
||||
}),
|
||||
).toEqual({
|
||||
shouldBlockDrop: false,
|
||||
shouldUpdatePosition: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow drops with position updates when record sorting is inactive', () => {
|
||||
expect(
|
||||
getBoardCardDropBehavior({
|
||||
hasRecordSorts: false,
|
||||
sourceDroppableId: 'new',
|
||||
destinationDroppableId: 'won',
|
||||
}),
|
||||
).toEqual({
|
||||
shouldBlockDrop: false,
|
||||
shouldUpdatePosition: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
export const getBoardCardDropBehavior = ({
|
||||
hasRecordSorts,
|
||||
sourceDroppableId,
|
||||
destinationDroppableId,
|
||||
}: {
|
||||
hasRecordSorts: boolean;
|
||||
sourceDroppableId: string;
|
||||
destinationDroppableId: string;
|
||||
}) => {
|
||||
const isMovingInsideSameRecordGroup =
|
||||
sourceDroppableId === destinationDroppableId;
|
||||
|
||||
return {
|
||||
shouldBlockDrop: hasRecordSorts && isMovingInsideSameRecordGroup,
|
||||
shouldUpdatePosition: !hasRecordSorts,
|
||||
};
|
||||
};
|
||||
@@ -40,9 +40,15 @@ export const useProcessBoardCardDrop = () => {
|
||||
);
|
||||
|
||||
const processBoardCardDrop = useCallback(
|
||||
(boardCardDropResult: DropResult, selectedRecordIds: string[]) => {
|
||||
(
|
||||
boardCardDropResult: DropResult,
|
||||
selectedRecordIds: string[],
|
||||
options?: { shouldUpdatePosition?: boolean },
|
||||
) => {
|
||||
if (!isDefined(selectFieldMetadataItem)) return;
|
||||
|
||||
const shouldUpdatePosition = options?.shouldUpdatePosition ?? true;
|
||||
|
||||
processGroupDrop({
|
||||
groupDropResult: boardCardDropResult,
|
||||
store,
|
||||
@@ -51,7 +57,10 @@ export const useProcessBoardCardDrop = () => {
|
||||
recordIndexRecordIdsByGroupCallbackFamilyState,
|
||||
onUpdateRecord: ({ recordId, position }, targetRecordGroupValue) => {
|
||||
updateDroppedRecordOnBoard(
|
||||
{ recordId, position },
|
||||
{
|
||||
recordId,
|
||||
position: shouldUpdatePosition ? position : undefined,
|
||||
},
|
||||
targetRecordGroupValue,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -45,10 +45,6 @@ export const useUpdateDroppedRecordOnBoard = () => {
|
||||
recordStoreFamilyState.atomFamily(recordId),
|
||||
) as Record<string, unknown> | null | undefined;
|
||||
|
||||
if (!isDefined(newPosition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefined(initialRecord)) {
|
||||
return;
|
||||
}
|
||||
@@ -94,7 +90,10 @@ export const useUpdateDroppedRecordOnBoard = () => {
|
||||
|
||||
const isSamePosition = initialRecord.position === newPosition;
|
||||
|
||||
if (movingInsideSameRecordGroup && isSamePosition) {
|
||||
if (
|
||||
movingInsideSameRecordGroup &&
|
||||
(!isDefined(newPosition) || isSamePosition)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -126,25 +125,32 @@ export const useUpdateDroppedRecordOnBoard = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const targetGroupRecordsWithIds = extractRecordPositions(
|
||||
currentRecordIdsInTargetRecordGroup,
|
||||
store,
|
||||
);
|
||||
if (isDefined(newPosition)) {
|
||||
const targetGroupRecordsWithIds = extractRecordPositions(
|
||||
currentRecordIdsInTargetRecordGroup,
|
||||
store,
|
||||
);
|
||||
|
||||
const newTargetRecordGroupWithIds = [
|
||||
...targetGroupRecordsWithIds,
|
||||
{
|
||||
id: recordId,
|
||||
position: newPosition,
|
||||
},
|
||||
];
|
||||
const newTargetRecordGroupWithIds = [
|
||||
...targetGroupRecordsWithIds,
|
||||
{
|
||||
id: recordId,
|
||||
position: newPosition,
|
||||
},
|
||||
];
|
||||
|
||||
newTargetRecordGroupWithIds.sort(sortByProperty('position', 'asc'));
|
||||
newTargetRecordGroupWithIds.sort(sortByProperty('position', 'asc'));
|
||||
|
||||
store.set(
|
||||
recordIndexRecordIdsByGroupCallbackFamilyState(targetRecordGroupId),
|
||||
newTargetRecordGroupWithIds.map((record) => record.id),
|
||||
);
|
||||
store.set(
|
||||
recordIndexRecordIdsByGroupCallbackFamilyState(targetRecordGroupId),
|
||||
newTargetRecordGroupWithIds.map((record) => record.id),
|
||||
);
|
||||
} else {
|
||||
store.set(
|
||||
recordIndexRecordIdsByGroupCallbackFamilyState(targetRecordGroupId),
|
||||
[...currentRecordIdsInTargetRecordGroup, recordId],
|
||||
);
|
||||
}
|
||||
|
||||
upsertRecordsInStore({
|
||||
partialRecords: [
|
||||
@@ -155,7 +161,7 @@ export const useUpdateDroppedRecordOnBoard = () => {
|
||||
(initialRecord as { __typename?: string })?.__typename ??
|
||||
'Record',
|
||||
[selectFieldMetadataItem.name]: targetRecordGroupValue,
|
||||
position: newPosition,
|
||||
...(isDefined(newPosition) && { position: newPosition }),
|
||||
} as ObjectRecord,
|
||||
],
|
||||
});
|
||||
@@ -164,7 +170,7 @@ export const useUpdateDroppedRecordOnBoard = () => {
|
||||
idToUpdate: recordId,
|
||||
updateOneRecordInput: {
|
||||
[selectFieldMetadataItem.name]: targetRecordGroupValue,
|
||||
position: newPosition,
|
||||
...(isDefined(newPosition) && { position: newPosition }),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
@@ -1,4 +1 @@
|
||||
export {
|
||||
SettingsDatePickerInput as EventLogDatePickerInput,
|
||||
type SettingsDatePickerInputProps as EventLogDatePickerInputProps,
|
||||
} from '@/settings/components/SettingsDatePickerInput';
|
||||
export { SettingsDatePickerInput as EventLogDatePickerInput } from '@/settings/components/SettingsDatePickerInput';
|
||||
|
||||
@@ -12,18 +12,16 @@ import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
||||
|
||||
import {
|
||||
type EventLogRecord,
|
||||
EventLogTable,
|
||||
type EventLogTable,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
import {
|
||||
type ColumnConfig,
|
||||
getColumnsForEventLogTable,
|
||||
} from '@/settings/event-logs/utils/getColumnsForEventLogTable';
|
||||
import { EventLogJsonCell } from '@/settings/event-logs/components/EventLogJsonCell';
|
||||
|
||||
type EventLogResultsTableProps = {
|
||||
records: EventLogRecord[];
|
||||
@@ -106,9 +104,6 @@ export const EventLogResultsTable = ({
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { t } = useLingui();
|
||||
|
||||
const showObjectEventColumns = selectedTable === EventLogTable.OBJECT_EVENT;
|
||||
const showApplicationLogColumns =
|
||||
selectedTable === EventLogTable.APPLICATION_LOG;
|
||||
const baseColumns = getColumnsForEventLogTable(selectedTable);
|
||||
|
||||
const [columnWidths, setColumnWidths] = useState<Record<string, number>>(() =>
|
||||
@@ -117,7 +112,6 @@ export const EventLogResultsTable = ({
|
||||
|
||||
const [resizingColumn, setResizingColumn] = useState<string | null>(null);
|
||||
|
||||
// Reset column widths when switching tables to avoid undefined widths for new columns
|
||||
useEffect(() => {
|
||||
setColumnWidths(
|
||||
Object.fromEntries(baseColumns.map((col) => [col.id, col.defaultWidth])),
|
||||
@@ -242,85 +236,21 @@ export const EventLogResultsTable = ({
|
||||
</StyledResizableHeaderContainer>
|
||||
))}
|
||||
</TableRow>
|
||||
{records.map((record, index) => (
|
||||
{records.map((record) => (
|
||||
<TableRow
|
||||
key={`${record.timestamp}-${record.event}-${index}`}
|
||||
key={`${record.timestamp}-${record.event}`}
|
||||
gridTemplateColumns={gridTemplateColumns}
|
||||
>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{record.event}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{beautifyPastDateRelativeToNow(record.timestamp)}
|
||||
</TableCell>
|
||||
{showApplicationLogColumns ? (
|
||||
<>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{record.properties?.level ?? '-'}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{record.properties?.message ?? '-'}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{record.properties?.executionId ?? '-'}
|
||||
</TableCell>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{record.userId ?? '-'}
|
||||
</TableCell>
|
||||
{showObjectEventColumns && (
|
||||
<>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{record.recordId ?? '-'}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{record.objectMetadataId ?? '-'}
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
<EventLogJsonCell value={record.properties} />
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
{baseColumns.map((column) => (
|
||||
<TableCell
|
||||
key={column.id}
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{column.renderCell(record)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</Table>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
@@ -8,34 +10,26 @@ type EventLogTableSelectorProps = {
|
||||
onChange: (value: EventLogTable) => void;
|
||||
};
|
||||
|
||||
const TABLE_LABELS: Record<EventLogTable, MessageDescriptor> = {
|
||||
[EventLogTable.PAGEVIEW]: msg`Page Views`,
|
||||
[EventLogTable.WORKSPACE_EVENT]: msg`Workspace Events`,
|
||||
[EventLogTable.OBJECT_EVENT]: msg`Object Events`,
|
||||
[EventLogTable.USAGE_EVENT]: msg`Usage Events`,
|
||||
[EventLogTable.APPLICATION_LOG]: msg`Application Logs`,
|
||||
};
|
||||
|
||||
export const EventLogTableSelector = ({
|
||||
value,
|
||||
onChange,
|
||||
}: EventLogTableSelectorProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: EventLogTable.PAGEVIEW,
|
||||
label: t`Page Views`,
|
||||
},
|
||||
{
|
||||
value: EventLogTable.WORKSPACE_EVENT,
|
||||
label: t`Workspace Events`,
|
||||
},
|
||||
{
|
||||
value: EventLogTable.OBJECT_EVENT,
|
||||
label: t`Object Events`,
|
||||
},
|
||||
{
|
||||
value: EventLogTable.USAGE_EVENT,
|
||||
label: t`Usage Events`,
|
||||
},
|
||||
{
|
||||
value: EventLogTable.APPLICATION_LOG,
|
||||
label: t`Application Logs`,
|
||||
},
|
||||
];
|
||||
const options = (
|
||||
Object.entries(TABLE_LABELS) as [EventLogTable, MessageDescriptor][]
|
||||
).map(([table, label]) => ({
|
||||
value: table,
|
||||
label: t(label),
|
||||
}));
|
||||
|
||||
return (
|
||||
<Select
|
||||
|
||||
@@ -1,24 +1,51 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { isClickHouseConfiguredState } from '@/client-config/states/isClickHouseConfiguredState';
|
||||
import { SettingsEmptyPlaceholder } from '@/settings/components/SettingsEmptyPlaceholder';
|
||||
import { SettingsEnterpriseFeatureGateCard } from '@/settings/components/SettingsEnterpriseFeatureGateCard';
|
||||
import { SettingsOptionCardContentButton } from '@/settings/components/SettingsOptions/SettingsOptionCardContentButton';
|
||||
import { EventLogFilters } from '@/settings/event-logs/components/EventLogFilters';
|
||||
import { EventLogResultsTable } from '@/settings/event-logs/components/EventLogResultsTable';
|
||||
import { EventLogTableSelector } from '@/settings/event-logs/components/EventLogTableSelector';
|
||||
import { useEventLogsLiveStream } from '@/settings/event-logs/hooks/useEventLogsLiveStream';
|
||||
import { useEventLogs } from '@/settings/event-logs/hooks/useQueryEventLogs';
|
||||
import { type EventLogFiltersState } from '@/settings/event-logs/types/EventLogFiltersState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconRefresh } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
import {
|
||||
IconArrowUp,
|
||||
IconLock,
|
||||
IconPlayerPause,
|
||||
IconPlayerPlay,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button, IconButton } from 'twenty-ui/input';
|
||||
import { Card } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { EventLogTable } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
BillingEntitlementKey,
|
||||
EventLogTable,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { isGraphqlErrorOfType } from '~/utils/is-graphql-error-of-type.util';
|
||||
|
||||
const StyledRoot = styled.div`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[6]};
|
||||
margin: 0 auto;
|
||||
max-width: 760px;
|
||||
min-height: 0;
|
||||
padding: ${themeCssVariables.spacing[6]} ${themeCssVariables.spacing[8]}
|
||||
${themeCssVariables.spacing[8]};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledCardContent = styled.div`
|
||||
display: flex;
|
||||
@@ -40,8 +67,10 @@ const StyledSelectorGrow = styled.div`
|
||||
|
||||
const StyledResults = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
min-height: 0;
|
||||
`;
|
||||
|
||||
const StyledRecordCount = styled.span`
|
||||
@@ -50,10 +79,9 @@ const StyledRecordCount = styled.span`
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
`;
|
||||
|
||||
// The results table scrolls internally and loads more as you reach the bottom,
|
||||
// so it needs a bounded height.
|
||||
const StyledTableWrapper = styled.div`
|
||||
height: 480px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
@@ -64,45 +92,64 @@ export const SettingsLogs = () => {
|
||||
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const isClickHouseConfigured = useAtomStateValue(isClickHouseConfiguredState);
|
||||
const billing = useAtomStateValue(billingState);
|
||||
const navigateSettings = useNavigateSettings();
|
||||
|
||||
const hasEnterpriseAccess =
|
||||
currentWorkspace?.hasValidSignedEnterpriseKey === true;
|
||||
const isBillingEnabled = billing?.isBillingEnabled ?? false;
|
||||
const hasAuditLogsEntitlement =
|
||||
currentWorkspace?.billingEntitlements?.some(
|
||||
(entitlement) =>
|
||||
entitlement.key === BillingEntitlementKey.AUDIT_LOGS &&
|
||||
entitlement.value,
|
||||
) === true;
|
||||
|
||||
const [selectedTable, setSelectedTable] = useState<EventLogTable>(
|
||||
EventLogTable.PAGEVIEW,
|
||||
);
|
||||
const [filters, setFilters] = useState<EventLogFiltersState>({});
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
|
||||
const isApplicationLog = selectedTable === EventLogTable.APPLICATION_LOG;
|
||||
const canQuery =
|
||||
isClickHouseConfigured && (isApplicationLog || hasEnterpriseAccess);
|
||||
isClickHouseConfigured && (isApplicationLog || hasAuditLogsEntitlement);
|
||||
|
||||
const {
|
||||
records,
|
||||
totalCount,
|
||||
hasNextPage,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
loadMore,
|
||||
} = useEventLogs(
|
||||
{
|
||||
table: selectedTable,
|
||||
filters: {
|
||||
eventType: filters.eventType,
|
||||
userWorkspaceId: filters.userWorkspaceId,
|
||||
dateRange: filters.dateRange
|
||||
? {
|
||||
start: filters.dateRange.start?.toISOString(),
|
||||
end: filters.dateRange.end?.toISOString(),
|
||||
}
|
||||
: undefined,
|
||||
recordId: filters.recordId,
|
||||
objectMetadataId: filters.objectMetadataId,
|
||||
const { records, totalCount, hasNextPage, loading, error, loadMore } =
|
||||
useEventLogs(
|
||||
{
|
||||
table: selectedTable,
|
||||
filters: {
|
||||
eventType: filters.eventType,
|
||||
userWorkspaceId: filters.userWorkspaceId,
|
||||
dateRange: filters.dateRange
|
||||
? {
|
||||
start: filters.dateRange.start?.toISOString(),
|
||||
end: filters.dateRange.end?.toISOString(),
|
||||
}
|
||||
: undefined,
|
||||
recordId: filters.recordId,
|
||||
objectMetadataId: filters.objectMetadataId,
|
||||
},
|
||||
first: RECORDS_PER_PAGE,
|
||||
},
|
||||
first: RECORDS_PER_PAGE,
|
||||
},
|
||||
{ skip: !canQuery },
|
||||
{ skip: !canQuery },
|
||||
);
|
||||
|
||||
const hasActiveFilters =
|
||||
isDefined(filters.eventType) ||
|
||||
isDefined(filters.userWorkspaceId) ||
|
||||
isDefined(filters.recordId) ||
|
||||
isDefined(filters.objectMetadataId) ||
|
||||
isDefined(filters.dateRange?.start) ||
|
||||
isDefined(filters.dateRange?.end);
|
||||
|
||||
const liveRecords = useEventLogsLiveStream({
|
||||
table: selectedTable,
|
||||
enabled: !isPaused && !hasActiveFilters && canQuery,
|
||||
});
|
||||
|
||||
const displayedRecords = useMemo(
|
||||
() => [...liveRecords, ...records],
|
||||
[liveRecords, records],
|
||||
);
|
||||
|
||||
const handleTableChange = (table: EventLogTable) => {
|
||||
@@ -114,39 +161,63 @@ export const SettingsLogs = () => {
|
||||
setFilters(newFilters);
|
||||
};
|
||||
|
||||
const renderUpgradeCard = () => (
|
||||
<Card rounded>
|
||||
<SettingsOptionCardContentButton
|
||||
Icon={IconLock}
|
||||
title={t`Upgrade to access audit logs`}
|
||||
description={t`Only application logs are available on your current plan. Other log types require an Enterprise subscription.`}
|
||||
Button={
|
||||
<Button
|
||||
title={t`Upgrade`}
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
size="small"
|
||||
Icon={IconArrowUp}
|
||||
onClick={() =>
|
||||
navigateSettings(
|
||||
isBillingEnabled
|
||||
? SettingsPath.Billing
|
||||
: SettingsPath.AdminPanelEnterprise,
|
||||
)
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const renderResults = () => {
|
||||
if (!isApplicationLog && !hasEnterpriseAccess) {
|
||||
return (
|
||||
<SettingsEnterpriseFeatureGateCard
|
||||
title={t`Enterprise feature`}
|
||||
description={t`Upgrade to Enterprise to access this log type.`}
|
||||
buttonTitle={t`Activate`}
|
||||
/>
|
||||
);
|
||||
if (!isApplicationLog && !hasAuditLogsEntitlement) {
|
||||
return renderUpgradeCard();
|
||||
}
|
||||
|
||||
if (!isClickHouseConfigured) {
|
||||
return (
|
||||
<SettingsEmptyPlaceholder>
|
||||
{t`Audit logs require ClickHouse to be configured. Please contact your administrator.`}
|
||||
{t`Logs require ClickHouse to be configured. Please contact your administrator.`}
|
||||
</SettingsEmptyPlaceholder>
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(error)) {
|
||||
if (isGraphqlErrorOfType(error, 'NO_ENTITLEMENT')) {
|
||||
return renderUpgradeCard();
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsEmptyPlaceholder>
|
||||
{t`Something went wrong while loading audit logs. Please try again.`}
|
||||
{t`Something went wrong while loading logs. Please try again.`}
|
||||
</SettingsEmptyPlaceholder>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledResults>
|
||||
<StyledRecordCount>{t`${records.length} of ${totalCount}`}</StyledRecordCount>
|
||||
<StyledRecordCount>{t`${displayedRecords.length} of ${totalCount + liveRecords.length}`}</StyledRecordCount>
|
||||
<StyledTableWrapper>
|
||||
<EventLogResultsTable
|
||||
records={records}
|
||||
records={displayedRecords}
|
||||
loading={loading}
|
||||
hasNextPage={hasNextPage}
|
||||
onLoadMore={loadMore}
|
||||
@@ -158,7 +229,7 @@ export const SettingsLogs = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledRoot>
|
||||
<Card rounded fullWidth>
|
||||
<StyledCardContent>
|
||||
<StyledSelectorRow>
|
||||
@@ -168,17 +239,15 @@ export const SettingsLogs = () => {
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</StyledSelectorGrow>
|
||||
<IconButton
|
||||
Icon={IconRefresh}
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
ariaLabel={t`Refresh`}
|
||||
onClick={() => {
|
||||
if (canQuery) {
|
||||
void refetch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{canQuery && (
|
||||
<IconButton
|
||||
Icon={isPaused ? IconPlayerPlay : IconPlayerPause}
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
ariaLabel={isPaused ? t`Resume` : t`Pause`}
|
||||
onClick={() => setIsPaused((previous) => !previous)}
|
||||
/>
|
||||
)}
|
||||
</StyledSelectorRow>
|
||||
<EventLogFilters
|
||||
table={selectedTable}
|
||||
@@ -189,6 +258,6 @@ export const SettingsLogs = () => {
|
||||
</Card>
|
||||
|
||||
{renderResults()}
|
||||
</>
|
||||
</StyledRoot>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@ export const GET_EVENT_LOGS = gql`
|
||||
properties
|
||||
recordId
|
||||
objectMetadataId
|
||||
isCustom
|
||||
}
|
||||
totalCount
|
||||
pageInfo {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const EVENT_LOGS_LIVE_SUBSCRIPTION = gql`
|
||||
subscription EventLogsLive($table: EventLogTable!) {
|
||||
eventLogsLive(table: $table) {
|
||||
event
|
||||
timestamp
|
||||
userId
|
||||
properties
|
||||
recordId
|
||||
objectMetadataId
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,68 @@
|
||||
import { print, type ExecutionResult } from 'graphql';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { EVENT_LOGS_LIVE_SUBSCRIPTION } from '@/settings/event-logs/graphql/subscriptions/EventLogsLiveSubscription';
|
||||
import { sseClientState } from '@/sse-db-event/states/sseClientState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { captureException } from '@sentry/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
type EventLogRecord,
|
||||
type EventLogTable,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type EventLogsLivePayload = {
|
||||
eventLogsLive: EventLogRecord[] | null;
|
||||
};
|
||||
|
||||
const EVENT_LOGS_LIVE_SUBSCRIPTION_QUERY = print(EVENT_LOGS_LIVE_SUBSCRIPTION);
|
||||
|
||||
export const useEventLogsLiveStream = ({
|
||||
table,
|
||||
enabled,
|
||||
}: {
|
||||
table: EventLogTable;
|
||||
enabled: boolean;
|
||||
}): EventLogRecord[] => {
|
||||
const sseClient = useAtomStateValue(sseClientState);
|
||||
const [liveRecords, setLiveRecords] = useState<EventLogRecord[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setLiveRecords([]);
|
||||
}, [table]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
setLiveRecords([]);
|
||||
}
|
||||
}, [enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || !isDefined(sseClient)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dispose = sseClient.subscribe<EventLogsLivePayload>(
|
||||
{
|
||||
query: EVENT_LOGS_LIVE_SUBSCRIPTION_QUERY,
|
||||
variables: { table },
|
||||
},
|
||||
{
|
||||
next: (value: ExecutionResult<EventLogsLivePayload>) => {
|
||||
const incoming = value.data?.eventLogsLive;
|
||||
|
||||
if (isDefined(incoming) && incoming.length > 0) {
|
||||
setLiveRecords((previous) => [...incoming, ...previous]);
|
||||
}
|
||||
},
|
||||
error: (error) => captureException(error),
|
||||
complete: () => {},
|
||||
},
|
||||
);
|
||||
|
||||
return () => dispose();
|
||||
}, [enabled, sseClient, table]);
|
||||
|
||||
return liveRecords;
|
||||
};
|
||||
@@ -20,7 +20,7 @@ export const useEventLogs = (
|
||||
input: EventLogQueryInput,
|
||||
options?: { skip?: boolean },
|
||||
) => {
|
||||
const { data, loading, error, refetch, fetchMore } = useQuery<
|
||||
const { data, loading, error, fetchMore } = useQuery<
|
||||
EventLogsData,
|
||||
EventLogsVariables
|
||||
>(GET_EVENT_LOGS, {
|
||||
@@ -70,7 +70,6 @@ export const useEventLogs = (
|
||||
hasNextPage,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
loadMore,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
|
||||
import { EventLogTable } from '~/generated-metadata/graphql';
|
||||
|
||||
export type ColumnConfig = {
|
||||
id: string;
|
||||
label: MessageDescriptor;
|
||||
minWidth: number;
|
||||
defaultWidth: number;
|
||||
};
|
||||
|
||||
const DEFAULT_COLUMNS: ColumnConfig[] = [
|
||||
{ id: 'event', label: msg`Event`, minWidth: 100, defaultWidth: 200 },
|
||||
{
|
||||
id: 'timestamp',
|
||||
label: msg`Timestamp`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 150,
|
||||
},
|
||||
{ id: 'userId', label: msg`User`, minWidth: 100, defaultWidth: 150 },
|
||||
{
|
||||
id: 'properties',
|
||||
label: msg`Properties`,
|
||||
minWidth: 200,
|
||||
defaultWidth: 400,
|
||||
},
|
||||
];
|
||||
|
||||
const OBJECT_EVENT_COLUMNS: ColumnConfig[] = [
|
||||
{ id: 'event', label: msg`Event`, minWidth: 100, defaultWidth: 180 },
|
||||
{
|
||||
id: 'timestamp',
|
||||
label: msg`Timestamp`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 130,
|
||||
},
|
||||
{ id: 'userId', label: msg`User`, minWidth: 100, defaultWidth: 130 },
|
||||
{
|
||||
id: 'recordId',
|
||||
label: msg`Record ID`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 130,
|
||||
},
|
||||
{
|
||||
id: 'objectMetadataId',
|
||||
label: msg`Object ID`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 130,
|
||||
},
|
||||
{
|
||||
id: 'properties',
|
||||
label: msg`Properties`,
|
||||
minWidth: 150,
|
||||
defaultWidth: 300,
|
||||
},
|
||||
];
|
||||
|
||||
const USAGE_EVENT_COLUMNS: ColumnConfig[] = [
|
||||
{
|
||||
id: 'event',
|
||||
label: msg`Resource Type`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 130,
|
||||
},
|
||||
{
|
||||
id: 'timestamp',
|
||||
label: msg`Timestamp`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 140,
|
||||
},
|
||||
{ id: 'userId', label: msg`User`, minWidth: 100, defaultWidth: 130 },
|
||||
{
|
||||
id: 'properties',
|
||||
label: msg`Details`,
|
||||
minWidth: 200,
|
||||
defaultWidth: 400,
|
||||
},
|
||||
];
|
||||
|
||||
const APPLICATION_LOG_COLUMNS: ColumnConfig[] = [
|
||||
{
|
||||
id: 'event',
|
||||
label: msg`Function`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 160,
|
||||
},
|
||||
{
|
||||
id: 'timestamp',
|
||||
label: msg`Timestamp`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 140,
|
||||
},
|
||||
{ id: 'level', label: msg`Level`, minWidth: 60, defaultWidth: 80 },
|
||||
{
|
||||
id: 'message',
|
||||
label: msg`Message`,
|
||||
minWidth: 200,
|
||||
defaultWidth: 400,
|
||||
},
|
||||
{
|
||||
id: 'executionId',
|
||||
label: msg`Execution ID`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 140,
|
||||
},
|
||||
];
|
||||
|
||||
const COLUMNS_BY_TABLE: Record<EventLogTable, ColumnConfig[]> = {
|
||||
[EventLogTable.OBJECT_EVENT]: OBJECT_EVENT_COLUMNS,
|
||||
[EventLogTable.USAGE_EVENT]: USAGE_EVENT_COLUMNS,
|
||||
[EventLogTable.APPLICATION_LOG]: APPLICATION_LOG_COLUMNS,
|
||||
[EventLogTable.WORKSPACE_EVENT]: DEFAULT_COLUMNS,
|
||||
[EventLogTable.PAGEVIEW]: DEFAULT_COLUMNS,
|
||||
};
|
||||
|
||||
export const getColumnsForEventLogTable = (
|
||||
table: EventLogTable,
|
||||
): ColumnConfig[] => {
|
||||
return COLUMNS_BY_TABLE[table] ?? DEFAULT_COLUMNS;
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
|
||||
import { EventLogJsonCell } from '@/settings/event-logs/components/EventLogJsonCell';
|
||||
import {
|
||||
type EventLogRecord,
|
||||
EventLogTable,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
||||
|
||||
export type ColumnConfig = {
|
||||
id: string;
|
||||
label: MessageDescriptor;
|
||||
minWidth: number;
|
||||
defaultWidth: number;
|
||||
renderCell: (record: EventLogRecord) => ReactNode;
|
||||
};
|
||||
|
||||
const EVENT_COLUMN: ColumnConfig = {
|
||||
id: 'event',
|
||||
label: msg`Event`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 200,
|
||||
renderCell: (record) => record.event,
|
||||
};
|
||||
|
||||
const TIMESTAMP_COLUMN: ColumnConfig = {
|
||||
id: 'timestamp',
|
||||
label: msg`Timestamp`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 150,
|
||||
renderCell: (record) => beautifyPastDateRelativeToNow(record.timestamp),
|
||||
};
|
||||
|
||||
const USER_COLUMN: ColumnConfig = {
|
||||
id: 'userId',
|
||||
label: msg`User`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 150,
|
||||
renderCell: (record) => record.userId ?? '-',
|
||||
};
|
||||
|
||||
const PROPERTIES_COLUMN: ColumnConfig = {
|
||||
id: 'properties',
|
||||
label: msg`Properties`,
|
||||
minWidth: 200,
|
||||
defaultWidth: 400,
|
||||
renderCell: (record) => <EventLogJsonCell value={record.properties} />,
|
||||
};
|
||||
|
||||
const DEFAULT_COLUMNS: ColumnConfig[] = [
|
||||
EVENT_COLUMN,
|
||||
TIMESTAMP_COLUMN,
|
||||
USER_COLUMN,
|
||||
PROPERTIES_COLUMN,
|
||||
];
|
||||
|
||||
const OBJECT_EVENT_COLUMNS: ColumnConfig[] = [
|
||||
{ ...EVENT_COLUMN, defaultWidth: 180 },
|
||||
{ ...TIMESTAMP_COLUMN, defaultWidth: 130 },
|
||||
{ ...USER_COLUMN, defaultWidth: 130 },
|
||||
{
|
||||
id: 'recordId',
|
||||
label: msg`Record ID`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 130,
|
||||
renderCell: (record) => record.recordId ?? '-',
|
||||
},
|
||||
{
|
||||
id: 'objectMetadataId',
|
||||
label: msg`Object ID`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 130,
|
||||
renderCell: (record) => record.objectMetadataId ?? '-',
|
||||
},
|
||||
{ ...PROPERTIES_COLUMN, minWidth: 150, defaultWidth: 300 },
|
||||
];
|
||||
|
||||
const USAGE_EVENT_COLUMNS: ColumnConfig[] = [
|
||||
{ ...EVENT_COLUMN, label: msg`Resource Type`, defaultWidth: 130 },
|
||||
{ ...TIMESTAMP_COLUMN, defaultWidth: 140 },
|
||||
{ ...USER_COLUMN, defaultWidth: 130 },
|
||||
{ ...PROPERTIES_COLUMN, label: msg`Details` },
|
||||
];
|
||||
|
||||
const APPLICATION_LOG_COLUMNS: ColumnConfig[] = [
|
||||
{ ...EVENT_COLUMN, label: msg`Function`, defaultWidth: 160 },
|
||||
{ ...TIMESTAMP_COLUMN, defaultWidth: 140 },
|
||||
{
|
||||
id: 'level',
|
||||
label: msg`Level`,
|
||||
minWidth: 60,
|
||||
defaultWidth: 80,
|
||||
renderCell: (record) => record.properties?.level ?? '-',
|
||||
},
|
||||
{
|
||||
id: 'message',
|
||||
label: msg`Message`,
|
||||
minWidth: 200,
|
||||
defaultWidth: 400,
|
||||
renderCell: (record) => record.properties?.message ?? '-',
|
||||
},
|
||||
{
|
||||
id: 'executionId',
|
||||
label: msg`Execution ID`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 140,
|
||||
renderCell: (record) => record.properties?.executionId ?? '-',
|
||||
},
|
||||
];
|
||||
|
||||
const COLUMNS_BY_TABLE: Record<EventLogTable, ColumnConfig[]> = {
|
||||
[EventLogTable.OBJECT_EVENT]: OBJECT_EVENT_COLUMNS,
|
||||
[EventLogTable.USAGE_EVENT]: USAGE_EVENT_COLUMNS,
|
||||
[EventLogTable.APPLICATION_LOG]: APPLICATION_LOG_COLUMNS,
|
||||
[EventLogTable.WORKSPACE_EVENT]: DEFAULT_COLUMNS,
|
||||
[EventLogTable.PAGEVIEW]: DEFAULT_COLUMNS,
|
||||
};
|
||||
|
||||
export const getColumnsForEventLogTable = (
|
||||
table: EventLogTable,
|
||||
): ColumnConfig[] => {
|
||||
return COLUMNS_BY_TABLE[table] ?? DEFAULT_COLUMNS;
|
||||
};
|
||||
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 110 KiB |
@@ -1,169 +0,0 @@
|
||||
import { useContext, useState } from 'react';
|
||||
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { H2Title, IconCopy } from 'twenty-ui/display';
|
||||
import { CodeEditor, IconButton } from 'twenty-ui/input';
|
||||
import { Card, CardContent, Section } from 'twenty-ui/layout';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
|
||||
const StyledCoverImage = styled.div`
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
height: 160px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledConfigButtonsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
position: absolute;
|
||||
right: ${themeCssVariables.spacing[3]};
|
||||
top: ${themeCssVariables.spacing[3]};
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const StyledCoverCardContent = styled(CardContent)`
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
const StyledCopyButton = styled(IconButton)`
|
||||
background-color: ${themeCssVariables.background.transparent.lighter};
|
||||
border: 1px solid ${themeCssVariables.border.color.medium};
|
||||
`;
|
||||
|
||||
const StyledEditorContainer = styled.div`
|
||||
.monaco-editor,
|
||||
.monaco-editor .overflow-guard {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.monaco-editor .line-hover {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
`;
|
||||
|
||||
type McpAuthMethod = 'oauth' | 'api-key';
|
||||
|
||||
export const SettingsAiMCP = () => {
|
||||
const { t } = useLingui();
|
||||
const { copyToClipboard } = useCopyToClipboard();
|
||||
const [authMethod, setAuthMethod] = useState<McpAuthMethod>('oauth');
|
||||
const { colorScheme } = useContext(ThemeContext);
|
||||
const coverImage =
|
||||
colorScheme === 'light'
|
||||
? '/images/ai/ai-mcp-cover-light.svg'
|
||||
: '/images/ai/ai-mcp-cover-dark.svg';
|
||||
|
||||
const oauthConfig = JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
twenty: {
|
||||
type: 'streamable-http',
|
||||
url: `${REACT_APP_SERVER_BASE_URL}/mcp`,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
const apiKeyConfig = JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
twenty: {
|
||||
type: 'streamable-http',
|
||||
url: `${REACT_APP_SERVER_BASE_URL}/mcp`,
|
||||
headers: {
|
||||
Authorization: 'Bearer [API_KEY]',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
const isOAuth = authMethod === 'oauth';
|
||||
const activeConfig = isOAuth ? oauthConfig : apiKeyConfig;
|
||||
const editorHeight = isOAuth ? 170 : 230;
|
||||
|
||||
const codeEditorOptions = {
|
||||
readOnly: true,
|
||||
domReadOnly: true,
|
||||
renderLineHighlight: 'none' as const,
|
||||
renderLineHighlightOnlyWhenFocus: false,
|
||||
lineNumbers: 'off' as const,
|
||||
folding: false,
|
||||
selectionHighlight: false,
|
||||
occurrencesHighlight: 'off' as const,
|
||||
scrollBeyondLastLine: false,
|
||||
hover: {
|
||||
enabled: false,
|
||||
},
|
||||
guides: {
|
||||
indentation: false,
|
||||
bracketPairs: false,
|
||||
bracketPairsHorizontal: false,
|
||||
},
|
||||
padding: {
|
||||
top: 12,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`MCP Server`}
|
||||
description={t`Access your workspace data from your favorite MCP client like Claude Desktop, Windsurf or Cursor.`}
|
||||
/>
|
||||
<Card rounded>
|
||||
<StyledCoverCardContent divider>
|
||||
<StyledCoverImage
|
||||
style={{ backgroundImage: `url('${coverImage}')` }}
|
||||
/>
|
||||
</StyledCoverCardContent>
|
||||
<StyledCoverCardContent>
|
||||
<StyledEditorContainer style={{ position: 'relative' }}>
|
||||
<StyledConfigButtonsContainer>
|
||||
<Select
|
||||
dropdownId="mcp-auth-method-select"
|
||||
value={authMethod}
|
||||
onChange={(value) => setAuthMethod(value as McpAuthMethod)}
|
||||
options={[
|
||||
{ label: t`OAuth`, value: 'oauth' },
|
||||
{ label: t`API Key`, value: 'api-key' },
|
||||
]}
|
||||
selectSizeVariant="small"
|
||||
dropdownWidth={GenericDropdownContentWidth.Medium}
|
||||
dropdownOffset={{ x: 0, y: 4 }}
|
||||
/>
|
||||
<StyledCopyButton
|
||||
Icon={IconCopy}
|
||||
onClick={() => {
|
||||
copyToClipboard(
|
||||
activeConfig,
|
||||
t`MCP Configuration copied to clipboard`,
|
||||
);
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
</StyledConfigButtonsContainer>
|
||||
<CodeEditor
|
||||
value={activeConfig}
|
||||
language="json"
|
||||
options={codeEditorOptions}
|
||||
height={editorHeight}
|
||||
/>
|
||||
</StyledEditorContainer>
|
||||
</StyledCoverCardContent>
|
||||
</Card>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 92 KiB |
@@ -50,10 +50,6 @@ export const SettingsGeneral = () => {
|
||||
);
|
||||
|
||||
const renderActiveTabContent = () => {
|
||||
if (activeTabId === GENERAL_TAB_LOGS) {
|
||||
return <SettingsLogs />;
|
||||
}
|
||||
|
||||
if (activeTabId === GENERAL_TAB_SECURITY) {
|
||||
return <SettingsSecuritySettings />;
|
||||
}
|
||||
@@ -97,7 +93,13 @@ export const SettingsGeneral = () => {
|
||||
}
|
||||
links={[{ children: t`Workspace` }, { children: t`General` }]}
|
||||
>
|
||||
<SettingsPageContainer>{renderActiveTabContent()}</SettingsPageContainer>
|
||||
{activeTabId === GENERAL_TAB_LOGS ? (
|
||||
<SettingsLogs />
|
||||
) : (
|
||||
<SettingsPageContainer>
|
||||
{renderActiveTabContent()}
|
||||
</SettingsPageContainer>
|
||||
)}
|
||||
</SettingsPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 88 KiB |
@@ -35,7 +35,6 @@ FRONTEND_URL=http://localhost:3001
|
||||
# AUTH_GOOGLE_APIS_CALLBACK_URL=http://localhost:3000/auth/google-apis/get-access-token
|
||||
# CODE_INTERPRETER_TYPE=LOCAL
|
||||
# LOGIC_FUNCTION_TYPE=LOCAL
|
||||
# LOGIC_FUNCTION_LOGS_ENABLED=true
|
||||
# STORAGE_TYPE=local
|
||||
# STORAGE_LOCAL_PATH=.local-storage
|
||||
# SUPPORT_DRIVER=front
|
||||
|
||||
@@ -105,6 +105,10 @@ describe('ClickHouseService', () => {
|
||||
table: 'test_table',
|
||||
values: testData,
|
||||
format: 'JSONEachRow',
|
||||
clickhouse_settings: {
|
||||
async_insert: 1,
|
||||
wait_for_async_insert: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -28,10 +28,6 @@ export class ClickHouseService implements OnModuleInit, OnModuleDestroy {
|
||||
response: true,
|
||||
request: true,
|
||||
},
|
||||
clickhouse_settings: {
|
||||
async_insert: 1,
|
||||
wait_for_async_insert: 1,
|
||||
},
|
||||
application: 'twenty',
|
||||
log: { level: ClickHouseLogLevel.OFF },
|
||||
});
|
||||
@@ -88,10 +84,6 @@ export class ClickHouseService implements OnModuleInit, OnModuleDestroy {
|
||||
response: true,
|
||||
request: true,
|
||||
},
|
||||
clickhouse_settings: {
|
||||
async_insert: 1,
|
||||
wait_for_async_insert: 1,
|
||||
},
|
||||
application: 'twenty',
|
||||
log: { level: ClickHouseLogLevel.OFF },
|
||||
});
|
||||
@@ -282,6 +274,10 @@ export class ClickHouseService implements OnModuleInit, OnModuleDestroy {
|
||||
table,
|
||||
values: chunk,
|
||||
format: 'JSONEachRow',
|
||||
clickhouse_settings: {
|
||||
async_insert: 1,
|
||||
wait_for_async_insert: 1,
|
||||
},
|
||||
});
|
||||
chunk = [];
|
||||
currentSizeBytes = 0;
|
||||
|
||||
@@ -12,3 +12,8 @@ export const formatDateTimeForClickHouse = (date: Date | string): string => {
|
||||
|
||||
export const formatDateForClickHouse = (date: Date): string =>
|
||||
date.toISOString().slice(0, 10);
|
||||
|
||||
// ClickHouse returns DateTime64 values as naive strings (YYYY-MM-DD HH:mm:ss.SSS) in UTC.
|
||||
// Parse them as UTC explicitly, otherwise `new Date` assumes the server's local timezone.
|
||||
export const parseClickHouseDateTime = (value: string): Date =>
|
||||
new Date(`${value.replace(' ', 'T')}Z`);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/audit/utils/events/object-event/object-record-created';
|
||||
import { OBJECT_RECORD_DELETED_EVENT } from 'src/engine/core-modules/audit/utils/events/object-event/object-record-delete';
|
||||
import { OBJECT_RECORD_UPDATED_EVENT } from 'src/engine/core-modules/audit/utils/events/object-event/object-record-updated';
|
||||
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated';
|
||||
import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-deactivated';
|
||||
import { type GenericTrackEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/event-logs/emit/events/object-event/object-record-created';
|
||||
import { OBJECT_RECORD_DELETED_EVENT } from 'src/engine/core-modules/event-logs/emit/events/object-event/object-record-delete';
|
||||
import { OBJECT_RECORD_UPDATED_EVENT } from 'src/engine/core-modules/event-logs/emit/events/object-event/object-record-updated';
|
||||
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/event-logs/emit/events/workspace-event/custom-domain/custom-domain-activated';
|
||||
import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/event-logs/emit/events/workspace-event/custom-domain/custom-domain-deactivated';
|
||||
import { type GenericTrackEvent } from 'src/engine/core-modules/event-logs/emit/events/workspace-event/track';
|
||||
import { formatDateTimeForClickHouse } from 'src/database/clickHouse/clickHouse.util';
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
import { USER_WORKSPACE_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
|
||||
|
||||
@@ -56,14 +56,19 @@ export const typeORMCoreModuleOptions: TypeOrmModuleOptions = {
|
||||
migrationsRun: false,
|
||||
migrationsTableName: '_typeorm_migrations',
|
||||
metadataTableName: '_typeorm_generated_columns_and_materialized_views',
|
||||
// The TypeORM migration system is frozen — historical migrations live in
|
||||
// `legacy-typeorm-migrations-do-not-add/` and are loaded here only so the
|
||||
// `_typeorm_migrations` table stays consistent for older deployments.
|
||||
// Do NOT add new files there: write a fast/slow instance command instead.
|
||||
// See `packages/twenty-server/docs/UPGRADE_COMMANDS.md`.
|
||||
migrations:
|
||||
process.env.IS_BILLING_ENABLED === 'true'
|
||||
? [
|
||||
`${isJest ? 'src/' : 'dist/'}database/typeorm/core/migrations/common/*{.ts,.js}`,
|
||||
`${isJest ? 'src/' : 'dist/'}database/typeorm/core/migrations/billing/*{.ts,.js}`,
|
||||
`${isJest ? 'src/' : 'dist/'}database/typeorm/core/legacy-typeorm-migrations-do-not-add/common/*{.ts,.js}`,
|
||||
`${isJest ? 'src/' : 'dist/'}database/typeorm/core/legacy-typeorm-migrations-do-not-add/billing/*{.ts,.js}`,
|
||||
]
|
||||
: [
|
||||
`${isJest ? 'src/' : 'dist/'}database/typeorm/core/migrations/common/*{.ts,.js}`,
|
||||
`${isJest ? 'src/' : 'dist/'}database/typeorm/core/legacy-typeorm-migrations-do-not-add/common/*{.ts,.js}`,
|
||||
],
|
||||
ssl:
|
||||
process.env.PG_SSL_ALLOW_SELF_SIGNED === 'true'
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Legacy TypeORM migrations — do not add new files here
|
||||
|
||||
This directory contains historical TypeORM migrations (`common/` and `billing/`).
|
||||
They are kept so that older deployments can still replay them against the
|
||||
`_typeorm_migrations` table.
|
||||
|
||||
**The TypeORM migration system is frozen.** Do not add new files here.
|
||||
|
||||
The active upgrade system is **instance commands** (fast / slow) and
|
||||
**workspace commands**, registered with `@RegisteredInstanceCommand` and
|
||||
`@RegisteredWorkspaceCommand`. Generate one with:
|
||||
|
||||
```bash
|
||||
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
|
||||
```
|
||||
|
||||
See `packages/twenty-server/docs/UPGRADE_COMMANDS.md` for the full guide and
|
||||
`packages/twenty-server/src/database/commands/upgrade-version-command/` for
|
||||
existing examples.
|
||||
|
||||
Note: `../migrations/utils/` is **not** legacy — those SQL helpers are still
|
||||
imported by current instance/workspace commands and live outside this folder
|
||||
on purpose.
|
||||