Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5bda5f0d68 | ||
|
|
5a1ae372dc | ||
|
|
33a403c0bd |
@@ -0,0 +1,106 @@
|
||||
name: CI New UI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
merge_group:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
package.json
|
||||
yarn.lock
|
||||
packages/twenty-new-ui/**
|
||||
packages/twenty-shared/**
|
||||
new-ui-task:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test]
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Run ${{ matrix.task }}
|
||||
run: npx nx ${{ matrix.task }} twenty-new-ui
|
||||
new-ui-sb-build:
|
||||
needs: changed-files-check
|
||||
if: >-
|
||||
always() &&
|
||||
(github.event_name == 'push' ||
|
||||
needs.changed-files-check.outputs.any_changed == 'true')
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build storybook
|
||||
run: npx nx storybook:build twenty-new-ui
|
||||
- name: Upload storybook build
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: storybook-twenty-new-ui
|
||||
path: packages/twenty-new-ui/storybook-static
|
||||
retention-days: 1
|
||||
new-ui-sb-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
needs: new-ui-sb-build
|
||||
if: always() && needs.new-ui-sb-build.result == 'success'
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
|
||||
with:
|
||||
fetch-depth: 10
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build dependencies
|
||||
run: npx nx build twenty-shared
|
||||
- name: Download storybook build
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
with:
|
||||
name: storybook-twenty-new-ui
|
||||
path: packages/twenty-new-ui/storybook-static
|
||||
- name: Install Playwright
|
||||
run: |
|
||||
cd packages/twenty-new-ui
|
||||
npx playwright install
|
||||
- name: Run storybook tests
|
||||
run: npx nx storybook:test twenty-new-ui
|
||||
- name: Upload screenshots for visual regression
|
||||
if: always() && !cancelled()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: argos-screenshots-twenty-new-ui
|
||||
path: packages/twenty-new-ui/screenshots
|
||||
retention-days: 1
|
||||
ci-new-ui-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, new-ui-task, new-ui-sb-build, new-ui-sb-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -3,10 +3,13 @@ name: Visual Regression Dispatch
|
||||
# Dispatches visual regression processing to ci-privileged after CI completes.
|
||||
# Runs in the context of the base repo (not the fork) so it has access to secrets,
|
||||
# making it work for external contributor PRs.
|
||||
#
|
||||
# All dispatches use the same event_type=visual-regression with project/artifact_name
|
||||
# in the payload. ci-privileged routes to the correct Argos project based on these.
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ['CI UI']
|
||||
workflows: ['CI UI', 'CI New UI']
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
@@ -15,19 +18,37 @@ permissions:
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
dispatch-pr:
|
||||
if: >-
|
||||
github.event.workflow_run.event == 'pull_request' &&
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
resolve-context:
|
||||
if: github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
outputs:
|
||||
workflow_name: ${{ steps.context.outputs.workflow_name }}
|
||||
artifact_name: ${{ steps.context.outputs.artifact_name }}
|
||||
has_artifact: ${{ steps.check-artifact.outputs.exists }}
|
||||
is_pr: ${{ steps.pr-info.outputs.has_pr }}
|
||||
pr_number: ${{ steps.pr-info.outputs.pr_number }}
|
||||
merge_base_sha: ${{ steps.merge-base.outputs.sha }}
|
||||
steps:
|
||||
- name: Resolve workflow context
|
||||
id: context
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
with:
|
||||
script: |
|
||||
const name = context.payload.workflow_run.name;
|
||||
const artifactName = name === 'CI New UI'
|
||||
? 'argos-screenshots-twenty-new-ui'
|
||||
: 'argos-screenshots-twenty-ui';
|
||||
core.setOutput('workflow_name', name);
|
||||
core.setOutput('artifact_name', artifactName);
|
||||
core.info(`Workflow: ${name}, artifact: ${artifactName}`);
|
||||
|
||||
- name: Check if screenshots artifact exists
|
||||
id: check-artifact
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
with:
|
||||
script: |
|
||||
const artifactName = 'argos-screenshots-twenty-ui';
|
||||
const artifactName = '${{ steps.context.outputs.artifact_name }}';
|
||||
const runId = context.payload.workflow_run.id;
|
||||
|
||||
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
@@ -44,7 +65,9 @@ jobs:
|
||||
}
|
||||
|
||||
- name: Get PR number
|
||||
if: steps.check-artifact.outputs.exists == 'true'
|
||||
if: >-
|
||||
steps.check-artifact.outputs.exists == 'true' &&
|
||||
github.event.workflow_run.event == 'pull_request'
|
||||
id: pr-info
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
with:
|
||||
@@ -111,73 +134,152 @@ jobs:
|
||||
core.setOutput('sha', '');
|
||||
}
|
||||
|
||||
# ── Dispatch: twenty-ui pixel diff (CI UI, PRs + main) ──
|
||||
dispatch-twenty-ui:
|
||||
needs: resolve-context
|
||||
if: >-
|
||||
needs.resolve-context.outputs.workflow_name == 'CI UI' &&
|
||||
needs.resolve-context.outputs.has_artifact == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Dispatch to ci-privileged
|
||||
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
PR_NUMBER: ${{ steps.pr-info.outputs.pr_number }}
|
||||
PR_NUMBER: ${{ needs.resolve-context.outputs.pr_number }}
|
||||
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
COMMIT: ${{ github.event.workflow_run.head_sha }}
|
||||
REFERENCE_COMMIT: ${{ steps.merge-base.outputs.sha }}
|
||||
REFERENCE_COMMIT: ${{ needs.resolve-context.outputs.merge_base_sha }}
|
||||
ARTIFACT_NAME: ${{ needs.resolve-context.outputs.artifact_name }}
|
||||
run: |
|
||||
ARGS=(
|
||||
--method POST
|
||||
-f event_type=visual-regression
|
||||
-f "client_payload[pr_number]=$PR_NUMBER"
|
||||
-f "client_payload[project]=twenty-ui"
|
||||
-f "client_payload[artifact_name]=$ARTIFACT_NAME"
|
||||
-f "client_payload[run_id]=$WORKFLOW_RUN_ID"
|
||||
-f "client_payload[repo]=$REPOSITORY"
|
||||
-f "client_payload[branch]=$BRANCH"
|
||||
-f "client_payload[commit]=$COMMIT"
|
||||
)
|
||||
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
ARGS+=(-f "client_payload[pr_number]=$PR_NUMBER")
|
||||
fi
|
||||
if [ -n "$REFERENCE_COMMIT" ]; then
|
||||
ARGS+=(-f "client_payload[reference_commit]=$REFERENCE_COMMIT")
|
||||
fi
|
||||
|
||||
gh api repos/twentyhq/ci-privileged/dispatches "${ARGS[@]}"
|
||||
|
||||
# ── Dispatch: twenty-new-ui pixel diff (CI New UI, PRs + main) ──
|
||||
dispatch-twenty-new-ui:
|
||||
needs: resolve-context
|
||||
if: >-
|
||||
needs.resolve-context.outputs.workflow_name == 'CI New UI' &&
|
||||
needs.resolve-context.outputs.has_artifact == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Dispatch to ci-privileged
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
COMMIT: ${{ github.event.workflow_run.head_sha }}
|
||||
PR_NUMBER: ${{ needs.resolve-context.outputs.pr_number }}
|
||||
REFERENCE_COMMIT: ${{ needs.resolve-context.outputs.merge_base_sha }}
|
||||
ARTIFACT_NAME: ${{ needs.resolve-context.outputs.artifact_name }}
|
||||
run: |
|
||||
ARGS=(
|
||||
--method POST
|
||||
-f event_type=visual-regression
|
||||
-f "client_payload[project]=twenty-new-ui"
|
||||
-f "client_payload[artifact_name]=$ARTIFACT_NAME"
|
||||
-f "client_payload[run_id]=$WORKFLOW_RUN_ID"
|
||||
-f "client_payload[repo]=$REPOSITORY"
|
||||
-f "client_payload[branch]=$BRANCH"
|
||||
-f "client_payload[commit]=$COMMIT"
|
||||
)
|
||||
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
ARGS+=(-f "client_payload[pr_number]=$PR_NUMBER")
|
||||
fi
|
||||
if [ -n "$REFERENCE_COMMIT" ]; then
|
||||
ARGS+=(-f "client_payload[reference_commit]=$REFERENCE_COMMIT")
|
||||
fi
|
||||
|
||||
gh api repos/twentyhq/ci-privileged/dispatches "${ARGS[@]}"
|
||||
|
||||
# ── Dispatch: cross-comparison baseline (CI UI on main → twenty-ui-vs-new-ui) ──
|
||||
dispatch-comparison-baseline:
|
||||
needs: resolve-context
|
||||
if: >-
|
||||
needs.resolve-context.outputs.workflow_name == 'CI UI' &&
|
||||
needs.resolve-context.outputs.has_artifact == 'true' &&
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
github.event.workflow_run.head_branch == 'main'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Dispatch to ci-privileged (comparison baseline)
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
COMMIT: ${{ github.event.workflow_run.head_sha }}
|
||||
ARTIFACT_NAME: ${{ needs.resolve-context.outputs.artifact_name }}
|
||||
run: |
|
||||
gh api repos/twentyhq/ci-privileged/dispatches \
|
||||
--method POST \
|
||||
-f event_type=visual-regression \
|
||||
-f "client_payload[project]=twenty-ui-vs-new-ui" \
|
||||
-f "client_payload[artifact_name]=$ARTIFACT_NAME" \
|
||||
-f "client_payload[run_id]=$WORKFLOW_RUN_ID" \
|
||||
-f "client_payload[repo]=$REPOSITORY" \
|
||||
-f "client_payload[branch]=$BRANCH" \
|
||||
-f "client_payload[commit]=$COMMIT"
|
||||
|
||||
# ── Dispatch: cross-comparison PR (CI New UI on PRs → twenty-ui-vs-new-ui) ──
|
||||
dispatch-comparison-pr:
|
||||
needs: resolve-context
|
||||
if: >-
|
||||
needs.resolve-context.outputs.workflow_name == 'CI New UI' &&
|
||||
needs.resolve-context.outputs.has_artifact == 'true' &&
|
||||
needs.resolve-context.outputs.is_pr == 'true' &&
|
||||
github.event.workflow_run.event == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Dispatch to ci-privileged (comparison PR)
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
COMMIT: ${{ github.event.workflow_run.head_sha }}
|
||||
PR_NUMBER: ${{ needs.resolve-context.outputs.pr_number }}
|
||||
REFERENCE_COMMIT: ${{ needs.resolve-context.outputs.merge_base_sha }}
|
||||
ARTIFACT_NAME: ${{ needs.resolve-context.outputs.artifact_name }}
|
||||
run: |
|
||||
ARGS=(
|
||||
--method POST
|
||||
-f event_type=visual-regression
|
||||
-f "client_payload[project]=twenty-ui-vs-new-ui"
|
||||
-f "client_payload[artifact_name]=$ARTIFACT_NAME"
|
||||
-f "client_payload[run_id]=$WORKFLOW_RUN_ID"
|
||||
-f "client_payload[repo]=$REPOSITORY"
|
||||
-f "client_payload[branch]=$BRANCH"
|
||||
-f "client_payload[commit]=$COMMIT"
|
||||
-f "client_payload[pr_number]=$PR_NUMBER"
|
||||
)
|
||||
|
||||
if [ -n "$REFERENCE_COMMIT" ]; then
|
||||
ARGS+=(-f "client_payload[reference_commit]=$REFERENCE_COMMIT")
|
||||
fi
|
||||
|
||||
gh api repos/twentyhq/ci-privileged/dispatches "${ARGS[@]}"
|
||||
|
||||
dispatch-main:
|
||||
if: >-
|
||||
github.event.workflow_run.event == 'push' &&
|
||||
github.event.workflow_run.head_branch == 'main' &&
|
||||
github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- name: Check if screenshots artifact exists
|
||||
id: check-artifact
|
||||
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
|
||||
with:
|
||||
script: |
|
||||
const runId = context.payload.workflow_run.id;
|
||||
const { data: artifacts } = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
run_id: runId,
|
||||
});
|
||||
|
||||
const found = artifacts.artifacts.some(a => a.name === 'argos-screenshots-twenty-ui');
|
||||
core.setOutput('exists', found ? 'true' : 'false');
|
||||
|
||||
if (!found) {
|
||||
core.info(`Artifact not found in run ${runId} — skipping`);
|
||||
}
|
||||
|
||||
- name: Dispatch to ci-privileged
|
||||
if: steps.check-artifact.outputs.exists == 'true'
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CI_PRIVILEGED_DISPATCH_TOKEN }}
|
||||
WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
REPOSITORY: ${{ github.repository }}
|
||||
BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
COMMIT: ${{ github.event.workflow_run.head_sha }}
|
||||
run: |
|
||||
gh api repos/twentyhq/ci-privileged/dispatches \
|
||||
--method POST \
|
||||
-f event_type=visual-regression \
|
||||
-f "client_payload[run_id]=$WORKFLOW_RUN_ID" \
|
||||
-f "client_payload[repo]=$REPOSITORY" \
|
||||
-f "client_payload[branch]=$BRANCH" \
|
||||
-f "client_payload[commit]=$COMMIT"
|
||||
|
||||
@@ -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,6 +1434,11 @@ type EnterpriseSubscriptionStatusDTO {
|
||||
isCancellationScheduled: Boolean!
|
||||
}
|
||||
|
||||
type Analytics {
|
||||
"""Boolean that confirms query was dispatched"""
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type VerificationRecord {
|
||||
type: String!
|
||||
key: String!
|
||||
@@ -2616,11 +2621,6 @@ type SendEmailOutput {
|
||||
error: String
|
||||
}
|
||||
|
||||
type Analytics {
|
||||
"""Boolean that confirms query was dispatched"""
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type EventLogRecord {
|
||||
event: String!
|
||||
timestamp: DateTime!
|
||||
@@ -3007,6 +3007,7 @@ type Query {
|
||||
): IndexConnection!
|
||||
findManyAgents: [Agent!]!
|
||||
findOneAgent(input: AgentIdInput!): Agent!
|
||||
myConnectedAccounts: [ConnectedAccountPublicDTO!]!
|
||||
getRoles: [Role!]!
|
||||
getToolIndex: [ToolIndexEntry!]!
|
||||
getToolInputSchema(toolName: String!): JSON
|
||||
@@ -3027,7 +3028,6 @@ 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,6 +3200,8 @@ 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!
|
||||
@@ -3252,6 +3254,7 @@ 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!
|
||||
@@ -3280,7 +3283,6 @@ 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!
|
||||
@@ -3342,8 +3344,6 @@ 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!
|
||||
@@ -3360,7 +3360,7 @@ type Mutation {
|
||||
syncMarketplaceCatalog: Boolean!
|
||||
createDevelopmentApplication(universalIdentifier: String!, name: String!): DevelopmentApplication!
|
||||
generateApplicationToken(applicationId: UUID!): ApplicationTokenPair!
|
||||
syncApplication(manifest: JSON!, dryRun: Boolean): WorkspaceMigration!
|
||||
syncApplication(manifest: JSON!): WorkspaceMigration!
|
||||
uploadApplicationFile(file: Upload!, applicationUniversalIdentifier: String!, fileFolder: FileFolder!, filePath: String!): File!
|
||||
upgradeApplication(appRegistrationId: String!, targetVersion: String!): Boolean!
|
||||
renewApplicationToken(applicationRefreshToken: String!): ApplicationTokenPair!
|
||||
@@ -3716,6 +3716,11 @@ input RevokeApiKeyInput {
|
||||
id: UUID!
|
||||
}
|
||||
|
||||
enum AnalyticsType {
|
||||
PAGEVIEW
|
||||
TRACK
|
||||
}
|
||||
|
||||
input CreateApprovedAccessDomainInput {
|
||||
domain: String!
|
||||
email: String!
|
||||
@@ -4425,11 +4430,6 @@ input EditSsoInput {
|
||||
status: SSOIdentityProviderStatus!
|
||||
}
|
||||
|
||||
enum AnalyticsType {
|
||||
PAGEVIEW
|
||||
TRACK
|
||||
}
|
||||
|
||||
input SendEmailInput {
|
||||
connectedAccountId: String!
|
||||
to: String!
|
||||
@@ -4498,7 +4498,6 @@ type Subscription {
|
||||
onEventSubscription(eventStreamId: String!): EventSubscription
|
||||
logicFunctionLogs(input: LogicFunctionLogsInput!): LogicFunctionLogs!
|
||||
onAgentChatEvent(threadId: UUID!): AgentChatEvent!
|
||||
eventLogsLive(table: EventLogTable!): [EventLogRecord!]
|
||||
}
|
||||
|
||||
input LogicFunctionLogsInput {
|
||||
|
||||
@@ -1091,6 +1091,12 @@ 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']
|
||||
@@ -2308,12 +2314,6 @@ 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,6 +2613,7 @@ export interface Query {
|
||||
indexMetadatas: IndexConnection
|
||||
findManyAgents: Agent[]
|
||||
findOneAgent: Agent
|
||||
myConnectedAccounts: ConnectedAccountPublicDTO[]
|
||||
getRoles: Role[]
|
||||
getToolIndex: ToolIndexEntry[]
|
||||
getToolInputSchema?: Scalars['JSON']
|
||||
@@ -2624,7 +2625,6 @@ export interface Query {
|
||||
getViewGroup?: ViewGroup
|
||||
myMessageFolders: MessageFolder[]
|
||||
myMessageChannels: MessageChannel[]
|
||||
myConnectedAccounts: ConnectedAccountPublicDTO[]
|
||||
myCalendarChannels: CalendarChannel[]
|
||||
minimalMetadata: MinimalMetadata
|
||||
appConnections: AppConnection[]
|
||||
@@ -2724,6 +2724,8 @@ export interface Mutation {
|
||||
updateApiKey?: ApiKey
|
||||
revokeApiKey?: ApiKey
|
||||
assignRoleToApiKey: Scalars['Boolean']
|
||||
createObjectEvent: Analytics
|
||||
trackAnalytics: Analytics
|
||||
skipSyncEmailOnboardingStep: OnboardingStepSuccess
|
||||
skipBookOnboardingStep: OnboardingStepSuccess
|
||||
checkoutSession: BillingSession
|
||||
@@ -2776,6 +2778,7 @@ export interface Mutation {
|
||||
createOneAgent: Agent
|
||||
updateOneAgent: Agent
|
||||
deleteOneAgent: Agent
|
||||
deleteConnectedAccount: ConnectedAccountPublicDTO
|
||||
updateWorkspaceMemberRole: WorkspaceMember
|
||||
createOneRole: Role
|
||||
updateOneRole: Role
|
||||
@@ -2804,7 +2807,6 @@ export interface Mutation {
|
||||
updateMessageChannel: MessageChannel
|
||||
createEmailGroupChannel: CreateEmailGroupChannelOutput
|
||||
deleteEmailGroupChannel: MessageChannel
|
||||
deleteConnectedAccount: ConnectedAccountPublicDTO
|
||||
updateCalendarChannel: CalendarChannel
|
||||
createChatThread: AgentChatThread
|
||||
sendChatMessage: SendChatMessageResult
|
||||
@@ -2866,8 +2868,6 @@ export interface Mutation {
|
||||
createSAMLIdentityProvider: SetupSso
|
||||
deleteSSOIdentityProvider: DeleteSso
|
||||
editSSOIdentityProvider: EditSso
|
||||
createObjectEvent: Analytics
|
||||
trackAnalytics: Analytics
|
||||
duplicateDashboard: DuplicatedDashboard
|
||||
impersonate: Impersonate
|
||||
sendEmail: SendEmailOutput
|
||||
@@ -2892,17 +2892,16 @@ export interface Mutation {
|
||||
__typename: 'Mutation'
|
||||
}
|
||||
|
||||
export type WorkspaceMigrationActionType = 'delete' | 'create' | 'update'
|
||||
|
||||
export type AnalyticsType = 'PAGEVIEW' | 'TRACK'
|
||||
|
||||
export type WorkspaceMigrationActionType = 'delete' | 'create' | 'update'
|
||||
|
||||
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'
|
||||
}
|
||||
|
||||
@@ -4050,6 +4049,13 @@ 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
|
||||
@@ -5358,13 +5364,6 @@ 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
|
||||
@@ -5672,6 +5671,7 @@ export interface QueryGenqlSelection{
|
||||
filter: IndexFilter} })
|
||||
findManyAgents?: AgentGenqlSelection
|
||||
findOneAgent?: (AgentGenqlSelection & { __args: {input: AgentIdInput} })
|
||||
myConnectedAccounts?: ConnectedAccountPublicDTOGenqlSelection
|
||||
getRoles?: RoleGenqlSelection
|
||||
getToolIndex?: ToolIndexEntryGenqlSelection
|
||||
getToolInputSchema?: { __args: {toolName: Scalars['String']} }
|
||||
@@ -5689,7 +5689,6 @@ 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)} })
|
||||
@@ -5812,6 +5811,8 @@ 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)} })
|
||||
@@ -5864,6 +5865,7 @@ 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} })
|
||||
@@ -5892,7 +5894,6 @@ 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)} })
|
||||
@@ -5954,8 +5955,6 @@ 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} })
|
||||
@@ -5973,7 +5972,7 @@ export interface MutationGenqlSelection{
|
||||
syncMarketplaceCatalog?: boolean | number
|
||||
createDevelopmentApplication?: (DevelopmentApplicationGenqlSelection & { __args: {universalIdentifier: Scalars['String'], name: Scalars['String']} })
|
||||
generateApplicationToken?: (ApplicationTokenPairGenqlSelection & { __args: {applicationId: Scalars['UUID']} })
|
||||
syncApplication?: (WorkspaceMigrationGenqlSelection & { __args: {manifest: Scalars['JSON'], dryRun?: (Scalars['Boolean'] | null)} })
|
||||
syncApplication?: (WorkspaceMigrationGenqlSelection & { __args: {manifest: Scalars['JSON']} })
|
||||
uploadApplicationFile?: (FileGenqlSelection & { __args: {file: Scalars['Upload'], applicationUniversalIdentifier: Scalars['String'], fileFolder: FileFolder, filePath: Scalars['String']} })
|
||||
upgradeApplication?: { __args: {appRegistrationId: Scalars['String'], targetVersion: Scalars['String']} }
|
||||
renewApplicationToken?: (ApplicationTokenPairGenqlSelection & { __args: {applicationRefreshToken: Scalars['String']} })
|
||||
@@ -6357,7 +6356,6 @@ 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
|
||||
}
|
||||
@@ -7005,6 +7003,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 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"')
|
||||
@@ -8133,14 +8139,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 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"')
|
||||
@@ -8992,17 +8990,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:
|
||||
- "3010:3000"
|
||||
- "3000: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}/${PG_DATABASE_NAME}
|
||||
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default
|
||||
SERVER_URL: ${SERVER_URL}
|
||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
||||
DISABLE_DB_MIGRATIONS: ${DISABLE_DB_MIGRATIONS}
|
||||
@@ -46,6 +46,11 @@ 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
|
||||
@@ -59,7 +64,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}/${PG_DATABASE_NAME}
|
||||
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default
|
||||
SERVER_URL: ${SERVER_URL}
|
||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
||||
DISABLE_DB_MIGRATIONS: "true" # it already runs on the server
|
||||
@@ -96,7 +101,38 @@ 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:
|
||||
|
||||
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 70 KiB |
@@ -3315,7 +3315,6 @@ export type MutationStopAgentChatStreamArgs = {
|
||||
|
||||
|
||||
export type MutationSyncApplicationArgs = {
|
||||
dryRun?: InputMaybe<Scalars['Boolean']>;
|
||||
manifest: Scalars['JSON'];
|
||||
};
|
||||
|
||||
@@ -4933,18 +4932,12 @@ 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;
|
||||
};
|
||||
@@ -7410,14 +7403,7 @@ 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 }>, 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 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 UpdateLabPublicFeatureFlagMutationVariables = Exact<{
|
||||
input: UpdateLabPublicFeatureFlagInput;
|
||||
@@ -8268,8 +8254,7 @@ 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":"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 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 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,7 +8,6 @@ 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';
|
||||
@@ -83,13 +82,8 @@ export const RecordBoardDragDropContext = ({
|
||||
}
|
||||
|
||||
const existingRecordSorts = store.get(currentRecordSorts);
|
||||
const boardCardDropBehavior = getBoardCardDropBehavior({
|
||||
hasRecordSorts: existingRecordSorts.length > 0,
|
||||
sourceDroppableId: result.source.droppableId,
|
||||
destinationDroppableId: result.destination.droppableId,
|
||||
});
|
||||
|
||||
if (boardCardDropBehavior.shouldBlockDrop) {
|
||||
if (existingRecordSorts.length > 0) {
|
||||
store.set(isRecordBoardDropProcessingCallbackState, false);
|
||||
endRecordDrag();
|
||||
openModal(RECORD_INDEX_REMOVE_SORTING_MODAL_ID);
|
||||
@@ -97,9 +91,7 @@ export const RecordBoardDragDropContext = ({
|
||||
}
|
||||
|
||||
try {
|
||||
processBoardCardDrop(result, originalDragSelection, {
|
||||
shouldUpdatePosition: boardCardDropBehavior.shouldUpdatePosition,
|
||||
});
|
||||
processBoardCardDrop(result, originalDragSelection);
|
||||
} catch (error) {
|
||||
store.set(isRecordBoardDropProcessingCallbackState, false);
|
||||
endRecordDrag();
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
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,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,17 +0,0 @@
|
||||
export const getBoardCardDropBehavior = ({
|
||||
hasRecordSorts,
|
||||
sourceDroppableId,
|
||||
destinationDroppableId,
|
||||
}: {
|
||||
hasRecordSorts: boolean;
|
||||
sourceDroppableId: string;
|
||||
destinationDroppableId: string;
|
||||
}) => {
|
||||
const isMovingInsideSameRecordGroup =
|
||||
sourceDroppableId === destinationDroppableId;
|
||||
|
||||
return {
|
||||
shouldBlockDrop: hasRecordSorts && isMovingInsideSameRecordGroup,
|
||||
shouldUpdatePosition: !hasRecordSorts,
|
||||
};
|
||||
};
|
||||
@@ -40,15 +40,9 @@ export const useProcessBoardCardDrop = () => {
|
||||
);
|
||||
|
||||
const processBoardCardDrop = useCallback(
|
||||
(
|
||||
boardCardDropResult: DropResult,
|
||||
selectedRecordIds: string[],
|
||||
options?: { shouldUpdatePosition?: boolean },
|
||||
) => {
|
||||
(boardCardDropResult: DropResult, selectedRecordIds: string[]) => {
|
||||
if (!isDefined(selectFieldMetadataItem)) return;
|
||||
|
||||
const shouldUpdatePosition = options?.shouldUpdatePosition ?? true;
|
||||
|
||||
processGroupDrop({
|
||||
groupDropResult: boardCardDropResult,
|
||||
store,
|
||||
@@ -57,10 +51,7 @@ export const useProcessBoardCardDrop = () => {
|
||||
recordIndexRecordIdsByGroupCallbackFamilyState,
|
||||
onUpdateRecord: ({ recordId, position }, targetRecordGroupValue) => {
|
||||
updateDroppedRecordOnBoard(
|
||||
{
|
||||
recordId,
|
||||
position: shouldUpdatePosition ? position : undefined,
|
||||
},
|
||||
{ recordId, position },
|
||||
targetRecordGroupValue,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -45,6 +45,10 @@ export const useUpdateDroppedRecordOnBoard = () => {
|
||||
recordStoreFamilyState.atomFamily(recordId),
|
||||
) as Record<string, unknown> | null | undefined;
|
||||
|
||||
if (!isDefined(newPosition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefined(initialRecord)) {
|
||||
return;
|
||||
}
|
||||
@@ -90,10 +94,7 @@ export const useUpdateDroppedRecordOnBoard = () => {
|
||||
|
||||
const isSamePosition = initialRecord.position === newPosition;
|
||||
|
||||
if (
|
||||
movingInsideSameRecordGroup &&
|
||||
(!isDefined(newPosition) || isSamePosition)
|
||||
) {
|
||||
if (movingInsideSameRecordGroup && isSamePosition) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -125,32 +126,25 @@ export const useUpdateDroppedRecordOnBoard = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(newPosition)) {
|
||||
const targetGroupRecordsWithIds = extractRecordPositions(
|
||||
currentRecordIdsInTargetRecordGroup,
|
||||
store,
|
||||
);
|
||||
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),
|
||||
);
|
||||
} else {
|
||||
store.set(
|
||||
recordIndexRecordIdsByGroupCallbackFamilyState(targetRecordGroupId),
|
||||
[...currentRecordIdsInTargetRecordGroup, recordId],
|
||||
);
|
||||
}
|
||||
store.set(
|
||||
recordIndexRecordIdsByGroupCallbackFamilyState(targetRecordGroupId),
|
||||
newTargetRecordGroupWithIds.map((record) => record.id),
|
||||
);
|
||||
|
||||
upsertRecordsInStore({
|
||||
partialRecords: [
|
||||
@@ -161,7 +155,7 @@ export const useUpdateDroppedRecordOnBoard = () => {
|
||||
(initialRecord as { __typename?: string })?.__typename ??
|
||||
'Record',
|
||||
[selectFieldMetadataItem.name]: targetRecordGroupValue,
|
||||
...(isDefined(newPosition) && { position: newPosition }),
|
||||
position: newPosition,
|
||||
} as ObjectRecord,
|
||||
],
|
||||
});
|
||||
@@ -170,7 +164,7 @@ export const useUpdateDroppedRecordOnBoard = () => {
|
||||
idToUpdate: recordId,
|
||||
updateOneRecordInput: {
|
||||
[selectFieldMetadataItem.name]: targetRecordGroupValue,
|
||||
...(isDefined(newPosition) && { position: newPosition }),
|
||||
position: newPosition,
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
@@ -1 +1,4 @@
|
||||
export { SettingsDatePickerInput as EventLogDatePickerInput } from '@/settings/components/SettingsDatePickerInput';
|
||||
export {
|
||||
SettingsDatePickerInput as EventLogDatePickerInput,
|
||||
type SettingsDatePickerInputProps as EventLogDatePickerInputProps,
|
||||
} from '@/settings/components/SettingsDatePickerInput';
|
||||
|
||||
@@ -12,16 +12,18 @@ 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,
|
||||
type EventLogTable,
|
||||
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[];
|
||||
@@ -104,6 +106,9 @@ 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>>(() =>
|
||||
@@ -112,6 +117,7 @@ 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])),
|
||||
@@ -236,21 +242,85 @@ export const EventLogResultsTable = ({
|
||||
</StyledResizableHeaderContainer>
|
||||
))}
|
||||
</TableRow>
|
||||
{records.map((record) => (
|
||||
{records.map((record, index) => (
|
||||
<TableRow
|
||||
key={`${record.timestamp}-${record.event}`}
|
||||
key={`${record.timestamp}-${record.event}-${index}`}
|
||||
gridTemplateColumns={gridTemplateColumns}
|
||||
>
|
||||
{baseColumns.map((column) => (
|
||||
<TableCell
|
||||
key={column.id}
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{column.renderCell(record)}
|
||||
</TableCell>
|
||||
))}
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</Table>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
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';
|
||||
@@ -10,26 +8,34 @@ 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 = (
|
||||
Object.entries(TABLE_LABELS) as [EventLogTable, MessageDescriptor][]
|
||||
).map(([table, label]) => ({
|
||||
value: table,
|
||||
label: t(label),
|
||||
}));
|
||||
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`,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Select
|
||||
|
||||
@@ -1,51 +1,24 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { 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 { SettingsOptionCardContentButton } from '@/settings/components/SettingsOptions/SettingsOptionCardContentButton';
|
||||
import { SettingsEnterpriseFeatureGateCard } from '@/settings/components/SettingsEnterpriseFeatureGateCard';
|
||||
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 {
|
||||
IconArrowUp,
|
||||
IconLock,
|
||||
IconPlayerPause,
|
||||
IconPlayerPlay,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button, IconButton } from 'twenty-ui/input';
|
||||
import { IconRefresh } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
import { Card } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
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%;
|
||||
`;
|
||||
import { EventLogTable } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledCardContent = styled.div`
|
||||
display: flex;
|
||||
@@ -67,10 +40,8 @@ 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`
|
||||
@@ -79,9 +50,10 @@ 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`
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
height: 480px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
@@ -92,64 +64,45 @@ export const SettingsLogs = () => {
|
||||
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const isClickHouseConfigured = useAtomStateValue(isClickHouseConfiguredState);
|
||||
const billing = useAtomStateValue(billingState);
|
||||
const navigateSettings = useNavigateSettings();
|
||||
|
||||
const isBillingEnabled = billing?.isBillingEnabled ?? false;
|
||||
const hasAuditLogsEntitlement =
|
||||
currentWorkspace?.billingEntitlements?.some(
|
||||
(entitlement) =>
|
||||
entitlement.key === BillingEntitlementKey.AUDIT_LOGS &&
|
||||
entitlement.value,
|
||||
) === true;
|
||||
const hasEnterpriseAccess =
|
||||
currentWorkspace?.hasValidSignedEnterpriseKey === 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 || hasAuditLogsEntitlement);
|
||||
isClickHouseConfigured && (isApplicationLog || hasEnterpriseAccess);
|
||||
|
||||
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,
|
||||
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,
|
||||
},
|
||||
{ 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],
|
||||
first: RECORDS_PER_PAGE,
|
||||
},
|
||||
{ skip: !canQuery },
|
||||
);
|
||||
|
||||
const handleTableChange = (table: EventLogTable) => {
|
||||
@@ -161,63 +114,39 @@ 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 && !hasAuditLogsEntitlement) {
|
||||
return renderUpgradeCard();
|
||||
if (!isApplicationLog && !hasEnterpriseAccess) {
|
||||
return (
|
||||
<SettingsEnterpriseFeatureGateCard
|
||||
title={t`Enterprise feature`}
|
||||
description={t`Upgrade to Enterprise to access this log type.`}
|
||||
buttonTitle={t`Activate`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isClickHouseConfigured) {
|
||||
return (
|
||||
<SettingsEmptyPlaceholder>
|
||||
{t`Logs require ClickHouse to be configured. Please contact your administrator.`}
|
||||
{t`Audit 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 logs. Please try again.`}
|
||||
{t`Something went wrong while loading audit logs. Please try again.`}
|
||||
</SettingsEmptyPlaceholder>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledResults>
|
||||
<StyledRecordCount>{t`${displayedRecords.length} of ${totalCount + liveRecords.length}`}</StyledRecordCount>
|
||||
<StyledRecordCount>{t`${records.length} of ${totalCount}`}</StyledRecordCount>
|
||||
<StyledTableWrapper>
|
||||
<EventLogResultsTable
|
||||
records={displayedRecords}
|
||||
records={records}
|
||||
loading={loading}
|
||||
hasNextPage={hasNextPage}
|
||||
onLoadMore={loadMore}
|
||||
@@ -229,7 +158,7 @@ export const SettingsLogs = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledRoot>
|
||||
<>
|
||||
<Card rounded fullWidth>
|
||||
<StyledCardContent>
|
||||
<StyledSelectorRow>
|
||||
@@ -239,15 +168,17 @@ export const SettingsLogs = () => {
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</StyledSelectorGrow>
|
||||
{canQuery && (
|
||||
<IconButton
|
||||
Icon={isPaused ? IconPlayerPlay : IconPlayerPause}
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
ariaLabel={isPaused ? t`Resume` : t`Pause`}
|
||||
onClick={() => setIsPaused((previous) => !previous)}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
Icon={IconRefresh}
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
ariaLabel={t`Refresh`}
|
||||
onClick={() => {
|
||||
if (canQuery) {
|
||||
void refetch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</StyledSelectorRow>
|
||||
<EventLogFilters
|
||||
table={selectedTable}
|
||||
@@ -258,6 +189,6 @@ export const SettingsLogs = () => {
|
||||
</Card>
|
||||
|
||||
{renderResults()}
|
||||
</StyledRoot>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ export const GET_EVENT_LOGS = gql`
|
||||
properties
|
||||
recordId
|
||||
objectMetadataId
|
||||
isCustom
|
||||
}
|
||||
totalCount
|
||||
pageInfo {
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,68 +0,0 @@
|
||||
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, fetchMore } = useQuery<
|
||||
const { data, loading, error, refetch, fetchMore } = useQuery<
|
||||
EventLogsData,
|
||||
EventLogsVariables
|
||||
>(GET_EVENT_LOGS, {
|
||||
@@ -70,6 +70,7 @@ export const useEventLogs = (
|
||||
hasNextPage,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
loadMore,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
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;
|
||||
};
|
||||
@@ -1,125 +0,0 @@
|
||||
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: 110 KiB After Width: | Height: | Size: 112 KiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 111 KiB |
@@ -0,0 +1,169 @@
|
||||
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: 88 KiB After Width: | Height: | Size: 86 KiB |
|
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 90 KiB |
@@ -50,6 +50,10 @@ export const SettingsGeneral = () => {
|
||||
);
|
||||
|
||||
const renderActiveTabContent = () => {
|
||||
if (activeTabId === GENERAL_TAB_LOGS) {
|
||||
return <SettingsLogs />;
|
||||
}
|
||||
|
||||
if (activeTabId === GENERAL_TAB_SECURITY) {
|
||||
return <SettingsSecuritySettings />;
|
||||
}
|
||||
@@ -93,13 +97,7 @@ export const SettingsGeneral = () => {
|
||||
}
|
||||
links={[{ children: t`Workspace` }, { children: t`General` }]}
|
||||
>
|
||||
{activeTabId === GENERAL_TAB_LOGS ? (
|
||||
<SettingsLogs />
|
||||
) : (
|
||||
<SettingsPageContainer>
|
||||
{renderActiveTabContent()}
|
||||
</SettingsPageContainer>
|
||||
)}
|
||||
<SettingsPageContainer>{renderActiveTabContent()}</SettingsPageContainer>
|
||||
</SettingsPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 94 KiB |
@@ -215,11 +215,63 @@ Apollo error formatting, and the icon/theme-color pickers tied to Twenty's icon
|
||||
- **Workbench** — Storybook (`@storybook/react-vite`). Every component has stories covering variants, sizes, and states (via `storybook-addon-pseudo-states`), in light and dark, with `autodocs`.
|
||||
- **Functional** — component/interaction tests via `@storybook/addon-vitest` (real browser); unit tests (Jest) for hooks/utilities; coverage gate via `@storybook/addon-coverage`.
|
||||
- **Accessibility** — Storybook a11y addon (axe-core) with `parameters.a11y.test = 'error'` so violations fail CI.
|
||||
- **Visual parity** — visual regression (Chromatic or test-runner image snapshots) plus side-by-side stories rendering the old and new component with identical props; a pixel-diff threshold is the per-component acceptance gate.
|
||||
- **Visual parity** — visual regression via Argos (self-hosted) plus a cross-package comparison project that diffs `twenty-new-ui` stories against `twenty-ui` stories with identical names; a pixel-diff threshold is the per-component acceptance gate. See [Visual regression](#visual-regression) below.
|
||||
- **Performance & size** — `size-limit` per entry point with budgets; tree-shaking fixtures (importing one component must not pull the library); build-time tracking; render benchmarks via React Profiler; load-time via Lighthouse/Playwright on the built Storybook. As one concrete benchmark, a dedicated **stress story** renders a very large number of a single component (e.g. 10,000 buttons) and measures total render time — compared against the `twenty-ui` equivalent and gated against a budget to catch per-instance overhead regressions.
|
||||
|
||||
CI surfaces a per-PR diff table (`twenty-ui` vs `twenty-new-ui`) for size, a11y, and visual changes.
|
||||
|
||||
## Visual regression
|
||||
|
||||
Two Argos projects (on argos.twenty-internal.com) provide visual regression in CI:
|
||||
|
||||
1. **`twenty-new-ui`** — pixel diff of `twenty-new-ui` stories against the `main` branch baseline. Catches regressions introduced by a PR.
|
||||
2. **`twenty-ui-vs-new-ui`** — cross-package comparison. The baseline is always `twenty-ui` screenshots from `main`; PR builds upload `twenty-new-ui` screenshots and diff them against the `twenty-ui` baseline. This shows exactly which components still differ between the two implementations.
|
||||
|
||||
For the cross-package comparison to produce meaningful diffs, stories in `twenty-new-ui` must use the **same title hierarchy** as `twenty-ui` (e.g. `UI/Input/Toggle`).
|
||||
|
||||
### Local visual diff
|
||||
|
||||
Run a pixel diff of `twenty-new-ui` components against `twenty-ui` using the self-hosted Argos instance.
|
||||
|
||||
**Prerequisites:**
|
||||
- AWS SSO configured and logged in (`aws sso login --profile twenty-dev`)
|
||||
- `twenty-infra/super-cli` cloned (sibling of this repo)
|
||||
|
||||
**1. Start the Argos tunnel**
|
||||
|
||||
In the `twenty-infra/super-cli` directory:
|
||||
|
||||
yarn cli argos-tunnel
|
||||
|
||||
This port-forwards the Argos service to `http://127.0.0.1:4002`.
|
||||
Wait until the CLI shows "Argos tunnel is running".
|
||||
|
||||
**2. Set your Argos token**
|
||||
|
||||
Create a `.env` file in `packages/twenty-new-ui/` (gitignored):
|
||||
|
||||
ARGOS_TOKEN=<your-token-from-argos-project-settings>
|
||||
|
||||
**3. Run the visual diff**
|
||||
|
||||
From the repo root:
|
||||
|
||||
npx nx storybook:visual-diff twenty-new-ui
|
||||
|
||||
This builds Storybook, captures screenshots of every story, and uploads
|
||||
them to Argos with build name `<username>/twenty-new-ui`. The diff
|
||||
compares against the latest approved baseline.
|
||||
|
||||
To run `twenty-ui`'s visual diff in the same Argos instance (to build the
|
||||
cross-package comparison baseline):
|
||||
|
||||
npx nx storybook:visual-diff twenty-ui
|
||||
|
||||
**4. View results**
|
||||
|
||||
Open `http://127.0.0.1:4002` in your browser (while the tunnel is running)
|
||||
to review diffs.
|
||||
|
||||
## Build & publishing
|
||||
|
||||
- Vite library mode, dual ESM/CJS, `vite-plugin-dts`, `vite-plugin-svgr`; SCSS via Vite's built-in `sass`; no Babel.
|
||||
@@ -267,7 +319,7 @@ a passing visual-parity diff, and a within-budget size entry.
|
||||
1. Published package name: `twenty-new-ui` now, renamed to `twenty-ui` at cut-over (Phase 6).
|
||||
2. Styling: confirm SCSS Modules vs vanilla-extract vs plain CSS Modules.
|
||||
3. Variants helper: `clsx` + `data-*` vs `cva`.
|
||||
4. Visual regression tooling: Chromatic vs self-hosted image snapshots.
|
||||
4. ~~Visual regression tooling: Chromatic vs self-hosted image snapshots.~~ **Resolved:** Argos (self-hosted at argos.twenty-internal.com). See [Visual regression](#visual-regression).
|
||||
5. How aggressively to drop `framer-motion` in favor of CSS/Base UI transitions.
|
||||
6. Scope of `assets` / `testing` / `json-visualizer`: port verbatim or modernize.
|
||||
7. Where to draw the generic-vs-app-specific line for `modules/ui`, and whether hybrid components live as a headless core in `twenty-new-ui` with a thin app wrapper in `twenty-front`.
|
||||
|
||||
@@ -7,7 +7,6 @@ import chalk from 'chalk';
|
||||
export type AppDevOnceCommandOptions = {
|
||||
appPath?: string;
|
||||
verbose?: boolean;
|
||||
dryRun?: boolean;
|
||||
};
|
||||
|
||||
export class AppDevOnceCommand {
|
||||
@@ -18,17 +17,12 @@ export class AppDevOnceCommand {
|
||||
|
||||
const remoteName = ConfigService.getActiveRemote();
|
||||
|
||||
console.log(
|
||||
chalk.blue(
|
||||
`${options.dryRun ? 'Previewing application diff' : 'Syncing application'} on ${remoteName}...`,
|
||||
),
|
||||
);
|
||||
console.log(chalk.blue(`Syncing application on ${remoteName}...`));
|
||||
console.log(chalk.gray(`App path: ${appPath}\n`));
|
||||
|
||||
const result = await appDevOnce({
|
||||
appPath,
|
||||
verbose: options.verbose,
|
||||
dryRun: options.dryRun,
|
||||
onProgress: (message) => console.log(chalk.gray(message)),
|
||||
});
|
||||
|
||||
@@ -37,16 +31,6 @@ export class AppDevOnceCommand {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
console.log(
|
||||
chalk.green(
|
||||
`\n✓ Dry run complete for ${result.data.applicationDisplayName} — no changes were applied`,
|
||||
),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.green(
|
||||
`\n✓ Synced ${result.data.applicationDisplayName} (${result.data.fileCount} file${result.data.fileCount === 1 ? '' : 's'})`,
|
||||
|
||||
@@ -22,7 +22,6 @@ export const registerDevCommands = (program: Command): void => {
|
||||
verbose?: boolean;
|
||||
debug?: boolean;
|
||||
debounceMs?: string;
|
||||
dryRun?: boolean;
|
||||
},
|
||||
) => {
|
||||
const commonOptions = {
|
||||
@@ -34,10 +33,7 @@ export const registerDevCommands = (program: Command): void => {
|
||||
};
|
||||
|
||||
if (options.once) {
|
||||
await devOnceCommand.execute({
|
||||
...commonOptions,
|
||||
dryRun: options.dryRun,
|
||||
});
|
||||
await devOnceCommand.execute(commonOptions);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -52,10 +48,6 @@ export const registerDevCommands = (program: Command): void => {
|
||||
'-o, --once',
|
||||
'Build and sync once, then exit (useful for CI, scripts, and pre-commit hooks)',
|
||||
)
|
||||
.option(
|
||||
'--dry-run',
|
||||
'Preview the metadata changes without applying them (requires --once)',
|
||||
)
|
||||
.option('--debounceMs <ms>', 'Debounce in ms (default: 2 000)')
|
||||
.option('-v, --verbose', 'Show detailed logs')
|
||||
.option('-d, --debug', 'Show detailed logs (alias for --verbose)')
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import path from 'path';
|
||||
import { OUTPUT_DIR, type Manifest } from 'twenty-shared/application';
|
||||
import { type SyncAction } from 'twenty-shared/metadata';
|
||||
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import {
|
||||
@@ -14,7 +13,6 @@ import { manifestUpdateChecksums } from '@/cli/utilities/build/manifest/manifest
|
||||
import { writeManifestToOutput } from '@/cli/utilities/build/manifest/manifest-writer';
|
||||
import { ClientService } from '@/cli/utilities/client/client-service';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import { formatSyncActionsSummary } from '@/cli/utilities/dev/orchestrator/steps/format-sync-actions-summary';
|
||||
import { formatManifestValidationErrors } from '@/cli/utilities/error/format-manifest-validation-errors';
|
||||
import { serializeError } from '@/cli/utilities/error/serialize-error';
|
||||
import { FileUploader } from '@/cli/utilities/file/file-uploader';
|
||||
@@ -24,7 +22,6 @@ import { APP_ERROR_CODES, type CommandResult } from '@/cli/types';
|
||||
export type AppDevOnceOptions = {
|
||||
appPath: string;
|
||||
verbose?: boolean;
|
||||
dryRun?: boolean;
|
||||
onProgress?: (message: string) => void;
|
||||
};
|
||||
|
||||
@@ -35,19 +32,10 @@ export type AppDevOnceResult = {
|
||||
applicationUniversalIdentifier: string;
|
||||
};
|
||||
|
||||
const reportMetadataChanges = (
|
||||
data: { actions: SyncAction[] },
|
||||
onProgress?: (message: string) => void,
|
||||
): void => {
|
||||
for (const event of formatSyncActionsSummary(data.actions)) {
|
||||
onProgress?.(event.message);
|
||||
}
|
||||
};
|
||||
|
||||
const innerAppDevOnce = async (
|
||||
options: AppDevOnceOptions,
|
||||
): Promise<CommandResult<AppDevOnceResult>> => {
|
||||
const { appPath, onProgress, verbose, dryRun } = options;
|
||||
const { appPath, onProgress, verbose } = options;
|
||||
|
||||
onProgress?.('Checking server...');
|
||||
|
||||
@@ -132,47 +120,6 @@ const innerAppDevOnce = async (
|
||||
|
||||
await writeManifestToOutput(appPath, manifest);
|
||||
|
||||
if (dryRun) {
|
||||
onProgress?.(
|
||||
'Computing metadata diff (dry run, nothing will be applied)...',
|
||||
);
|
||||
|
||||
const dryRunResult = await apiService.syncApplication(manifest, {
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
if (!dryRunResult.success) {
|
||||
const errorEvents = verbose
|
||||
? null
|
||||
: formatManifestValidationErrors(dryRunResult.error);
|
||||
|
||||
const message = errorEvents
|
||||
? errorEvents.map((event) => event.message).join('\n')
|
||||
: `Dry run failed with error: ${serializeError(dryRunResult.error)}`;
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.SYNC_FAILED,
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
reportMetadataChanges(dryRunResult.data, onProgress);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
outputDir: path.join(appPath, OUTPUT_DIR),
|
||||
fileCount: buildResult.builtFileInfos.size,
|
||||
applicationDisplayName: manifest.application.displayName,
|
||||
applicationUniversalIdentifier:
|
||||
manifest.application.universalIdentifier,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
onProgress?.('Registering application...');
|
||||
|
||||
const configService = new ConfigService();
|
||||
@@ -265,8 +212,6 @@ const innerAppDevOnce = async (
|
||||
};
|
||||
}
|
||||
|
||||
reportMetadataChanges(syncResult.data, onProgress);
|
||||
|
||||
onProgress?.('Generating API client...');
|
||||
|
||||
try {
|
||||
|
||||
@@ -73,16 +73,13 @@ export class ApiService {
|
||||
return this.applicationApi.createDevelopmentApplication(...args);
|
||||
}
|
||||
|
||||
syncApplication(
|
||||
manifest: Manifest,
|
||||
options?: { dryRun?: boolean },
|
||||
): Promise<
|
||||
syncApplication(manifest: Manifest): Promise<
|
||||
ApiResponse<{
|
||||
applicationUniversalIdentifier: string;
|
||||
actions: SyncAction[];
|
||||
}>
|
||||
> {
|
||||
return this.applicationApi.syncApplication(manifest, options);
|
||||
return this.applicationApi.syncApplication(manifest);
|
||||
}
|
||||
|
||||
uninstallApplication(universalIdentifier: string): Promise<ApiResponse> {
|
||||
|
||||
@@ -252,10 +252,7 @@ export class ApplicationApi {
|
||||
}
|
||||
}
|
||||
|
||||
async syncApplication(
|
||||
manifest: Manifest,
|
||||
options?: { dryRun?: boolean },
|
||||
): Promise<
|
||||
async syncApplication(manifest: Manifest): Promise<
|
||||
ApiResponse<{
|
||||
applicationUniversalIdentifier: string;
|
||||
actions: SyncAction[];
|
||||
@@ -263,15 +260,15 @@ export class ApplicationApi {
|
||||
> {
|
||||
try {
|
||||
const mutation = `
|
||||
mutation SyncApplication($manifest: JSON!, $dryRun: Boolean) {
|
||||
syncApplication(manifest: $manifest, dryRun: $dryRun) {
|
||||
mutation SyncApplication($manifest: JSON!) {
|
||||
syncApplication(manifest: $manifest) {
|
||||
applicationUniversalIdentifier
|
||||
actions
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const variables = { manifest, dryRun: options?.dryRun ?? false };
|
||||
const variables = { manifest };
|
||||
|
||||
const response: AxiosResponse = await this.client.post(
|
||||
'/metadata',
|
||||
|
||||
@@ -10,12 +10,6 @@ describe('formatSyncActionsSummary', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('reports no changes when actions are missing from the response', () => {
|
||||
expect(formatSyncActionsSummary(undefined)).toEqual([
|
||||
{ message: 'No metadata changes', status: 'info' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('summarizes created, updated and deleted actions with their identifiers', () => {
|
||||
const events = formatSyncActionsSummary([
|
||||
{
|
||||
|
||||
@@ -24,17 +24,15 @@ const getActionLabel = (action: SyncAction): string => {
|
||||
};
|
||||
|
||||
export const formatSyncActionsSummary = (
|
||||
actions: SyncAction[] | undefined,
|
||||
actions: SyncAction[],
|
||||
): OrchestratorStateStepEvent[] => {
|
||||
const definedActions = actions ?? [];
|
||||
|
||||
if (definedActions.length === 0) {
|
||||
if (actions.length === 0) {
|
||||
return [{ message: 'No metadata changes', status: 'info' }];
|
||||
}
|
||||
|
||||
const counts = { create: 0, update: 0, delete: 0 };
|
||||
|
||||
for (const action of definedActions) {
|
||||
for (const action of actions) {
|
||||
counts[action.type] += 1;
|
||||
}
|
||||
|
||||
@@ -48,7 +46,7 @@ export const formatSyncActionsSummary = (
|
||||
{ message: `Metadata changes: ${summaryParts.join(', ')}`, status: 'info' },
|
||||
];
|
||||
|
||||
const visibleActions = definedActions.slice(0, MAX_DETAIL_LINES);
|
||||
const visibleActions = actions.slice(0, MAX_DETAIL_LINES);
|
||||
|
||||
for (const action of visibleActions) {
|
||||
events.push({
|
||||
@@ -57,7 +55,7 @@ export const formatSyncActionsSummary = (
|
||||
});
|
||||
}
|
||||
|
||||
const hiddenCount = definedActions.length - visibleActions.length;
|
||||
const hiddenCount = actions.length - visibleActions.length;
|
||||
|
||||
if (hiddenCount > 0) {
|
||||
events.push({
|
||||
|
||||
@@ -35,6 +35,7 @@ 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,10 +105,6 @@ describe('ClickHouseService', () => {
|
||||
table: 'test_table',
|
||||
values: testData,
|
||||
format: 'JSONEachRow',
|
||||
clickhouse_settings: {
|
||||
async_insert: 1,
|
||||
wait_for_async_insert: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -28,6 +28,10 @@ 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 },
|
||||
});
|
||||
@@ -84,6 +88,10 @@ 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 },
|
||||
});
|
||||
@@ -274,10 +282,6 @@ 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,8 +12,3 @@ 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/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 { 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 { 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,19 +56,14 @@ 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/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/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/migrations/common/*{.ts,.js}`,
|
||||
],
|
||||
ssl:
|
||||
process.env.PG_SSL_ALLOW_SELF_SIGNED === 'true'
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
# 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.
|
||||