diff --git a/.dockerignore b/.dockerignore index 68e160bdc..5f801e041 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,8 +6,8 @@ node_modules/ apps/magic-link/node_modules apps/magic-link/dist -apps/client-ts/node_modules -apps/client-ts/.next +apps/webapp/node_modules +apps/webapp/.next packages/api/node_modules packages/api/dist \ No newline at end of file diff --git a/.env.example b/.env.example index f88aba857..7f63481fd 100644 --- a/.env.example +++ b/.env.example @@ -2,14 +2,18 @@ # API Backend # ================================================ ENV=dev -DISTRIBUTION=selfhosted # selfhosted or managed +DISTRIBUTION=selfhost # selfhost or managed PANORA_BASE_API_URL=http://localhost:3000 JWT_SECRET=secret_jwt ENCRYPT_CRYPTO_SECRET_KEY="0123456789abcdef0123456789abcdef" -#Managed only + +# Only used when DISTRIBUTION=managed SENTRY_DSN= SENTRY_ENABLED=FALSE +POSTHOG_HOST= +POSTHOG_KEY= +PH_TELEMETRY= #FALSE or TRUE # ================================================ # REDIS @@ -20,9 +24,14 @@ REDIS_PASS=A3vniod98Zbuvn9u5 #REDIS_TLS= - # ================================================ +# Tip: use mailtrap.io for local development +EMAIL_SENDING_ADDRESS=hello@panora.dev +SMTP_HOST= +SMTP_PORT= +SMTP_USER= +SMTP_PASSWORD= # ================================================ # Database # ================================================ @@ -30,12 +39,23 @@ POSTGRES_USER=my_user POSTGRES_DB=panora_db POSTGRES_HOST=postgres POSTGRES_PASSWORD=my_password + # Endpoint on which realtime webhooks are sent to -WEBHOOK_INGRESS=http://localhost:3000 +WEBHOOK_INGRESS=YOUR_ENDPOINT_URL_TO_RECEIVE_PANORA_WEBHOOKS + +# Mandatory only when DISTRIBUTION=selfhost +# 1. Execute cp ngrok.yml.example ngrok.yml +# 2. Uncomment ngrok service in docker-compose{.dev, .source}.yml +# Endpoint (an Ngrok tunnel domain) when you have to test your OAuth App and needs a redirectUri that redirects to your localhost +# (useful for contributors that might need to test their oAuth flow) +REDIRECT_TUNNEL_INGRESS=NGROK_DOMAIN + # Each Provider is of form PROVIDER_VERTICAL_SOFTWAREMODE_ATTRIBUTE +# check (https://docs.panora.dev/open-source/contributors) +# OAuth : ATTRIBUTE c [CLIENT_ID, CLIENT_SECRET] & {SUBDOMAIN} (some providers might need a subdomain) # ================================================ -# Integration Providers +# Credentials of Integration Providers # ================================================ # CRM # Hubspot @@ -50,16 +70,15 @@ PIPEDRIVE_CRM_CLOUD_CLIENT_SECRET= # Zendesk ZENDESK_CRM_CLOUD_CLIENT_ID= ZENDESK_CRM_CLOUD_CLIENT_SECRET= -# Freshsales -FRESHSALES_CRM_CLOUD_CLIENT_ID= -FRESHSALES_CRM_CLOUD_CLIENT_SECRET= # Attio ATTIO_CRM_CLOUD_CLIENT_ID= ATTIO_CRM_CLOUD_CLIENT_SECRET= - # Close CLOSE_CRM_CLOUD_CLIENT_ID= CLOSE_CRM_CLOUD_CLIENT_SECRET= +# Microsft Dynamics Sales +MICROSOFTDYNAMICSSALES_CRM_CLOUD_CLIENT_ID= +MICROSOFTDYNAMICSSALES_CRM_CLOUD_CLIENT_SECRET= # ================================================ # Ticketing @@ -68,23 +87,71 @@ CLOSE_CRM_CLOUD_CLIENT_SECRET= ZENDESK_TICKETING_CLOUD_CLIENT_ID= ZENDESK_TICKETING_CLOUD_CLIENT_SECRET= ZENDESK_TICKETING_CLOUD_SUBDOMAIN= +# Jira JIRA_TICKETING_CLOUD_CLIENT_ID= JIRA_TICKETING_CLOUD_CLIENT_SECRET= -GORGIAS_TICKETING_CLOUD_CLIENT_ID= -GORGIAS_TICKETING_CLOUD_CLIENT_SECRET= -GORGIAS_TICKETING_CLOUD_SUBDOMAIN= +# Front FRONT_TICKETING_CLOUD_CLIENT_ID= FRONT_TICKETING_CLOUD_CLIENT_SECRET= +# Gitlab GITLAB_TICKETING_CLOUD_CLIENT_ID= GITLAB_TICKETING_CLOUD_CLIENT_SECRET= +# Github +GITHUB_TICKETING_CLOUD_CLIENT_ID= +GITHUB_TICKETING_CLOUD_CLIENT_SECRET= +# Linear +LINEAR_TICKETING_CLOUD_CLIENT_ID= +LINEAR_TICKETING_CLOUD_CLIENT_SECRET= + +# ================================================ +# File Storage +# ================================================ +# Box +BOX_FILESTORAGE_CLOUD_CLIENT_ID= +BOX_FILESTORAGE_CLOUD_CLIENT_SECRET= + + +# ================================================ +# HRIS +# ================================================ +# Deel +DEEL_HRIS_CLOUD_CLIENT_ID= +DEEL_HRIS_CLOUD_CLIENT_SECRET= +# Sage +SAGE_HRIS_CLOUD_CLIENT_ID= +SAGE_HRIS_CLOUD_CLIENT_SECRET= +# Gusto +GUSTO_HRIS_CLOUD_CLIENT_ID= +GUSTO_HRIS_CLOUD_CLIENT_SECRET= + + +# ================================================ +# ECOMMERCE +# ================================================ +# Shopify +SHOPIFY_ECOMMERCE_CLOUD_CLIENT_ID= +SHOPIFY_ECOMMERCE_CLOUD_CLIENT_SECRET= +# Webflow +WEBFLOW_ECOMMERCE_CLOUD_CLIENT_ID= +WEBFLOW_ECOMMERCE_CLOUD_CLIENT_SECRET= +# Amazon +AMAZON_ECOMMERCE_CLOUD_CLIENT_ID= +AMAZON_ECOMMERCE_CLOUD_CLIENT_SECRET= +# Woo Commerce +WOOCOMMERCE_ECOMMERCE_CLOUD_CLIENT_ID= +WOOCOMMERCE_ECOMMERCE_CLOUD_CLIENT_SECRET= +# Squarespace +SQUARESPACE_ECOMMERCE_CLOUD_CLIENT_ID= +SQUARESPACE_ECOMMERCE_CLOUD_CLIENT_SECRET= + + + # ================================================ # Webapp settings # Must be set in the perspective of the end user browser NEXT_PUBLIC_BACKEND_DOMAIN=http://localhost:3000 # https://api.panora.dev/ NEXT_PUBLIC_MAGIC_LINK_DOMAIN=http://localhost:81 -NEXT_PUBLIC_POSTHOG_KEY= -NEXT_PUBLIC_POSTHOG_HOST= NEXT_PUBLIC_WEBAPP_DOMAIN="http://localhost" -# Disable Next.js spyware -NEXT_TELEMETRY_DISABLED=1 \ No newline at end of file +NEXT_PUBLIC_DISTRIBUTION="selfhost" # selfhost or managed + diff --git a/.github/workflows/docker.check-build.dashboard.selfhosted.yml b/.github/workflows/docker.check-build.dashboard.selfhosted.yml index 7c2f2f5b3..72a152c4c 100644 --- a/.github/workflows/docker.check-build.dashboard.selfhosted.yml +++ b/.github/workflows/docker.check-build.dashboard.selfhosted.yml @@ -23,14 +23,12 @@ jobs: with: platforms: linux/amd64,linux/arm64 context: . - file: ./apps/client-ts/Dockerfile + file: ./apps/webapp/Dockerfile push: false tags: panoradotdev/frontend-webapp:selfhosted build-args: | NEXT_PUBLIC_BACKEND_DOMAIN=${{ secrets.NEXT_PUBLIC_BACKEND_DOMAIN }} NEXT_PUBLIC_MAGIC_LINK_DOMAIN=${{ secrets.NEXT_PUBLIC_MAGIC_LINK_DOMAIN }} - NEXT_PUBLIC_STYTCH_PROJECT_ID=${{ secrets.NEXT_PUBLIC_STYTCH_PROJECT_ID }} - NEXT_PUBLIC_STYTCH_SECRET=${{ secrets.NEXT_PUBLIC_STYTCH_SECRET }} - NEXT_PUBLIC_STYTCH_PROJECT_ENV=${{ secrets.NEXT_PUBLIC_STYTCH_PROJECT_ENV }} - NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN=${{ secrets.NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN }} - NEXT_PUBLIC_DISTRIBUTION=${{ env.DISTRIBUTION }} \ No newline at end of file + NEXT_PUBLIC_DISTRIBUTION=${{ env.DISTRIBUTION }} + NEXT_PUBLIC_REDIRECT_WEBHOOK_INGRESS=${{ secrets.NEXT_PUBLIC_REDIRECT_WEBHOOK_INGRESS }} + NEXT_PUBLIC_WEBAPP_DOMAIN= ${{env.NEXT_PUBLIC_WEBAPP_DOMAIN}} \ No newline at end of file diff --git a/.github/workflows/docker.export.backend.selfhosted.yml b/.github/workflows/docker.export.backend.selfhosted.yml index 14bb2abcf..ae829a456 100644 --- a/.github/workflows/docker.export.backend.selfhosted.yml +++ b/.github/workflows/docker.export.backend.selfhosted.yml @@ -35,7 +35,7 @@ jobs: build-args: | PANORA_BASE_API_URL=${{ env.PANORA_BASE_API_URL }} DISTRIBUTION=${{ env.DISTRIBUTION }} - ENV=${{ ENV }} + ENV=${{ env.ENV }} DATABASE_URL=postgresql://${{env.POSTGRES_USER}}:${{secrets.POSTGRES_PASSWORD}}@${{env.POSTGRES_HOST}}:5432/${{env.POSTGRES_DB}}?ssl=false JWT_SECRET=${{ secrets.JWT_SECRET }} REDIS_HOST=redis @@ -50,3 +50,4 @@ jobs: ZENDESK_CLIENT_SECRET=${{ secrets.ZENDESK_CLIENT_SECRET }} ZENDESK_TICKETING_CLIENT_ID=${{ secrets.ZENDESK_TICKETING_CLIENT_ID }} ZENDESK_TICKETING_CLIENT_SECRET=${{ secrets.ZENDESK_TICKETING_CLIENT_SECRET }} + REDIRECT_TUNNEL_INGRESS=${{ env.REDIRECT_TUNNEL_INGRESS}} \ No newline at end of file diff --git a/.github/workflows/docker.export.frontend-dashboard.selfhosted.yml b/.github/workflows/docker.export.frontend-dashboard.selfhosted.yml index 4fd7ba4ec..5d559907c 100644 --- a/.github/workflows/docker.export.frontend-dashboard.selfhosted.yml +++ b/.github/workflows/docker.export.frontend-dashboard.selfhosted.yml @@ -29,14 +29,12 @@ jobs: with: platforms: linux/amd64,linux/arm64 context: . - file: ./apps/client-ts/Dockerfile + file: ./apps/webapp/Dockerfile push: true tags: panoradotdev/frontend-webapp:selfhosted build-args: | NEXT_PUBLIC_BACKEND_DOMAIN=${{ secrets.NEXT_PUBLIC_BACKEND_DOMAIN }} NEXT_PUBLIC_MAGIC_LINK_DOMAIN=${{ secrets.NEXT_PUBLIC_MAGIC_LINK_DOMAIN }} - NEXT_PUBLIC_STYTCH_PROJECT_ID=${{ secrets.NEXT_PUBLIC_STYTCH_PROJECT_ID }} - NEXT_PUBLIC_STYTCH_SECRET=${{ secrets.NEXT_PUBLIC_STYTCH_SECRET }} - NEXT_PUBLIC_STYTCH_PROJECT_ENV=${{ secrets.NEXT_PUBLIC_STYTCH_PROJECT_ENV }} - NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN=${{ secrets.NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN }} - NEXT_PUBLIC_DISTRIBUTION=${{ env.DISTRIBUTION }} \ No newline at end of file + NEXT_PUBLIC_DISTRIBUTION=${{ env.DISTRIBUTION }} + NEXT_PUBLIC_REDIRECT_WEBHOOK_INGRESS=${{ env.NEXT_PUBLIC_REDIRECT_WEBHOOK_INGRESS }} + NEXT_PUBLIC_WEBAPP_DOMAIN= ${{env.NEXT_PUBLIC_WEBAPP_DOMAIN}} \ No newline at end of file diff --git a/.github/workflows/liblab_update.yml b/.github/workflows/liblab_update.yml deleted file mode 100644 index e016be8e0..000000000 --- a/.github/workflows/liblab_update.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Latest liblab updates - -on: - workflow_dispatch: - schedule: - - cron: "0 11 * * *" # 11am UTC corresponds to 5am CST - -jobs: - generate-sdks-and-publish-prs: - name: Generate SDKs and publish PRs - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Generate SDKs and publish PRs - uses: liblaber/sdk-updates@v1.0.0 - with: - liblab_token: ${{ secrets.LIBLAB_TOKEN }} - liblab_github_token: ${{ secrets.LIBLAB_GITHUB_TOKEN }} diff --git a/.github/workflows/merge_code_samples.yaml b/.github/workflows/merge_code_samples.yaml new file mode 100644 index 000000000..709593272 --- /dev/null +++ b/.github/workflows/merge_code_samples.yaml @@ -0,0 +1,25 @@ +name: Merge Code Samples Into OpenAPI Spec +permissions: + checks: write + contents: write + pull-requests: write + statuses: write +"on": + workflow_dispatch: + inputs: + force: + description: Force generation of SDKs + type: boolean + default: false + schedule: + - cron: 0 0 * * * +jobs: + generate: + uses: speakeasy-api/sdk-generation-action/.github/workflows/workflow-executor.yaml@v15 + with: + force: ${{ github.event.inputs.force }} + mode: pr + speakeasy_version: latest + secrets: + github_access_token: ${{ secrets.GITHUB_TOKEN }} + speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }} diff --git a/.github/workflows/sdk-generation.yml b/.github/workflows/sdk-generation.yml deleted file mode 100644 index 765148736..000000000 --- a/.github/workflows/sdk-generation.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Generate TS SDK -permissions: - checks: write - contents: write - pull-requests: write - statuses: write -"on": - workflow_dispatch: - inputs: - force: - description: Force generation of SDKs - type: boolean - default: false - schedule: - - cron: 0 0 * * * -jobs: - generate: - uses: speakeasy-api/sdk-generation-action/.github/workflows/workflow-executor.yaml@v15 - with: - force: ${{ github.event.inputs.force }} - mode: pr - speakeasy_version: latest - secrets: - github_access_token: ${{ secrets.GH_TOKEN }} - speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }} diff --git a/.github/workflows/sdks.yaml b/.github/workflows/sdks.yaml deleted file mode 100644 index 1f6b84f36..000000000 --- a/.github/workflows/sdks.yaml +++ /dev/null @@ -1,43 +0,0 @@ -name: Generate SDKs using liblab - -on: - push: - paths: - - "packages/api/swagger/swagger-spec.json" - # Add any other triggers you want here, such as a push to the spec file - workflow_call: - workflow_dispatch: -jobs: - build-and-pr: - name: Generate SDKs and create PRs - runs-on: ubuntu-latest - if: github.run_number != 1 - env: - LIBLAB_CI: true - LIBLAB_TOKEN: ${{ secrets.LIBLAB_TOKEN }} - LIBLAB_GITHUB_TOKEN: ${{ secrets.LIBLAB_GITHUB_TOKEN }} - steps: - - name: Check if secrets are set - run: | - if [[ -n "${{ secrets.LIBLAB_TOKEN }}" && -n "${{ secrets.LIBLAB_GITHUB_TOKEN }}" ]]; then - echo "Secrets are set, proceeding with the workflow" - else - echo "Error: Secrets LIBLAB_TOKEN and LIBLAB_GITHUB_TOKEN are required, see https://developers.liblab.com/tutorials/integrate-with-github-actions/#create-tokens" - echo "::error::Secrets LIBLAB_TOKEN and LIBLAB_GITHUB_TOKEN are required, see https://developers.liblab.com/tutorials/integrate-with-github-actions/#create-tokens" - exit 1 - fi - - name: Checkout - uses: actions/checkout@v4 - - name: Setup Node.js environment - uses: actions/setup-node@v4 - with: - node-version: "20" - - name: Install liblab - run: npm install -g liblab - working-directory: packages/api - - name: Start Build - run: liblab build --yes - working-directory: packages/api - - name: Create PRs to GitHub repos - run: liblab pr - working-directory: packages/api diff --git a/.gitignore b/.gitignore index 2377557f2..dacccb54b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +ngrok.yml **/node_modules/ **/dist/ node_modules @@ -12,4 +13,5 @@ redis_data .pnpm-store/ .npmrc .vscode -ngrok.yml \ No newline at end of file +docs/objects +.magicodeignore \ No newline at end of file diff --git a/.speakeasy/workflow.lock b/.speakeasy/workflow.lock new file mode 100644 index 000000000..665bf259f --- /dev/null +++ b/.speakeasy/workflow.lock @@ -0,0 +1,26 @@ +speakeasyVersion: 1.374.2 +sources: + merge-code-samples-into-spec: + sourceNamespace: merge-code-samples-into-spec + sourceRevisionDigest: sha256:1bcf3a8cca852c571571fad60c90aad27624f86e4f2fb58e3777f0c3a6ec712a + sourceBlobDigest: sha256:ebdf0ba69a79a32d558c774cfe0f0c32d742319ed3c3d3d887bf14ac67f417d8 + tags: + - latest + - main +targets: {} +workflow: + workflowVersion: 1.0.0 + speakeasyVersion: latest + sources: + merge-code-samples-into-spec: + inputs: + - location: registry.speakeasyapi.dev/panora/panora/panora-open-api-swagger + overlays: + - location: registry.speakeasyapi.dev/panora/panora/code-samples-typescript-my-first-target:main + - location: registry.speakeasyapi.dev/panora/panora/code-samples-python:main + - location: registry.speakeasyapi.dev/panora/panora/go-sdk:main + - location: registry.speakeasyapi.dev/panora/panora/code-samples-ruby:main + output: packages/api/swagger/openapi-with-code-samples.yaml + registry: + location: registry.speakeasyapi.dev/panora/panora/merge-code-samples-into-spec + targets: {} diff --git a/.speakeasy/workflow.yaml b/.speakeasy/workflow.yaml index be9b4256c..ccbfb2253 100644 --- a/.speakeasy/workflow.yaml +++ b/.speakeasy/workflow.yaml @@ -1,11 +1,15 @@ workflowVersion: 1.0.0 +speakeasyVersion: latest sources: - OpenAPI-Github-Main: + merge-code-samples-into-spec: inputs: - - location: https://raw.githubusercontent.com/panoratech/Panora/main/packages/api/swagger/swagger-spec.json + - location: registry.speakeasyapi.dev/panora/panora/panora-open-api-swagger + overlays: + - location: registry.speakeasyapi.dev/panora/panora/code-samples-typescript-my-first-target:main + - location: registry.speakeasyapi.dev/panora/panora/code-samples-python:main + - location: registry.speakeasyapi.dev/panora/panora/go-sdk:main + - location: registry.speakeasyapi.dev/panora/panora/code-samples-ruby:main + output: packages/api/swagger/openapi-with-code-samples.yaml registry: - location: registry.speakeasyapi.dev/panora/panora/open-api-github-main -targets: - my-first-target: - target: typescript - source: OpenAPI-Github-Main + location: registry.speakeasyapi.dev/panora/panora/merge-code-samples-into-spec +targets: {} diff --git a/INTEGRATIONS.md b/INTEGRATIONS.md index ef32780ba..49879add2 100644 --- a/INTEGRATIONS.md +++ b/INTEGRATIONS.md @@ -197,6 +197,7 @@ export interface IContactMapper { unify( source: OriginalContactOutput | OriginalContactOutput[], + connectionId: string, customFieldMappings?: { slug: string; remote_id: string; @@ -217,6 +218,7 @@ export class My3rdPartyMapper implements IContactMapper { unify( source: 3rdPartyContactOutput | 3rdPartyContactOutput[], + connectionId: string, customFieldMappings?: { slug: string; remote_id: string; @@ -273,11 +275,7 @@ Don't forget to add your service you've defined at step 1 inside the module unde ```ts @Module({ - imports: [ - BullModule.registerQueue({ - name: 'webhookDelivery', - }), - ], + controllers: [ContactController], providers: [ ContactService, diff --git a/README.md b/README.md index d0a2356c6..60f91bc15 100644 --- a/README.md +++ b/README.md @@ -15,18 +15,35 @@

