diff --git a/.github/workflows/BuildAndPush.yaml b/.github/workflows/BuildAndPush.yaml new file mode 100644 index 000000000000..84d4ef34bd9a --- /dev/null +++ b/.github/workflows/BuildAndPush.yaml @@ -0,0 +1,91 @@ +name: Build and Push +run-name: ${{ github.actor }} is testing out Building the project +on: [push] +jobs: + Build-Lead360: + runs-on: ubuntu-latest + steps: + - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!" + - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." + - name: Check out repository code + uses: actions/checkout@v4 + - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." + - run: echo "🖥️ The workflow is now ready to test your code on the runner." + - run: echo "🍏 This job's status is ${{ job.status }}." + - name: Setup Node.js environment + uses: actions/setup-node@v4.0.3 + with: + node-version-file: package.json + - name: Setup NVM and yarn + run: | + sudo apt-get install curl + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash + corepack enable + - name: Setup PostgreSQL Database + run: make postgres-on-linux + - name: Setup Environment variables + run: | + cp ./packages/twenty-front/.env.example ./packages/twenty-front/.env + cp ./packages/twenty-server/.env.example ./packages/twenty-server/.env + - name: Install dependencies + run: yarn + - name: Build Project + run: | + npx nx database:reset twenty-server + npx nx build twenty-server + npx nx build twenty-front + + Deploy-lead360-App: + needs: Build-Lead360 + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Install doctl + uses: digitalocean/action-doctl@v2 + with: + token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} + + - name: Setup Environment variables + run: | + cp ./packages/twenty-server/.env.example ./packages/twenty-server/.env + + - name: Replace ConfigMap and Secret Values + run: | + sed -i "s||${{ secrets.POSTGRES_DB }}|" ./lead360.yaml + sed -i "s||$(echo -n ${{ secrets.POSTGRES_USER }} | base64)|" ./lead360.yaml + sed -i "s||$(echo -n ${{ secrets.POSTGRES_PASSWORD }} | base64)|" ./lead360.yaml + sed -i "s||$(echo -n ${{ secrets.ACCESS_TOKEN_SECRET }} | base64)|" ./lead360.yaml + sed -i "s||$(echo -n ${{ secrets.LOGIN_TOKEN_SECRET }} | base64)|" ./lead360.yaml + sed -i "s||$(echo -n ${{ secrets.REFRESH_TOKEN_SECRET }} | base64)|" ./lead360.yaml + sed -i "s||$(echo -n ${{ secrets.SIGN_IN_PREFILLED }} | base64)|" ./lead360.yaml + + - name: Build Lead360DB container image + run: docker build -t ${{ secrets.REGISTRY_NAME }}/db:latest -f ./DockerfileDB . + + - name: Build Lead360 container image + run: docker build -t ${{ secrets.REGISTRY_NAME }}/lead360v1:latest -f ./Dockerfile . + + - name: Log in to DigitalOcean Container Registry with short-lived credentials + run: doctl registry login --expiry-seconds 1200 + + - name: Push image to DigitalOcean Container Registry + run: docker push ${{ secrets.REGISTRY_NAME }}/db:latest + + - name: Push image to DigitalOcean Container Registry + run: docker push ${{ secrets.REGISTRY_NAME }}/lead360v1:latest + + - name: Save DigitalOcean kubeconfig with short-lived credentials + run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 ${{ secrets.CLUSTER_NAME }} + + - name : Apply Deployment + run : kubectl apply -f ./lead360.yaml + + - name: Restart lead360db deployment + run: kubectl rollout restart deployment/lead360db-deployment + + - name: Restart lead360 deployment + run: kubectl rollout restart deployment/lead360-deployment + + diff --git a/.gitignore b/.gitignore index 376216c452c4..569a21e50ad2 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ storybook-static .eslintcache .cache .nyc_output +packages/twenty-front/package.json diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000000..2c3d1eae27d8 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,80 @@ +# Base image for common dependencies +FROM node:18.17.1-alpine as common-deps + +WORKDIR /app + +# Copy only the necessary files for dependency resolution +COPY ./package.json ./yarn.lock ./.yarnrc.yml ./tsconfig.base.json ./nx.json /app/ +COPY ./.yarn/releases /app/.yarn/releases + +COPY ./packages/twenty-emails/package.json /app/packages/twenty-emails/ +COPY ./packages/twenty-server/package.json /app/packages/twenty-server/ +COPY ./packages/twenty-server/patches /app/packages/twenty-server/patches +COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/ +COPY ./packages/twenty-front/package.json /app/packages/twenty-front/ + +# Install all dependencies +RUN yarn && yarn cache clean && npx nx reset + + +# Build the back +FROM common-deps as twenty-server-build + +# Copy sourcecode after installing dependences to accelerate subsequents builds +COPY ./packages/twenty-emails /app/packages/twenty-emails +COPY ./packages/twenty-server /app/packages/twenty-server + +RUN npx nx run twenty-server:build + +RUN mv /app/packages/twenty-server/dist /app/packages/twenty-server/build +RUN npx nx run twenty-server:build:packageJson +RUN mv /app/packages/twenty-server/dist/package.json /app/packages/twenty-server/package.json +RUN rm -rf /app/packages/twenty-server/dist +RUN mv /app/packages/twenty-server/build /app/packages/twenty-server/dist + +RUN yarn workspaces focus --production twenty-emails twenty-server + + +# Build the front +FROM common-deps as twenty-front-build + +ARG REACT_APP_SERVER_BASE_URL + +COPY ./packages/twenty-front /app/packages/twenty-front +COPY ./packages/twenty-ui /app/packages/twenty-ui +RUN npx nx build twenty-front + + +# Final stage: Run the application +FROM node:18.17.1-alpine as twenty + +# Used to run healthcheck in docker +RUN apk add --no-cache curl jq + +COPY ./packages/twenty-docker/twenty/entrypoint.sh /app/entrypoint.sh +RUN chmod +x /app/entrypoint.sh + +WORKDIR /app/packages/twenty-server + +ARG REACT_APP_SERVER_BASE_URL +ENV REACT_APP_SERVER_BASE_URL $REACT_APP_SERVER_BASE_URL +ARG SENTRY_RELEASE +ENV SENTRY_RELEASE $SENTRY_RELEASE + +# Copy built applications from previous stages +COPY --chown=1000 --from=twenty-server-build /app /app +COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server /app/packages/twenty-server +COPY --chown=1000 --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front + +# Set metadata and labels +LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty +LABEL org.opencontainers.image.description="This image provides a consistent and reproducible environment for the backend and frontend, ensuring it deploys faster and runs the same way regardless of the deployment environment." + +RUN mkdir /app/.local-storage +RUN chown -R 1000 /app + +# Use non root user with uid 1000 +USER 1000 + +CMD ["node", "dist/src/main"] +ENTRYPOINT ["/app/entrypoint.sh"] diff --git a/DockerfileDB b/DockerfileDB new file mode 100644 index 000000000000..91cf357a4e0b --- /dev/null +++ b/DockerfileDB @@ -0,0 +1,54 @@ +ARG IMAGE_TAG='15.5.0-debian-11-r15' + +FROM bitnami/postgresql:${IMAGE_TAG} + +ARG PG_MAIN_VERSION=15 +ARG PG_GRAPHQL_VERSION=1.5.1 +ARG WRAPPERS_VERSION=0.2.0 +ARG TARGETARCH + +USER root + +RUN set -eux; \ + ARCH="$(dpkg --print-architecture)"; \ + case "${ARCH}" in \ + aarch64|arm64) \ + TARGETARCH='arm64'; \ + ;; \ + amd64|x86_64) \ + TARGETARCH='amd64'; \ + ;; \ + *) \ + echo "Unsupported arch: ${ARCH}"; \ + exit 1; \ + ;; \ + esac; + +RUN apt update && apt install build-essential git curl default-libmysqlclient-dev -y + +# Install precompiled pg_graphql extensions +COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${PG_MAIN_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql--${PG_GRAPHQL_VERSION}.sql \ + /opt/bitnami/postgresql/share/extension/ +COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${PG_MAIN_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql.control \ + /opt/bitnami/postgresql/share/extension/ +COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${PG_MAIN_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql.so \ + /opt/bitnami/postgresql/lib/ + +# Install precompiled supabase wrappers extensions +RUN curl -L "https://github.com/supabase/wrappers/releases/download/v${WRAPPERS_VERSION}/wrappers-v${WRAPPERS_VERSION}-pg${PG_MAIN_VERSION}-${TARGETARCH}-linux-gnu.deb" -o wrappers.deb +RUN dpkg --install wrappers.deb +RUN cp /usr/share/postgresql/${PG_MAIN_VERSION}/extension/wrappers* /opt/bitnami/postgresql/share/extension/ +RUN cp /usr/lib/postgresql/${PG_MAIN_VERSION}/lib/wrappers* /opt/bitnami/postgresql/lib/ + +RUN export PATH=/usr/local/pgsql/bin/:$PATH +RUN export PATH=/usr/local/mysql/bin/:$PATH +RUN git clone https://github.com/EnterpriseDB/mysql_fdw.git +WORKDIR mysql_fdw +RUN make USE_PGXS=1 +RUN make USE_PGXS=1 install + +COPY ./packages/twenty-docker/twenty-postgres/init.sql /docker-entrypoint-initdb.d/ + +USER 1001 +ENTRYPOINT ["/opt/bitnami/scripts/postgresql/entrypoint.sh"] +CMD ["/opt/bitnami/scripts/postgresql/run.sh"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000000..4c72abd98a2e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +version: "3.9" +services: + lead360db: + build: + context: . + dockerfile: DockerfileDB + volumes: + - twenty_db_data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=postgres + - POSTGRES_PASSWORD=postgres + - POSTGRES_DB=default + ports: + - "5431:5432" + lead360: + build: + context: . + dockerfile: Dockerfile + ports: + - "3000:3000" # only this port is exposed + +volumes: + twenty_db_data: + name: twenty_db_data + diff --git a/lead360-airflow.yaml b/lead360-airflow.yaml new file mode 100644 index 000000000000..94f53dd4b0b3 --- /dev/null +++ b/lead360-airflow.yaml @@ -0,0 +1,58 @@ +apiVersion: v1 +kind: Service +metadata: + name: lead360-airflow-service +spec: + selector: + component: webserver + type: LoadBalancer + + ports: + - protocol: TCP + port: 8080 + targetPort: 8080 + +--- + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-lead360-airflow + annotations: + cert-manager.io/issuer: letsencrypt-nginx +spec: + tls: + - hosts: + - airflow.lead360-synapsenet.shop + secretName: letsencrypt-nginx-airflow + ingressClassName: nginx + rules: + - host: airflow.lead360-synapsenet.shop + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: lead360-airflow-service + port: + number: 8080 + + +--- + +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: letsencrypt-nginx-airflow +spec: + acme: + email: admin@synapsenet.co + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: lead360-airflow-private-key + solvers: + # Use the HTTP-01 challenge provider + - http01: + ingress: + class: nginx \ No newline at end of file diff --git a/lead360-restart.yaml b/lead360-restart.yaml new file mode 100644 index 000000000000..e4af3ce28376 --- /dev/null +++ b/lead360-restart.yaml @@ -0,0 +1,182 @@ +#lead360-env-varibales +apiVersion: v1 +kind: ConfigMap +metadata: + name: lead360-config +data: + postgres-db: + +--- + +#lead360-secrets +apiVersion: v1 +kind: Secret +metadata: + name: lead360-secret +data: + #base64-encoded + postgres-user: + postgres-password: + access-token-secret: + login-token-secret: + refresh-token-secret: + sign-in-prefilled: + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lead360db-deployment +spec: + replicas: 0 + selector: + matchLabels: + app: lead360db + template: + metadata: + labels: + app: lead360db + spec: + containers: + - name: lead360db-container + image: registry.digitalocean.com/twentydocker/{{}} + ports: + - containerPort: 5432 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: lead360-secret + key: postgres-user + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: lead360-secret + key: postgres-password + - name: POSTGRES_DB + valueFrom: + configMapKeyRef: + name: lead360-config + key: postgres-db + volumeMounts: + - name: twenty-db-data + mountPath: /var/lib/postgresql/data + volumes: + - name: twenty-db-data + emptyDir: {} + +--- +apiVersion: v1 +kind: Service +metadata: + name: lead360db-service +spec: + selector: + app: lead360db + ports: + - protocol: TCP + port: 5432 + targetPort: 5432 + + +--- + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lead360-deployment +spec: + replicas: 0 + selector: + matchLabels: + app: lead360 + template: + metadata: + labels: + app: lead360 + spec: + containers: + - name: l360 + image: registry.digitalocean.com/twentydocker/lead360v1 + ports: + - containerPort: 3000 + env: + - name: PG_DATABASE_URL + value: 'postgres://twenty:twenty@lead360db-service:5432/default' + - name: ACCESS_TOKEN_SECRET + valueFrom: + secretKeyRef: + name: lead360-secret + key: access-token-secret + - name: LOGIN_TOKEN_SECRET + valueFrom: + secretKeyRef: + name: lead360-secret + key: login-token-secret + - name: REFRESH_TOKEN_SECRET + valueFrom: + secretKeyRef: + name: lead360-secret + key: refresh-token-secret + - name: SIGN_IN_PREFILLED + valueFrom: + secretKeyRef: + name: lead360-secret + key: sign-in-prefilled +--- +apiVersion: v1 +kind: Service +metadata: + name: lead360-service +spec: + selector: + app: lead360 + type: LoadBalancer + ports: + - protocol: TCP + port: 3000 + targetPort: 3000 + + + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-lead360 + annotations: + cert-manager.io/issuer: letsencrypt-nginx +spec: + tls: + - hosts: + - lead360-synapsenet.shop + secretName: letsencrypt-nginx + ingressClassName: nginx + rules: + - host: lead360-synapsenet.shop + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: lead360-service + port: + number: 3000 + +--- + +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: letsencrypt-nginx +spec: + acme: + email: admin@synapsenet.co + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: lead360-private-key + solvers: + # Use the HTTP-01 challenge provider + - http01: + ingress: + class: nginx diff --git a/lead360-superset.yaml b/lead360-superset.yaml new file mode 100644 index 000000000000..c130ef923574 --- /dev/null +++ b/lead360-superset.yaml @@ -0,0 +1,58 @@ +apiVersion: v1 +kind: Service +metadata: + name: lead360-superset-service +spec: + selector: + app: superset + type: LoadBalancer + + ports: + - protocol: TCP + port: 8088 + targetPort: 8088 + +--- + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-lead360-superset + annotations: + cert-manager.io/issuer: letsencrypt-nginx +spec: + tls: + - hosts: + - superset.lead360-synapsenet.shop + secretName: letsencrypt-nginx-superset + ingressClassName: nginx + rules: + - host: superset.lead360-synapsenet.shop + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: lead360-superset-service + port: + number: 8088 + + +--- + +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: letsencrypt-nginx-superset +spec: + acme: + email: admin@synapsenet.co + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: lead360-superset-private-key + solvers: + # Use the HTTP-01 challenge provider + - http01: + ingress: + class: nginx \ No newline at end of file diff --git a/lead360.yaml b/lead360.yaml new file mode 100644 index 000000000000..0debe6322599 --- /dev/null +++ b/lead360.yaml @@ -0,0 +1,182 @@ +#lead360-env-varibales +apiVersion: v1 +kind: ConfigMap +metadata: + name: lead360-config +data: + postgres-db: "" + +--- + +#lead360-secrets +apiVersion: v1 +kind: Secret +metadata: + name: lead360-secret +data: + #base64-encoded + postgres-user: "" + postgres-password: "" + access-token-secret: "" + login-token-secret: "" + refresh-token-secret: "" + sign-in-prefilled: "" + +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lead360db-deployment +spec: + replicas: 1 + selector: + matchLabels: + app: lead360db + template: + metadata: + labels: + app: lead360db + spec: + containers: + - name: lead360db-container + image: registry.digitalocean.com/twentydocker/db + ports: + - containerPort: 5432 + env: + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: lead360-secret + key: postgres-user + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: lead360-secret + key: postgres-password + - name: POSTGRES_DB + valueFrom: + configMapKeyRef: + name: lead360-config + key: postgres-db + volumeMounts: + - name: twenty-db-data + mountPath: /var/lib/postgresql/data + volumes: + - name: twenty-db-data + emptyDir: {} + +--- +apiVersion: v1 +kind: Service +metadata: + name: lead360db-service +spec: + selector: + app: lead360db + ports: + - protocol: TCP + port: 5432 + targetPort: 5432 + + +--- + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: lead360-deployment +spec: + replicas: 1 + selector: + matchLabels: + app: lead360 + template: + metadata: + labels: + app: lead360 + spec: + containers: + - name: l360 + image: registry.digitalocean.com/twentydocker/lead360v1 + ports: + - containerPort: 3000 + env: + - name: PG_DATABASE_URL + value: 'postgres://twenty:twenty@lead360db-service:5432/default' + - name: ACCESS_TOKEN_SECRET + valueFrom: + secretKeyRef: + name: lead360-secret + key: access-token-secret + - name: LOGIN_TOKEN_SECRET + valueFrom: + secretKeyRef: + name: lead360-secret + key: login-token-secret + - name: REFRESH_TOKEN_SECRET + valueFrom: + secretKeyRef: + name: lead360-secret + key: refresh-token-secret + - name: SIGN_IN_PREFILLED + valueFrom: + secretKeyRef: + name: lead360-secret + key: sign-in-prefilled +--- +apiVersion: v1 +kind: Service +metadata: + name: lead360-service +spec: + selector: + app: lead360 + type: LoadBalancer + ports: + - protocol: TCP + port: 3000 + targetPort: 3000 + + + +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: ingress-lead360 + annotations: + cert-manager.io/issuer: letsencrypt-nginx +spec: + tls: + - hosts: + - lead360-synapsenet.shop + secretName: letsencrypt-nginx + ingressClassName: nginx + rules: + - host: lead360-synapsenet.shop + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: lead360-service + port: + number: 3000 + +--- + +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: letsencrypt-nginx +spec: + acme: + email: admin@synapsenet.co + server: https://acme-v02.api.letsencrypt.org/directory + privateKeySecretRef: + name: lead360-private-key + solvers: + # Use the HTTP-01 challenge provider + - http01: + ingress: + class: nginx diff --git a/myairflow-values.yaml b/myairflow-values.yaml new file mode 100644 index 000000000000..1060b6f50eff --- /dev/null +++ b/myairflow-values.yaml @@ -0,0 +1,22 @@ +dags: + gitSync: + enabled: true + repo: git@github.com:synapsenet-arena/lead360-workflow.git + branch: feature/workflow-campaign + depth: 1 + # the number of consecutive failures allowed before aborting + maxFailures: 0 + # subpath within the repo where dags are located + # should be "" if dags are at repo root + subPath: "/dags" + sshKeySecret: + +registry: + secretName: digitalocean-secrets + + # Example: + connection: + user: + pass: + host: + email: diff --git a/mysuperset-values.yaml b/mysuperset-values.yaml new file mode 100644 index 000000000000..70cf850778c0 --- /dev/null +++ b/mysuperset-values.yaml @@ -0,0 +1,892 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +# Default values for superset. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# A README is automatically generated from this file to document it, using helm-docs (see https://github.com/norwoodj/helm-docs) +# To update it, install helm-docs and run helm-docs from the root of this chart + +# -- Provide a name to override the name of the chart +nameOverride: ~ +# -- Provide a name to override the full names of resources +fullnameOverride: ~ + +# -- User ID directive. This user must have enough permissions to run the bootstrap script +# Running containers as root is not recommended in production. Change this to another UID - e.g. 1000 to be more secure +runAsUser: 0 + +# -- Specify rather or not helm should create the secret described in `secret-env.yaml` template +secretEnv: + # -- Change to false in order to support externally created secret (Binami "Sealed Secrets" for Kubernetes or External Secrets Operator) + # note: when externally creating the secret, the chart still expects to pull values from a secret with the name of the release defaults to `release-name-superset-env` - full logic located in _helpers.tpl file: `define "superset.fullname"` + create: true + +# -- Specify service account name to be used +serviceAccountName: ~ +serviceAccount: + # -- Create custom service account for Superset. If create: true and serviceAccountName is not provided, `superset.fullname` will be used. + create: false + annotations: {} + +# -- Install additional packages and do any other bootstrap configuration in this script +# For production clusters it's recommended to build own image with this step done in CI +# @default -- see `values.yaml` +bootstrapScript: | + #!/bin/bash + if [ ! -f ~/bootstrap ]; then echo "Running Superset with uid {{ .Values.runAsUser }}" > ~/bootstrap; fi + +# -- The name of the secret which we will use to generate a superset_config.py file +# Note: this secret must have the key superset_config.py in it and can include other files as well +configFromSecret: '{{ template "superset.fullname" . }}-config' + +# -- The name of the secret which we will use to populate env vars in deployed pods +# This can be useful for secret keys, etc. +envFromSecret: '{{ template "superset.fullname" . }}-env' +# -- This can be a list of templated strings +envFromSecrets: [] + +# -- Extra environment variables that will be passed into pods +extraEnv: {} + # Different gunicorn settings, refer to the gunicorn documentation + # https://docs.gunicorn.org/en/stable/settings.html# + # These variables are used as Flags at the gunicorn startup + # https://github.com/apache/superset/blob/master/docker/run-server.sh#L22 + # Extend timeout to allow long running queries. + # GUNICORN_TIMEOUT: 300 + # Increase the gunicorn worker amount, can improve performance drastically + # See: https://docs.gunicorn.org/en/stable/design.html#how-many-workers + # SERVER_WORKER_AMOUNT: 4 + # WORKER_MAX_REQUESTS: 0 + # WORKER_MAX_REQUESTS_JITTER: 0 + # SERVER_THREADS_AMOUNT: 20 + # GUNICORN_KEEPALIVE: 2 + # SERVER_LIMIT_REQUEST_LINE: 0 + # SERVER_LIMIT_REQUEST_FIELD_SIZE: 0 + + # OAUTH_HOME_DOMAIN: .. + # # If a whitelist is not set, any address that can use your OAuth2 endpoint will be able to login. + # # this includes any random Gmail address if your OAuth2 Web App is set to External. + # OAUTH_WHITELIST_REGEX: ... + +# -- Extra environment variables in RAW format that will be passed into pods +extraEnvRaw: [] + # Load DB password from other secret (e.g. for zalando operator) + # - name: DB_PASS + # valueFrom: + # secretKeyRef: + # name: superset.superset-postgres.credentials.postgresql.acid.zalan.do + # key: password + +# -- Extra environment variables to pass as secrets +extraSecretEnv: {} + # MAPBOX_API_KEY: ... + # # Google API Keys: https://console.cloud.google.com/apis/credentials + # GOOGLE_KEY: ... + # GOOGLE_SECRET: ... + # # Generate your own secret key for encryption. Use openssl rand -base64 42 to generate a good key + # SUPERSET_SECRET_KEY: 'CHANGE_ME_TO_A_COMPLEX_RANDOM_SECRET' + +# -- Extra files to mount on `/app/pythonpath` +extraConfigs: {} + # import_datasources.yaml: | + # databases: + # - allow_file_upload: true + # allow_ctas: true + # allow_cvas: true + # database_name: example-db + # extra: "{\r\n \"metadata_params\": {},\r\n \"engine_params\": {},\r\n \"\ + # metadata_cache_timeout\": {},\r\n \"schemas_allowed_for_file_upload\": []\r\n\ + # }" + # sqlalchemy_uri: example://example-db.local + # tables: [] + +# -- Extra files to mount on `/app/pythonpath` as secrets +extraSecrets: {} + +extraVolumes: [] + # - name: customConfig + # configMap: + # name: '{{ template "superset.fullname" . }}-custom-config' + # - name: additionalSecret + # secret: + # secretName: my-secret + # defaultMode: 0600 + +extraVolumeMounts: [] + # - name: customConfig + # mountPath: /mnt/config + # readOnly: true + # - name: additionalSecret: + # mountPath: /mnt/secret + +# -- A dictionary of overrides to append at the end of superset_config.py - the name does not matter +# WARNING: the order is not guaranteed +# Files can be passed as helm --set-file configOverrides.my-override=my-file.py +configOverrides: + # extend_timeout: | + # # Extend timeout to allow long running queries. + # SUPERSET_WEBSERVER_TIMEOUT = ... + # enable_oauth: | + # from flask_appbuilder.security.manager import (AUTH_DB, AUTH_OAUTH) + # AUTH_TYPE = AUTH_OAUTH + # OAUTH_PROVIDERS = [ + # { + # "name": "google", + # "whitelist": [ os.getenv("OAUTH_WHITELIST_REGEX", "") ], + # "icon": "fa-google", + # "token_key": "access_token", + # "remote_app": { + # "client_id": os.environ.get("GOOGLE_KEY"), + # "client_secret": os.environ.get("GOOGLE_SECRET"), + # "api_base_url": "https://www.googleapis.com/oauth2/v2/", + # "client_kwargs": {"scope": "email profile"}, + # "request_token_url": None, + # "access_token_url": "https://accounts.google.com/o/oauth2/token", + # "authorize_url": "https://accounts.google.com/o/oauth2/auth", + # "authorize_params": {"hd": os.getenv("OAUTH_HOME_DOMAIN", "")} + # } + # } + # ] + # # Map Authlib roles to superset roles + # AUTH_ROLE_ADMIN = 'Admin' + # AUTH_ROLE_PUBLIC = 'Public' + # # Will allow user self registration, allowing to create Flask users from Authorized User + # AUTH_USER_REGISTRATION = True + # # The default user self registration role + # AUTH_USER_REGISTRATION_ROLE = "Admin" + secret: | + SECRET_KEY = '' + +# -- Same as above but the values are files +configOverridesFiles: {} + # extend_timeout: extend_timeout.py + # enable_oauth: enable_oauth.py + +configMountPath: "/app/pythonpath" + +extraConfigMountPath: "/app/configs" + +image: + repository: apachesuperset.docker.scarf.sh/apache/superset + tag: ~ + pullPolicy: IfNotPresent + +imagePullSecrets: [] + +initImage: + repository: apache/superset + tag: dockerize + pullPolicy: IfNotPresent + +service: + type: ClusterIP + port: 8088 + annotations: {} + # cloud.google.com/load-balancer-type: "Internal" + loadBalancerIP: ~ + nodePort: + # -- (int) + http: nil + +ingress: + enabled: false + ingressClassName: ~ + annotations: {} + # kubernetes.io/tls-acme: "true" + ## Extend timeout to allow long running queries. + # nginx.ingress.kubernetes.io/proxy-connect-timeout: "300" + # nginx.ingress.kubernetes.io/proxy-read-timeout: "300" + # nginx.ingress.kubernetes.io/proxy-send-timeout: "300" + path: / + pathType: ImplementationSpecific + hosts: + - chart-example.local + tls: [] + extraHostsRaw: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # The limits below will apply to all Superset components. To set individual resource limitations refer to the pod specific values below. + # The pod specific values will overwrite anything that is set here. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# -- Custom hostAliases for all superset pods +## https://kubernetes.io/docs/tasks/network/customize-hosts-file-for-pods/ +hostAliases: [] +# - hostnames: +# - nodns.my.lan +# ip: 18.27.36.45 + +# Superset node configuration +supersetNode: + replicas: + enabled: true + replicaCount: 1 + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + # -- Sets the [pod disruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) for supersetNode pods + podDisruptionBudget: + # -- Whether the pod disruption budget should be created + enabled: false + # -- If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget + minAvailable: 1 + # -- If set, minAvailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget + maxUnavailable: 1 + + # -- Startup command + # @default -- See `values.yaml` + command: + - "/bin/sh" + - "-c" + - ". {{ .Values.configMountPath }}/superset_bootstrap.sh; /usr/bin/run-server.sh" + connections: + # -- Change in case of bringing your own redis and then also set redis.enabled:false + redis_host: '{{ .Release.Name }}-redis-headless' + redis_port: "6379" + redis_user: "" + # redis_password: superset + redis_cache_db: "1" + redis_celery_db: "0" + # Or SSL port is usually 6380 + # Update following for using Redis with SSL + redis_ssl: + enabled: false + ssl_cert_reqs: CERT_NONE + # You need to change below configuration incase bringing own PostgresSQL instance and also set postgresql.enabled:false + db_host: '{{ .Release.Name }}-postgresql' + db_port: "5432" + db_user: superset + db_pass: superset + db_name: superset + env: {} + # -- If true, forces deployment to reload on each upgrade + forceReload: false + # -- Init containers + # @default -- a container waiting for postgres + initContainers: + - name: wait-for-postgres + image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}" + imagePullPolicy: "{{ .Values.initImage.pullPolicy }}" + envFrom: + - secretRef: + name: "{{ tpl .Values.envFromSecret . }}" + command: + - /bin/sh + - -c + - dockerize -wait "tcp://$DB_HOST:$DB_PORT" -timeout 120s + + # -- Launch additional containers into supersetNode pod + extraContainers: [] + # -- Annotations to be added to supersetNode deployment + deploymentAnnotations: {} + # -- Labels to be added to supersetNode deployment + deploymentLabels: {} + # -- Affinity to be added to supersetNode deployment + affinity: {} + # -- TopologySpreadConstrains to be added to supersetNode deployments + topologySpreadConstraints: [] + # -- Annotations to be added to supersetNode pods + podAnnotations: {} + # -- Labels to be added to supersetNode pods + podLabels: {} + startupProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 15 + timeoutSeconds: 1 + failureThreshold: 60 + periodSeconds: 5 + successThreshold: 1 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 15 + timeoutSeconds: 1 + failureThreshold: 3 + periodSeconds: 15 + successThreshold: 1 + readinessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 15 + timeoutSeconds: 1 + failureThreshold: 3 + periodSeconds: 15 + successThreshold: 1 + # -- Resource settings for the supersetNode pods - these settings overwrite might existing values from the global resources object defined above. + resources: {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + podSecurityContext: {} + containerSecurityContext: {} + strategy: {} + # type: RollingUpdate + # rollingUpdate: + # maxSurge: 25% + # maxUnavailable: 25% + +# Superset Celery worker configuration +supersetWorker: + replicas: + enabled: true + replicaCount: 1 + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + # -- Sets the [pod disruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) for supersetWorker pods + podDisruptionBudget: + # -- Whether the pod disruption budget should be created + enabled: false + # -- If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget + minAvailable: 1 + # -- If set, minAvailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget + maxUnavailable: 1 + # -- Worker startup command + # @default -- a `celery worker` command + command: + - "/bin/sh" + - "-c" + - ". {{ .Values.configMountPath }}/superset_bootstrap.sh; celery --app=superset.tasks.celery_app:app worker" + # -- If true, forces deployment to reload on each upgrade + forceReload: false + # -- Init container + # @default -- a container waiting for postgres and redis + initContainers: + - name: wait-for-postgres-redis + image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}" + imagePullPolicy: "{{ .Values.initImage.pullPolicy }}" + envFrom: + - secretRef: + name: "{{ tpl .Values.envFromSecret . }}" + command: + - /bin/sh + - -c + - dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s + # -- Launch additional containers into supersetWorker pod + extraContainers: [] + # -- Annotations to be added to supersetWorker deployment + deploymentAnnotations: {} + # -- Labels to be added to supersetWorker deployment + deploymentLabels: {} + # -- Affinity to be added to supersetWorker deployment + affinity: {} + # -- TopologySpreadConstrains to be added to supersetWorker deployments + topologySpreadConstraints: [] + # -- Annotations to be added to supersetWorker pods + podAnnotations: {} + # -- Labels to be added to supersetWorker pods + podLabels: {} + # -- Resource settings for the supersetWorker pods - these settings overwrite might existing values from the global resources object defined above. + resources: {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + podSecurityContext: {} + containerSecurityContext: {} + strategy: {} + # type: RollingUpdate + # rollingUpdate: + # maxSurge: 25% + # maxUnavailable: 25% + livenessProbe: + exec: + # -- Liveness probe command + # @default -- a `celery inspect ping` command + command: + - sh + - -c + - celery -A superset.tasks.celery_app:app inspect ping -d celery@$HOSTNAME + initialDelaySeconds: 120 + timeoutSeconds: 60 + failureThreshold: 3 + periodSeconds: 60 + successThreshold: 1 + # -- No startup/readiness probes by default since we don't really care about its startup time (it doesn't serve traffic) + startupProbe: {} + # -- No startup/readiness probes by default since we don't really care about its startup time (it doesn't serve traffic) + readinessProbe: {} + # -- Set priorityClassName for supersetWorker pods + priorityClassName: ~ + +# Superset beat configuration (to trigger scheduled jobs like reports) +supersetCeleryBeat: + # -- This is only required if you intend to use alerts and reports + enabled: false + # -- Sets the [pod disruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) for supersetCeleryBeat pods + podDisruptionBudget: + # -- Whether the pod disruption budget should be created + enabled: false + # -- If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget + minAvailable: 1 + # -- If set, minAvailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget + maxUnavailable: 1 + # -- Command + # @default -- a `celery beat` command + command: + - "/bin/sh" + - "-c" + - ". {{ .Values.configMountPath }}/superset_bootstrap.sh; celery --app=superset.tasks.celery_app:app beat --pidfile /tmp/celerybeat.pid --schedule /tmp/celerybeat-schedule" + # -- If true, forces deployment to reload on each upgrade + forceReload: false + # -- List of init containers + # @default -- a container waiting for postgres + initContainers: + - name: wait-for-postgres-redis + image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}" + imagePullPolicy: "{{ .Values.initImage.pullPolicy }}" + envFrom: + - secretRef: + name: "{{ tpl .Values.envFromSecret . }}" + command: + - /bin/sh + - -c + - dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s + # -- Launch additional containers into supersetCeleryBeat pods + extraContainers: [] + # -- Annotations to be added to supersetCeleryBeat deployment + deploymentAnnotations: {} + # -- Affinity to be added to supersetCeleryBeat deployment + affinity: {} + # -- TopologySpreadConstrains to be added to supersetCeleryBeat deployments + topologySpreadConstraints: [] + # -- Annotations to be added to supersetCeleryBeat pods + podAnnotations: {} + # -- Labels to be added to supersetCeleryBeat pods + podLabels: {} + # -- Resource settings for the CeleryBeat pods - these settings overwrite might existing values from the global resources object defined above. + resources: {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + podSecurityContext: {} + containerSecurityContext: {} + # -- Set priorityClassName for CeleryBeat pods + priorityClassName: ~ + +supersetCeleryFlower: + # -- Enables a Celery flower deployment (management UI to monitor celery jobs) + # WARNING: on superset 1.x, this requires a Superset image that has `flower<1.0.0` installed (which is NOT the case of the default images) + # flower>=1.0.0 requires Celery 5+ which Superset 1.5 does not support + enabled: false + replicaCount: 1 + # -- Sets the [pod disruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) for supersetCeleryFlower pods + podDisruptionBudget: + # -- Whether the pod disruption budget should be created + enabled: false + # -- If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget + minAvailable: 1 + # -- If set, minAvailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget + maxUnavailable: 1 + # -- Command + # @default -- a `celery flower` command + command: + - "/bin/sh" + - "-c" + - "celery --app=superset.tasks.celery_app:app flower" + service: + type: ClusterIP + annotations: {} + loadBalancerIP: ~ + port: 5555 + nodePort: + # -- (int) + http: nil + startupProbe: + httpGet: + path: /api/workers + port: flower + initialDelaySeconds: 5 + timeoutSeconds: 1 + failureThreshold: 60 + periodSeconds: 5 + successThreshold: 1 + livenessProbe: + httpGet: + path: /api/workers + port: flower + initialDelaySeconds: 5 + timeoutSeconds: 1 + failureThreshold: 3 + periodSeconds: 5 + successThreshold: 1 + readinessProbe: + httpGet: + path: /api/workers + port: flower + initialDelaySeconds: 5 + timeoutSeconds: 1 + failureThreshold: 3 + periodSeconds: 5 + successThreshold: 1 + # -- List of init containers + # @default -- a container waiting for postgres and redis + initContainers: + - name: wait-for-postgres-redis + image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}" + imagePullPolicy: "{{ .Values.initImage.pullPolicy }}" + envFrom: + - secretRef: + name: "{{ tpl .Values.envFromSecret . }}" + command: + - /bin/sh + - -c + - dockerize -wait "tcp://$DB_HOST:$DB_PORT" -wait "tcp://$REDIS_HOST:$REDIS_PORT" -timeout 120s + # -- Launch additional containers into supersetCeleryFlower pods + extraContainers: [] + # -- Annotations to be added to supersetCeleryFlower deployment + deploymentAnnotations: {} + # -- Affinity to be added to supersetCeleryFlower deployment + affinity: {} + # -- TopologySpreadConstrains to be added to supersetCeleryFlower deployments + topologySpreadConstraints: [] + # -- Annotations to be added to supersetCeleryFlower pods + podAnnotations: {} + # -- Labels to be added to supersetCeleryFlower pods + podLabels: {} + # -- Resource settings for the CeleryBeat pods - these settings overwrite might existing values from the global resources object defined above. + resources: {} + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + podSecurityContext: {} + containerSecurityContext: {} + # -- Set priorityClassName for supersetCeleryFlower pods + priorityClassName: ~ + +supersetWebsockets: + # -- This is only required if you intend to use `GLOBAL_ASYNC_QUERIES` in `ws` mode + # see https://github.com/apache/superset/blob/master/CONTRIBUTING.md#async-chart-queries + enabled: false + replicaCount: 1 + # -- Sets the [pod disruption budget](https://kubernetes.io/docs/tasks/run-application/configure-pdb/) for supersetWebsockets pods + podDisruptionBudget: + # -- Whether the pod disruption budget should be created + enabled: false + # -- If set, maxUnavailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget + minAvailable: 1 + # -- If set, minAvailable must not be set - see https://kubernetes.io/docs/tasks/run-application/configure-pdb/#specifying-a-poddisruptionbudget + maxUnavailable: 1 + ingress: + path: /ws + pathType: Prefix + image: + # -- There is no official image (yet), this one is community-supported + repository: oneacrefund/superset-websocket + tag: latest + pullPolicy: IfNotPresent + # -- The config.json to pass to the server, see https://github.com/apache/superset/tree/master/superset-websocket + # Note that the configuration can also read from environment variables (which will have priority), see https://github.com/apache/superset/blob/master/superset-websocket/src/config.ts for a list of supported variables + # @default -- see `values.yaml` + config: + { + "port": 8080, + "logLevel": "debug", + "logToFile": false, + "logFilename": "app.log", + "statsd": { "host": "127.0.0.1", "port": 8125, "globalTags": [] }, + "redis": + { + "port": 6379, + "host": "127.0.0.1", + "password": "", + "db": 0, + "ssl": false, + }, + "redisStreamPrefix": "async-events-", + "jwtSecret": "CHANGE-ME", + "jwtCookieName": "async-token", + } + service: + type: ClusterIP + annotations: {} + loadBalancerIP: ~ + port: 8080 + nodePort: + # -- (int) + http: nil + command: [] + resources: {} + # -- Launch additional containers into supersetWebsockets pods + extraContainers: [] + deploymentAnnotations: {} + # -- Affinity to be added to supersetWebsockets deployment + affinity: {} + # -- TopologySpreadConstrains to be added to supersetWebsockets deployments + topologySpreadConstraints: [] + podAnnotations: {} + podLabels: {} + strategy: {} + podSecurityContext: {} + containerSecurityContext: {} + startupProbe: + httpGet: + path: /health + port: ws + initialDelaySeconds: 5 + timeoutSeconds: 1 + failureThreshold: 60 + periodSeconds: 5 + successThreshold: 1 + livenessProbe: + httpGet: + path: /health + port: ws + initialDelaySeconds: 5 + timeoutSeconds: 1 + failureThreshold: 3 + periodSeconds: 5 + successThreshold: 1 + readinessProbe: + httpGet: + path: /health + port: ws + initialDelaySeconds: 5 + timeoutSeconds: 1 + failureThreshold: 3 + periodSeconds: 5 + successThreshold: 1 + # -- Set priorityClassName for supersetWebsockets pods + priorityClassName: ~ + +init: + # Configure resources + # Warning: fab command consumes a lot of ram and can + # cause the process to be killed due to OOM if it exceeds limit + # Make sure you are giving a strong password for the admin user creation( else make sure you are changing after setup) + # Also change the admin email to your own custom email. + resources: {} + # limits: + # cpu: + # memory: + # requests: + # cpu: + # memory: + # -- Command + # @default -- a `superset_init.sh` command + command: + - "/bin/sh" + - "-c" + - ". {{ .Values.configMountPath }}/superset_bootstrap.sh; . {{ .Values.configMountPath }}/superset_init.sh" + enabled: true + jobAnnotations: + "helm.sh/hook": post-install,post-upgrade + "helm.sh/hook-delete-policy": "before-hook-creation" + loadExamples: false + createAdmin: true + adminUser: + username: admin + firstname: Superset + lastname: Admin + email: admin@superset.com + password: admin + # -- List of initContainers + # @default -- a container waiting for postgres + initContainers: + - name: wait-for-postgres + image: "{{ .Values.initImage.repository }}:{{ .Values.initImage.tag }}" + imagePullPolicy: "{{ .Values.initImage.pullPolicy }}" + envFrom: + - secretRef: + name: "{{ tpl .Values.envFromSecret . }}" + command: + - /bin/sh + - -c + - dockerize -wait "tcp://$DB_HOST:$DB_PORT" -timeout 120s + # -- A Superset init script + # @default -- a script to create admin user and initialize roles + initscript: |- + #!/bin/sh + set -eu + echo "Upgrading DB schema..." + superset db upgrade + echo "Initializing roles..." + superset init + {{ if .Values.init.createAdmin }} + echo "Creating admin user..." + superset fab create-admin \ + --username {{ .Values.init.adminUser.username }} \ + --firstname {{ .Values.init.adminUser.firstname }} \ + --lastname {{ .Values.init.adminUser.lastname }} \ + --email {{ .Values.init.adminUser.email }} \ + --password {{ .Values.init.adminUser.password }} \ + || true + {{- end }} + {{ if .Values.init.loadExamples }} + echo "Loading examples..." + superset load_examples + {{- end }} + if [ -f "{{ .Values.extraConfigMountPath }}/import_datasources.yaml" ]; then + echo "Importing database connections.... " + superset import_datasources -p {{ .Values.extraConfigMountPath }}/import_datasources.yaml + fi + # -- Launch additional containers into init job pod + extraContainers: [] + ## Annotations to be added to init job pods + podAnnotations: {} + podSecurityContext: {} + containerSecurityContext: {} + ## Tolerations to be added to init job pods + tolerations: [] + ## Affinity to be added to init job pods + affinity: {} + # -- TopologySpreadConstrains to be added to init job + topologySpreadConstraints: [] + # -- Set priorityClassName for init job pods + priorityClassName: ~ + +# -- Configuration values for the postgresql dependency. +# ref: https://github.com/bitnami/charts/tree/main/bitnami/postgresql +# @default -- see `values.yaml` +postgresql: + ## + ## Use the PostgreSQL chart dependency. + ## Set to false if bringing your own PostgreSQL. + enabled: true + + ## Authentication parameters + auth: + ## The name of an existing secret that contains the postgres password. + existingSecret: + ## PostgreSQL name for a custom user to create + username: superset + ## PostgreSQL password for the custom user to create. Ignored if `auth.existingSecret` with key `password` is provided + password: superset + ## PostgreSQL name for a custom database to create + database: superset + + image: + tag: "14.6.0-debian-11-r13" + + ## PostgreSQL Primary parameters + primary: + ## + ## Persistent Volume Storage configuration. + ## ref: https://kubernetes.io/docs/user-guide/persistent-volumes + persistence: + ## + ## Enable PostgreSQL persistence using Persistent Volume Claims. + enabled: true + ## + ## Persistent class + # storageClass: classname + ## + ## Access modes: + accessModes: + - ReadWriteOnce + ## PostgreSQL port + service: + ports: + postgresql: "5432" + +# -- Configuration values for the Redis dependency. +# ref: https://github.com/bitnami/charts/blob/master/bitnami/redis +# More documentation can be found here: https://artifacthub.io/packages/helm/bitnami/redis +# @default -- see `values.yaml` +redis: + ## + ## Use the redis chart dependency. + ## + ## If you are bringing your own redis, you can set the host in supersetNode.connections.redis_host + ## + ## Set to false if bringing your own redis. + enabled: true + ## + ## Set architecture to standalone/replication + architecture: standalone + ## + ## Auth configuration: + ## + auth: + ## Enable password authentication + enabled: false + ## The name of an existing secret that contains the redis password. + existingSecret: "" + ## Name of the key containing the secret. + existingSecretKey: "" + ## Redis password + password: superset + ## + ## Master configuration + ## + master: + ## + ## Image configuration + # image: + ## + ## docker registry secret names (list) + # pullSecrets: nil + ## + ## Configure persistance + persistence: + ## + ## Use a PVC to persist data. + enabled: false + ## + ## Persistent class + # storageClass: classname + ## + ## Access mode: + accessModes: + - ReadWriteOnce + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +# -- TopologySpreadConstrains to be added to all deployments +topologySpreadConstraints: [] + +# -- Set priorityClassName for superset pods +priorityClassName: ~ \ No newline at end of file diff --git a/package.json b/package.json index 44e90e7e33fd..4b89c1d62219 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@sniptt/guards": "^0.2.0", "@stoplight/elements": "^8.0.5", "@storybook/icons": "^1.2.9", + "@superset-ui/embedded-sdk": "0.1.0-alpha.11", "@swc/jest": "^0.2.29", "@tabler/icons-react": "^2.44.0", "@types/dompurify": "^3.0.5", diff --git a/packages/twenty-chrome-extension/codegen.ts b/packages/twenty-chrome-extension/codegen.ts index fef2e0e8ef49..b857d627832d 100644 --- a/packages/twenty-chrome-extension/codegen.ts +++ b/packages/twenty-chrome-extension/codegen.ts @@ -13,7 +13,7 @@ const config: CodegenConfig = { }, ], overwrite: true, - documents: ['./src/**/*.ts', '!src/generated/**/*.*'], + documents: ['./src/**/*.ts', '!src/generated/**/*.*' ], generates: { './src/generated/graphql.tsx': { plugins: [ diff --git a/packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts b/packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts index eefcfefac725..b945a9ea46e4 100644 --- a/packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts +++ b/packages/twenty-chrome-extension/src/contentScript/extractPersonProfile.ts @@ -130,4 +130,4 @@ export const insertButtonForPerson = async () => { }); } } -}; +}; \ No newline at end of file diff --git a/packages/twenty-chrome-extension/src/options/Sidepanel.tsx b/packages/twenty-chrome-extension/src/options/Sidepanel.tsx index 3bea68701af3..deedb1f7f8f4 100644 --- a/packages/twenty-chrome-extension/src/options/Sidepanel.tsx +++ b/packages/twenty-chrome-extension/src/options/Sidepanel.tsx @@ -37,7 +37,6 @@ const StyledActionContainer = styled.div` justify-content: center; width: 300px; `; - const Sidepanel = () => { const [isAuthenticated, setIsAuthenticated] = useState(false); const [clientUrl, setClientUrl] = useState( diff --git a/packages/twenty-docker/twenty/entrypoint.sh b/packages/twenty-docker/twenty/entrypoint.sh index 17adc89521ab..943814171ab4 100755 --- a/packages/twenty-docker/twenty/entrypoint.sh +++ b/packages/twenty-docker/twenty/entrypoint.sh @@ -13,5 +13,28 @@ if [ "${ENABLE_DB_MIGRATIONS}" = "true" ] && [ ! -f /app/docker-data/db_status ] touch /app/docker-data/db_status fi + +# Path to the flag file +FLAG_FILE="/app/.initialized" + +# Check if the flag file exists +if [ ! -f "$FLAG_FILE" ]; then + echo "Running initialization commands..." + # Run your initialization commands here + echo "yarn could not be found, installing... yarn" + yarn + + echo "Running database setup and migrations..." + npx nx database:init:prod twenty-server + + echo "#######################--Migrations and Schemas created successfully..#######################--" + + touch "$FLAG_FILE" + + echo "Initialization completed." +else + echo "Initialization already completed. Skipping..." +fi + # Continue with the original Docker command exec "$@" diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json index 8ff1595fab09..8f534aeaa6b7 100644 --- a/packages/twenty-front/package.json +++ b/packages/twenty-front/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "build": "npx vite build && sh ./scripts/inject-runtime-env.sh", - "build:sourcemaps": "VITE_BUILD_SOURCEMAP=true NODE_OPTIONS=--max-old-space-size=4096 npx nx build", + "build:sourcemaps": "VITE_BUILD_SOURCEMAP=true NODE_OPTIONS=--max-old-space-size=8192 npx nx build", "start:prod": "NODE_ENV=production npx vite --host", "tsup": "npx tsup" }, diff --git a/packages/twenty-front/public/icons/android/android-launchericon-192-192-1.png b/packages/twenty-front/public/icons/android/android-launchericon-192-192-1.png new file mode 100644 index 000000000000..0bb732d89c9b Binary files /dev/null and b/packages/twenty-front/public/icons/android/android-launchericon-192-192-1.png differ diff --git a/packages/twenty-front/public/icons/android/android-launchericon-192-192-2.png b/packages/twenty-front/public/icons/android/android-launchericon-192-192-2.png new file mode 100644 index 000000000000..b4ec9d99e3bb Binary files /dev/null and b/packages/twenty-front/public/icons/android/android-launchericon-192-192-2.png differ diff --git a/packages/twenty-front/public/icons/android/android-launchericon-192-192.png b/packages/twenty-front/public/icons/android/android-launchericon-192-192.png index 0bb732d89c9b..23e1625ec1a3 100644 Binary files a/packages/twenty-front/public/icons/android/android-launchericon-192-192.png and b/packages/twenty-front/public/icons/android/android-launchericon-192-192.png differ diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx index 75460ee3766b..e3367cef671f 100644 --- a/packages/twenty-front/src/App.tsx +++ b/packages/twenty-front/src/App.tsx @@ -24,6 +24,7 @@ import { ApolloMetadataClientProvider } from '@/object-metadata/components/Apoll import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider'; import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider'; import { AppPath } from '@/types/AppPath'; +import { CustomPath } from '@/types/CustomPath'; import { SettingsPath } from '@/types/SettingsPath'; import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager'; import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope'; @@ -42,6 +43,11 @@ import { Authorize } from '~/pages/auth/Authorize'; import { Invite } from '~/pages/auth/Invite'; import { PasswordReset } from '~/pages/auth/PasswordReset'; import { SignInUp } from '~/pages/auth/SignInUp'; +import { CampaignForm } from '~/pages/campaigns/CampaignForm'; +import { CampaignForm2 } from '~/pages/campaigns/CampaignForm2'; +import { CampaignForm3 } from '~/pages/campaigns/CampaignForm3'; +import { Campaigns } from '~/pages/campaigns/Campaigns'; +import Dashboard from '~/pages/campaigns/Dashboard'; import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect'; import { NotFound } from '~/pages/not-found/NotFound'; import { RecordIndexPage } from '~/pages/object-record/RecordIndexPage'; @@ -52,6 +58,7 @@ import { CreateWorkspace } from '~/pages/onboarding/CreateWorkspace'; import { InviteTeam } from '~/pages/onboarding/InviteTeam'; import { PaymentSuccess } from '~/pages/onboarding/PaymentSuccess'; import { SyncEmails } from '~/pages/onboarding/SyncEmails'; +import { Segment } from '~/pages/Segment/Segment'; import { SettingsAccounts } from '~/pages/settings/accounts/SettingsAccounts'; import { SettingsAccountsCalendars } from '~/pages/settings/accounts/SettingsAccountsCalendars'; import { SettingsAccountsEmails } from '~/pages/settings/accounts/SettingsAccountsEmails'; @@ -315,8 +322,20 @@ const createRouter = ( }> } /> - , - ), + }> + + } /> + } /> + } /> + } /> + + } /> + + } /> + + + + ) ); export const App = () => { diff --git a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/effect-components/PageChangeEffect.tsx index 19da0d36d74c..62984ea14f0a 100644 --- a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx +++ b/packages/twenty-front/src/effect-components/PageChangeEffect.tsx @@ -20,11 +20,16 @@ import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation'; import { usePageChangeEffectNavigateLocation } from '~/hooks/usePageChangeEffectNavigateLocation'; import { isDefined } from '~/utils/isDefined'; +import { CustomPath } from '@/types/CustomPath'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; + // TODO: break down into smaller functions and / or hooks // - moved usePageChangeEffectNavigateLocation into dedicated hook export const PageChangeEffect = () => { const navigate = useNavigate(); const isMatchingLocation = useIsMatchingLocation(); + const { enqueueSnackBar } = useSnackBar(); const [previousLocation, setPreviousLocation] = useState(''); @@ -52,12 +57,39 @@ export const PageChangeEffect = () => { }, [location, previousLocation]); useEffect(() => { + const isMatchingOngoingUserCreationRoute = + isMatchingLocation(AppPath.SignIn) || + isMatchingLocation(AppPath.Invite) || + isMatchingLocation(AppPath.Verify); + + const isMatchingOnboardingRoute = + isMatchingOngoingUserCreationRoute || + isMatchingLocation(AppPath.CreateWorkspace) || + isMatchingLocation(AppPath.CreateProfile) || + isMatchingLocation(AppPath.PlanRequired) || + isMatchingLocation(AppPath.PlanRequiredSuccess); + + const navigateToSignUp = () => { + enqueueSnackBar('workspace does not exist', { + variant: SnackBarVariant.Error, + }); + navigate(AppPath.SignUp); + }; + if (isDefined(pageChangeEffectNavigateLocation)) { navigate(pageChangeEffectNavigateLocation); } + if ( + isMatchingLocation(CustomPath.CampaignForm) || + isMatchingLocation(CustomPath.CampaignForm2) || + isMatchingLocation(CustomPath.CampaignForm3) + ) { + console.log('Path Location:', location.pathname); + navigate(location.pathname); + return; + } }, [navigate, pageChangeEffectNavigateLocation]); - - useEffect(() => { +useEffect(() => { switch (true) { case isMatchingLocation(AppPath.RecordIndexPage): { setHotkeyScope(TableHotkeyScope.Table, { @@ -73,6 +105,28 @@ export const PageChangeEffect = () => { }); break; } + case isMatchingLocation(CustomPath.CampaignForm): { + setHotkeyScope(PageHotkeyScope.CampaignForm, { + goto: true, + keyboardShortcutMenu: true, + }); + break; + } + case isMatchingLocation(CustomPath.CampaignForm2): { + setHotkeyScope(PageHotkeyScope.CampaignForm, { + goto: true, + keyboardShortcutMenu: true, + }); + break; + } + case isMatchingLocation(CustomPath.CampaignForm3): { + setHotkeyScope(PageHotkeyScope.CampaignForm, { + goto: true, + keyboardShortcutMenu: true, + }); + break; + } + case isMatchingLocation(AppPath.OpportunitiesPage): { setHotkeyScope(PageHotkeyScope.OpportunitiesPage, { goto: true, @@ -104,14 +158,6 @@ export const PageChangeEffect = () => { setHotkeyScope(PageHotkeyScope.CreateWokspace); break; } - case isMatchingLocation(AppPath.SyncEmails): { - setHotkeyScope(PageHotkeyScope.SyncEmail); - break; - } - case isMatchingLocation(AppPath.InviteTeam): { - setHotkeyScope(PageHotkeyScope.InviteTeam); - break; - } case isMatchingLocation(AppPath.PlanRequired): { setHotkeyScope(PageHotkeyScope.PlanRequired); break; @@ -176,4 +222,4 @@ export const PageChangeEffect = () => { }, [isCaptchaScriptLoaded, isMatchingLocation, requestFreshCaptchaToken]); return <>; -}; +}; \ No newline at end of file diff --git a/packages/twenty-front/src/index.tsx b/packages/twenty-front/src/index.tsx index 06527d80050b..d825cd299698 100644 --- a/packages/twenty-front/src/index.tsx +++ b/packages/twenty-front/src/index.tsx @@ -16,6 +16,7 @@ import { App } from './App'; import './index.css'; import 'react-loading-skeleton/dist/skeleton.css'; +import CampaignContext from '~/pages/campaigns/CampaignContext'; const root = ReactDOM.createRoot( document.getElementById('root') ?? document.body, @@ -31,7 +32,9 @@ root.render( - + + + diff --git a/packages/twenty-front/src/modules/activities/Leads/components/DisplayLeads.tsx b/packages/twenty-front/src/modules/activities/Leads/components/DisplayLeads.tsx new file mode 100644 index 000000000000..c31e9e50a3b4 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/Leads/components/DisplayLeads.tsx @@ -0,0 +1,179 @@ +import { DateTimeDisplay } from '@/ui/field/display/components/DateTimeDisplay'; +import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay'; +import { NumberDisplay } from '@/ui/field/display/components/NumberDisplay'; +import { Checkbox } from '@/ui/input/components/Checkbox'; +import styled from '@emotion/styled'; +import { + StyledCountContainer, + StyledComboInputContainer, + StyledLabelContainer, + StyledTable, + StyledTableRow, + StyledTableHeaderCell, + StyledTableCell, + StyledCheckLabelContainer, +} from '~/pages/Segment/SegmentStyles'; +import { capitalize } from '~/utils/string/capitalize'; +import { + leadsDataState, + totalLeadsCountState, + selectedIDState, + unselectedIDState, + checkboxState, + isCheckedState, + cursorState, + loadingState, +} from '@/activities/Leads/components/LeadAtoms'; +import { useRecoilState } from 'recoil'; +import { PageTitle } from '@/ui/utilities/page-title/PageTitle'; +import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; +import { AnimatedPlaceholderEmptyTextContainer } from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; +import { + AnimatedPlaceholderErrorContainer, + AnimatedPlaceholderErrorTitle, +} from '@/ui/layout/animated-placeholder/components/ErrorPlaceholderStyled'; + +const StyledBackDrop = styled.div` + align-items: center; + display: flex; + flex-direction: column; + justify-content: center; + width: 100%; + height: 70vh; +`; +export type DisplayLeadsProps = { + lastLeadRef: React.LegacyRef | undefined; + handleRemoveContactedLeads: () => void; + handleMasterCheckboxChange: (event: any) => void; + handleCheckboxChange: (event: any, lead: string) => void; + date: Date; +}; + +export const DisplayLeads = ({ + lastLeadRef, + date, + handleRemoveContactedLeads, + handleMasterCheckboxChange, + handleCheckboxChange, +}: DisplayLeadsProps) => { + const [leadsData, setLeadsData] = useRecoilState(leadsDataState); + const [totalLeadsCount, setTotalLeadsCount] = + useRecoilState(totalLeadsCountState); + const [selectedID, setSelectedID] = useRecoilState(selectedIDState); + const [unselectedID, setUnselectedID] = useRecoilState(unselectedIDState); + const [checkbox, setCheckbox] = useRecoilState(checkboxState); + const [isChecked, setIsChecked] = useRecoilState(isCheckedState); + const [cursor, setCursor] = useRecoilState(cursorState); + const [loading, setLoading] = useRecoilState(loadingState); + + const fieldsToDisplay = + leadsData.length > 0 + ? Object.keys(leadsData[0].node).filter( + (field) => field !== '__typename' && field !== 'id', + ) + : []; + + return ( + <> + {leadsData.length === 0 ? ( + <> + + + + + + + No leads available + + + + + + ) : ( + <> + + + + Leads fetched at: + + + + + + Total Leads: + + + + + + + Selected Leads: + + + + + + + Unselected Leads: + + + + + handleRemoveContactedLeads()} + /> + Remove leads that were contacted previously + + + + + + + + handleMasterCheckboxChange(event)} + /> + + + {fieldsToDisplay.map((name) => ( + + + {capitalize(name)} + + + ))} + + {leadsData.map((leadEdge: any) => { + const lead = leadEdge?.node; + return ( + + + + handleCheckboxChange(event, lead.id) + } + /> + + {fieldsToDisplay.map((name) => ( + + {lead[name]} + + ))} + + ); + })} + {cursor && loading && ( + + Loading more... + + )} + + + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/activities/Leads/components/FetchAndUpdateLeads.ts b/packages/twenty-front/src/modules/activities/Leads/components/FetchAndUpdateLeads.ts new file mode 100644 index 000000000000..27362136cab9 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/Leads/components/FetchAndUpdateLeads.ts @@ -0,0 +1,180 @@ +import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { FILTER_LEADS } from '@/users/graphql/queries/filterLeads'; +import { GET_CAMPAIGN_LISTS } from '@/users/graphql/queries/getCampaignList'; +import { useLazyQuery } from '@apollo/client'; +import { GET_CAMPAIGN_TRIGGER } from '@/users/graphql/queries/getOneCampaignTrigger'; +import { + leadsDataState, + totalLeadsCountState, + cursorState, + loadingState, + selectedIDState, + unselectedIDState, + checkboxState, + filterState, + allLeadId, + opportunitesLeadIdsState, +} from '@/activities/Leads/components/LeadAtoms'; +import { useRecoilState } from 'recoil'; +import { useRef } from 'react'; +import { useCampaign } from '~/pages/campaigns/CampaignUseContext'; + +export const FetchAndUpdateLeads = ( + targetableObject: ActivityTargetableObject, +) => { + const [leadsData, setLeadsData] = useRecoilState(leadsDataState); + const [totalLeadsCount, setTotalLeadsCount] = + useRecoilState(totalLeadsCountState); + let campaignId = ''; + const [filter, setFilter] = useRecoilState(filterState); + const [cursor, setCursor] = useRecoilState(cursorState); + const [loading, setLoading] = useRecoilState(loadingState); + const lastLeadRef = useRef(null); + const [selectedID, setSelectedID] = useRecoilState(selectedIDState); + const [unselectedID, setUnselectedID] = useRecoilState(unselectedIDState); + const [checkbox, setCheckbox] = useRecoilState(checkboxState); + const { campaignData, setCampaignData } = useCampaign(); + let [opportunitiesLeadIds, setOpportunitiesLeadIds] = useRecoilState( + opportunitesLeadIdsState, + ); + + const [filterleads, { data: filterLeadsData }] = useLazyQuery(FILTER_LEADS, { + fetchPolicy: 'network-only', + }); + + let [selectedCampaign, { data: selectedCampaignData }] = useLazyQuery( + GET_CAMPAIGN_LISTS, + { + fetchPolicy: 'network-only', + }, + ); + let [selectedCampaignTrigger, { data: selectedCampaignTriggerData }] = + useLazyQuery(GET_CAMPAIGN_TRIGGER, { + fetchPolicy: 'network-only', + }); + + const fetchLeads = async () => { + try { + if (targetableObject.targetObjectNameSingular === 'campaignTrigger') { + const data = await selectedCampaignTrigger({ + variables: { + objectRecordId: targetableObject.id, + }, + }); + + campaignId = data.data.campaignTrigger.campaignId; + } else if (targetableObject.targetObjectNameSingular === 'campaign') { + campaignId = targetableObject.id; + } + + const data = await selectedCampaign({ + variables: { + filter: { + id: { eq: campaignId }, + }, + }, + }); + + const filter = JSON.parse( + data.data.campaigns.edges[0].node.segment.filters, + ); + setFilter(filter); + + const result = await filterleads({ variables: filter }); + const leadsCount = result.data?.leads?.totalCount || 0; + setTotalLeadsCount(leadsCount); + setLeadsData(result.data.leads.edges); + result.data.leads.edges.forEach((leadEdge: any) => { + const lead = leadEdge?.node; + setSelectedID(selectedID.add(lead.id)); + let leadId = lead.id; + }); + + for (const id of selectedID.keys()) { + allLeadId[id as string] = true; + } + setCheckbox({ + ...checkbox, + ...allLeadId, + }); + + const querystamp = new Date().toISOString(); + setCursor(result.data.leads.pageInfo.endCursor); + if (result.data.leads.pageInfo.hasNextPage == true) { + setLoading(true); + } + setCampaignData({ + ...campaignData, + querystamp: querystamp, + }); + } catch (error) { + console.error('Error fetching campaign segment filter:', error); + } + }; + + const loadMore = async () => { + if (loading) { + const result = await filterleads({ + variables: { + ...filter, + lastCursor: cursor, + }, + }); + + result.data.leads.edges.forEach((leadEdge: any) => { + const lead = leadEdge?.node; + setSelectedID(selectedID.add(lead.id)); + const id = lead.id; + allLeadId[id] = true; + }); + + setCheckbox({ + ...checkbox, + ...allLeadId, + }); + + setCursor(result.data.leads.pageInfo.endCursor); + const newLeadsData = result.data.leads.edges; + + for (const id of unselectedID.keys()) { + setSelectedID(selectedID.add(id)); + unselectedID.delete(id); + setUnselectedID(unselectedID); + } + + for (const id of selectedID.keys()) { + allLeadId[id as string] = true; + } + + setCheckbox({ + ...checkbox, + ...allLeadId, + }); + + const removeleadsfromSelected = (prevLeadsData: any[]) => + prevLeadsData.filter((lead: any) => + opportunitiesLeadIds.has(lead.node.id), + ); + + const removeleads = (prevLeadsData: any[]) => + prevLeadsData.filter( + (lead: any) => !opportunitiesLeadIds.has(lead.node.id), + ); + + const removeFromSelected = removeleadsfromSelected(newLeadsData); + for (const lead of removeFromSelected) { + selectedID.delete(lead.node.id); + unselectedID.delete(lead.node.id); + } + setSelectedID(selectedID); + setUnselectedID(unselectedID); + console.log(selectedID, 'selected'); + + const leadsRemoved = removeleads(newLeadsData); + setLeadsData([...leadsData, ...leadsRemoved]); + setTotalLeadsCount(totalLeadsCount - removeFromSelected.length); + } + }; + + return { fetchLeads, loadMore, filterLeadsData }; +}; diff --git a/packages/twenty-front/src/modules/activities/Leads/components/LeadAtoms.ts b/packages/twenty-front/src/modules/activities/Leads/components/LeadAtoms.ts new file mode 100644 index 000000000000..4d99200807ec --- /dev/null +++ b/packages/twenty-front/src/modules/activities/Leads/components/LeadAtoms.ts @@ -0,0 +1,69 @@ +import { atom } from 'recoil'; + +interface CheckboxState { + [key: string]: boolean; +} +interface AllLeadId { + [key: string]: boolean; +} + +interface LeadNode { + id: string; + [key: string]: any; +} + +interface LeadEdge { + node: LeadNode; +} + +export const leadsDataState = atom({ + key: 'leadsData', + default: [], +}); + +export const totalLeadsCountState = atom({ + key: 'totalLeadsCount', + default: 0, +}); + +export const cursorState = atom({ + key: 'cursor', + default: null, +}); + +export const loadingState = atom({ + key: 'loading', + default: false, +}); + +export const selectedIDState = atom>({ + key: 'selectedID', + default: new Set(), +}); + +export const unselectedIDState = atom>({ + key: 'unSelectedID', + default: new Set(), +}); + +export const checkboxState = atom({ + key: 'checkbox', + default: {}, +}); + +export const filterState = atom>({ + key: 'filterStare', + default: {}, +}); + +export const opportunitesLeadIdsState = atom>({ + key: 'opportunitesLeadIdsState', + default: new Set(), +}); + +export const isCheckedState = atom({ + key: 'isCheckedState', + default: true, +}); + +export const allLeadId: AllLeadId = {}; diff --git a/packages/twenty-front/src/modules/activities/Leads/components/Leads.tsx b/packages/twenty-front/src/modules/activities/Leads/components/Leads.tsx new file mode 100644 index 000000000000..9905f3035ac6 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/Leads/components/Leads.tsx @@ -0,0 +1,123 @@ +import { useEffect, useRef, useState } from 'react'; +import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { useCampaign } from '~/pages/campaigns/CampaignUseContext'; +import { IconRefresh } from '@tabler/icons-react'; +import { IconButton } from '@/ui/input/button/components/IconButton'; +import { DisplayLeads } from '@/activities/Leads/components/DisplayLeads'; +import { + StyledButtonContainer, + StyledContainer, + StyledInputCard, +} from '@/activities/Leads/components/LeadsStyles'; +import { useRecoilState, useResetRecoilState } from 'recoil'; +import { FetchAndUpdateLeads } from '@/activities/Leads/components/FetchAndUpdateLeads'; +import { + leadsDataState, + totalLeadsCountState, + cursorState, + loadingState, + selectedIDState, + unselectedIDState, + checkboxState, + isCheckedState, +} from '@/activities/Leads/components/LeadAtoms'; +import { LeadsCheckbox } from '@/activities/Leads/components/LeadsCheckbox'; +export const Leads = ({ + targetableObject, +}: { + targetableObject: ActivityTargetableObject; +}) => { + const [leadsData] = useRecoilState(leadsDataState); + const [cursor] = useRecoilState(cursorState); + const lastLeadRef = useRef(null); + const [selectedID, setSelectedID] = useRecoilState(selectedIDState); + const [unselectedID, setUnselectedID] = useRecoilState(unselectedIDState); + const { campaignData, setCampaignData } = useCampaign(); + const date = new Date(campaignData.querystamp.toString()); + const resetIsChecked = useResetRecoilState(isCheckedState); + const resetCheckbox = useResetRecoilState(checkboxState); + const resetLeadsData = useResetRecoilState(leadsDataState); + const resetLoading = useResetRecoilState(loadingState); + const resetlCursor = useResetRecoilState(cursorState); + const resetTotalLeadsCount = useResetRecoilState(totalLeadsCountState); + + const { fetchLeads, loadMore } = FetchAndUpdateLeads(targetableObject); + const { + handleRemoveContactedLeads, + handleCheckboxChange, + handleMasterCheckboxChange, + } = LeadsCheckbox(targetableObject); + const clearSelectedID = () => { + setSelectedID(new Set()); + setUnselectedID(new Set()); + }; + + useEffect(() => { + fetchLeads(); + return () => { + resetLeadsData(); + resetTotalLeadsCount(); + resetIsChecked(); + resetCheckbox(); + resetLoading(); + resetlCursor(); + clearSelectedID(); + }; + }, [targetableObject.id]); + + const onIntersection = async (entries: any) => { + const firstEntry = entries[0]; + + if (firstEntry.isIntersecting && cursor) { + loadMore(); + } + }; + + useEffect(() => { + const observer = new IntersectionObserver(onIntersection); + if (observer && lastLeadRef.current) { + observer.observe(lastLeadRef.current); + } + + return () => { + if (observer) { + observer.disconnect(); + } + }; + }, [leadsData]); + + const [fieldsToDisplay, setFieldsToDisplay] = useState([]); + + useEffect(() => { + if (leadsData[0]) { + setFieldsToDisplay( + Object.keys(leadsData[0].node).filter( + (field) => field !== '__typename' && field !== 'id', + ), + ); + } + }, [leadsData]); + + return ( + <> + + + + + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/activities/Leads/components/LeadsCheckbox.tsx b/packages/twenty-front/src/modules/activities/Leads/components/LeadsCheckbox.tsx new file mode 100644 index 000000000000..bdee92ea70c2 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/Leads/components/LeadsCheckbox.tsx @@ -0,0 +1,157 @@ +import { + leadsDataState, + totalLeadsCountState, + selectedIDState, + unselectedIDState, + checkboxState, + allLeadId, + opportunitesLeadIdsState, + isCheckedState, +} from '@/activities/Leads/components/LeadAtoms'; +import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { GET_CONTACTED_OPPORTUNITIES } from '@/users/graphql/queries/getContactedOpportunities'; +import { useLazyQuery } from '@apollo/client'; +import { useRecoilState } from 'recoil'; +import { useCampaign } from '~/pages/campaigns/CampaignUseContext'; + +export const LeadsCheckbox = (targetableObject: ActivityTargetableObject) => { + const [leadsData, setLeadsData] = useRecoilState(leadsDataState); + const [totalLeadsCount, setTotalLeadsCount] = + useRecoilState(totalLeadsCountState); + const [selectedID, setSelectedID] = useRecoilState(selectedIDState); + const [unselectedID, setUnselectedID] = useRecoilState(unselectedIDState); + const [checkbox, setCheckbox] = useRecoilState(checkboxState); + const [isChecked, setIsChecked] = useRecoilState(isCheckedState); + const { campaignData, setCampaignData } = useCampaign(); + let [opportunitiesLeadIds, setOpportunitiesLeadIds] = useRecoilState( + opportunitesLeadIdsState, + ); + let [contactedOpportunities, { data: contactedOpportunitiesData }] = + useLazyQuery(GET_CONTACTED_OPPORTUNITIES, { + fetchPolicy: 'network-only', + }); + const handleRemoveContactedLeads = async () => { + try { + let hasNextPage = true; + let cursor = null; + + while (hasNextPage) { + const data = await contactedOpportunities({ + variables: { + objectRecordId: targetableObject.id, + opportunityLeadFilter: { + stage: { + eq: 'INFORMED', + }, + messageStatus: { + eq: 'SUCCESS', + }, + }, + lastCursor: cursor, + }, + }); + + data.data.campaign.opportunities.edges.forEach( + (edge: { node: { leadId: string } }) => { + setOpportunitiesLeadIds(opportunitiesLeadIds.add(edge.node.leadId)); + }, + ); + + cursor = data.data.campaign.opportunities.pageInfo.endCursor; + hasNextPage = data.data.campaign.opportunities.pageInfo.hasNextPage; + console.log(hasNextPage); + } + const removeleadsfromSelected = (prevLeadsData: any[]) => + prevLeadsData.filter((lead: any) => + opportunitiesLeadIds.has(lead.node.id), + ); + + const removeleads = (prevLeadsData: any[]) => + prevLeadsData.filter( + (lead: any) => !opportunitiesLeadIds.has(lead.node.id), + ); + + const removeFromSelected = removeleadsfromSelected(leadsData); + console.log(removeFromSelected, 'remove'); + for (const lead of removeFromSelected) { + selectedID.delete(lead.node.id); + unselectedID.delete(lead.node.id); + } + setSelectedID(selectedID); + setUnselectedID(unselectedID); + console.log(selectedID, 'selected'); + + setLeadsData(removeleads(leadsData)); + setTotalLeadsCount(totalLeadsCount - removeFromSelected.length); + } catch (error) { + console.error('Error fetching contacted opportunities:', error); + } + }; + + const handleCheckboxChange = (event: any, leadId: string): boolean => { + const { checked } = event.target; + if (checked) { + setSelectedID(selectedID.add(leadId)); + unselectedID.delete(leadId); + setUnselectedID(unselectedID); + setCheckbox({ + ...checkbox, + [leadId]: true, + }); + } else { + selectedID.delete(leadId); + setSelectedID(selectedID); + setUnselectedID(unselectedID.add(leadId)); + setCheckbox({ + ...checkbox, + [leadId]: false, + }); + } + + setIsChecked(checked); + setCampaignData({ + ...campaignData, + selectedId: Array.from(selectedID), + unSelectedId: Array.from(unselectedID), + }); + return false; + }; + + const handleMasterCheckboxChange = (event: any) => { + const { checked } = event.target; + if (checked) { + for (const id of unselectedID.keys()) { + setSelectedID(selectedID.add(id)); + unselectedID.delete(id); + setUnselectedID(unselectedID); + } + for (const id of selectedID.keys()) { + allLeadId[id as string] = true; + } + } else { + for (const id of selectedID.keys()) { + selectedID.delete(id); + setSelectedID(selectedID); + setUnselectedID(unselectedID.add(id)); + } + for (const id of unselectedID.keys()) { + allLeadId[id as string] = false; + } + } + + setCheckbox({ + ...checkbox, + ...allLeadId, + }); + setCampaignData({ + ...campaignData, + selectedId: Array.from(selectedID), + unSelectedId: Array.from(unselectedID), + }); + }; + return { + handleRemoveContactedLeads, + handleCheckboxChange, + handleMasterCheckboxChange, + }; +}; diff --git a/packages/twenty-front/src/modules/activities/Leads/components/LeadsStyles.ts b/packages/twenty-front/src/modules/activities/Leads/components/LeadsStyles.ts new file mode 100644 index 000000000000..0cfc90d369a6 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/Leads/components/LeadsStyles.ts @@ -0,0 +1,24 @@ +import styled from '@emotion/styled'; + +export const StyledButtonContainer = styled.div` + display: inline-flex; + justify-content: flex-end; + margin-top: ${({ theme }) => theme.spacing(2)}; +`; + +export const StyledContainer = styled.div` + align-items: flex-start; + align-self: stretch; + display: flex; + flex-direction: column; + justify-content: center; +`; + +export const StyledInputCard = styled.div` + align-items: flex-start; + display: flex; + flex-direction: column; + height: auto; + justify-content: space-evenly; + width: 100%; +`; \ No newline at end of file diff --git a/packages/twenty-front/src/modules/activities/Schedule/components/schedule.tsx b/packages/twenty-front/src/modules/activities/Schedule/components/schedule.tsx new file mode 100644 index 000000000000..08b1584c3f07 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/Schedule/components/schedule.tsx @@ -0,0 +1,198 @@ +import { useState } from 'react'; +import styled from '@emotion/styled'; +import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { useCampaign } from '~/pages/campaigns/CampaignUseContext'; +import { + Checkbox, + CheckboxShape, + CheckboxSize, + CheckboxVariant, +} from '@/ui/input/components/Checkbox'; +import DateTimePicker from '@/ui/input/components/internal/date/components/DateTimePicker'; +import { IconCalendar } from '@tabler/icons-react'; + +const StyledContainer = styled.div` + align-items: flex-start; + align-self: stretch; + display: flex; + flex-direction: column; + justify-content: center; + padding: 8px 24px; +`; + +const StyledTitleBar = styled.div` + display: flex; + justify-content: space-between; + margin-bottom: ${({ theme }) => theme.spacing(4)}; + margin-top: ${({ theme }) => theme.spacing(4)}; + place-items: center; + width: 100%; +`; + +const StyledTitle = styled.h3` + color: ${({ theme }) => theme.font.color.primary}; + font-weight: ${({ theme }) => theme.font.weight.medium}; + font-size:${({ theme }) => theme.font.size.md} +`; + +const StyledScheduleTitle = styled.h3` + color: ${({ theme }) => theme.font.color.primary}; + font-weight: ${({ theme }) => theme.font.weight.medium}; + font-size:${({ theme }) => theme.font.size.xs} +`; + +const StyledTaskRows = styled.div` + background-color: ${({ theme }) => theme.background.secondary}; + border: 1px solid ${({ theme }) => theme.border.color.light}; + border-radius: ${({ theme }) => theme.border.radius.md}; + width: 100%; +`; + +const StyledComboInputContainer = styled.div` + display: flex; + align-items: center; + align-self: center; + justify-content: start; + margin: ${({ theme }) => theme.spacing(10)}; +`; + +const StyledLabel = styled.span` + color: ${({ theme }) => theme.font.color.tertiary}; + font-size: ${({ theme }) => theme.font.size.xs}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + margin-left: ${({ theme }) => theme.spacing(2)}; + margin-right: ${({ theme }) => theme.spacing(6)}; + display: flex; + align-items: center; +`; + +const StyledCheckboxLabel = styled.span` + margin-left: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledCount = styled.span` + color: ${({ theme }) => theme.font.color.light}; + margin-left: ${({ theme }) => theme.spacing(2)}; +`; + +export const Schedule = ({ + targetableObject, +}: { + targetableObject: ActivityTargetableObject; +}) => { + const [showStartDateTimePicker, setShowStartDateTimePicker] = useState(false); + const [showStopDateTimePicker, setShowStopDateTimePicker] = useState(false); + const [startDate, setStartDate] = useState(null); + const [stopDate, setStopDate] = useState(null); + const { campaignData, setCampaignData } = useCampaign(); + + const handleStartCheckboxChange = (event: React.ChangeEvent) => { + const currentDate = new Date(); + setStartDate(currentDate); + setCampaignData({ + ...campaignData, + startDate: currentDate, + }); + }; + + const handleStopCheckboxChange = (event: React.ChangeEvent) => { + const currentDate = new Date(); + setStopDate(currentDate); + setCampaignData({ + ...campaignData, + endDate: currentDate, + }); + }; + return ( + + + {/* + This Campaign was run4 times. + */} + + + Start + + + + + Immediately + + + setShowStartDateTimePicker(!showStartDateTimePicker) + } + indeterminate={false} + variant={CheckboxVariant.Primary} + size={CheckboxSize.Small} + shape={CheckboxShape.Squared} + /> + + Start Date/Time + + {showStartDateTimePicker && ( + + setCampaignData({ + ...campaignData, + startDate: selectedDate, + }) + } + minDate={new Date()} + /* value={campaignData?.startDate} */ + /> + )} + + + + Stop + + + + + Immediately + + + setShowStopDateTimePicker(!showStopDateTimePicker) + } + indeterminate={false} + variant={CheckboxVariant.Primary} + size={CheckboxSize.Small} + shape={CheckboxShape.Squared} + /> + + Stop Date/Time + + {showStopDateTimePicker && ( + + setCampaignData({ + ...campaignData, + endDate: selectedDate, + }) + } + minDate={new Date()} + /> + )} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/activities/components/ActivityTitle.tsx b/packages/twenty-front/src/modules/activities/components/ActivityTitle.tsx new file mode 100644 index 000000000000..4a9982214a9d --- /dev/null +++ b/packages/twenty-front/src/modules/activities/components/ActivityTitle.tsx @@ -0,0 +1,201 @@ +import { useRef } from 'react'; +import { useApolloClient } from '@apollo/client'; +import styled from '@emotion/styled'; +import { isNonEmptyString } from '@sniptt/guards'; +import { useRecoilState } from 'recoil'; +import { Key } from 'ts-key-enum'; +import { useDebouncedCallback } from 'use-debounce'; + +import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; +import { activityTitleFamilyState } from '@/activities/states/activityTitleFamilyState'; +import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState'; +import { canCreateActivityState } from '@/activities/states/canCreateActivityState'; +import { Activity } from '@/activities/types/Activity'; +import { ActivityEditorHotkeyScope } from '@/activities/types/ActivityEditorHotkeyScope'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { modifyRecordFromCache } from '@/object-record/cache/utils/modifyRecordFromCache'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { + Checkbox, + CheckboxShape, + CheckboxSize, +} from '@/ui/input/components/Checkbox'; +import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { isDefined } from '~/utils/isDefined'; +import { useCampaign } from '~/pages/campaigns/CampaignUseContext'; + +const StyledEditableTitleInput = styled.input<{ + completed: boolean; + value: string; +}>` + background: transparent; + + border: none; + color: ${({ theme, value }) => + value ? theme.font.color.primary : theme.font.color.light}; + display: flex; + + flex-direction: column; + font-size: ${({ theme }) => theme.font.size.xl}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + + line-height: ${({ theme }) => theme.text.lineHeight.md}; + outline: none; + text-decoration: ${({ completed }) => (completed ? 'line-through' : 'none')}; + &::placeholder { + color: ${({ theme }) => theme.font.color.light}; + } + width: calc(100% - ${({ theme }) => theme.spacing(2)}); +`; + +const StyledContainer = styled.div` + display: flex; + flex-direction: row; + gap: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + +type ActivityTitleProps = { + activityId: string; +}; + +export const ActivityTitle = ({ activityId }: ActivityTitleProps) => { + const [activityInStore, setActivityInStore] = useRecoilState( + recordStoreFamilyState(activityId), + ); + + const cache = useApolloClient().cache; + + const [activityTitle, setActivityTitle] = useRecoilState( + activityTitleFamilyState({ activityId }), + ); + + const activity = activityInStore as Activity; + + const [canCreateActivity, setCanCreateActivity] = useRecoilState( + canCreateActivityState, + ); + + const { upsertActivity } = useUpsertActivity(); + const {campaignData}=useCampaign(); + const value=`Lead stage changed from [ ${campaignData.draftValue} to ${campaignData.fieldValue} ]` + const { + setHotkeyScopeAndMemorizePreviousScope, + goBackToPreviousHotkeyScope, + } = usePreviousHotkeyScope(); + const titleInputRef = useRef(null); + + useScopedHotkeys( + Key.Escape, + () => { + handleBlur(); + }, + ActivityEditorHotkeyScope.ActivityTitle, + ); + + const handleBlur = () => { + goBackToPreviousHotkeyScope(); + titleInputRef.current?.blur(); + }; + + const handleFocus = () => { + setHotkeyScopeAndMemorizePreviousScope( + ActivityEditorHotkeyScope.ActivityTitle, + ); + }; + + const [activityTitleHasBeenSet, setActivityTitleHasBeenSet] = useRecoilState( + activityTitleHasBeenSetFamilyState({ + activityId: activityId, + }), + ); + + const { objectMetadataItem: objectMetadataItemActivity } = + useObjectMetadataItem({ + objectNameSingular: CoreObjectNameSingular.Activity, + }); + + const persistTitleDebounced = useDebouncedCallback((newTitle: string) => { + upsertActivity({ + activity, + input: { + title: newTitle, + }, + }); + + if (!activityTitleHasBeenSet) { + setActivityTitleHasBeenSet(true); + } + }, 500); + + const setTitleDebounced = useDebouncedCallback((newTitle: string) => { + setActivityInStore((currentActivity) => { + return { + ...currentActivity, + id: activity.id, + title: newTitle, + __typename: activity.__typename, + }; + }); + + if (isNonEmptyString(newTitle) && !canCreateActivity) { + setCanCreateActivity(true); + } + + modifyRecordFromCache({ + recordId: activity.id, + fieldModifiers: { + title: () => { + return newTitle; + }, + }, + cache: cache, + objectMetadataItem: objectMetadataItemActivity, + }); + }, 500); + + const handleTitleChange = (newTitle: string) => { + setActivityTitle(newTitle); + + setTitleDebounced(newTitle); + + persistTitleDebounced(newTitle); + }; + + const handleActivityCompletionChange = (value: boolean) => { + upsertActivity({ + activity, + input: { + completedAt: value ? new Date().toISOString() : null, + }, + }); + }; + + const completed = isDefined(activity.completedAt); + + return ( + + {activity.type === 'Task' && ( + handleActivityCompletionChange(value)} + /> + )} + handleTitleChange(event.target.value)} + value={activityTitle} + completed={completed} + onBlur={handleBlur} + onFocus={handleFocus} + /> + + ); +}; diff --git a/packages/twenty-front/src/modules/activities/components/CustomResolverFetchMoreLoader.tsx b/packages/twenty-front/src/modules/activities/components/CustomResolverFetchMoreLoader.tsx index b10ff28025bf..fc3726822786 100644 --- a/packages/twenty-front/src/modules/activities/components/CustomResolverFetchMoreLoader.tsx +++ b/packages/twenty-front/src/modules/activities/components/CustomResolverFetchMoreLoader.tsx @@ -30,4 +30,4 @@ export const CustomResolverFetchMoreLoader = ({ {loading && Loading more...} ); -}; +}; \ No newline at end of file diff --git a/packages/twenty-front/src/modules/activities/contexts/TimelineActivityContext.ts b/packages/twenty-front/src/modules/activities/contexts/TimelineActivityContext.ts new file mode 100644 index 000000000000..cd50acc26319 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/contexts/TimelineActivityContext.ts @@ -0,0 +1,10 @@ +import { createContext } from 'react'; + +type TimelineActivityContextValue = { + labelIdentifierValue: string; +}; + +export const TimelineActivityContext = + createContext({ + labelIdentifierValue: '', + }); diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadPreview.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadPreview.tsx index e381b1bac358..a7953e4b0c3a 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadPreview.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadPreview.tsx @@ -85,7 +85,6 @@ type EmailThreadPreviewProps = { divider?: boolean; thread: TimelineThread; }; - export const EmailThreadPreview = ({ divider, thread, diff --git a/packages/twenty-front/src/modules/activities/formTemplate/components/formTemplate.tsx b/packages/twenty-front/src/modules/activities/formTemplate/components/formTemplate.tsx new file mode 100644 index 000000000000..914ee457e1db --- /dev/null +++ b/packages/twenty-front/src/modules/activities/formTemplate/components/formTemplate.tsx @@ -0,0 +1,140 @@ +import { useEffect, useState } from 'react'; +import styled from '@emotion/styled'; +import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { useLazyQuery } from '@apollo/client'; +import { GET_CAMPAIGN_LISTS } from '@/users/graphql/queries/getCampaignList'; +import { Form1 } from '~/pages/campaigns/Form1'; +import { Form2 } from '~/pages/campaigns/Form2'; +import { Form3 } from '~/pages/campaigns/Form3'; +import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay'; +import { TextDisplay } from '@/ui/field/display/components/TextDisplay'; +import { DateDisplay } from '@/ui/field/display/components/DateDisplay'; +import { GET_CAMPAIGN_TRIGGER } from '@/users/graphql/queries/getOneCampaignTrigger'; + +const StyledComboInputContainer = styled.div` + display: flex; + align-items: center; + align-self: center; + justify-content: start; + margin: ${({ theme }) => theme.spacing(10)}; +`; + +const StyledTitleBar = styled.div` + display: flex; + width: 100%; + flex-direction: row; +`; + +const StyledLabelContainer = styled.div` + color: ${({ theme }) => theme.font.color.tertiary}; + width: auto; + margin-right: ${({ theme }) => theme.spacing(4)}; +`; + +export const FormTemplate = ({ + targetableObject, +}: { + targetableObject: ActivityTargetableObject; +}) => { + let [selectedCampaign, { data: selectedCampaignData }] = + useLazyQuery(GET_CAMPAIGN_LISTS); + let [selectedCampaignTrigger, { data: selectedCampaignTriggerData }] = + useLazyQuery(GET_CAMPAIGN_TRIGGER, { + fetchPolicy: 'network-only', + }); + let campaignId = ''; + const [form, setForm] = useState(''); + const [formFetched, setFormFetched] = useState({ + name: '', + description: '', + createdAt: '', + status: '', + }); + + useEffect(() => { + const fetchFormDetails = async () => { + try { + if (targetableObject.targetObjectNameSingular === 'campaignTrigger') { + const data = await selectedCampaignTrigger({ + variables: { + objectRecordId: targetableObject.id, + }, + }); + + campaignId = data.data.campaignTrigger.campaignId; + } else if (targetableObject.targetObjectNameSingular === 'campaign') { + campaignId = targetableObject.id; + } + const data = await selectedCampaign({ + variables: { + filter: { + id: { eq: campaignId }, + }, + }, + }); + const formTemplateName = + data?.data?.campaigns?.edges[0]?.node?.formTemplate?.value; + setForm(formTemplateName); + setFormFetched({ + name: data?.data?.campaigns?.edges[0]?.node?.formTemplate?.name || '', + description: + data?.data?.campaigns?.edges[0]?.node?.formTemplate?.description || + '', + createdAt: + data?.data?.campaigns?.edges[0]?.node?.formTemplate?.createdAt || + '', + status: + data?.data?.campaigns?.edges[0]?.node?.formTemplate?.status || '', + }); + } catch (error) { + console.error('Error fetching form template:', error); + } + }; + + fetchFormDetails(); + }, [targetableObject.id, selectedCampaign]); + + const renderForm = () => { + switch (form) { + case 'CampaignForm': + return ; + case 'CampaignForm2': + return ; + case 'CampaignForm3': + return ; + default: + return null; + } + }; + return ( + <> + + + + Name: + + + + {/* + + Description: + + + */} + + + Created At: + + + + + + Status: + + + + + {renderForm()} + + ); +}; diff --git a/packages/twenty-front/src/modules/activities/messageTemplate/components/messageTemplate.tsx b/packages/twenty-front/src/modules/activities/messageTemplate/components/messageTemplate.tsx new file mode 100644 index 000000000000..b92085136b7b --- /dev/null +++ b/packages/twenty-front/src/modules/activities/messageTemplate/components/messageTemplate.tsx @@ -0,0 +1,123 @@ +import { useEffect, useState } from 'react'; +import styled from '@emotion/styled'; +import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { GET_CAMPAIGN_LISTS } from '@/users/graphql/queries/getCampaignList'; +import { useLazyQuery } from '@apollo/client'; +import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay'; + +import { TextDisplay } from '@/ui/field/display/components/TextDisplay'; +import { TextArea } from '@/ui/input/components/TextArea'; +import { GET_CAMPAIGN_TRIGGER } from '@/users/graphql/queries/getOneCampaignTrigger'; + +const StyledDetailContainer = styled.div` + display: flex; + margin: ${({ theme }) => theme.spacing(6)}; + // margin-left: ${({ theme }) => theme.spacing(6)}; +`; + +const StyledLabelContainer = styled.div` + color: ${({ theme }) => theme.font.color.tertiary}; + flex: 1; + margin-left: ${({ theme }) => theme.spacing(0)}; +`; + +const StyledValueContainer = styled.div` + flex: 5; + margin-left: ${({ theme }) => theme.spacing(0)}; +`; +const StyledAreaValueContainer = styled.div` + flex: 5; + margin-left: ${({ theme }) => theme.spacing(0)}; +`; + +export const MessageTemplate = ({ + targetableObject, +}: { + targetableObject: ActivityTargetableObject; +}) => { + const [messageTemplate, setMessageTemplate] = useState([]); + let [selectedCampaign] = useLazyQuery(GET_CAMPAIGN_LISTS); + let [selectedCampaignTrigger, { data: selectedCampaignTriggerData }] = + useLazyQuery(GET_CAMPAIGN_TRIGGER, { + fetchPolicy: 'network-only', + }); + let campaignId = ''; + const messageTeamplateDetails = async () => { + try { + if (targetableObject.targetObjectNameSingular === 'campaignTrigger') { + const data = await selectedCampaignTrigger({ + variables: { + objectRecordId: targetableObject.id, + }, + }); + + campaignId = data.data.campaignTrigger.campaignId; + } else if (targetableObject.targetObjectNameSingular === 'campaign') { + campaignId = targetableObject.id; + } + const data = await selectedCampaign({ + variables: { + filter: { + id: { eq: campaignId }, + }, + }, + }); + setMessageTemplate([ + data?.data?.campaigns?.edges[0]?.node?.messageTemplate, + ]); + console.log(data, '********'); + } catch (error) { + console.error('Error fetching message template:', error); + } + }; + + useEffect(() => { + messageTeamplateDetails(); + }, [targetableObject.id, selectedCampaign]); + + return ( + <> + + + Name + + + {!messageTemplate && } + + + + + + Channel Type + + + {!messageTemplate && } + + + + + + Status + + + {!messageTemplate && } + + + + + + Body + + + {!messageTemplate && } +