- Website 🌎 - Documentation 📖 - Status 🟢 + Website 🌎 - Documentation 📖 - Discord 👽

-### Have you met anyone who loves developing integrations? *No.* That’s why we designed an easy developer experience that you’ll enjoy +# 🕹️ Try -- **Simple developer experience:** easy to self-host, uses industry-standard data models, and is extensible -- **Builder-friendly terms:** Panora is open-source, and offers generous tips for contributors +- Prerequisite: You should have Git and Docker installed -### More than a devtool: Panora helps you put your product at the core of your customer's daily workflows + 1. Get the code -Your customers expect all of their tools to work well together. Panora avoids your team spending hundreds of hours building and maintaining integrations instead of your core product. +``` + git clone https://github.com/panoratech/Panora.git + ``` + + 2. Go to Panora folder + +``` + cd Panora && cp .env.example .env + ``` + + 3. Start + +``` + docker compose -f docker-compose.source.yml up + ``` + +Panora is now running! Follow our [Quickstart Guide](https://docs.panora.dev/quick-start) to start adding integrations to your product ! + +See also [our selfhost guide here !](https://docs.panora.dev/open-source/selfhost/self-host-guide) # ✨ Core Features @@ -41,50 +58,164 @@ Your customers expect all of their tools to work well together. Panora avoids yo Panora supports integration with the following objects across multiple platforms: -### CRM +[Here is an extensive list of all integrations !](https://docs.panora.dev/integrations-catalog) -| | Contacts | Deals | Notes | Engagements | Tasks | Users | Companies | -|-----------------------------------------------|:--------:|:-----:|:-----:|:-----------:|:-----:|:-----:|:---------:| -| Hubspot | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | -| Pipedrive | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | -| Zoho CRM | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | -| Zendesk Sell | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | -| Attio | ✔️ | | | | | | ✔️ | +### CRM Unified API -### Ticketing +| | Contacts | Deals | Notes | Engagements | Tasks | Users | Companies | Stage | +|-----------------------------------------------|:--------:|:-----:|:-----:|:-----------:|:-----:|:-----:|:---------:|:---------:| +| Hubspot | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | ✔️ | +| Pipedrive | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | |✔️ | +| Zoho CRM | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | | +| Zendesk Sell | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | ✔️| +| Attio | ✔️ | ✔️ | ✔️ | | ✔️ | ✔️ | ✔️ | ✔️ | +| Close | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ |✔️ | -| | Tickets | Comments | Users | Contacts | Accounts | Tags | Teams | Collections | +### Ticketing Unified API + +| | Tickets | Comments | Users | Contacts | Accounts | Tags | Teams | Collections | |-------------|:----------:|:-------:|:-------:|:------------:|:-------:|:-------:|:------:|:-------------:| | Zendesk | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | Front | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | Jira | ✔ | ✔ | ✔ | | | ✔ | ✔ | ✔ | -| Gorgias | ✔ | ✔ | ✔ | ✔ | | ✔ | ✔ | | +| Gitlab | ✔ | ✔ | ✔ | | | | | ✔| +| Github | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔| | + +### ATS Unified API (New!) + +| | Activities | Applications | Candidates | Departments | Interviews | Jobs | Offers | Offices | Scorecard | Users | Eeocs | Job Interview Stage | Tags | Reject Reasons | +|-------------|:----------:|:------------:|:----------:|:-----------:|:----------:|:----:|:------:|:-------:|:---------:|:-----:|:-------:|:-------:|:-------:|:-------:| +| Ashby | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | ✔ | | ✔| ✔| | + +### HRIS Unified API (New!) + +| | Bankinfos | Benefits | Companies | Dependents | Employee | Employee Payroll Runs | Employer Benefits | Employments | Groups | Locations | Paygroups | Payrollrun | Timeoff | Timeoff Balances | Timesheet Entries | +|-------------|:----------:|:------------:|:----------:|:-----------:|:----------:|:----:|:------:|:-------:|:---------:|:-----:|:-----:|:-----:|:-----:|:-----:| :-----:| +| Gusto | | ✔ | ✔ | | ✔ | | ✔ | ✔ | ✔ | ✔ | | | | | | + +### File Storage Unified API + +| | Drives | Files | Folders | Groups | Users | Permissions | Shared Links | +|-----------------------------------------------|:--------:|:-----:|:-----:|:-----------:|:-----:|:-----:|:---------:| +| Google Drive | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | | +| Box | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | +| Dropbox | | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | | +| OneDrive | ✔️ | ✔️ | ✔️| ✔️ | ✔️ | | | + +### Ecommerce Unified API + +| | Customers | Orders | Fulfillments | Fulfillment Orders | Products | +|-----------------------------------------------|:--------:|:-----:|:-----:|:-----------:|:-----:| +| Amazon | ✔️ | ✔️ | | | | +| Shopify | ✔️ | ✔️ | ✔️ | ✔️ | ✔️ | +| Squarespace | ✔️ | ✔️ | | | ✔️ | +| Woocommerce | ✔️ | ✔️ | | | ✔️ | Your favourite software is missing? [Ask the community to build a connector!](https://github.com/panoratech/Panora/issues/new) -# 🕹️ Try the Open-Source version +# 🚢 Roadmap -- Prerequisite: You should have Git and Docker installed +## 🧠 Retrieval Engine for RAG - 1. Get the code +- [ ] Access and manage data from any source, including documents, chunk & vectors +- [ ] Semantic, keyword and hybrid search against a vector database + +## 🪄 Integrations Coming Soon + +#### CRM + +- [x] Microsoft Dynamics 365 +- [x] Linear +- [x] Redtail CRM +- [x] Wealthbox +- [x] Leadsquared +- [ ] Salesforce +- [ ] Affinity CRM +- [ ] Odoo +- [ ] Intelliflow +- [ ] Xplan +- [ ] Plannr +- [ ] ACT! +- [ ] Jungo +- [ ] Surefire +- [ ] Velocity + +#### Ticketing + +- [ ] Service Now +- [ ] Wrike +- [ ] Dixa +- [ ] Service Now +- [ ] Asana +- [ ] Aha +- [ ] Clickup + +#### Accounting + +- [ ] Wave Financial +- [ ] Xero +- [ ] Quickbooks + +#### File Storage + +- [ ] Google Drive +- [ ] Dropbox +- [ ] Sharepoint +- [ ] One Drive + +#### Productivity -``` - git clone https://github.com/panoratech/Panora.git - ``` +- [ ] Slack +- [ ] Notion - 2. Go to Panora folder +#### HRIS -``` - cd Panora && cp .env.example .env - ``` +- [ ] Workday +- [ ] ADP Workforce +- [x] Sage +- [x] Deel +- [ ] BambooHR +- [ ] Rippling - 3. Start +#### Ecommerce -``` - docker compose up - ``` +- [ ] Ebay +- [ ] Faire +- [x] Webflow +- [ ] Mercado Libre +- [ ] Prestashop +- [ ] Magento +- [ ] BigCommerce + +#### ATS + +- [ ] Greenhouse +- [ ] Lever +- [ ] Avature + +#### Cybersecurity + +- [ ] Snyk +- [ ] Qualys +- [ ] Crowdstrike +- [ ] Semgrep +- [ ] Rapids7InsightVm +- [ ] Tenable +- [ ] SentinelOne +- [ ] Microsoft Defender + +#### Legacy Softwares + +- [ ] Netsuite (Accounting) +- [ ] SAP (ERP) +- [ ] Ariba +- [ ] Concur +- [ ] Magaya (TMS) +- [ ] Cargowise (TMS) + +# 👾 Join the community -Visit our [Quickstart Guide](https://docs.panora.dev/quick-start) to start adding integrations to your product +- [Join the Discord](https://discord.com/invite/PH5k7gGubt) # 🤔 Questions? Ask the core team diff --git a/apps/client-ts/Dockerfile b/apps/client-ts/Dockerfile deleted file mode 100644 index 9557e5761..000000000 --- a/apps/client-ts/Dockerfile +++ /dev/null @@ -1,126 +0,0 @@ -# run directly from the repo root directory -# docker build -f ./apps/client-ts/Dockerfile . -FROM node:20-alpine AS base -# ======================================================================= -# Turbo: Prepare a standalone workspace for docker -FROM base AS builder -RUN apk add --no-cache libc6-compat -RUN apk update - -# Set pnpm -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PATH" -RUN corepack enable - -WORKDIR /app -RUN pnpm add -g turbo@1.13.4 -COPY . . -RUN turbo prune client-ts --docker - -#check content -RUN ls -la ./out/full/apps/client-ts - -# ======================================================================= -# Install Deps and build project using PNPM -FROM base AS installer -RUN apk add --no-cache libc6-compat -RUN apk update -# Set pnpm -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PATH" - - -ARG NEXT_PUBLIC_STYTCH_PROJECT_ID -ARG NEXT_PUBLIC_STYTCH_SECRET -ARG NEXT_PUBLIC_STYTCH_PROJECT_ENV -ARG NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN -ARG NEXT_PUBLIC_DISTRIBUTION -ARG NEXT_PUBLIC_BACKEND_DOMAIN -ARG NEXT_PUBLIC_MAGIC_LINK_DOMAIN -ARG NEXT_PUBLIC_WEBAPP_DOMAIN - -ENV NEXT_PUBLIC_STYTCH_PROJECT_ID="$NEXT_PUBLIC_STYTCH_PROJECT_ID" -ENV NEXT_PUBLIC_STYTCH_SECRET="$NEXT_PUBLIC_STYTCH_SECRET" -ENV NEXT_PUBLIC_STYTCH_PROJECT_ENV="$NEXT_PUBLIC_STYTCH_PROJECT_ENV" -ENV NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN="$NEXT_PUBLIC_STYTCH_PUBLIC_TOKEN" - -ENV NEXT_PUBLIC_DISTRIBUTION="$NEXT_PUBLIC_DISTRIBUTION" -ENV NEXT_PUBLIC_BACKEND_DOMAIN="${NEXT_PUBLIC_BACKEND_DOMAIN}" -ENV NEXT_PUBLIC_MAGIC_LINK_DOMAIN="${NEXT_PUBLIC_MAGIC_LINK_DOMAIN}" -ENV NEXT_PUBLIC_WEBAPP_DOMAIN="${NEXT_PUBLIC_WEBAPP_DOMAIN}" - -RUN corepack enable - -WORKDIR /app - -# Tweak in case of symlink issue -#ARG node-linker="hoisted" -#ARG package-import-method="copy" -#ARG symlink="false" - -RUN ls -la - -# First install the dependencies (as they change less often) -COPY .gitignore .gitignore -COPY --from=builder /app/out/json/ . - -# 🔴🔴🔴 possible bug due to missing dependencies here, when using "standalone mode" -COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml - -# install dependencies -RUN pnpm install --shamefully-hoist - -# Build the project -COPY --from=builder ./app/out/full/ . -RUN pnpm run build - -# check content -#RUN ls -la /app/apps/client-ts/ -#RUN ls -la /app/apps/client-ts/.next/ - -CMD cd /app/apps/client-ts/ && pnpm run start - -#RUN cd /app/apps/client-ts && node .next/standalone/server.js -#RUN ls -la /app/apps/client-ts/.next/standalone - -# # Node.js server - serving dynamic content -# # # ======================================================================== -# FROM node:20-alpine AS runner -# WORKDIR /app - -# # set hostname to localhost -# ENV HOSTNAME "0.0.0.0" -# ENV PORT 3000 - -# # Import Standalone files -# COPY --from=installer ./app/apps/client-ts/public ./public -# COPY --from=installer ./app/apps/client-ts/.next/static ./.next/static -# COPY --from=installer ./app/apps/client-ts/.next/standalone ./ - -# RUN ls -a ./ - - -# # Expose port and run -# EXPOSE 3000 -# CMD ["node", "server.js"] - - -# # For serving static content - some nextjs features will be lost -# # ======================================================================== -# FROM nginx:stable-alpine as runner - -# #ARG VITE_BACKEND_DOMAIN -# #ARG VITE_FRONTEND_DOMAIN -# #ENV VITE_BACKEND_DOMAIN="$VITE_BACKEND_DOMAIN" -# #ENV VITE_FRONTEND_DOMAIN="$VITE_FRONTEND_DOMAIN" -# COPY --from=installer ./app/apps/client-ts/dist/standalone/ /usr/share/nginx/html -# RUN ls -l /usr/share/nginx/html/ - -# RUN echo "***********************" -# RUN cat /etc/nginx/conf.d/default.conf -# RUN echo "***********************" - -# COPY apps/client-ts/nginx.conf /etc/nginx/conf.d/default.conf - -# EXPOSE 80 -# CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/apps/client-ts/src/components/ApiKeys/columns.tsx b/apps/client-ts/src/components/ApiKeys/columns.tsx deleted file mode 100644 index 067bd7943..000000000 --- a/apps/client-ts/src/components/ApiKeys/columns.tsx +++ /dev/null @@ -1,98 +0,0 @@ -"use client" - -import { ColumnDef } from "@tanstack/react-table" -import { Badge } from "@/components/ui/badge" -import { ApiKey } from "./schema" -import { DataTableColumnHeader } from "../shared/data-table-column-header" -import { DataTableRowActions } from "../shared/data-table-row-actions" -import { PasswordInput } from "../ui/password-input" -import { useState } from "react" -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip" -import { Button } from "../ui/button" -import { toast } from "sonner" -import { Card } from "antd" - -export function useColumns() { - const [copiedState, setCopiedState] = useState<{ [key: string]: boolean }>({}); - - const handleCopy = (token: string) => { - navigator.clipboard.writeText(token); - setCopiedState((prevState) => ({ - ...prevState, - [token]: true, - })); - toast.success("Api key copied", { - action: { - label: "Close", - onClick: () => console.log("Close"), - }, - }) - setTimeout(() => { - setCopiedState((prevState) => ({ - ...prevState, - [token]: false, - })); - }, 2000); // Reset copied state after 2 seconds - }; - - return [ - { - accessorKey: "name", - header: ({ column }) => ( - - ), - cell: ({ row }) =>
{row.getValue("name")}
, - enableSorting: false, - enableHiding: false, - }, - { - accessorKey: "token", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const token: string = row.getValue("token"); - const copied = copiedState[token] || false; - - return ( -
-
- -
-
handleCopy(token)} - > - - - - - - -

Copy Key

-
-
-
- -
-
- ); - }, - }, - { - id: "actions", - cell: ({ row }) => , - }, - ] as ColumnDef[]; -} diff --git a/apps/client-ts/src/components/Configuration/Connector/ConnectorDisplay.tsx b/apps/client-ts/src/components/Configuration/Connector/ConnectorDisplay.tsx deleted file mode 100644 index 1ac4f283f..000000000 --- a/apps/client-ts/src/components/Configuration/Connector/ConnectorDisplay.tsx +++ /dev/null @@ -1,586 +0,0 @@ -import { zodResolver } from "@hookform/resolvers/zod" -import { useForm } from "react-hook-form" -import { Button } from "@/components/ui/button" -import { Label } from "@/components/ui/label" -import { Separator } from "@/components/ui/separator" -import { Switch } from "@/components/ui/switch" -import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form" -import { PasswordInput } from "@/components/ui/password-input" -import { z } from "zod" -import config from "@/lib/config" -import { AuthStrategy, providerToType, Provider, extractProvider, extractVertical, needsSubdomain } from "@panora/shared" -import { useEffect, useState } from "react" -import useProjectStore from "@/state/projectStore" -import { usePostHog } from 'posthog-js/react' -import { Input } from "@/components/ui/input" -import useConnectionStrategies from "@/hooks/get/useConnectionStrategies" -import { DataTableFacetedFilter } from "@/components/shared/data-table-faceted-filter" -import useCreateConnectionStrategy from "@/hooks/create/useCreateConnectionStrategy" -import useUpdateConnectionStrategy from "@/hooks/update/useUpdateConnectionStrategy" -import useConnectionStrategyAuthCredentials from "@/hooks/get/useConnectionStrategyAuthCredentials" -import { useQueryClient } from "@tanstack/react-query" -import { toast } from "sonner" - -interface ItemDisplayProps { - item?: Provider -} - -const formSchema = z.object({ - subdomain: z.string({ - required_error: "Please Enter a Subdomain", - }).optional(), - client_id : z.string({ - required_error: "Please Enter a Client ID", - }).optional(), - client_secret : z.string({ - required_error: "Please Enter a Client Secret", - }).optional(), - scope : z.string({ - required_error: "Please Enter a scope", - }).optional(), - api_key: z.string({ - required_error: "Please Enter a API Key", - }).optional(), - username: z.string({ - required_error: "Please Enter Username", - }).optional(), - secret: z.string({ - required_error: "Please Enter Secret", - }).optional(), -}) - -export function ConnectorDisplay({ item }: ItemDisplayProps) { - const [copied, setCopied] = useState(false); - const [switchEnabled, setSwitchEnabled] = useState(false); - const { idProject } = useProjectStore() - const { data: connectionStrategies, isLoading: isConnectionStrategiesLoading, error: isConnectionStategiesError } = useConnectionStrategies() - const { createCsPromise } = useCreateConnectionStrategy(); - const { updateCsPromise } = useUpdateConnectionStrategy() - const { mutateAsync: fetchCredentials, data: fetchedData } = useConnectionStrategyAuthCredentials(); - const queryClient = useQueryClient(); - - const posthog = usePostHog() - - const mappingConnectionStrategies = connectionStrategies?.filter((cs) => extractVertical(cs.type).toLowerCase() == item?.vertical && extractProvider(cs.type).toLowerCase() == item?.name) - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - subdomain: "", - client_id: "", - client_secret: "", - scope: "", - api_key: "", - username: "", - secret: "", - }, - }) - - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(`${config.API_URL}/connections/oauth/callback`) - setCopied(true); - toast.success("Redirect uri copied", { - action: { - label: "Close", - onClick: () => console.log("Close"), - }, - }) - setTimeout(() => setCopied(false), 2000); // Reset copied state after 2 seconds - } catch (err) { - console.error('Failed to copy: ', err); - } - }; - - function onSubmit(values: z.infer) { - const { client_id, client_secret, scope, api_key, secret, username, subdomain } = values; - const performUpdate = mappingConnectionStrategies && mappingConnectionStrategies.length > 0; - switch (item?.authStrategy) { - case AuthStrategy.oauth2: - const needs_subdomain = needsSubdomain(item.name.toLowerCase(), item.vertical!.toLowerCase()); - if (client_id === "" || client_secret === "" || scope === "") { - if (client_id === "") { - form.setError("client_id", { "message": "Please Enter Client ID" }); - } - if (client_secret === "") { - form.setError("client_secret", { "message": "Please Enter Client Secret" }); - } - if (scope === "") { - form.setError("scope", { "message": "Please Enter the scope" }); - } - break; - } - if(needs_subdomain && subdomain == ""){ - form.setError("subdomain", { "message": "Please Enter Subdomain" }); - } - let ATTRIBUTES = []; - let VALUES = []; - if(needs_subdomain){ - ATTRIBUTES = ["subdomain", "client_id", "client_secret", "scope"], - VALUES = [subdomain!, client_id!, client_secret!, scope!] - }else{ - ATTRIBUTES = ["client_id", "client_secret", "scope"], - VALUES = [client_id!, client_secret!, scope!] - } - if (performUpdate) { - const dataToUpdate = mappingConnectionStrategies[0]; - toast.promise( - updateCsPromise({ - id_cs: dataToUpdate.id_connection_strategy, - updateToggle: false, - status: dataToUpdate.status, - attributes: ATTRIBUTES, - values: VALUES - }), - { - loading: 'Loading...', - success: (data: any) => { - queryClient.setQueryData(['connection-strategies'], (oldQueryData = []) => { - return oldQueryData.map((CS) => CS.id_connection_strategy === data.id_connection_strategy ? data : CS) - }); - return ( -
- -
- Changes have been saved -
-
- ) - ; - }, - error: (err: any) => err.message || 'Error' - }); - posthog?.capture("Connection_strategy_OAuth2_updated", { - id_project: idProject, - mode: config.DISTRIBUTION - }); - } else { - toast.promise( - createCsPromise({ - type: providerToType(item?.name, item?.vertical!, AuthStrategy.oauth2), - attributes: ATTRIBUTES, - values: VALUES - }), - { - loading: 'Loading...', - success: (data: any) => { - queryClient.setQueryData(['connections-strategies'], (oldQueryData = []) => { - return [...oldQueryData, data]; - }); - return ( -
- -
- Changes have been saved -
-
- ) - ; - }, - error: (err: any) => err.message || 'Error' - }); - posthog?.capture("Connection_strategy_OAuth2_created", { - id_project: idProject, - mode: config.DISTRIBUTION - }); - } - form.reset(); - console.log(values) - break; - - case AuthStrategy.api_key: - if (values.api_key === "") { - form.setError("api_key", { "message": "Please Enter API Key" }); - break; - } - if (performUpdate) { - const dataToUpdate = mappingConnectionStrategies[0]; - toast.promise( - updateCsPromise({ - id_cs: dataToUpdate.id_connection_strategy, - updateToggle: false, - status: dataToUpdate.status, - attributes: ["api_key"], - values: [api_key!] - }), - { - loading: 'Loading...', - success: (data: any) => { - queryClient.setQueryData(['connection-strategies'], (oldQueryData = []) => { - return oldQueryData.map((CS) => CS.id_connection_strategy === data.id_connection_strategy ? data : CS) - }); - return ( -
- -
- Changes have been saved -
-
- ) - ; - }, - error: (err: any) => err.message || 'Error' - }); - posthog?.capture("Connection_strategy_API_KEY_updated", { - id_project: idProject, - mode: config.DISTRIBUTION - }); - } else { - toast.promise( - createCsPromise({ - type: providerToType(item?.name, item?.vertical!, AuthStrategy.api_key), - attributes: ["api_key"], - values: [api_key!] - }), - { - loading: 'Loading...', - success: (data: any) => { - queryClient.setQueryData(['connections-strategies'], (oldQueryData = []) => { - return [...oldQueryData, data]; - }); - return ( -
- -
- Changes have been saved -
-
- ) - ; - }, - error: (err: any) => err.message || 'Error' - }); - posthog?.capture("Connection_strategy_API_KEY_created", { - id_project: idProject, - mode: config.DISTRIBUTION - }); - } - form.reset(); - console.log(values) - break; - - case AuthStrategy.basic: - if (values.username === "" || values.secret === "") { - if (values.username === "") { - form.setError("username", { "message": "Please Enter Username" }); - } - if (values.secret === "") { - form.setError("secret", { "message": "Please Enter Secret" }); - } - break; - } - if (performUpdate) { - const dataToUpdate = mappingConnectionStrategies[0]; - toast.promise( - updateCsPromise({ - id_cs: dataToUpdate.id_connection_strategy, - updateToggle: false, - status: dataToUpdate.status, - attributes: ["username", "secret"], - values: [username!, secret!] - }), - { - loading: 'Loading...', - success: (data: any) => { - queryClient.setQueryData(['connection-strategies'], (oldQueryData = []) => { - return oldQueryData.map((CS) => CS.id_connection_strategy === data.id_connection_strategy ? data : CS) - }); - return ( -
- -
- Changes have been saved -
-
- ) - ; - }, - error: (err: any) => err.message || 'Error' - }); - posthog?.capture("Connection_strategy_BASIC_AUTH_updated", { - id_project: idProject, - mode: config.DISTRIBUTION - }); - - } else { - toast.promise( - createCsPromise({ - type: providerToType(item?.name, item?.vertical!, AuthStrategy.basic), - attributes: ["username", "secret"], - values: [username!, secret!] - }), - { - loading: 'Loading...', - success: (data: any) => { - queryClient.setQueryData(['connections-strategies'], (oldQueryData = []) => { - return [...oldQueryData, data]; - }); - return ( -
- -
- Changes have been saved -
-
- ) - ; - }, - error: (err: any) => err.message || 'Error' - }); - posthog?.capture("Connection_strategy_BASIC_AUTH_created", { - id_project: idProject, - mode: config.DISTRIBUTION - }); - } - form.reset(); - console.log(values) - break; - } - } - - useEffect(() => { - if (mappingConnectionStrategies && mappingConnectionStrategies.length > 0) { - fetchCredentials({ - type: mappingConnectionStrategies[0].type, - attributes: item?.authStrategy === AuthStrategy.oauth2 ? needsSubdomain(item.name.toLowerCase(), item.vertical!.toLowerCase()) ? ["subdomain", "client_id", "client_secret", "scope"] : ["client_id", "client_secret", "scope"] - : item?.authStrategy === AuthStrategy.api_key ? ["api_key"] : ["username", "secret"] - }, { - onSuccess(data) { - if (item?.authStrategy === AuthStrategy.oauth2) { - let i = 0; - if(needsSubdomain(item.name.toLowerCase(), item.vertical!.toLowerCase())){ - form.setValue("subdomain", data[i]); - i = 1; - } - form.setValue("client_id", data[i]); - form.setValue("client_secret", data[i + 1]); - form.setValue("scope", data[i + 2]); - } - if (item?.authStrategy === AuthStrategy.api_key) { - form.setValue("api_key", data[0]); - } - if (item?.authStrategy === AuthStrategy.basic) { - form.setValue("username", data[0]); - form.setValue("secret", data[1]); - } - setSwitchEnabled(mappingConnectionStrategies[0].status === true); - } - }); - } else { - form.reset(); - setSwitchEnabled(false); - } - }, [connectionStrategies, item]); - - const handleSwitchChange = (enabled: boolean) => { - if (mappingConnectionStrategies && mappingConnectionStrategies.length > 0) { - const dataToUpdate = mappingConnectionStrategies[0]; - toast.promise( - updateCsPromise({ - id_cs: dataToUpdate.id_connection_strategy, - updateToggle: true - }), - { - loading: 'Loading...', - success: (data: any) => { - queryClient.setQueryData(['connection-strategies'], (oldQueryData = []) => { - return oldQueryData.map((CS) => CS.id_connection_strategy === data.id_connection_strategy ? data : CS) - }); - return ( -
- -
- Changes have been saved -
-
- ) - ; - }, - error: (err: any) => err.message || 'Error' - }); - - setSwitchEnabled(enabled); - } - }; - - return ( -
- - {item ? ( -
-
-
- -
-
{`${item.name.substring(0, 1).toUpperCase()}${item.name.substring(1)}`}
-
{item.description}
- {mappingConnectionStrategies && mappingConnectionStrategies.length > 0 && ( -
- -
- )} -
-
-
- -
-
- - { item.authStrategy == AuthStrategy.oauth2 && - <> - { needsSubdomain(item.name.toLowerCase(), item.vertical!.toLowerCase()) && -
- ( - - Subdomain - - - - - - )} - /> -
- } -
- ( - - Client ID - - - - - - )} - /> -
-
- ( - - Client Secret - - - - - - )} - /> -
-
- ( - - Scopes - - - - - - )} - /> -
-
- Redirect URI -
- - -
-
- - } - { - item.authStrategy == AuthStrategy.api_key && - <> -
- ( - - API Key - - - - - - )} - /> -
- - } - { - item.authStrategy == AuthStrategy.basic && - <> -
- ( - - Username - - - - - - )} - /> -
-
- ( - - Secret - - - - - - )} - /> -
- - } - -
- -
- -
- ) : ( -
- No connector selected -
- )} -
- ) -} diff --git a/apps/client-ts/src/components/Connection/columns.tsx b/apps/client-ts/src/components/Connection/columns.tsx deleted file mode 100644 index ceb3c965f..000000000 --- a/apps/client-ts/src/components/Connection/columns.tsx +++ /dev/null @@ -1,155 +0,0 @@ -"use client" - -import { ColumnDef } from "@tanstack/react-table" -import { Badge } from "@/components/ui/badge" -import { Connection } from "./schema" -import { DataTableColumnHeader } from "./../shared/data-table-column-header" -import React from "react" -import { ClipboardIcon } from '@radix-ui/react-icons' -import { toast } from "sonner" -import { getLogoURL } from "@panora/shared" -import { formatISODate, truncateMiddle } from "@/lib/utils" -import { Button } from "../ui/button" - -const connectionTokenComponent = ({row}:{row:any}) => { - const handleCopy = async () => { - try { - await navigator.clipboard.writeText(row.getValue("connectionToken")); - toast.success("Connection token copied", { - action: { - label: "Close", - onClick: () => console.log("Close"), - }, - }) - } catch (err) { - console.error('Failed to copy: ', err); - } - }; - - return ( -
- {truncateMiddle(row.getValue("connectionToken"),6)} - - -
- ) -} - -export const columns: ColumnDef[] = [ - { - accessorKey: "app", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const provider = (row.getValue("app") as string).toLowerCase(); - return ( -
- - - {provider} - -
- ) - }, - }, - { - accessorKey: "category", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - return ( -
- {/*status.icon && ( - - )*/} - {row.getValue("category")} -
- ) - }, - filterFn: (row, id, value) => { - return value.includes(row.getValue(id)) - }, - }, - { - accessorKey: "vertical", - header: ({ column }) => ( - - ), - cell: ({ row }) =>
{row.getValue("vertical") as string}
, - enableSorting: false, - enableHiding: false, - }, - { - accessorKey: "status", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - /*const direction = priorities.find( - (direction) => direction.value === row.getValue("status") - ) - - if (!direction) { - return null - }*/ - - return ( -
- {row.getValue("status")} -
- ) - }, - filterFn: (row, id, value) => { - return value.includes(row.getValue(id)) - }, - }, - { - accessorKey: "linkedUser", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - /*const status = statuses.find( - (status) => status.value === row.getValue("linkedUser") - ) - - if (!status) { - return null - }*/ - - return ( -
- {truncateMiddle(row.getValue("linkedUser"), 10)} -
- ) - }, - filterFn: (row, id, value) => { - return value.includes(row.getValue(id)) - }, - }, - { - accessorKey: "date", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - //const label = labels.find((label) => label.value === row.original.date) - - return ( -
- {formatISODate(row.getValue("date"))} -
- ) - }, - }, - { - accessorKey: "connectionToken", - header: ({ column }) => ( - - ), - cell: connectionTokenComponent - } -] \ No newline at end of file diff --git a/apps/client-ts/src/components/Nav/main-nav.tsx b/apps/client-ts/src/components/Nav/main-nav.tsx deleted file mode 100644 index 73bb49dff..000000000 --- a/apps/client-ts/src/components/Nav/main-nav.tsx +++ /dev/null @@ -1,71 +0,0 @@ -'use client' - -import { usePathname } from "next/navigation"; -import { MouseEvent, useEffect, useState } from "react"; - -export function MainNav({ - onLinkClick, - className, - ...props -}: { - onLinkClick: (name: string) => void; - className: string; -}) { - const [selectedItem, setSelectedItem] = useState(""); - const pathname = usePathname(); - - useEffect(() => { - setSelectedItem(pathname.substring(1)) - }, [pathname]) - - const navItemClassName = (itemName: string) => - `group flex items-center rounded-md px-2 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground cursor-pointer ${ - selectedItem === itemName ? 'bg-accent' : 'transparent' - } transition-colors`; - - function click(e: MouseEvent, name: string) { - e.preventDefault(); - onLinkClick(name); - } - - return ( - - ); -} \ No newline at end of file diff --git a/apps/client-ts/src/hooks/create/useCreateProfile.tsx b/apps/client-ts/src/hooks/create/useCreateProfile.tsx deleted file mode 100644 index 549539db9..000000000 --- a/apps/client-ts/src/hooks/create/useCreateProfile.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import config from '@/lib/config'; -import { useMutation } from '@tanstack/react-query'; - -interface IProfileDto { - first_name: string; - last_name: string; - email: string; - stytch_id_user: string; - strategy: string; - id_organization?: string -} - -const useCreateProfile = () => { - const add = async (data: IProfileDto) => { - const response = await fetch(`${config.API_URL}/auth/users/create`, { - method: 'POST', - body: JSON.stringify(data), - headers: { - 'Content-Type': 'application/json', - }, - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error(errorData.message || "Unknown error occurred"); - } - - return response.json(); - }; - const createProfilePromise = (data: IProfileDto) => { - return new Promise(async (resolve, reject) => { - try { - const result = await add(data); - resolve(result); - - } catch (error) { - reject(error); - } - }); - }; - return { - mutationFn: useMutation({ - mutationFn: add, - }), - createProfilePromise - /* - queryClient.invalidateQueries({ - queryKey: ['profiles'], - refetchType: 'active', - }) - queryClient.setQueryData(['profiles'], (oldQueryData = []) => { - return [...oldQueryData, data]; - }); - */ - }; -}; - -export default useCreateProfile; diff --git a/apps/embedded-catalog/react/CHANGELOG.md b/apps/embedded-catalog/react/CHANGELOG.md index 4a7ddef59..59b27f7af 100644 --- a/apps/embedded-catalog/react/CHANGELOG.md +++ b/apps/embedded-catalog/react/CHANGELOG.md @@ -1,5 +1,22 @@ # @panora/embedded-card-react +## 1.3.1 + +### Patch Changes + +- ded2580: Readme patch + +## 1.3.0 + +### Minor Changes + +- 773c515: update minor for npm packages + +### Patch Changes + +- Updated dependencies [773c515] + - @panora/shared@1.4.0 + ## 1.2.1 ### Patch Changes diff --git a/apps/embedded-catalog/react/README.md b/apps/embedded-catalog/react/README.md index c1e8f2e20..1940a6e64 100644 --- a/apps/embedded-catalog/react/README.md +++ b/apps/embedded-catalog/react/README.md @@ -21,36 +21,51 @@ or yarn add @panora/embedded-card-react ``` -## Import the component +## Import the components -```bash -# Import the css file +```ts import "@panora/embedded-card-react/dist/index.css"; -import PanoraProviderCard from "@panora/embedded-card-react"; +import { PanoraDynamicCatalogCard, PanoraProviderCard } from '@panora/embedded-card-react'; ``` ## Use the component - The `optionalApiUrl` is an optional prop to use the component with the self-hosted version of Panora. -```bash +```ts + + ``` ```ts -These are the types needed for the component. +These are the types needed for the components. + +The `` takes this props type: interface ProviderCardProp { name: string; projectId: string; - returnUrl: string; - linkedUserIdOrRemoteUserInfo: string; + linkedUserId: string; +} + +The `` takes this props type: + +interface DynamicCardProp { + projectId: string; + linkedUserId: string; + category?: ConnectorCategory; + optionalApiUrl?: string, } ``` diff --git a/apps/embedded-catalog/react/package.json b/apps/embedded-catalog/react/package.json index efd8bdcdf..7d1d95246 100644 --- a/apps/embedded-catalog/react/package.json +++ b/apps/embedded-catalog/react/package.json @@ -1,6 +1,6 @@ { "name": "@panora/embedded-card-react", - "version": "1.2.1", + "version": "1.3.1", "description": "", "main": "dist/index.js", "scripts": { @@ -23,10 +23,15 @@ "dependencies": { "@panora/shared": "workspace:^", "lucide-react": "^0.344.0", + "@radix-ui/react-label": "^2.0.2", + "react-hook-form": "^7.51.2", + "@hookform/resolvers": "^3.3.4", + "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-slot": "^1.0.2", "@tanstack/react-query": "^5.12.2", "class-variance-authority": "^0.7.0", + "zod": "^3.22.4", "clsx": "^2.1.0", "react-loader-spinner": "^5.4.5", "tailwind-merge": "^2.2.1" diff --git a/apps/embedded-catalog/react/src/components/Modal.tsx b/apps/embedded-catalog/react/src/components/Modal.tsx index 7b104bc6f..d8104d3d9 100644 --- a/apps/embedded-catalog/react/src/components/Modal.tsx +++ b/apps/embedded-catalog/react/src/components/Modal.tsx @@ -1,4 +1,5 @@ import React from 'react' +import {X} from 'lucide-react' const Modal = ({open,setOpen,children} : {open:boolean,setOpen: React.Dispatch>,children: React.ReactNode}) => { return ( @@ -17,12 +18,12 @@ const Modal = ({open,setOpen,children} : {open:boolean,setOpen: React.Dispatch - {/* */} + {children} diff --git a/apps/embedded-catalog/react/src/components/PanoraDynamicCatalog.tsx b/apps/embedded-catalog/react/src/components/PanoraDynamicCatalog.tsx index 463c7a153..f8208cc8e 100644 --- a/apps/embedded-catalog/react/src/components/PanoraDynamicCatalog.tsx +++ b/apps/embedded-catalog/react/src/components/PanoraDynamicCatalog.tsx @@ -1,5 +1,5 @@ import {useState,useEffect} from 'react' -import {providersArray, ConnectorCategory, categoryFromSlug, Provider,CONNECTORS_METADATA} from '@panora/shared'; +import {providersArray, ConnectorCategory, categoryFromSlug, Provider,CONNECTORS_METADATA, AuthStrategy} from '@panora/shared'; import useOAuth from '@/hooks/useOAuth'; import useProjectConnectors from '@/hooks/queries/useProjectConnectors'; import { Card } from './ui/card'; @@ -8,12 +8,41 @@ import { ArrowRightIcon } from '@radix-ui/react-icons'; import {ArrowLeftRight} from 'lucide-react' import Modal from './Modal'; import config from '@/helpers/config'; +import { useForm } from "react-hook-form"; +import * as z from "zod"; +import { Input } from "@/components/ui/input"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import useCreateApiKeyConnection from '@/hooks/queries/useCreateApiKeyConnection'; +import { LoadingSpinner } from './ui/loading-spinner'; +import { Label } from './ui/label'; + + export interface DynamicCardProp { projectId: string; linkedUserId: string; category?: ConnectorCategory; optionalApiUrl?: string, } +interface IApiKeyFormData { + apikey: string, + [key : string]: string +} + + +// const formSchema = z.object({ +// apiKey: z.string().min(2, { +// message: "Api Key must be at least 2 characters.", +// }) +// }) const DynamicCatalog = ({projectId,linkedUserId, category, optionalApiUrl} : DynamicCardProp) => { @@ -22,21 +51,36 @@ const DynamicCatalog = ({projectId,linkedUserId, category, optionalApiUrl} : Dyn const [selectedProvider, setSelectedProvider] = useState<{ provider: string; category: string; - }>(); + }>({ + provider: '', + category: '' + }); - const [loading, setLoading] = useState<{ - status: boolean; provider: string - }>({status: false, provider: ''}); + const [loading, setLoading] = useState(false); - const [error,setError] = useState(false); - const [startFlow, setStartFlow] = useState(false); + const [errorResponse,setErrorResponse] = useState<{ + errorPresent: boolean; errorMessage : string + }>({errorPresent:false,errorMessage:''}); + const [startFlow, setStartFlow] = useState(false); const [openSuccessDialog,setOpenSuccessDialog] = useState(false); + const [openApiKeyDialog,setOpenApiKeyDialog] = useState(false); const [currentProviderLogoURL,setCurrentProviderLogoURL] = useState('') const [currentProvider,setCurrentProvider] = useState('') const returnUrlWithWindow = (typeof window !== 'undefined') ? window.location.href : ''; + const {mutate : createApiKeyConnection} = useCreateApiKeyConnection(); + + + // const form = useForm>({ + // resolver: zodResolver(formSchema), + // defaultValues: { + // apiKey: "", + // }, + // }) + const {register,formState: {errors},handleSubmit,reset} = useForm(); + const [data, setData] = useState([]); @@ -47,7 +91,7 @@ const DynamicCatalog = ({projectId,linkedUserId, category, optionalApiUrl} : Dyn projectId: projectId, linkedUserId: linkedUserId, optionalApiUrl: optionalApiUrl, - onSuccess: () => { + onSuccess: () => { console.log('OAuth successful'); setOpenSuccessDialog(true); }, @@ -56,25 +100,26 @@ const DynamicCatalog = ({projectId,linkedUserId, category, optionalApiUrl} : Dyn const {data: connectorsForProject} = useProjectConnectors(projectId,optionalApiUrl ? optionalApiUrl : config.API_URL!); const onWindowClose = () => { - setSelectedProvider({ - provider: '', - category: '' - }); - setLoading({ - status: false, - provider: '' - }) + // setSelectedProvider({ + // provider: '', + // category: '' + // }); + setLoading(false) setStartFlow(false); } useEffect(() => { if (startFlow && isReady) { - open(onWindowClose); - } else if (startFlow && !isReady) { - setLoading({ - status: false, - provider: '' + setErrorResponse({errorPresent:false,errorMessage:''}); + + open(onWindowClose) + .catch((error : Error) => { + setLoading(false); + setStartFlow(false); + setErrorResponse({errorPresent:true,errorMessage:error.message}) }); + } else if (startFlow && !isReady) { + setLoading(false); } }, [startFlow, isReady]); @@ -102,8 +147,17 @@ const DynamicCatalog = ({projectId,linkedUserId, category, optionalApiUrl} : Dyn const logoPath = CONNECTORS_METADATA[category.toLowerCase()][walletName.toLowerCase()].logoPath; setCurrentProviderLogoURL(logoPath) setCurrentProvider(walletName.toLowerCase()) - setLoading({status: true, provider: selectedProvider?.provider!}); - setStartFlow(true); + if(CONNECTORS_METADATA[category.toLowerCase()][walletName.toLowerCase()].authStrategy.strategy===AuthStrategy.api_key) + { + setOpenApiKeyDialog(true); + } + else + { + setLoading(true); + setStartFlow(true); + } + + } function transformConnectorsStatus(connectors : {[key: string]: boolean}): { connector_name: string;category: string; status: string }[] { @@ -114,13 +168,55 @@ const DynamicCatalog = ({projectId,linkedUserId, category, optionalApiUrl} : Dyn return [{ connector_name: connector_name, category: category, - status: String(value) + status: value === null ? "true" : String(value) }]; } return []; }); } + const onCloseApiKeyDialog = (dialogState : boolean) => { + setOpenApiKeyDialog(dialogState); + reset(); + } + + const onApiKeySubmit = (values: IApiKeyFormData) => { + setErrorResponse({errorPresent:false,errorMessage:''}); + onCloseApiKeyDialog(false); + setLoading(true); + + // Creating API Key Connection + createApiKeyConnection({ + query : { + linkedUserId: linkedUserId, + projectId: projectId, + providerName: selectedProvider?.provider!, + vertical: selectedProvider?.category! + }, + data: values, + api_url: optionalApiUrl ?? config.API_URL! + }, + { + onSuccess: () => { + // setSelectedProvider({ + // provider: '', + // category: '' + // }); + + setLoading(false); + setOpenSuccessDialog(true); + }, + onError: (error) => { + setErrorResponse({errorPresent:true,errorMessage: error.message}); + setLoading(false); + // setSelectedProvider({ + // provider: '', + // category: '' + // }); + } + }) + } + return (
@@ -128,7 +224,8 @@ const DynamicCatalog = ({projectId,linkedUserId, category, optionalApiUrl} : Dyn return (
@@ -140,10 +237,21 @@ const DynamicCatalog = ({projectId,linkedUserId, category, optionalApiUrl} : Dyn {item.description!.substring(0, 300)}
- + ) + : + ( + + )}
+ + {item.name.toLowerCase()===selectedProvider?.provider && item.vertical?.toLowerCase()===selectedProvider?.category && errorResponse.errorPresent ?

{errorResponse.errorMessage}

: (<>)}
@@ -153,6 +261,65 @@ const DynamicCatalog = ({projectId,linkedUserId, category, optionalApiUrl} : Dyn )}) } + {/* Dialog for apikey input */} + + + + Enter a API key + + {/*
*/} + +
+
+ + +
{errors.apikey && (

{errors.apikey.message}

)}
+ + + {/*
*/} + {selectedProvider.provider!=='' && selectedProvider.category!=='' && CONNECTORS_METADATA[selectedProvider.category][selectedProvider.provider].authStrategy.properties?.map((fieldName : string) => + ( + <> + + + {errors[fieldName] && (

{errors[fieldName]?.message}

)} + + ))} +
+
+ + + + + + {/* */} + + + {/* OAuth Successful Modal */} diff --git a/apps/embedded-catalog/react/src/components/PanoraIntegrationCard.tsx b/apps/embedded-catalog/react/src/components/PanoraIntegrationCard.tsx index 92e2d4822..ad00f22bd 100644 --- a/apps/embedded-catalog/react/src/components/PanoraIntegrationCard.tsx +++ b/apps/embedded-catalog/react/src/components/PanoraIntegrationCard.tsx @@ -1,12 +1,28 @@ import useOAuth from '@/hooks/useOAuth'; import { useEffect, useState } from 'react'; -import { CONNECTORS_METADATA, ConnectorCategory } from '@panora/shared'; +import { AuthStrategy, CONNECTORS_METADATA, ConnectorCategory } from '@panora/shared'; import { Button } from './ui/button2'; import { Card } from './ui/card'; import { ArrowRightIcon } from '@radix-ui/react-icons'; import {ArrowLeftRight} from 'lucide-react' import Modal from './Modal'; - +import { Input } from "@/components/ui/input"; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import * as z from "zod"; +import config from '@/helpers/config'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import useCreateApiKeyConnection from '@/hooks/queries/useCreateApiKeyConnection'; +import { LoadingSpinner } from './ui/loading-spinner'; +import { Label } from './ui/label'; export interface ProviderCardProp { @@ -17,14 +33,41 @@ export interface ProviderCardProp { optionalApiUrl?: string, } +// const formSchema = z.object({ +// apiKey: z.string().min(2, { +// message: "Api Key must be at least 2 characters.", +// }) +// }) + +interface IApiKeyFormData { + apikey: string, + [key : string]: string +} + const PanoraIntegrationCard = ({name, category, projectId, linkedUserId, optionalApiUrl}: ProviderCardProp) => { - const [loading, setLoading] = useState(false); + const [loading, setLoading] = useState(false); const [openSuccessDialog,setOpenSuccessDialog] = useState(false); - const [startFlow, setStartFlow] = useState(false); + const [startFlow, setStartFlow] = useState(false); + const [openApiKeyDialog,setOpenApiKeyDialog] = useState(false); + const [errorResponse,setErrorResponse] = useState<{ + errorPresent: boolean; errorMessage : string + }>({errorPresent:false,errorMessage:''}); + + const {mutate : createApiKeyConnection} = useCreateApiKeyConnection(); + const returnUrlWithWindow = (typeof window !== 'undefined') ? window.location.href : ''; + // const form = useForm>({ + // resolver: zodResolver(formSchema), + // defaultValues: { + // apiKey: "", + // }, + // }) + const {register,formState: {errors},handleSubmit,reset} = useForm(); + + const { open, isReady } = useOAuth({ providerName: name.toLowerCase(), @@ -49,20 +92,67 @@ const PanoraIntegrationCard = ({name, category, projectId, linkedUserId, optiona useEffect(() => { if (startFlow && isReady) { - open(onWindowClose); - } else if (startFlow && !isReady) { + setErrorResponse({errorPresent:false,errorMessage:''}) + open(onWindowClose) + .catch((error : Error) => { + setLoading(false); + setStartFlow(false); + setErrorResponse({errorPresent:true,errorMessage:error.message}) + }); + } + else if (startFlow && !isReady) { setLoading(false); } }, [startFlow, isReady]); const handleStartFlow = () => { + if(CONNECTORS_METADATA[category.toLowerCase()][name.toLowerCase()].authStrategy.strategy===AuthStrategy.api_key) + { + setOpenApiKeyDialog(true); + } + else + { + setLoading(true); + setStartFlow(true); + } + + } + + const onCloseApiKeyDialog = (dialogState : boolean) => { + setOpenApiKeyDialog(dialogState); + reset(); + } + + const onApiKeySubmit = (values: IApiKeyFormData) => { + setErrorResponse({errorPresent:false,errorMessage:''}); + onCloseApiKeyDialog(false); setLoading(true); - setStartFlow(true); + + // Creating API Key Connection + createApiKeyConnection({ + query : { + linkedUserId: linkedUserId, + projectId: projectId, + providerName: name.toLowerCase(), + vertical: category.toLowerCase() + }, + data: values, + api_url: optionalApiUrl ?? config.API_URL! + }, + { + onSuccess: () => { + setLoading(false); + setOpenSuccessDialog(true); + }, + onError: (error) => { + setErrorResponse({errorPresent:true,errorMessage: error.message}); + setLoading(false) + } + }) } const CONNECTOR = CONNECTORS_METADATA[category!.toLowerCase()][name.toLowerCase()] - const img = CONNECTOR.logoPath; @@ -70,7 +160,7 @@ const PanoraIntegrationCard = ({name, category, projectId, linkedUserId, optiona
@@ -82,33 +172,97 @@ const PanoraIntegrationCard = ({name, category, projectId, linkedUserId, optiona {CONNECTOR.description!.substring(0, 300)}
- + ) + : + ( + + )} +
- + {errorResponse.errorPresent ?

{errorResponse.errorMessage}

: (<>)}
+ {/* Dialog for apikey input */} + + + + Enter a API key + + {/*
*/} + +
+
+ + +
{errors.apikey && (

{errors.apikey.message}

)}
+ + + {/*
*/} + {CONNECTORS_METADATA[category.toLowerCase()][name.toLowerCase()].authStrategy.properties?.map((fieldName :string) => + ( + <> + + + {errors[fieldName] && (

{errors[fieldName]?.message}

)} + + ))} +
+
+ + + + + + {/* */} + + + {/* OAuth Successful Modal */}
- - - - {name} - + + + {name}
- -
Connection Successful!
- -
The connection with {name} was successfully established. You can visit the Dashboard and verify the status.
- +
Connection Successful!
+
The connection with {name} was successfully established. You can visit the Dashboard and verify the status.
diff --git a/apps/embedded-catalog/react/src/components/ui/dialog.tsx b/apps/embedded-catalog/react/src/components/ui/dialog.tsx new file mode 100644 index 000000000..95b0d38ac --- /dev/null +++ b/apps/embedded-catalog/react/src/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { Cross2Icon } from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/apps/client-ts/src/components/ui/form.tsx b/apps/embedded-catalog/react/src/components/ui/form.tsx similarity index 100% rename from apps/client-ts/src/components/ui/form.tsx rename to apps/embedded-catalog/react/src/components/ui/form.tsx diff --git a/apps/embedded-catalog/react/src/components/ui/input.tsx b/apps/embedded-catalog/react/src/components/ui/input.tsx new file mode 100644 index 000000000..a92b8e0e5 --- /dev/null +++ b/apps/embedded-catalog/react/src/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/apps/embedded-catalog/react/src/components/ui/label.tsx b/apps/embedded-catalog/react/src/components/ui/label.tsx new file mode 100644 index 000000000..683faa793 --- /dev/null +++ b/apps/embedded-catalog/react/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/apps/embedded-catalog/react/src/hooks/queries/useCreateApiKeyConnection.tsx b/apps/embedded-catalog/react/src/hooks/queries/useCreateApiKeyConnection.tsx new file mode 100644 index 000000000..1761ec01a --- /dev/null +++ b/apps/embedded-catalog/react/src/hooks/queries/useCreateApiKeyConnection.tsx @@ -0,0 +1,50 @@ +import config from '@/helpers/config'; +import { useMutation } from '@tanstack/react-query'; + +interface IApiKeyConnectionDto { + query : { + providerName: string; // Name of the API Key provider + vertical: string; // Vertical (Crm, Ticketing, etc) + projectId: string; // Project ID + linkedUserId: string; // Linked User ID + }, + data: { + apikey: string, + [key : string]: string + }, + api_url: string +} + + + +// Adjusted useCreateApiKey hook to include a promise-returning function +const useCreateApiKeyConnection = () => { + const createApiKeyConnection = async (apiKeyConnectionData : IApiKeyConnectionDto) => { + const response = await fetch( + `${apiKeyConnectionData.api_url}/connections/basicorapikey/callback?state=${encodeURIComponent(JSON.stringify(apiKeyConnectionData.query))}`, { + method: 'POST', + body: JSON.stringify(apiKeyConnectionData.data), + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || "Unknown error occurred"); + } + + return response.json(); + }; + + return useMutation({ + mutationFn: createApiKeyConnection, + + onSuccess: () => { + console.log("Successfull !!!!") + } + + }); +}; + +export default useCreateApiKeyConnection; diff --git a/apps/embedded-catalog/react/src/hooks/queries/useProjectConnectors.tsx b/apps/embedded-catalog/react/src/hooks/queries/useProjectConnectors.tsx index 34de913ae..457198f65 100644 --- a/apps/embedded-catalog/react/src/hooks/queries/useProjectConnectors.tsx +++ b/apps/embedded-catalog/react/src/hooks/queries/useProjectConnectors.tsx @@ -5,7 +5,7 @@ const useProjectConnectors = (id: string,API_URL : string) => { return useQuery({ queryKey: ['project-connectors', id], queryFn: async (): Promise => { - const response = await fetch(`${API_URL}/project-connectors?projectId=${id}`); + const response = await fetch(`${API_URL}/project_connectors?projectId=${id}`); if (!response.ok) { throw new Error('Network response was not ok'); } diff --git a/apps/frontend-sdk/CHANGELOG.md b/apps/frontend-sdk/CHANGELOG.md new file mode 100644 index 000000000..e50a7e939 --- /dev/null +++ b/apps/frontend-sdk/CHANGELOG.md @@ -0,0 +1,24 @@ +# @panora/frontend-sdk + +## 1.2.0 + +### Minor Changes + +- f39a671: projectid param + +## 1.1.1 + +### Patch Changes + +- ded2580: Readme patch + +## 1.1.0 + +### Minor Changes + +- 773c515: update minor for npm packages + +### Patch Changes + +- Updated dependencies [773c515] + - @panora/shared@1.4.0 diff --git a/apps/frontend-sdk/README.md b/apps/frontend-sdk/README.md new file mode 100644 index 000000000..e51b3c32f --- /dev/null +++ b/apps/frontend-sdk/README.md @@ -0,0 +1,75 @@ + +## Frontend SDK (React) + +It is a React component aimed to be used in any of your pages so end-users can connect their 3rd parties in 1-click! + +## Installation + +```bash +npm i @panora/frontend-sdk +``` + +or + +```bash +pnpm i @panora/frontend-sdk +``` + +or + +```bash +yarn add @panora/frontend-sdk +``` + +## Use the component + +```ts + import { ConnectorCategory } from '@panora/shared' + import Panora from '@panora/frontend-sdk' + + const panora = new Panora({ apiKey: 'YOUR_PRIVATE_API_KEY' }); + + // kickstart the connection (OAuth, ApiKey, Basic) + panora.connect({ + providerName: "hubspot", + vertical: ConnectorCategory.Crm, + linkedUserId: "4c6ca51b-7b23-4e3a-9309-24d2d331a04d", + }) +``` + +```ts +The Panora SDK must be instantiated with this type: + +interface PanoraConfig { + apiKey: string; + overrideApiUrl: string; + // Optional (only if you are in selfhost mode and want to use localhost:3000), by default: api.panora.dev +} + +The .connect() function takes this type: + +interface ConnectOptions { + providerName: string; + vertical: ConnectorCategory; // Must be imported from @panora/shared + linkedUserId: string; // You can copy it from your Panora dahsbord under /configuration tab + credentials?: Credentials; // Optional if you try to use OAuth + options?: { + onSuccess?: () => void; + onError?: (error: Error) => void; + overrideReturnUrl?: string; + } +} + +By default, for OAuth we use Panora managed OAuth apps but if we dont have one registered OR you want to use your own, you must register that under /configuration tab from the webapp and it will automatically use these custom credentials ! + +interface Credentials { + username?: string; // Used for Basic Auth + password?: string; // Used for Basic Auth + apiKey?: string; // Used for Api Key Auth +} + +For Basic Auth some providers may only ask for username or password. + +In this case just specify either password or username depending on the 3rd party reference. + +``` diff --git a/apps/frontend-sdk/package.json b/apps/frontend-sdk/package.json new file mode 100644 index 000000000..f2392a1ff --- /dev/null +++ b/apps/frontend-sdk/package.json @@ -0,0 +1,17 @@ +{ + "name": "@panora/frontend-sdk", + "version": "1.2.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc" + }, + "dependencies": { + "@panora/shared": "workspace:^", + "axios": "^1.5.1" + }, + "devDependencies": { + "@types/node": "^20.3.1", + "typescript": "^5.1.3" + } +} \ No newline at end of file diff --git a/apps/frontend-sdk/src/index.ts b/apps/frontend-sdk/src/index.ts new file mode 100644 index 000000000..0f013b85c --- /dev/null +++ b/apps/frontend-sdk/src/index.ts @@ -0,0 +1,178 @@ +import axios from 'axios'; +import { ConnectorCategory, constructAuthUrl } from '@panora/shared'; + +interface PanoraConfig { + projectId: string; + overrideApiUrl?: string; +} + +interface Credentials { + username?: string; + password?: string; + apiKey?: string; +} + +interface ConnectOptions { + providerName: string; + vertical: ConnectorCategory; + linkedUserId: string; + credentials?: Credentials; + options?: { + onSuccess?: () => void; + onError?: (error: Error) => void; + overrideReturnUrl?: string; + } +} + +interface IGConnectionDto { + query: { + providerName: string; + vertical: string; + projectId: string; + linkedUserId: string; + }, + data: { + [key: string]: string; + } +} + +class Panora { + private apiUrl: string; + private projectId: string | null = null; + + constructor(config: PanoraConfig) { + this.projectId = config.projectId; + this.apiUrl = config.overrideApiUrl || 'https://api.panora.dev'; + } + + async connect(options: ConnectOptions): Promise { + const { providerName, vertical, linkedUserId, credentials, options: {onSuccess, onError, overrideReturnUrl} = {} } = options; + + try { + if(!this.projectId) throw new ReferenceError("ProjectId is invalid or undefined") + + if (credentials) { + // Handle API Key or Basic Auth + return this.handleCredentialsAuth(this.projectId, providerName, vertical, linkedUserId, credentials, onSuccess, onError); + } else { + // Handle OAuth + return this.handleOAuth(this.projectId, providerName, vertical, linkedUserId, overrideReturnUrl, onSuccess, onError); + } + } catch (error) { + if (onError) { + onError(error as Error); + } + return null; + } + } + + private async handleCredentialsAuth( + projectId: string, + providerName: string, + vertical: string, + linkedUserId: string, + credentials: Credentials, + onSuccess?: () => void, + onError?: (error: Error) => void + ): Promise { + const connectionData: IGConnectionDto = { + query: { + providerName, + vertical, + projectId, + linkedUserId, + }, + data: credentials as {[key: string]: any} + }; + + try { + const response = await fetch( + `${this.apiUrl}/connections/basicorapikey/callback?state=${encodeURIComponent(JSON.stringify(connectionData.query))}`, + { + method: 'POST', + body: JSON.stringify(connectionData.data), + headers: { + 'Content-Type': 'application/json', + }, + } + ); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.message || "Unknown error occurred"); + } + + if (onSuccess) { + onSuccess(); + } + } catch (error) { + if (onError) { + onError(error as Error); + } + } + + return null; + } + + private async handleOAuth( + projectId: string, + providerName: string, + vertical: string, + linkedUserId: string, + overrideReturnUrl?: string, + onSuccess?: () => void, + onError?: (error: Error) => void + ): Promise { + const returnUrl = overrideReturnUrl || `${window.location.origin}`; + + const authUrl = await constructAuthUrl({ + projectId, + linkedUserId, + providerName, + returnUrl, + apiUrl: this.apiUrl, + vertical + }); + + if (!authUrl) { + throw new Error(`Auth URL is invalid: ${authUrl}`); + } + + const width = 600, height = 600; + const left = (window.innerWidth - width) / 2; + const top = (window.innerHeight - height) / 2; + const authWindow = window.open(authUrl, 'OAuth', `width=${width},height=${height},top=${top},left=${left}`); + + if (authWindow) { + this.pollForRedirect(authWindow, returnUrl, onSuccess, onError); + } + + return authWindow; + } + + private pollForRedirect(authWindow: Window, returnUrl: string, onSuccess?: () => void, onError?: (error: Error) => void) { + const interval = setInterval(() => { + try { + const redirectedURL = authWindow.location.href; + if (redirectedURL.startsWith(returnUrl)) { + if (onSuccess) { + onSuccess(); + } + clearInterval(interval); + authWindow.close(); + } + } catch (e) { + // Ignore cross-origin errors + } + + if (authWindow.closed) { + clearInterval(interval); + if (onError) { + onError(new Error('Authentication window was closed')); + } + } + }, 500); + } +} + +export default Panora; \ No newline at end of file diff --git a/apps/frontend-sdk/tsconfig.json b/apps/frontend-sdk/tsconfig.json new file mode 100644 index 000000000..e5da16b07 --- /dev/null +++ b/apps/frontend-sdk/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "es6", + "module": "commonjs", + "declaration": true, + "outDir": "./dist", + "strict": true, + "esModuleInterop": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] + } \ No newline at end of file diff --git a/apps/magic-link/CHANGELOG.md b/apps/magic-link/CHANGELOG.md index 35ba23de5..ac8e2a9ad 100644 --- a/apps/magic-link/CHANGELOG.md +++ b/apps/magic-link/CHANGELOG.md @@ -1,5 +1,13 @@ # magic-link +## 0.0.14 + +### Patch Changes + +- Updated dependencies [773c515] + - @panora/shared@1.4.0 + - api@0.0.9 + ## 0.0.13 ### Patch Changes diff --git a/apps/magic-link/Dockerfile b/apps/magic-link/Dockerfile index b8efb57e2..8e9a8cad1 100644 --- a/apps/magic-link/Dockerfile +++ b/apps/magic-link/Dockerfile @@ -27,10 +27,8 @@ ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" ARG VITE_BACKEND_DOMAIN -ARG VITE_FRONTEND_DOMAIN ENV VITE_BACKEND_DOMAIN="$VITE_BACKEND_DOMAIN" -ENV VITE_FRONTEND_DOMAIN="$VITE_FRONTEND_DOMAIN" RUN corepack enable @@ -49,10 +47,6 @@ RUN pnpm turbo run build --filter=magic-link... # ======================================================================== FROM nginx:1.24-alpine3.17 as runner -#ARG VITE_BACKEND_DOMAIN -#ARG VITE_FRONTEND_DOMAIN -#ENV VITE_BACKEND_DOMAIN="$VITE_BACKEND_DOMAIN" -#ENV VITE_FRONTEND_DOMAIN="$VITE_FRONTEND_DOMAIN" COPY --from=installer ./app/apps/magic-link/dist/ /usr/share/nginx/html COPY apps/magic-link/nginx.conf /etc/nginx/conf.d/default.conf diff --git a/apps/magic-link/Dockerfile.dev b/apps/magic-link/Dockerfile.dev index f6f74b25d..26cdf0048 100644 --- a/apps/magic-link/Dockerfile.dev +++ b/apps/magic-link/Dockerfile.dev @@ -11,10 +11,8 @@ ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" ARG VITE_BACKEND_DOMAIN -ARG VITE_FRONTEND_DOMAIN ENV VITE_BACKEND_DOMAIN="$VITE_BACKEND_DOMAIN" -ENV VITE_FRONTEND_DOMAIN="$VITE_FRONTEND_DOMAIN" RUN corepack enable diff --git a/apps/magic-link/package.json b/apps/magic-link/package.json index 5d30fc0a9..f3d04a29c 100644 --- a/apps/magic-link/package.json +++ b/apps/magic-link/package.json @@ -1,7 +1,7 @@ { "name": "magic-link", "private": true, - "version": "0.0.13", + "version": "0.0.14", "type": "module", "engines": { "node": ">=20.9.0" @@ -14,14 +14,18 @@ "ci": "pnpm run lint && pnpm run build" }, "dependencies": { - "@panora/shared": "^1.3.0", - "@panora/typescript-sdk": "^1.0.3", + "@panora/shared": "^1.4.0", "@radix-ui/react-label": "^2.0.2", + "react-hook-form": "^7.51.2", + "@hookform/resolvers": "^3.3.4", + "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", "@tanstack/react-query": "^5.12.2", + "@radix-ui/react-icons": "^1.3.0", + "zod": "^3.22.4", "api": "workspace:*", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", diff --git a/apps/magic-link/src/App.css b/apps/magic-link/src/App.css index b9d355df2..56229e6af 100644 --- a/apps/magic-link/src/App.css +++ b/apps/magic-link/src/App.css @@ -3,6 +3,7 @@ margin: 0 auto; padding: 2rem; text-align: center; + background: #09090B; } .logo { diff --git a/apps/magic-link/src/components/Modal.tsx b/apps/magic-link/src/components/Modal.tsx index 348b83da1..095e5a539 100644 --- a/apps/magic-link/src/components/Modal.tsx +++ b/apps/magic-link/src/components/Modal.tsx @@ -1,30 +1,40 @@ "use client" import React, { useState } from 'react' -import {PartyPopper, Unplug,X} from 'lucide-react' +import {X} from 'lucide-react' + + +interface ModalProps { + open: boolean; + setOpen: (op: boolean) => void; + children: React.ReactNode; + backgroundClass?: string; + contentClass?: string; +} + +const Modal: React.FC = ({ + open, + setOpen, + children, + backgroundClass = "bg-black/20 backdrop-blur ", + contentClass = "" +}) => { + if (!open) return null; -const Modal = ({open,setOpen,children} : {open:boolean,setOpen: (op : boolean) => void,children: React.ReactNode}) => { return (
setOpen(false)} className={` fixed inset-0 flex justify-center items-center transition-colors - ${open ? "visible bg-black/20 backdrop-blur" : "invisible"} + ${backgroundClass} `} > {/* modal */}
e.stopPropagation()} className={` - bg-[#1d1d1d] border-green-900 rounded-xl shadow p-6 transition-all - ${open ? "scale-100 opacity-100" : "scale-125 opacity-0"} + ${contentClass} transition-all `} > - {/* */} {children}
diff --git a/apps/magic-link/src/components/ui/dialog.tsx b/apps/magic-link/src/components/ui/dialog.tsx new file mode 100644 index 000000000..95b0d38ac --- /dev/null +++ b/apps/magic-link/src/components/ui/dialog.tsx @@ -0,0 +1,122 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { Cross2Icon } from "@radix-ui/react-icons" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogClose, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/apps/magic-link/src/components/ui/form.tsx b/apps/magic-link/src/components/ui/form.tsx new file mode 100644 index 000000000..e1407aa93 --- /dev/null +++ b/apps/magic-link/src/components/ui/form.tsx @@ -0,0 +1,179 @@ +'use client' + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { Slot } from "@radix-ui/react-slot" +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form" + +import { cn } from "@/lib/utils" +import { Label } from "@/components/ui/label" +import { createContext } from "react" + +const Form = FormProvider + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName +} + +const FormFieldContext = createContext( + {} as FormFieldContextValue +) + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ) +} + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext) + const itemContext = React.useContext(FormItemContext) + const { getFieldState, formState } = useFormContext() + + const fieldState = getFieldState(fieldContext.name, formState) + + if (!fieldContext) { + throw new Error("useFormField should be used within ") + } + + const { id } = itemContext + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + } +} + +type FormItemContextValue = { + id: string +} + +const FormItemContext = createContext( + {} as FormItemContextValue +) + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId() + + return ( + +
+ + ) +}) +FormItem.displayName = "FormItem" + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField() + + return ( +
+ - - -
- - {(data as Provider[]).map((provider) => ( -
- handleWalletClick(provider.name, provider.vertical!)} - /> - + + +
+ +
+ {filteredProviders.map((provider) => ( +
handleProviderSelect(provider)} + > +
+ {provider.name} +
+ {formatProvider(provider.name)}
))} -
- - +
+ + {/* Basic Auth Dialog */} + + +
+ +
+ +
- {loading.status ? : } - {errorResponse.errorPresent ?

{errorResponse.errorMessage}

: (<>)} - - {/*
*/} - - - - {/* OAuth Successful Modal */} - -
-
-
- - + {selectedProvider?.category && selectedProvider?.provider && CONNECTORS_METADATA[selectedProvider.category]?.[selectedProvider.provider] && ( + <> +
+ {selectedProvider.provider} +
+

+ Connect your {selectedProvider.provider.charAt(0).toUpperCase() + selectedProvider.provider.slice(1)} Account +

+ + )} - {selectedProvider?.provider} +
+ {selectedProvider.provider !== '' && selectedProvider.category !== '' && + CONNECTORS_METADATA[selectedProvider.category][selectedProvider.provider].authStrategy.properties?.map((fieldName: string) => ( +
+ + {errors2[fieldName] &&

{errors2[fieldName]?.message}

} +
+ ))} + +

+ A third-party accountant will be added. +

+ + +
+
+ +
+ + {/* Domain Dialog */} + + +
+ +
+ +
+ {selectedProvider?.category && selectedProvider?.provider && CONNECTORS_METADATA[selectedProvider.category]?.[selectedProvider.provider] && ( + <> +
+ {selectedProvider.provider} +
+

+ Connect your {selectedProvider.provider.charAt(0).toUpperCase() + selectedProvider.provider.slice(1)} Account +

+ + )} -
+
{ e.preventDefault(); onDomainSubmit(); }} className="w-full space-y-4"> +
+ setEndUserDomain(e.target.value)} + /> + {errors2.end_user_domain &&

{errors2.end_user_domain.message}

} +
+ + {domainFormats[selectedProvider?.provider?.toLowerCase()] && ( +

+ e.g., {domainFormats[selectedProvider.provider.toLowerCase()]} +

+ )} + +

+ A third-party accountant will be added. +

+ + +
+
+ + -
Connection Successful!
+ {/* Success Dialog */} + +
+
+
+ +
+
+ + +
+ {currentProvider} +
+
+

+ Your data is being imported... +

+
+ + You've successfully connected your account! +
+ +
+
+
-
The connection with {currentProvider} was successfully established. You can visit the Dashboard and verify the status.
+ {/* Loading state */} + {loading.status && ( +
+
+ + Connecting to {loading.provider}... +
+
+ )} -
+ {/* Error message */} + {errorResponse.errorPresent && ( +
+

{errorResponse.errorMessage}

+
+ )}
- - ); }; diff --git a/apps/client-ts/.dockerignore b/apps/webapp/.dockerignore similarity index 100% rename from apps/client-ts/.dockerignore rename to apps/webapp/.dockerignore diff --git a/apps/client-ts/.eslintrc.json b/apps/webapp/.eslintrc.json similarity index 100% rename from apps/client-ts/.eslintrc.json rename to apps/webapp/.eslintrc.json diff --git a/apps/client-ts/.gitignore b/apps/webapp/.gitignore similarity index 100% rename from apps/client-ts/.gitignore rename to apps/webapp/.gitignore diff --git a/apps/client-ts/CHANGELOG.md b/apps/webapp/CHANGELOG.md similarity index 87% rename from apps/client-ts/CHANGELOG.md rename to apps/webapp/CHANGELOG.md index 732247bd3..c20a6ddf5 100644 --- a/apps/client-ts/CHANGELOG.md +++ b/apps/webapp/CHANGELOG.md @@ -1,4 +1,12 @@ -# client-ts +# webapp + +## 0.1.8 + +### Patch Changes + +- Updated dependencies [773c515] + - @panora/shared@1.4.0 + - api@0.0.9 ## 0.1.7 diff --git a/apps/webapp/Dockerfile b/apps/webapp/Dockerfile new file mode 100644 index 000000000..38519ee0c --- /dev/null +++ b/apps/webapp/Dockerfile @@ -0,0 +1,67 @@ +# run directly from the repo root directory +# docker build -f ./apps/webapp/Dockerfile . +FROM node:20-alpine AS base +# ======================================================================= +# Turbo: Prepare a standalone workspace for docker +FROM base AS builder +RUN apk add --no-cache libc6-compat +RUN apk update + +# Set pnpm +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable + +WORKDIR /app +RUN pnpm add -g turbo@1.13.4 +COPY . . +RUN turbo prune webapp --docker + +#check content +RUN ls -la ./out/full/apps/webapp + +# ======================================================================= +# Install Deps and build project using PNPM +FROM base AS installer +RUN apk add --no-cache libc6-compat +RUN apk update +# Set pnpm +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" + + +ARG NEXT_PUBLIC_DISTRIBUTION +ARG NEXT_PUBLIC_BACKEND_DOMAIN +ARG NEXT_PUBLIC_MAGIC_LINK_DOMAIN +ARG NEXT_PUBLIC_WEBAPP_DOMAIN +ARG NEXT_PUBLIC_REDIRECT_WEBHOOK_INGRESS + + +ENV NEXT_PUBLIC_DISTRIBUTION="$NEXT_PUBLIC_DISTRIBUTION" +ENV NEXT_PUBLIC_BACKEND_DOMAIN="${NEXT_PUBLIC_BACKEND_DOMAIN}" +ENV NEXT_PUBLIC_MAGIC_LINK_DOMAIN="${NEXT_PUBLIC_MAGIC_LINK_DOMAIN}" +ENV NEXT_PUBLIC_WEBAPP_DOMAIN="${NEXT_PUBLIC_WEBAPP_DOMAIN}" +ENV NEXT_PUBLIC_REDIRECT_WEBHOOK_INGRESS="${NEXT_PUBLIC_REDIRECT_WEBHOOK_INGRESS}" + +RUN corepack enable + +WORKDIR /app + +RUN ls -la + +# First install the dependencies (as they change less often) +COPY .gitignore .gitignore +COPY --from=builder /app/out/json/ . + +# 🔴🔴🔴 possible bug due to missing dependencies here, when using "standalone mode" +COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml + +# install dependencies +RUN pnpm install --shamefully-hoist + +# Build the project +COPY --from=builder ./app/out/full/ . +RUN pnpm run build + +CMD cd /app/apps/webapp/ && pnpm run start + diff --git a/apps/client-ts/Dockerfile.dev b/apps/webapp/Dockerfile.dev similarity index 64% rename from apps/client-ts/Dockerfile.dev rename to apps/webapp/Dockerfile.dev index 3b940a38f..3c9bc1a95 100644 --- a/apps/client-ts/Dockerfile.dev +++ b/apps/webapp/Dockerfile.dev @@ -1,5 +1,5 @@ # run directly from the repo root directory -# docker build -f ./apps/client-ts/Dockerfile.dev . +# docker build -f ./apps/webapp/Dockerfile.dev . FROM node:20-alpine AS base # ======================================================================= FROM base AS builder @@ -11,12 +11,8 @@ ENV PNPM_HOME="/pnpm" ENV PATH="$PNPM_HOME:$PATH" ARG VITE_BACKEND_DOMAIN -ARG VITE_FRONTEND_DOMAIN -ARG VITE_STYTCH_TOKEN ENV VITE_BACKEND_DOMAIN="$VITE_BACKEND_DOMAIN" -ENV VITE_FRONTEND_DOMAIN="$VITE_FRONTEND_DOMAIN" -ENV VITE_STYTCH_TOKEN="$VITE_STYTCH_TOKEN" RUN corepack enable @@ -24,4 +20,4 @@ WORKDIR /app RUN pnpm add -g turbo@1.13.4 # Start the Webapp -CMD cd apps/client-ts && pnpm install && pnpm run dev +CMD cd apps/webapp && pnpm install && pnpm run dev diff --git a/apps/client-ts/README.md b/apps/webapp/README.md similarity index 100% rename from apps/client-ts/README.md rename to apps/webapp/README.md diff --git a/apps/client-ts/components.json b/apps/webapp/components.json similarity index 100% rename from apps/client-ts/components.json rename to apps/webapp/components.json diff --git a/apps/client-ts/next.config.mjs b/apps/webapp/next.config.mjs similarity index 100% rename from apps/client-ts/next.config.mjs rename to apps/webapp/next.config.mjs diff --git a/apps/client-ts/nginx.conf b/apps/webapp/nginx.conf similarity index 100% rename from apps/client-ts/nginx.conf rename to apps/webapp/nginx.conf diff --git a/apps/client-ts/package-lock.json b/apps/webapp/package-lock.json similarity index 99% rename from apps/client-ts/package-lock.json rename to apps/webapp/package-lock.json index 8353e0048..299dd2f1d 100644 --- a/apps/client-ts/package-lock.json +++ b/apps/webapp/package-lock.json @@ -1,11 +1,11 @@ { - "name": "client-ts", + "name": "webapp", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "client-ts", + "name": "webapp", "version": "0.1.0", "dependencies": { "@hookform/resolvers": "^3.3.4", diff --git a/apps/client-ts/package.json b/apps/webapp/package.json similarity index 98% rename from apps/client-ts/package.json rename to apps/webapp/package.json index fdb77cf8a..d53fcd968 100644 --- a/apps/client-ts/package.json +++ b/apps/webapp/package.json @@ -1,6 +1,6 @@ { - "name": "client-ts", - "version": "0.1.7", + "name": "webapp", + "version": "0.1.8", "private": true, "scripts": { "dev": "next dev -p 8090", diff --git a/apps/client-ts/postcss.config.js b/apps/webapp/postcss.config.js similarity index 100% rename from apps/client-ts/postcss.config.js rename to apps/webapp/postcss.config.js diff --git a/apps/client-ts/public/avatars/01.png b/apps/webapp/public/avatars/01.png similarity index 100% rename from apps/client-ts/public/avatars/01.png rename to apps/webapp/public/avatars/01.png diff --git a/apps/client-ts/public/avatars/02.png b/apps/webapp/public/avatars/02.png similarity index 100% rename from apps/client-ts/public/avatars/02.png rename to apps/webapp/public/avatars/02.png diff --git a/apps/client-ts/public/avatars/03.png b/apps/webapp/public/avatars/03.png similarity index 100% rename from apps/client-ts/public/avatars/03.png rename to apps/webapp/public/avatars/03.png diff --git a/apps/client-ts/public/avatars/04.png b/apps/webapp/public/avatars/04.png similarity index 100% rename from apps/client-ts/public/avatars/04.png rename to apps/webapp/public/avatars/04.png diff --git a/apps/client-ts/public/avatars/05.png b/apps/webapp/public/avatars/05.png similarity index 100% rename from apps/client-ts/public/avatars/05.png rename to apps/webapp/public/avatars/05.png diff --git a/apps/client-ts/public/bg-panora.jpeg b/apps/webapp/public/bg-panora.jpeg similarity index 100% rename from apps/client-ts/public/bg-panora.jpeg rename to apps/webapp/public/bg-panora.jpeg diff --git a/apps/client-ts/public/bgbg.jpeg b/apps/webapp/public/bgbg.jpeg similarity index 100% rename from apps/client-ts/public/bgbg.jpeg rename to apps/webapp/public/bgbg.jpeg diff --git a/apps/client-ts/public/icons/google.tsx b/apps/webapp/public/icons/google.tsx similarity index 100% rename from apps/client-ts/public/icons/google.tsx rename to apps/webapp/public/icons/google.tsx diff --git a/apps/client-ts/public/icons/microsoft.tsx b/apps/webapp/public/icons/microsoft.tsx similarity index 100% rename from apps/client-ts/public/icons/microsoft.tsx rename to apps/webapp/public/icons/microsoft.tsx diff --git a/apps/client-ts/public/images.jpeg b/apps/webapp/public/images.jpeg similarity index 100% rename from apps/client-ts/public/images.jpeg rename to apps/webapp/public/images.jpeg diff --git a/apps/client-ts/public/logo-panora-black.png b/apps/webapp/public/logo-panora-black.png similarity index 100% rename from apps/client-ts/public/logo-panora-black.png rename to apps/webapp/public/logo-panora-black.png diff --git a/apps/client-ts/public/logo-panora-white-hq.png b/apps/webapp/public/logo-panora-white-hq.png similarity index 100% rename from apps/client-ts/public/logo-panora-white-hq.png rename to apps/webapp/public/logo-panora-white-hq.png diff --git a/apps/client-ts/public/logo.png b/apps/webapp/public/logo.png similarity index 100% rename from apps/client-ts/public/logo.png rename to apps/webapp/public/logo.png diff --git a/apps/client-ts/public/next.svg b/apps/webapp/public/next.svg similarity index 100% rename from apps/client-ts/public/next.svg rename to apps/webapp/public/next.svg diff --git a/apps/client-ts/public/providers/crm/attio.png b/apps/webapp/public/providers/crm/attio.png similarity index 100% rename from apps/client-ts/public/providers/crm/attio.png rename to apps/webapp/public/providers/crm/attio.png diff --git a/apps/client-ts/public/providers/crm/hubspot.jpg b/apps/webapp/public/providers/crm/hubspot.jpg similarity index 100% rename from apps/client-ts/public/providers/crm/hubspot.jpg rename to apps/webapp/public/providers/crm/hubspot.jpg diff --git a/apps/client-ts/public/providers/crm/pipedrive.png b/apps/webapp/public/providers/crm/pipedrive.png similarity index 100% rename from apps/client-ts/public/providers/crm/pipedrive.png rename to apps/webapp/public/providers/crm/pipedrive.png diff --git a/apps/client-ts/public/providers/crm/zendesk.png b/apps/webapp/public/providers/crm/zendesk.png similarity index 100% rename from apps/client-ts/public/providers/crm/zendesk.png rename to apps/webapp/public/providers/crm/zendesk.png diff --git a/apps/client-ts/public/providers/crm/zendesk_tcg.png b/apps/webapp/public/providers/crm/zendesk_tcg.png similarity index 100% rename from apps/client-ts/public/providers/crm/zendesk_tcg.png rename to apps/webapp/public/providers/crm/zendesk_tcg.png diff --git a/apps/client-ts/public/providers/crm/zoho.png b/apps/webapp/public/providers/crm/zoho.png similarity index 100% rename from apps/client-ts/public/providers/crm/zoho.png rename to apps/webapp/public/providers/crm/zoho.png diff --git a/apps/client-ts/public/providers/crm/zoho.webp b/apps/webapp/public/providers/crm/zoho.webp similarity index 100% rename from apps/client-ts/public/providers/crm/zoho.webp rename to apps/webapp/public/providers/crm/zoho.webp diff --git a/apps/client-ts/public/quickstart/1.png b/apps/webapp/public/quickstart/1.png similarity index 100% rename from apps/client-ts/public/quickstart/1.png rename to apps/webapp/public/quickstart/1.png diff --git a/apps/client-ts/public/quickstart/2.png b/apps/webapp/public/quickstart/2.png similarity index 100% rename from apps/client-ts/public/quickstart/2.png rename to apps/webapp/public/quickstart/2.png diff --git a/apps/client-ts/public/quickstart/3.png b/apps/webapp/public/quickstart/3.png similarity index 100% rename from apps/client-ts/public/quickstart/3.png rename to apps/webapp/public/quickstart/3.png diff --git a/apps/client-ts/public/quickstart/4.png b/apps/webapp/public/quickstart/4.png similarity index 100% rename from apps/client-ts/public/quickstart/4.png rename to apps/webapp/public/quickstart/4.png diff --git a/apps/client-ts/public/quickstart/5.png b/apps/webapp/public/quickstart/5.png similarity index 100% rename from apps/client-ts/public/quickstart/5.png rename to apps/webapp/public/quickstart/5.png diff --git a/apps/client-ts/public/quickstart/gif11.gif b/apps/webapp/public/quickstart/gif11.gif similarity index 100% rename from apps/client-ts/public/quickstart/gif11.gif rename to apps/webapp/public/quickstart/gif11.gif diff --git a/apps/client-ts/public/quickstart/gif12.gif b/apps/webapp/public/quickstart/gif12.gif similarity index 100% rename from apps/client-ts/public/quickstart/gif12.gif rename to apps/webapp/public/quickstart/gif12.gif diff --git a/apps/client-ts/public/vercel.svg b/apps/webapp/public/vercel.svg similarity index 100% rename from apps/client-ts/public/vercel.svg rename to apps/webapp/public/vercel.svg diff --git a/apps/client-ts/src/app/(Dashboard)/api-keys/page.tsx b/apps/webapp/src/app/(Dashboard)/api-keys/page.tsx similarity index 86% rename from apps/client-ts/src/app/(Dashboard)/api-keys/page.tsx rename to apps/webapp/src/app/(Dashboard)/api-keys/page.tsx index bb71e4f9a..3206e6f17 100644 --- a/apps/client-ts/src/app/(Dashboard)/api-keys/page.tsx +++ b/apps/webapp/src/app/(Dashboard)/api-keys/page.tsx @@ -46,26 +46,37 @@ const formSchema = z.object({ interface TSApiKeys { id_api_key: string; name : string; - token : string; } export default function Page() { const [open,setOpen] = useState(false) const [tsApiKeys,setTSApiKeys] = useState([]) + const [isKeyModalOpen, setIsKeyModalOpen] = useState(false); - const queryClient = useQueryClient(); + const queryClient = useQueryClient(); + const [newApiKey, setNewApiKey] = useState<{ key: string; expiration: Date } | null>(null); const {idProject} = useProjectStore(); const {profile} = useProfileStore(); const { createApiKeyPromise } = useCreateApiKey(); const { data: apiKeys, isLoading, error } = useApiKeys(); const columns = useColumns(); + useEffect(() => { + if (newApiKey) { + const timeUntilExpiration = newApiKey.expiration.getTime() - Date.now(); + const timer = setTimeout(() => { + setNewApiKey(null); + }, timeUntilExpiration); + + return () => clearTimeout(timer); + } + }, [newApiKey]); + useEffect(() => { const temp_tsApiKeys = apiKeys?.map((key) => ({ id_api_key: key.id_api_key, name: key.name || "", - token: key.api_key_hash, })) setTSApiKeys(temp_tsApiKeys) },[apiKeys]) @@ -107,6 +118,13 @@ export default function Page() { queryClient.setQueryData(['api-keys'], (oldQueryData = []) => { return [...oldQueryData, data]; }); + // Store the API key and its expiration time in state + setNewApiKey({ + key: data.api_key, + expiration: new Date(Date.now() + 60000), + }); + setIsKeyModalOpen(true); // Open the modal + return (
@@ -225,6 +243,22 @@ export default function Page() { {tsApiKeys && }
+ + + + Your New API Key + + This key will only be shown for the next minute. Please save it now. + + +
+ API Key:

{newApiKey?.key}

+
+ + + +
+
); } \ No newline at end of file diff --git a/apps/client-ts/src/app/(Dashboard)/b2c/profile/page.tsx b/apps/webapp/src/app/(Dashboard)/b2c/profile/page.tsx similarity index 96% rename from apps/client-ts/src/app/(Dashboard)/b2c/profile/page.tsx rename to apps/webapp/src/app/(Dashboard)/b2c/profile/page.tsx index 6fddc0616..67a372943 100644 --- a/apps/client-ts/src/app/(Dashboard)/b2c/profile/page.tsx +++ b/apps/webapp/src/app/(Dashboard)/b2c/profile/page.tsx @@ -65,9 +65,9 @@ const Profile = () => {

Connected user

-
+
- + )}
Login Page Image diff --git a/apps/webapp/src/app/b2c/login/reset-password/page.tsx b/apps/webapp/src/app/b2c/login/reset-password/page.tsx new file mode 100644 index 000000000..224d146b8 --- /dev/null +++ b/apps/webapp/src/app/b2c/login/reset-password/page.tsx @@ -0,0 +1,34 @@ +'use client'; + +import React, { Suspense } from 'react'; +import ResetPasswordForm from '@/components/Auth/CustomLoginComponent/ResetPasswordForm'; +import { useSearchParams } from 'next/navigation'; + +const SearchParamsWrapper = () => { + const searchParams = useSearchParams(); + const token = searchParams.get('token'); + const email = searchParams.get('email'); + + if (!token) { + return
Invalid or missing reset token. Please try the password reset process again.
; + } + if (!email) { + return
Invalid or missing email. Please try the password reset process again.
; + } + + return ; +}; + +const ResetPasswordPage = () => { + return ( +
+
+ Loading...
}> + + +
+
+ ); +}; + +export default ResetPasswordPage; \ No newline at end of file diff --git a/apps/client-ts/src/app/favicon.ico b/apps/webapp/src/app/favicon.ico similarity index 100% rename from apps/client-ts/src/app/favicon.ico rename to apps/webapp/src/app/favicon.ico diff --git a/apps/client-ts/src/app/globals.css b/apps/webapp/src/app/globals.css similarity index 100% rename from apps/client-ts/src/app/globals.css rename to apps/webapp/src/app/globals.css diff --git a/apps/client-ts/src/app/layout.tsx b/apps/webapp/src/app/layout.tsx similarity index 96% rename from apps/client-ts/src/app/layout.tsx rename to apps/webapp/src/app/layout.tsx index 8eb767539..a6d8f03e9 100644 --- a/apps/client-ts/src/app/layout.tsx +++ b/apps/webapp/src/app/layout.tsx @@ -9,7 +9,7 @@ const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Panora", - description: "Unfied API", + description: "Unified API", }; export default function RootLayout({ diff --git a/apps/webapp/src/components/ApiKeys/columns.tsx b/apps/webapp/src/components/ApiKeys/columns.tsx new file mode 100644 index 000000000..398fc172c --- /dev/null +++ b/apps/webapp/src/components/ApiKeys/columns.tsx @@ -0,0 +1,53 @@ +"use client" + +import { ColumnDef } from "@tanstack/react-table" +import { Badge } from "@/components/ui/badge" +import { ApiKey } from "./schema" +import { DataTableColumnHeader } from "../shared/data-table-column-header" +import { DataTableRowActions } from "../shared/data-table-row-actions" +import { PasswordInput } from "../ui/password-input" +import { useState } from "react" +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip" +import { Button } from "../ui/button" +import { toast } from "sonner" +import { Card } from "antd" + +export function useColumns() { + const [copiedState, setCopiedState] = useState<{ [key: string]: boolean }>({}); + + const handleCopy = (token: string) => { + navigator.clipboard.writeText(token); + setCopiedState((prevState) => ({ + ...prevState, + [token]: true, + })); + toast.success("Api key copied", { + action: { + label: "Close", + onClick: () => console.log("Close"), + }, + }) + setTimeout(() => { + setCopiedState((prevState) => ({ + ...prevState, + [token]: false, + })); + }, 2000); // Reset copied state after 2 seconds + }; + + return [ + { + accessorKey: "name", + header: ({ column }) => ( + + ), + cell: ({ row }) =>
{row.getValue("name")}
, + enableSorting: false, + enableHiding: false, + }, + { + id: "actions", + cell: ({ row }) => , + }, + ] as ColumnDef[]; +} diff --git a/apps/client-ts/src/components/ApiKeys/schema.ts b/apps/webapp/src/components/ApiKeys/schema.ts similarity index 88% rename from apps/client-ts/src/components/ApiKeys/schema.ts rename to apps/webapp/src/components/ApiKeys/schema.ts index dfb2acf32..b4db52b4b 100644 --- a/apps/client-ts/src/components/ApiKeys/schema.ts +++ b/apps/webapp/src/components/ApiKeys/schema.ts @@ -3,7 +3,6 @@ import { z } from "zod" export const apiKeySchema = z.object({ id_api_key: z.string(), name: z.string(), - token: z.string(), }) export type ApiKey = z.infer \ No newline at end of file diff --git a/apps/client-ts/src/components/Auth/CustomLoginComponent/CreateUserForm.tsx b/apps/webapp/src/components/Auth/CustomLoginComponent/CreateUserForm.tsx similarity index 100% rename from apps/client-ts/src/components/Auth/CustomLoginComponent/CreateUserForm.tsx rename to apps/webapp/src/components/Auth/CustomLoginComponent/CreateUserForm.tsx diff --git a/apps/webapp/src/components/Auth/CustomLoginComponent/ForgotPasswordForm.tsx b/apps/webapp/src/components/Auth/CustomLoginComponent/ForgotPasswordForm.tsx new file mode 100644 index 000000000..7bce34aad --- /dev/null +++ b/apps/webapp/src/components/Auth/CustomLoginComponent/ForgotPasswordForm.tsx @@ -0,0 +1,69 @@ +import React from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { toast } from 'sonner'; +import useInitiatePasswordRecovery from '@/hooks/create/useInitiatePasswordRecovery'; + +const formSchema = z.object({ + email: z.string().email({ message: 'Enter valid Email' }), +}); + +const ForgotPasswordForm = () => { + const { func } = useInitiatePasswordRecovery(); + + const sform = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + email: '', + }, + }); + + const onSubmit = (values: z.infer) => { + toast.promise( + func({ email: values.email }), + { + loading: 'Sending recovery email...', + success: 'Recovery email sent. Please check your inbox.', + error: 'Failed to send recovery email. Please try again.', + } + ); + }; + + return ( +
+ + + + Forgot Password + Enter your email to reset your password. + + + ( + + Email + + + + + + )} + /> + + + + + +
+ + ); +}; + +export default ForgotPasswordForm; \ No newline at end of file diff --git a/apps/client-ts/src/components/Auth/CustomLoginComponent/LoginUserForm.tsx b/apps/webapp/src/components/Auth/CustomLoginComponent/LoginUserForm.tsx similarity index 95% rename from apps/client-ts/src/components/Auth/CustomLoginComponent/LoginUserForm.tsx rename to apps/webapp/src/components/Auth/CustomLoginComponent/LoginUserForm.tsx index 1e49ab5e0..46c2e3735 100644 --- a/apps/client-ts/src/components/Auth/CustomLoginComponent/LoginUserForm.tsx +++ b/apps/webapp/src/components/Auth/CustomLoginComponent/LoginUserForm.tsx @@ -29,10 +29,11 @@ import { toast } from 'sonner' import useProfileStore from '@/state/profileStore'; import Cookies from 'js-cookie'; import { useQueryClient } from '@tanstack/react-query' +import Link from 'next/link' const formSchema = z.object({ email: z.string().email({ - message:"Enter valid Email" + message:"Enter valid Email" }), password : z.string().min(2, { message: "Enter Password.", @@ -132,6 +133,9 @@ const LoginUserForm = () => { + + Forgot Password? + diff --git a/apps/webapp/src/components/Auth/CustomLoginComponent/ResetPasswordForm.tsx b/apps/webapp/src/components/Auth/CustomLoginComponent/ResetPasswordForm.tsx new file mode 100644 index 000000000..1e291c834 --- /dev/null +++ b/apps/webapp/src/components/Auth/CustomLoginComponent/ResetPasswordForm.tsx @@ -0,0 +1,132 @@ +// src/components/Auth/CustomLoginComponent/ResetPasswordForm.tsx + +import React, { useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import * as z from 'zod'; +import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { Button } from '@/components/ui/button'; +import { toast } from 'sonner'; +import { useRouter } from 'next/navigation'; +import useResetPassword from '@/hooks/create/useResetPassword'; +import { Eye, EyeOff } from 'lucide-react'; + +const formSchema = z.object({ + newPassword: z + .string() + .min(8, { message: "Password must be at least 8 characters long" }) + .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/, { + message: "Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character", + }), + confirmPassword: z.string(), +}).refine((data) => data.newPassword === data.confirmPassword, { + message: "Passwords don't match", + path: ["confirmPassword"], +}); + +const ResetPasswordForm = ({ token, email }: {token: string; email: string}) => { + const router = useRouter(); + const { func } = useResetPassword(); + const [showPassword, setShowPassword] = useState(false); + const [showConfirmPassword, setShowConfirmPassword] = useState(false); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + newPassword: '', + confirmPassword: '', + }, + }); + + const onSubmit = async (values: z.infer) => { + try { + toast.promise( + func({ email, reset_token: token, new_password: values.newPassword }), + { + loading: 'Resetting password...', + success: 'Password reset successful. Please log in with your new password.', + error: 'Failed to reset password. Please try again.', + } + ); + router.push('/b2c/login'); + } catch (error) { + console.error('Password reset error:', error); + } + }; + + return ( +
+ + + + Reset Password + Enter your new password to reset your account. + + + ( + + New Password + +
+ + +
+
+ +
+ )} + /> + ( + + Confirm New Password + +
+ + +
+
+ +
+ )} + /> +
+ + + +
+
+ + ); +}; + +export default ResetPasswordForm; \ No newline at end of file diff --git a/apps/client-ts/src/components/Auth/EmailLoginForm.tsx b/apps/webapp/src/components/Auth/EmailLoginForm.tsx similarity index 100% rename from apps/client-ts/src/components/Auth/EmailLoginForm.tsx rename to apps/webapp/src/components/Auth/EmailLoginForm.tsx diff --git a/apps/client-ts/src/components/Auth/SAMLConnectionForm.tsx b/apps/webapp/src/components/Auth/SAMLConnectionForm.tsx similarity index 100% rename from apps/client-ts/src/components/Auth/SAMLConnectionForm.tsx rename to apps/webapp/src/components/Auth/SAMLConnectionForm.tsx diff --git a/apps/client-ts/src/components/Auth/SMSAuthenticateForm.tsx b/apps/webapp/src/components/Auth/SMSAuthenticateForm.tsx similarity index 100% rename from apps/client-ts/src/components/Auth/SMSAuthenticateForm.tsx rename to apps/webapp/src/components/Auth/SMSAuthenticateForm.tsx diff --git a/apps/client-ts/src/components/Auth/SMSSendForm.tsx b/apps/webapp/src/components/Auth/SMSSendForm.tsx similarity index 100% rename from apps/client-ts/src/components/Auth/SMSSendForm.tsx rename to apps/webapp/src/components/Auth/SMSSendForm.tsx diff --git a/apps/client-ts/src/components/Configuration/Catalog/CatalogWidget.tsx b/apps/webapp/src/components/Configuration/Catalog/CatalogWidget.tsx similarity index 97% rename from apps/client-ts/src/components/Configuration/Catalog/CatalogWidget.tsx rename to apps/webapp/src/components/Configuration/Catalog/CatalogWidget.tsx index 28e2050ff..8755a7174 100644 --- a/apps/client-ts/src/components/Configuration/Catalog/CatalogWidget.tsx +++ b/apps/webapp/src/components/Configuration/Catalog/CatalogWidget.tsx @@ -145,9 +145,9 @@ export function CatalogWidget() { {item.vertical} } - {item.authStrategy && - - {item.authStrategy} + {item.authStrategy.strategy && + + {item.authStrategy.strategy} }
diff --git a/apps/client-ts/src/components/Configuration/Catalog/CopySnippet.tsx b/apps/webapp/src/components/Configuration/Catalog/CopySnippet.tsx similarity index 97% rename from apps/client-ts/src/components/Configuration/Catalog/CopySnippet.tsx rename to apps/webapp/src/components/Configuration/Catalog/CopySnippet.tsx index f8ba24d40..896f02c2d 100644 --- a/apps/client-ts/src/components/Configuration/Catalog/CopySnippet.tsx +++ b/apps/webapp/src/components/Configuration/Catalog/CopySnippet.tsx @@ -28,7 +28,7 @@ export const CopySnippet = () => { `` ); @@ -50,7 +50,7 @@ export const CopySnippet = () => { name={"hubspot"} category={ConnectorCategory.Crm} projectId={"c9a1b1f8-466d-442d-a95e-11cdd00baf49"} - returnUrl={"https://acme.inc"} + optionalApiUrl={"https://acme.inc"} linkedUserId={"b860d6c1-28f9-485c-86cd-fb09e60f10a2"} />` ); @@ -131,7 +131,7 @@ export const CopySnippet = () => { projectId{`={'c9a1b1f8-466d-442d-a95e-11cdd00baf49'}`} - returnUrl{`={'https://acme.inc'}`} + optionalApiUrl{`={'https://acme.inc'}`} linkedUserId{`={'b860d6c1-28f9-485c-86cd-fb09e60f10a2'}`} @@ -191,7 +191,7 @@ export const CopySnippet = () => { projectId{`={'c9a1b1f8-466d-442d-a95e-11cdd00baf49'}`} - returnUrl{`={'https://acme.inc'}`} + optionalApiUrl{`={'https://acme.inc'}`} linkedUserId{`={'b860d6c1-28f9-485c-86cd-fb09e60f10a2'}`} diff --git a/apps/webapp/src/components/Configuration/Connector/ConnectorDisplay.tsx b/apps/webapp/src/components/Configuration/Connector/ConnectorDisplay.tsx new file mode 100644 index 000000000..94a38e8c5 --- /dev/null +++ b/apps/webapp/src/components/Configuration/Connector/ConnectorDisplay.tsx @@ -0,0 +1,424 @@ +import React, { useEffect, useState } from 'react'; +import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useQueryClient } from "@tanstack/react-query"; +import { usePostHog } from 'posthog-js/react'; +import { toast } from "sonner"; +import * as z from "zod"; + +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; +import { Switch } from "@/components/ui/switch"; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { PasswordInput } from "@/components/ui/password-input"; +import { Input } from "@/components/ui/input"; +import { DataTableFacetedFilter } from "@/components/shared/data-table-faceted-filter"; + +import config from "@/lib/config"; +import { AuthStrategy, providerToType, Provider, CONNECTORS_METADATA, extractProvider, extractVertical, needsSubdomain, needsScope } from "@panora/shared"; +import useProjectStore from "@/state/projectStore"; +import useConnectionStrategies from "@/hooks/get/useConnectionStrategies"; +import useCreateConnectionStrategy from "@/hooks/create/useCreateConnectionStrategy"; +import useUpdateConnectionStrategy from "@/hooks/update/useUpdateConnectionStrategy"; +import useConnectionStrategyAuthCredentials from "@/hooks/get/useConnectionStrategyAuthCredentials"; + +const formSchema = z.object({ + subdomain: z.string().optional(), + client_id: z.string().optional(), + client_secret: z.string().optional(), + scope: z.string().optional(), + api_key: z.string().optional(), + username: z.string().optional(), + secret: z.string().optional(), +}); + +interface ItemDisplayProps { + item?: Provider; +} + +export function ConnectorDisplay({ item }: ItemDisplayProps) { + const [copied, setCopied] = useState(false); + const [switchEnabled, setSwitchEnabled] = useState(false); + const { idProject } = useProjectStore(); + const queryClient = useQueryClient(); + const posthog = usePostHog(); + + const { data: connectionStrategies } = useConnectionStrategies(); + const { createCsPromise } = useCreateConnectionStrategy(); + const { updateCsPromise } = useUpdateConnectionStrategy(); + const { mutateAsync: fetchCredentials } = useConnectionStrategyAuthCredentials(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + subdomain: "", client_id: "", client_secret: "", scope: "", + api_key: "", username: "", secret: "", + }, + }); + + const mappingConnectionStrategies = connectionStrategies?.filter( + (cs) => extractVertical(cs.type).toLowerCase() === item?.vertical && + extractProvider(cs.type).toLowerCase() === item?.name + ); + + const oauthAttributes = CONNECTORS_METADATA[item?.vertical!]?.[item?.name!]?.options?.oauth_attributes || []; + + useEffect(() => { + oauthAttributes.forEach((attr: string) => { + formSchema.shape[attr as keyof z.infer] = z.string().optional(); + }); + }, [oauthAttributes]); + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(`${config.API_URL}/connections/oauth/callback`); + setCopied(true); + toast.success("Redirect URI copied"); + setTimeout(() => setCopied(false), 2000); + } catch (err) { + console.error('Failed to copy: ', err); + } + }; + + function onSubmit(values: z.infer) { + const { client_id, client_secret, scope, api_key, secret, username, subdomain } = values; + const performUpdate = mappingConnectionStrategies && mappingConnectionStrategies.length > 0; + const dynamicAttributes = oauthAttributes + .map((attr: string) => values[attr as keyof z.infer]) + .filter((value): value is string => value !== undefined); + + let attributes: string[] = []; + let attributeValues: string[] = []; + + switch (item?.authStrategy.strategy) { + case AuthStrategy.oauth2: + if (!client_id || !client_secret) { + form.setError("client_id", { message: "Please Enter Client ID" }); + form.setError("client_secret", { message: "Please Enter Client Secret" }); + return; + } + attributes = ["client_id", "client_secret", ...oauthAttributes]; + attributeValues = [client_id, client_secret, ...dynamicAttributes]; + + if (needsSubdomain(item.name.toLowerCase(), item.vertical!.toLowerCase())) { + if (!subdomain) { + form.setError("subdomain", { message: "Please Enter Subdomain" }); + return; + } + attributes.push("subdomain"); + attributeValues.push(subdomain); + } + + if (needsScope(item.name.toLowerCase(), item.vertical!.toLowerCase())) { + if (!scope) { + form.setError("scope", { message: "Please Enter the scope" }); + return; + } + attributes.push("scope"); + attributeValues.push(scope); + } + break; + + case AuthStrategy.api_key: + if (!api_key) { + form.setError("api_key", { message: "Please Enter API Key" }); + return; + } + attributes = ["api_key"]; + attributeValues = [api_key]; + break; + + case AuthStrategy.basic: + if (!username || !secret) { + form.setError("username", { message: "Please Enter Username" }); + form.setError("secret", { message: "Please Enter Secret" }); + return; + } + attributes = ["username", "secret"]; + attributeValues = [username, secret]; + break; + } + + const promise = performUpdate + ? updateCsPromise({ + id_cs: mappingConnectionStrategies[0].id_connection_strategy, + updateToggle: false, + status: mappingConnectionStrategies[0].status, + attributes, + values: attributeValues, + }) + : createCsPromise({ + type: providerToType(item?.name!, item?.vertical!, item?.authStrategy.strategy!), + attributes, + values: attributeValues, + }); + + toast.promise(promise, { + loading: 'Saving changes...', + success: (data: any) => { + queryClient.setQueryData( + ['connection-strategies'], + (oldData = []) => performUpdate + ? oldData.map(cs => cs.id_connection_strategy === data.id_connection_strategy ? data : cs) + : [...oldData, data] + ); + return "Changes saved successfully"; + }, + error: (err: any) => err.message || 'An error occurred', + }); + + posthog?.capture(`Connection_strategy_${item?.authStrategy.strategy}_${performUpdate ? 'updated' : 'created'}`, { + id_project: idProject, + mode: config.DISTRIBUTION + }); + + form.reset(); + } + + useEffect(() => { + if (mappingConnectionStrategies && mappingConnectionStrategies.length > 0) { + const attributes = + item?.authStrategy.strategy === AuthStrategy.oauth2 + ? [...(needsSubdomain(item.name.toLowerCase(), item.vertical!.toLowerCase()) ? ["subdomain"] : []), + "client_id", "client_secret", + ...(needsScope(item.name.toLowerCase(), item.vertical!.toLowerCase()) ? ["scope"] : []), + ...oauthAttributes] + : item?.authStrategy.strategy === AuthStrategy.api_key + ? ["api_key"] + : ["username", "secret"]; + + fetchCredentials({ type: mappingConnectionStrategies[0].type, attributes }, { + onSuccess(data) { + let i = 0; + if (item?.authStrategy.strategy === AuthStrategy.oauth2) { + if (needsSubdomain(item.name.toLowerCase(), item.vertical?.toLowerCase()!)) { + form.setValue("subdomain", data[i++]); + } + form.setValue("client_id", data[i++]); + form.setValue("client_secret", data[i++]); + if (needsScope(item.name.toLowerCase(), item.vertical?.toLowerCase()!)) { + form.setValue("scope", data[i++]); + } + oauthAttributes.forEach((attr: string) => { + form.setValue(attr as keyof z.infer, data[i++]); + }); + } else if (item?.authStrategy.strategy === AuthStrategy.api_key) { + form.setValue("api_key", data[0]); + } else if (item?.authStrategy.strategy === AuthStrategy.basic) { + form.setValue("username", data[0]); + form.setValue("secret", data[1]); + } + setSwitchEnabled(mappingConnectionStrategies[0].status === true); + } + }); + } else { + form.reset(); + setSwitchEnabled(false); + } + }, [connectionStrategies, item]); + + const handleSwitchChange = (enabled: boolean) => { + if (mappingConnectionStrategies && mappingConnectionStrategies.length > 0) { + const dataToUpdate = mappingConnectionStrategies[0]; + toast.promise( + updateCsPromise({ + id_cs: dataToUpdate.id_connection_strategy, + updateToggle: true + }), + { + loading: 'Updating status...', + success: (data: any) => { + queryClient.setQueryData(['connection-strategies'], (oldData = []) => + oldData.map(cs => cs.id_connection_strategy === data.id_connection_strategy ? data : cs) + ); + return "Status updated successfully"; + }, + error: (err: any) => err.message || 'An error occurred', + } + ); + setSwitchEnabled(enabled); + } + }; + + return ( +
+ + {item ? ( +
+
+
+ {item.name} +
+
{`${item.name.substring(0, 1).toUpperCase()}${item.name.substring(1)}`}
+
{item.description}
+ {mappingConnectionStrategies && mappingConnectionStrategies.length > 0 && ( +
+ +
+ )} +
+
+
+ +
+
+ + {item.authStrategy.strategy === AuthStrategy.oauth2 && ( + <> + {needsSubdomain(item.name.toLowerCase(), item.vertical!.toLowerCase()) && ( + ( + + Subdomain + + + + + + )} + /> + )} + ( + + Client ID + + + + + + )} + /> + ( + + Client Secret + + + + + + )} + /> + ( + + Scopes + + + + + + )} + /> + {oauthAttributes.map((attr: string) => ( + } + control={form.control} + render={({ field }) => ( + + {attr} + + + + + + )} + /> + ))} +
+ Redirect URI +
+ + +
+
+ + )} + {item.authStrategy.strategy === AuthStrategy.api_key && ( + ( + + API Key + + + + + + )} + /> + )} + {item.authStrategy.strategy === AuthStrategy.basic && ( + <> + ( + + Username + + + + + + )} + /> + ( + + Secret + + + + + + )} + /> + + )} + + + +
+ +
+ ) : ( +
+ No connector selected +
+ )} +
+ ); +} \ No newline at end of file diff --git a/apps/client-ts/src/components/Configuration/Connector/ConnectorLayout.tsx b/apps/webapp/src/components/Configuration/Connector/ConnectorLayout.tsx similarity index 100% rename from apps/client-ts/src/components/Configuration/Connector/ConnectorLayout.tsx rename to apps/webapp/src/components/Configuration/Connector/ConnectorLayout.tsx diff --git a/apps/client-ts/src/components/Configuration/Connector/ConnectorList.tsx b/apps/webapp/src/components/Configuration/Connector/ConnectorList.tsx similarity index 92% rename from apps/client-ts/src/components/Configuration/Connector/ConnectorList.tsx rename to apps/webapp/src/components/Configuration/Connector/ConnectorList.tsx index 8912954ce..092de227c 100644 --- a/apps/client-ts/src/components/Configuration/Connector/ConnectorList.tsx +++ b/apps/webapp/src/components/Configuration/Connector/ConnectorList.tsx @@ -56,9 +56,9 @@ export function ConnectorList({ items }: ConnectorListProps) { {item.vertical} } - {item.authStrategy && - - {item.authStrategy} + {item.authStrategy.strategy && + + {item.authStrategy.strategy} } diff --git a/apps/client-ts/src/components/Configuration/Connector/CustomConnectorPage.tsx b/apps/webapp/src/components/Configuration/Connector/CustomConnectorPage.tsx similarity index 100% rename from apps/client-ts/src/components/Configuration/Connector/CustomConnectorPage.tsx rename to apps/webapp/src/components/Configuration/Connector/CustomConnectorPage.tsx diff --git a/apps/client-ts/src/components/Configuration/Connector/VerticalSelector.tsx b/apps/webapp/src/components/Configuration/Connector/VerticalSelector.tsx similarity index 100% rename from apps/client-ts/src/components/Configuration/Connector/VerticalSelector.tsx rename to apps/webapp/src/components/Configuration/Connector/VerticalSelector.tsx diff --git a/apps/client-ts/src/components/Configuration/Connector/useConnector.tsx b/apps/webapp/src/components/Configuration/Connector/useConnector.tsx similarity index 100% rename from apps/client-ts/src/components/Configuration/Connector/useConnector.tsx rename to apps/webapp/src/components/Configuration/Connector/useConnector.tsx diff --git a/apps/client-ts/src/components/Configuration/DragDrop.tsx b/apps/webapp/src/components/Configuration/DragDrop.tsx similarity index 100% rename from apps/client-ts/src/components/Configuration/DragDrop.tsx rename to apps/webapp/src/components/Configuration/DragDrop.tsx diff --git a/apps/client-ts/src/components/Configuration/FieldMappings/FieldMappingsTable.tsx b/apps/webapp/src/components/Configuration/FieldMappings/FieldMappingsTable.tsx similarity index 100% rename from apps/client-ts/src/components/Configuration/FieldMappings/FieldMappingsTable.tsx rename to apps/webapp/src/components/Configuration/FieldMappings/FieldMappingsTable.tsx diff --git a/apps/client-ts/src/components/Configuration/FieldMappings/Stepper/stepper-form.tsx b/apps/webapp/src/components/Configuration/FieldMappings/Stepper/stepper-form.tsx similarity index 100% rename from apps/client-ts/src/components/Configuration/FieldMappings/Stepper/stepper-form.tsx rename to apps/webapp/src/components/Configuration/FieldMappings/Stepper/stepper-form.tsx diff --git a/apps/client-ts/src/components/Configuration/FieldMappings/columns.tsx b/apps/webapp/src/components/Configuration/FieldMappings/columns.tsx similarity index 100% rename from apps/client-ts/src/components/Configuration/FieldMappings/columns.tsx rename to apps/webapp/src/components/Configuration/FieldMappings/columns.tsx diff --git a/apps/client-ts/src/components/Configuration/FieldMappings/defineForm.tsx b/apps/webapp/src/components/Configuration/FieldMappings/defineForm.tsx similarity index 100% rename from apps/client-ts/src/components/Configuration/FieldMappings/defineForm.tsx rename to apps/webapp/src/components/Configuration/FieldMappings/defineForm.tsx diff --git a/apps/client-ts/src/components/Configuration/FieldMappings/mapForm.tsx b/apps/webapp/src/components/Configuration/FieldMappings/mapForm.tsx similarity index 100% rename from apps/client-ts/src/components/Configuration/FieldMappings/mapForm.tsx rename to apps/webapp/src/components/Configuration/FieldMappings/mapForm.tsx diff --git a/apps/client-ts/src/components/Configuration/FieldMappings/schema.ts b/apps/webapp/src/components/Configuration/FieldMappings/schema.ts similarity index 100% rename from apps/client-ts/src/components/Configuration/FieldMappings/schema.ts rename to apps/webapp/src/components/Configuration/FieldMappings/schema.ts diff --git a/apps/client-ts/src/components/Configuration/LinkedUsers/AddLinkedAccount.tsx b/apps/webapp/src/components/Configuration/LinkedUsers/AddLinkedAccount.tsx similarity index 100% rename from apps/client-ts/src/components/Configuration/LinkedUsers/AddLinkedAccount.tsx rename to apps/webapp/src/components/Configuration/LinkedUsers/AddLinkedAccount.tsx diff --git a/apps/client-ts/src/components/Configuration/LinkedUsers/LinkedUsersPage.tsx b/apps/webapp/src/components/Configuration/LinkedUsers/LinkedUsersPage.tsx similarity index 100% rename from apps/client-ts/src/components/Configuration/LinkedUsers/LinkedUsersPage.tsx rename to apps/webapp/src/components/Configuration/LinkedUsers/LinkedUsersPage.tsx diff --git a/apps/client-ts/src/components/Configuration/LinkedUsers/columns.tsx b/apps/webapp/src/components/Configuration/LinkedUsers/columns.tsx similarity index 100% rename from apps/client-ts/src/components/Configuration/LinkedUsers/columns.tsx rename to apps/webapp/src/components/Configuration/LinkedUsers/columns.tsx diff --git a/apps/client-ts/src/components/Configuration/LinkedUsers/schema.ts b/apps/webapp/src/components/Configuration/LinkedUsers/schema.ts similarity index 100% rename from apps/client-ts/src/components/Configuration/LinkedUsers/schema.ts rename to apps/webapp/src/components/Configuration/LinkedUsers/schema.ts diff --git a/apps/client-ts/src/components/Configuration/NavMenu.tsx b/apps/webapp/src/components/Configuration/NavMenu.tsx similarity index 100% rename from apps/client-ts/src/components/Configuration/NavMenu.tsx rename to apps/webapp/src/components/Configuration/NavMenu.tsx diff --git a/apps/client-ts/src/components/Configuration/Webhooks/AddWebhook.tsx b/apps/webapp/src/components/Configuration/Webhooks/AddWebhook.tsx similarity index 99% rename from apps/client-ts/src/components/Configuration/Webhooks/AddWebhook.tsx rename to apps/webapp/src/components/Configuration/Webhooks/AddWebhook.tsx index 9323c7975..b3537072a 100644 --- a/apps/client-ts/src/components/Configuration/Webhooks/AddWebhook.tsx +++ b/apps/webapp/src/components/Configuration/Webhooks/AddWebhook.tsx @@ -80,7 +80,6 @@ const AddWebhook = () => { createWebhookPromise({ url: values.url, description: values.description, - id_project: idProject, scope: selectedScopes, }), { diff --git a/apps/client-ts/src/components/Configuration/Webhooks/WebhooksPage.tsx b/apps/webapp/src/components/Configuration/Webhooks/WebhooksPage.tsx similarity index 100% rename from apps/client-ts/src/components/Configuration/Webhooks/WebhooksPage.tsx rename to apps/webapp/src/components/Configuration/Webhooks/WebhooksPage.tsx diff --git a/apps/client-ts/src/components/Configuration/Webhooks/columns.tsx b/apps/webapp/src/components/Configuration/Webhooks/columns.tsx similarity index 100% rename from apps/client-ts/src/components/Configuration/Webhooks/columns.tsx rename to apps/webapp/src/components/Configuration/Webhooks/columns.tsx diff --git a/apps/client-ts/src/components/Configuration/Webhooks/schema.ts b/apps/webapp/src/components/Configuration/Webhooks/schema.ts similarity index 100% rename from apps/client-ts/src/components/Configuration/Webhooks/schema.ts rename to apps/webapp/src/components/Configuration/Webhooks/schema.ts diff --git a/apps/client-ts/src/components/Connection/AddConnectionButton.tsx b/apps/webapp/src/components/Connection/AddConnectionButton.tsx similarity index 100% rename from apps/client-ts/src/components/Connection/AddConnectionButton.tsx rename to apps/webapp/src/components/Connection/AddConnectionButton.tsx diff --git a/apps/client-ts/src/components/Connection/ConnectionTable.tsx b/apps/webapp/src/components/Connection/ConnectionTable.tsx similarity index 92% rename from apps/client-ts/src/components/Connection/ConnectionTable.tsx rename to apps/webapp/src/components/Connection/ConnectionTable.tsx index 13f0aa700..539204a83 100644 --- a/apps/client-ts/src/components/Connection/ConnectionTable.tsx +++ b/apps/webapp/src/components/Connection/ConnectionTable.tsx @@ -42,13 +42,12 @@ export default function ConnectionTable() { if (error) { console.log("error connections.."); } - + const linkedConnections = (filter: string) => connections?.filter((connection) => connection.status == filter); - //console.log("connections are => "+ JSON.stringify(connections)) const ts = connections?.map((connection) => ({ organisation: nameOrg, app: connection.provider_slug, - vertical: connection.vertical, + vertical: connection.vertical, category: connection.token_type, status: connection.status, linkedUser: connection.id_linked_user, @@ -56,7 +55,13 @@ export default function ConnectionTable() { connectionToken: connection.connection_token! })) - + let link: string; + if(config.DISTRIBUTION == 'selfhost' && config.REDIRECT_WEBHOOK_INGRESS) { + link = `${config.MAGIC_LINK_DOMAIN}/?uniqueLink=${uniqueLink}&redirectIngressUri=${config.REDIRECT_WEBHOOK_INGRESS}` + }else{ + link = `${config.MAGIC_LINK_DOMAIN}/?uniqueLink=${uniqueLink}` + } + return ( <>
@@ -115,7 +120,7 @@ export default function ConnectionTable() {
- @@ -125,7 +130,7 @@ export default function ConnectionTable() { } - {ts && } + {ts && } diff --git a/apps/client-ts/src/components/Connection/CopyLinkInput.tsx b/apps/webapp/src/components/Connection/CopyLinkInput.tsx similarity index 91% rename from apps/client-ts/src/components/Connection/CopyLinkInput.tsx rename to apps/webapp/src/components/Connection/CopyLinkInput.tsx index 538861246..1946bf6b5 100644 --- a/apps/client-ts/src/components/Connection/CopyLinkInput.tsx +++ b/apps/webapp/src/components/Connection/CopyLinkInput.tsx @@ -13,9 +13,16 @@ const CopyLinkInput = () => { const {uniqueLink} = useMagicLinkStore(); + let link: string; + if(config.DISTRIBUTION == 'selfhost' && config.REDIRECT_WEBHOOK_INGRESS) { + link = `${config.MAGIC_LINK_DOMAIN}/?uniqueLink=${uniqueLink}&redirectIngressUri=${config.REDIRECT_WEBHOOK_INGRESS}` + }else{ + link = `${config.MAGIC_LINK_DOMAIN}/?uniqueLink=${uniqueLink}` + } + const handleCopy = async () => { try { - await navigator.clipboard.writeText(`${config.MAGIC_LINK_DOMAIN}/?uniqueLink=${uniqueLink}`); + await navigator.clipboard.writeText(link); toast.success("Magic link copied", { action: { label: "Close", @@ -34,7 +41,7 @@ const CopyLinkInput = () => { {uniqueLink !== 'https://' ? <> diff --git a/apps/client-ts/src/components/Connection/LoadingSpinner.tsx b/apps/webapp/src/components/Connection/LoadingSpinner.tsx similarity index 100% rename from apps/client-ts/src/components/Connection/LoadingSpinner.tsx rename to apps/webapp/src/components/Connection/LoadingSpinner.tsx diff --git a/apps/webapp/src/components/Connection/columns.tsx b/apps/webapp/src/components/Connection/columns.tsx new file mode 100644 index 000000000..96982c994 --- /dev/null +++ b/apps/webapp/src/components/Connection/columns.tsx @@ -0,0 +1,270 @@ +"use client" + +import { ColumnDef } from "@tanstack/react-table" +import { Badge } from "@/components/ui/badge" +import { Connection } from "./schema" +import { DataTableColumnHeader } from "../shared/data-table-column-header" +import React from "react" +import { ClipboardIcon } from '@radix-ui/react-icons' +import { toast } from "sonner" +import { getLogoURL } from "@panora/shared" +import { formatISODate, truncateMiddle } from "@/lib/utils" +import { Button } from "../ui/button" +import { Label } from "../ui/label" +import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover" +import useResync from "@/hooks/create/useResync" + +const connectionTokenComponent = ({row}:{row:any}) => { + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(row.getValue("connectionToken")); + toast.success("Connection token copied", { + action: { + label: "Close", + onClick: () => console.log("Close"), + }, + }) + } catch (err) { + console.error('Failed to copy: ', err); + } + }; + + return ( +
+ {truncateMiddle(row.getValue("connectionToken"),6)} + + +
+ ) +} + +const Customizer = ({ + logo, + name, + vertical, + lastSync, + authStrategy, + linkedUserId, + connectionToken, +}: { + logo: string; + name: string; + vertical: string; + lastSync: string; + authStrategy: string; + linkedUserId: string; + connectionToken: string; +}) => { + + const { resyncPromise } = useResync(); + + const handleResync = async (vertical: string, name: string, linkedUserId: string) => { + try { + toast.promise( + resyncPromise({ + vertical, + provider: name, + linkedUserId + }), + { + loading: 'Loading...', + success: (data: any) => { + return ( +
+ +
+ Resync for {vertical}:{name} initiated +
+
+ ) + ; + }, + error: (err: any) => err.message || 'Error' + }); + + /*posthog?.capture("resync_init", { + id_project: idProject, + mode: config.DISTRIBUTION + })*/ + } catch (err) { + console.error('Failed to resync: ', err); + } + }; + + return ( +
+
+
+
+ +
+ {`${name.substring(0, 1).toUpperCase()}${name.substring(1)}`} + {authStrategy} +
+
+
+
+
+
+ + {lastSync} +
+
+ + {truncateMiddle(connectionToken,6)} +
+
+ + {truncateMiddle(linkedUserId,6)} +
+
+
+ + +
+
+ + ) +} + +export const columns: ColumnDef[] = [ + { + accessorKey: "app", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + const provider = (row.getValue("app") as string).toLowerCase(); + return ( +
+ + + {provider} + +
+ ) + }, + }, + { + accessorKey: "category", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + return ( +
+ {/*status.icon && ( + + )*/} + {row.getValue("category")} +
+ ) + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)) + }, + }, + { + accessorKey: "vertical", + header: ({ column }) => ( + + ), + cell: ({ row }) =>
{row.getValue("vertical") as string}
, + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "linkedUser", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + /*const status = statuses.find( + (status) => status.value === row.getValue("linkedUser") + ) + + if (!status) { + return null + }*/ + + return ( +
+ {truncateMiddle(row.getValue("linkedUser"), 10)} +
+ ) + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)) + }, + }, + { + accessorKey: "date", + header: ({ column }) => ( + + ), + cell: ({ row }) => { + //const label = labels.find((label) => label.value === row.original.date) + + return ( +
+ {formatISODate(row.getValue("date"))} +
+ ) + }, + }, + { + accessorKey: "connectionToken", + header: ({ column }) => ( + + ), + cell: connectionTokenComponent + }, + { + accessorKey: "status", + header: ({ column }) => ( + '' + ), + cell: ({ row }) => { + return ( +
+ + + + + + + + +
+ ) + }, + filterFn: (row, id, value) => { + return value.includes(row.getValue(id)) + }, + } +]; \ No newline at end of file diff --git a/apps/client-ts/src/components/Connection/schema.ts b/apps/webapp/src/components/Connection/schema.ts similarity index 100% rename from apps/client-ts/src/components/Connection/schema.ts rename to apps/webapp/src/components/Connection/schema.ts diff --git a/apps/client-ts/src/components/Events/EventsTable.tsx b/apps/webapp/src/components/Events/EventsTable.tsx similarity index 97% rename from apps/client-ts/src/components/Events/EventsTable.tsx rename to apps/webapp/src/components/Events/EventsTable.tsx index 88befe029..680d91197 100644 --- a/apps/client-ts/src/components/Events/EventsTable.tsx +++ b/apps/webapp/src/components/Events/EventsTable.tsx @@ -18,7 +18,7 @@ export default function EventsTable() { error, } = useEvents({ page: pagination.page, - pageSize: pagination.pageSize, + limit: pagination.limit, }); const transformedEvents = events?.map((event: Event) => ({ diff --git a/apps/client-ts/src/components/Events/columns.tsx b/apps/webapp/src/components/Events/columns.tsx similarity index 100% rename from apps/client-ts/src/components/Events/columns.tsx rename to apps/webapp/src/components/Events/columns.tsx diff --git a/apps/client-ts/src/components/Events/schema.ts b/apps/webapp/src/components/Events/schema.ts similarity index 100% rename from apps/client-ts/src/components/Events/schema.ts rename to apps/webapp/src/components/Events/schema.ts diff --git a/apps/client-ts/src/components/Nav/main-nav-sm.tsx b/apps/webapp/src/components/Nav/main-nav-sm.tsx similarity index 100% rename from apps/client-ts/src/components/Nav/main-nav-sm.tsx rename to apps/webapp/src/components/Nav/main-nav-sm.tsx diff --git a/apps/webapp/src/components/Nav/main-nav.tsx b/apps/webapp/src/components/Nav/main-nav.tsx new file mode 100644 index 000000000..7732ae037 --- /dev/null +++ b/apps/webapp/src/components/Nav/main-nav.tsx @@ -0,0 +1,128 @@ +'use client' + +import { BookOpen, ExternalLink, KeyRound, Scroll, Settings2, Share2, SquareGantt } from "lucide-react"; +import { usePathname } from "next/navigation"; +import { MouseEvent, ReactNode, useEffect, useState } from "react"; + +interface MainNavProps { + onLinkClick: (name: string) => void; + className: string; +} + +interface NavLinkProps { + name: string; + content: ReactNode; + onClick: (name: string) => void; + isSelected: boolean; + isExternal?: boolean; + href?: string; +} + +export function NavLink({ + name, + content, + onClick, + isSelected, + isExternal = false, + href = '#' +}: NavLinkProps): JSX.Element { + const navItemClassName = `group flex gap-1 items-center rounded-md px-2 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground cursor-pointer ${isSelected ? 'bg-accent' : 'transparent' + } transition-colors`; + + function handleClick(e: MouseEvent): void { + if (!isExternal) { + e.preventDefault(); + onClick(name); + } + } + + return ( + + {content} + {isExternal && } + + ); +} + +export function MainNav({ + onLinkClick, + className, + ...props +}: MainNavProps) { + const [selectedItem, setSelectedItem] = useState(""); + const pathname = usePathname(); + + useEffect(() => { + setSelectedItem(pathname.substring(1)) + }, [pathname]) + + return ( + + ); +} + +const navItems: Omit[] = [ + { + name: 'connections', + content: ( + <> + Connections + + ), + }, + { + name: 'events', + content: ( + <> + Events + + ), + }, + { + name: 'configuration', + content: ( + <> + Configuration + + ), + }, + { + name: 'api-keys', + content: ( + <> + API Keys + + ), + }, + { + name: 'docs', + content: ( + <> +

Docs

+ + ), + isExternal: true, + href: "https://docs.panora.dev/", + }, +]; diff --git a/apps/client-ts/src/components/Nav/theme-provider.tsx b/apps/webapp/src/components/Nav/theme-provider.tsx similarity index 100% rename from apps/client-ts/src/components/Nav/theme-provider.tsx rename to apps/webapp/src/components/Nav/theme-provider.tsx diff --git a/apps/client-ts/src/components/Nav/theme-toggle.tsx b/apps/webapp/src/components/Nav/theme-toggle.tsx similarity index 100% rename from apps/client-ts/src/components/Nav/theme-toggle.tsx rename to apps/webapp/src/components/Nav/theme-toggle.tsx diff --git a/apps/client-ts/src/components/Nav/user-nav.tsx b/apps/webapp/src/components/Nav/user-nav.tsx similarity index 100% rename from apps/client-ts/src/components/Nav/user-nav.tsx rename to apps/webapp/src/components/Nav/user-nav.tsx diff --git a/apps/client-ts/src/components/Provider/provider.tsx b/apps/webapp/src/components/Provider/provider.tsx similarity index 100% rename from apps/client-ts/src/components/Provider/provider.tsx rename to apps/webapp/src/components/Provider/provider.tsx diff --git a/apps/client-ts/src/components/RootLayout/index.tsx b/apps/webapp/src/components/RootLayout/index.tsx similarity index 98% rename from apps/client-ts/src/components/RootLayout/index.tsx rename to apps/webapp/src/components/RootLayout/index.tsx index 64f90ff34..4ea5f5493 100644 --- a/apps/client-ts/src/components/RootLayout/index.tsx +++ b/apps/webapp/src/components/RootLayout/index.tsx @@ -97,7 +97,6 @@ export const RootLayout = ({children}:{children:React.ReactNode}) => {
-

Project

Rows per page