├── .env.example ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── c4gt.md │ ├── feature_request.md │ └── task.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── dalgo-cd.yml │ ├── dalgo-ci.yml │ └── dalgo-docker-release.yml ├── .gitignore ├── .husky └── pre-commit ├── .prettierrc.json ├── CODE_OF_CONDUCT.md ├── Docker ├── .dockerignore ├── .env.example ├── Dockerfile ├── Dockerfile.dev ├── docker-compose.dev.yaml └── docker-compose.yaml ├── LICENSE ├── README.md ├── cypress.config.ts ├── cypress.env.template.json ├── cypress ├── e2e │ ├── Notifications │ │ └── Notifications.cy.ts │ ├── Settings │ │ └── UserManagement.cy.ts │ ├── auth │ │ ├── Logout.cy.ts │ │ ├── Signup.cy.ts │ │ └── login.cy.ts │ ├── dataAnalysis │ │ └── dataAnalysis.cy.ts │ ├── header │ │ └── Header.cy.ts │ ├── ingest │ │ ├── AddSource.cy.ts │ │ ├── Connection.cy.ts │ │ ├── Ingest.cy.ts │ │ └── Orchestrate.cy.ts │ └── sidemenu │ │ └── Sidemenu.cy.ts ├── fixtures │ └── profile.json ├── support │ ├── commands.ts │ ├── component-index.html │ ├── component.ts │ └── e2e.ts └── tsconfig.json ├── docker-build.sh ├── docker-compose.sh ├── jest.config.ts ├── jest.setup.ts ├── next.config.js ├── package.json ├── public ├── favicon_new.png ├── favicon_new.svg ├── loginbanner.png ├── next.svg ├── thirteen.svg └── vercel.svg ├── sentry.client.config.ts ├── sentry.edge.config.ts ├── sentry.server.config.ts ├── src ├── assets │ ├── icons │ │ ├── UI4T │ │ │ ├── aggregate.svg │ │ │ ├── arithmetic.svg │ │ │ ├── case.svg │ │ │ ├── cast.svg │ │ │ ├── coalesce.svg │ │ │ ├── concat.svg │ │ │ ├── drop.svg │ │ │ ├── filter.svg │ │ │ ├── flatten.svg │ │ │ ├── generic.svg │ │ │ ├── groupby.svg │ │ │ ├── join.svg │ │ │ ├── pivot.svg │ │ │ ├── rename.svg │ │ │ ├── replace.svg │ │ │ ├── union.svg │ │ │ └── unpivot.svg │ │ ├── UsageIcon.tsx │ │ ├── aianalysis.tsx │ │ ├── aidataAnalysis.tsx │ │ ├── aisettings.tsx │ │ ├── analysis.tsx │ │ ├── arrow_back_ios.svg │ │ ├── cancel.tsx │ │ ├── check-large.svg │ │ ├── check.svg │ │ ├── connection.svg │ │ ├── content_copy.svg │ │ ├── dalgoIcon.svg │ │ ├── dataQuality.tsx │ │ ├── datatable.svg │ │ ├── delete.png │ │ ├── delete.svg │ │ ├── drag.svg │ │ ├── edit.svg │ │ ├── elementary.svg │ │ ├── explore.tsx │ │ ├── flow.svg │ │ ├── folder.svg │ │ ├── hamburger.svg │ │ ├── info.svg │ │ ├── ingest.tsx │ │ ├── logout.svg │ │ ├── manage_accounts.tsx │ │ ├── notifications.tsx │ │ ├── notifications_unread.tsx │ │ ├── orchestrate.tsx │ │ ├── pipeline.tsx │ │ ├── profile.svg │ │ ├── settings.tsx │ │ ├── success.svg │ │ ├── switch-chart.svg │ │ ├── switch-filter.svg │ │ ├── sync.svg │ │ ├── thumb_up (1).svg │ │ ├── thumb_up.svg │ │ └── transform.tsx │ └── images │ │ ├── airbyte.ico │ │ ├── airbytelogo.webp │ │ ├── banner.png │ │ ├── dbt.png │ │ ├── github_transform.png │ │ ├── logo.svg │ │ ├── old_logo.svg │ │ ├── pattern.png │ │ ├── prefect-logo-black.png │ │ ├── superset.png │ │ ├── supersetlogo.png │ │ └── ui_transform.png ├── components │ ├── Charts │ │ ├── BarChart.tsx │ │ ├── DateTimeInsights.tsx │ │ ├── NumberInsights.tsx │ │ ├── RangeChart.tsx │ │ ├── StatsChart.tsx │ │ ├── StringInsights.tsx │ │ └── __tests__ │ │ │ ├── BarChart.test.tsx │ │ │ ├── DateTimeInsights.test.tsx │ │ │ ├── NumberInsights.test.tsx │ │ │ ├── RangeChart.test.tsx │ │ │ ├── StatsChart.test.tsx │ │ │ └── StringInsights.test.tsx │ ├── ConfigInput │ │ ├── ConfigInput.tsx │ │ └── __tests__ │ │ │ └── ConfigInput.test.tsx │ ├── Connections │ │ ├── ConnectionSyncHistory.tsx │ │ ├── Connections.tsx │ │ ├── CreateConnectionForm.tsx │ │ ├── PendingActions.tsx │ │ ├── SchemaChangeDetailsForm.tsx │ │ └── __tests__ │ │ │ ├── ClearConnection.test.tsx │ │ │ ├── ConnectionSyncHistory.test.tsx │ │ │ ├── Connections.test.tsx │ │ │ ├── CreateConnection.test.tsx │ │ │ ├── DeleteConnection.test.tsx │ │ │ ├── EditConnection.test.tsx │ │ │ ├── QueueTooltip.test.tsx │ │ │ ├── RefreshConnection.test.tsx │ │ │ ├── SchemaChangeDetailsForm.test.tsx │ │ │ └── SyncConnection.test.tsx │ ├── DBT │ │ ├── CreateOrgTaskForm.tsx │ │ ├── DBTDocs.tsx │ │ ├── DBTSetup.tsx │ │ ├── DBTTarget.tsx │ │ ├── DBTTaskList.tsx │ │ ├── DBTTransformType.tsx │ │ ├── Elementary.tsx │ │ └── __tests__ │ │ │ ├── CreateOrgTaskForm.test.tsx │ │ │ ├── CreateWorkspace.test.tsx │ │ │ ├── DBTDocs.test.tsx │ │ │ ├── DBTTaskList.test.tsx │ │ │ ├── DBTTransformType.test.tsx │ │ │ ├── EditWorkspace.test.tsx │ │ │ ├── Elementary.test.tsx │ │ │ └── ExecuteDbtJobs.test.tsx │ ├── DataAnalysis │ │ ├── DeactivatedMsg.tsx │ │ ├── Disclaimer.tsx │ │ ├── LLMSummary.tsx │ │ ├── OverwriteBox.tsx │ │ ├── SavedSession.tsx │ │ ├── SqlWrite.tsx │ │ ├── TopBar.tsx │ │ └── __tests__ │ │ │ ├── DeactivatedMsg.test.tsx │ │ │ ├── Disclaimer.test.tsx │ │ │ ├── SavedSession.test.tsx │ │ │ ├── SqlWrite.test.tsx │ │ │ └── TopBar.test.tsx │ ├── Destinations │ │ ├── DestinationForm.tsx │ │ ├── Destinations.tsx │ │ ├── __tests__ │ │ │ ├── CreateDestinationForm.test.tsx │ │ │ ├── Destinations.test.tsx │ │ │ └── EditDestinationForm.test.tsx │ │ └── helpers.ts │ ├── Dialog │ │ ├── ConfirmationDialog.tsx │ │ ├── CustomDialog.tsx │ │ └── __tests__ │ │ │ ├── ConfirmationDialog.test.tsx │ │ │ └── CustomDialog.test.tsx │ ├── Explore │ │ ├── Explore.tsx │ │ └── __tests__ │ │ │ └── Explore.test.tsx │ ├── Flows │ │ ├── FlowCreate.tsx │ │ ├── FlowLogs.tsx │ │ ├── FlowRunHistory.tsx │ │ ├── Flows.tsx │ │ ├── SingleFlowRunHistory.tsx │ │ ├── TaskSequence.tsx │ │ └── __tests__ │ │ │ ├── FlowCreate.test.tsx │ │ │ ├── FlowLogs.test.tsx │ │ │ ├── FlowRunHistory.test.tsx │ │ │ ├── Flows.test.tsx │ │ │ ├── SingleFlowRunHistory.test.tsx │ │ │ └── TaskSequence.test.tsx │ ├── Header │ │ ├── Header.module.css │ │ ├── Header.test.tsx │ │ └── Header.tsx │ ├── Invitations │ │ ├── Invitations.tsx │ │ ├── InviteUserForm.tsx │ │ └── __tests__ │ │ │ ├── Invitations.test.tsx │ │ │ └── InviteUserForm.test.tsx │ ├── Layouts │ │ ├── Auth.tsx │ │ ├── Main.tsx │ │ └── __tests__ │ │ │ └── Main.test.tsx │ ├── List │ │ ├── List.tsx │ │ └── __tests__ │ │ │ └── List.test.tsx │ ├── Logs │ │ ├── LogCard.tsx │ │ ├── LogSummaryBlock.tsx │ │ ├── LogSummaryCard.tsx │ │ └── __tests__ │ │ │ └── LogSummaryBlock.test.tsx │ ├── MultiTagInput.test.tsx │ ├── MultiTagInput.tsx │ ├── Notifications │ │ ├── ManageNotificaitons.tsx │ │ ├── Notifications.module.css │ │ ├── PreferencesForm.tsx │ │ └── __tests__ │ │ │ ├── ManageNotifications.test.tsx │ │ │ └── PreferencesForm.test.tsx │ ├── Org │ │ ├── CreateOrgForm.tsx │ │ └── __tests__ │ │ │ └── CreateOrgForm.test.tsx │ ├── PageHead.tsx │ ├── ProductWalk │ │ ├── ProductWalk.tsx │ │ └── WalkThroughContent.tsx │ ├── Settings │ │ ├── AI_settings │ │ │ ├── AiEnablePanel.tsx │ │ │ └── __tests__ │ │ │ │ └── AiEnablePanel.test.tsx │ │ ├── ServicesInfo.tsx │ │ ├── SubscriptionInfo.tsx │ │ └── __tests__ │ │ │ ├── ServicesInfo.test.tsx │ │ │ └── SubscriptionInfo.test.tsx │ ├── SideDrawer │ │ ├── SideDrawer.teststofix.tsx_ │ │ ├── SideDrawer.tsx │ │ └── __tests__ │ │ │ └── SideDrawer.test.tsx │ ├── Sources │ │ ├── SourceForm.tsx │ │ ├── Sources.tsx │ │ └── __tests__ │ │ │ ├── CreateSourceForm.test.tsx │ │ │ ├── EditSourceForm.test.tsx │ │ │ └── Sources.test.tsx │ ├── ToastMessage │ │ ├── ToastHelper.ts │ │ ├── ToastMessage.tsx │ │ └── __tests__ │ │ │ └── ToastMessage.test.tsx │ ├── TransformWorkflow │ │ └── FlowEditor │ │ │ ├── Components │ │ │ ├── Canvas.tsx │ │ │ ├── InfoBox.tsx │ │ │ ├── LowerSectionTabs │ │ │ │ ├── LogsPane.tsx │ │ │ │ ├── PreviewPane.tsx │ │ │ │ ├── StatisticsPane.tsx │ │ │ │ └── __tests__ │ │ │ │ │ ├── LogsPane.test.tsx │ │ │ │ │ ├── PreviewPane.test.tsx │ │ │ │ │ └── StatisticsPane.test.tsx │ │ │ ├── Nodes │ │ │ │ ├── DbtSourceModelNode.tsx │ │ │ │ ├── OperationNode.tsx │ │ │ │ └── __tests__ │ │ │ │ │ ├── DbtSourceModelNode.test.tsx │ │ │ │ │ └── OperationNode.test.tsx │ │ │ ├── OperationConfigLayout.test.tsx │ │ │ ├── OperationConfigLayout.tsx │ │ │ ├── OperationPanel │ │ │ │ ├── CreateTableOrAddFunction.tsx │ │ │ │ └── Forms │ │ │ │ │ ├── AggregationOpForm.tsx │ │ │ │ │ ├── ArithmeticOpForm.tsx │ │ │ │ │ ├── CaseWhenOpForm.tsx │ │ │ │ │ ├── CastColumnOpForm.tsx │ │ │ │ │ ├── CoalesceOpForm.tsx │ │ │ │ │ ├── CreateTableForm.tsx │ │ │ │ │ ├── DropColumnOpForm.tsx │ │ │ │ │ ├── FlattenJsonOpForm.tsx │ │ │ │ │ ├── GenericColumnOpForm.tsx │ │ │ │ │ ├── GenericSqlOpForm.tsx │ │ │ │ │ ├── GroupByOpForm.tsx │ │ │ │ │ ├── JoinOpForm.tsx │ │ │ │ │ ├── PivotOpForm.tsx │ │ │ │ │ ├── RenameColumnOpForm.tsx │ │ │ │ │ ├── ReplaceValueOpForm.tsx │ │ │ │ │ ├── UnionTablesOpForm.tsx │ │ │ │ │ ├── UnpivotOpForm.tsx │ │ │ │ │ ├── WhereFilterOpForm.tsx │ │ │ │ │ └── __tests__ │ │ │ │ │ ├── AggregationOpForm.test.tsx │ │ │ │ │ ├── ArithmeticOpForm.test.tsx │ │ │ │ │ ├── CaseWhenOpForm.test.tsx │ │ │ │ │ ├── CastColumnOpForm.test.tsx │ │ │ │ │ ├── CoalesceOpForm.test.tsx │ │ │ │ │ ├── CreateTableForm.test.tsx │ │ │ │ │ ├── DropColumnOpForm.test.tsx │ │ │ │ │ ├── FlattenJsonOpForm.test.tsx │ │ │ │ │ ├── GenericColumnOpForm.test.tsx │ │ │ │ │ ├── GenericSqlOpForm.test.tsx │ │ │ │ │ ├── GroupByOpForm.test.tsx │ │ │ │ │ ├── JoinOpForm.test.tsx │ │ │ │ │ ├── PivotOpForm.test.tsx │ │ │ │ │ ├── RenameColumnOpForm.test.tsx │ │ │ │ │ ├── ReplaceValueOpForm.test.tsx │ │ │ │ │ ├── UnionTablesOpForm.test.tsx │ │ │ │ │ ├── UnpivotOpForm.test.tsx │ │ │ │ │ ├── WhereFilterOpForm.test.tsx │ │ │ │ │ └── helpers.ts │ │ │ ├── ProjectTree.tsx │ │ │ └── dummynodes.ts │ │ │ ├── FlowEditor.tsx │ │ │ ├── __tests__ │ │ │ └── FlowEditor.test.tsx │ │ │ └── constant.ts │ ├── UI │ │ ├── Autocomplete │ │ │ └── Autocomplete.tsx │ │ ├── GridTable │ │ │ └── GridTable.tsx │ │ ├── Input │ │ │ └── Input.tsx │ │ ├── Menu │ │ │ └── Menu.tsx │ │ └── Tooltip │ │ │ └── Tooltip.tsx │ ├── UserManagement │ │ ├── ManageUsers.tsx │ │ └── __tests__ │ │ │ └── ManageUsers.test.tsx │ └── Workflow │ │ └── Editor.tsx ├── config │ ├── constant.ts │ ├── menu.tsx │ └── theme.ts ├── contexts │ ├── ConnectionSyncLogsContext.tsx │ ├── ContextProvider.tsx │ ├── DbtRunLogsContext.tsx │ ├── FlowEditorCanvasContext.tsx │ ├── FlowEditorPreviewContext.tsx │ ├── TrackingContext.tsx │ └── reducers │ │ ├── CurrentOrgReducer.ts │ │ ├── OrgUsersReducer.ts │ │ ├── ToastReducer.ts │ │ └── UnsavedChangesReducer.ts ├── customHooks │ ├── useLockCanvas.tsx │ ├── useOpForm.tsx │ ├── useQueryParams.tsx │ └── useSyncLock.tsx ├── helpers │ ├── ConnectorConfigInput.tsx │ ├── http.tsx │ └── websocket.tsx ├── instrumentation.ts ├── middleware.ts ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── _error.jsx │ ├── analysis │ │ ├── data-analysis.tsx │ │ ├── index.tsx │ │ └── usage.tsx │ ├── api │ │ └── auth │ │ │ └── [...nextauth].ts │ ├── changepassword │ │ └── index.tsx │ ├── createorg │ │ └── index.tsx │ ├── data-quality.tsx │ ├── explore │ │ └── index.tsx │ ├── forgotpassword │ │ └── index.tsx │ ├── index.tsx │ ├── invitations │ │ └── index.tsx │ ├── login │ │ └── index.tsx │ ├── notifications │ │ └── index.tsx │ ├── pipeline │ │ ├── index.tsx │ │ ├── ingest.tsx │ │ ├── orchestrate.tsx │ │ └── transform.tsx │ ├── resetpassword │ │ └── index.tsx │ ├── settings │ │ ├── ai-settings.tsx │ │ ├── index.tsx │ │ └── user-management │ │ │ └── index.tsx │ ├── signup │ │ └── index.tsx │ ├── verifyemail │ │ ├── index.tsx │ │ └── resend.tsx │ └── wren │ │ └── index.tsx ├── styles │ ├── Common.module.css │ ├── Home.module.css │ ├── Login.module.css │ └── globals.css ├── tests │ ├── login.test.tsx │ └── signup.test.tsx └── utils │ ├── common.tsx │ ├── editor.tsx │ └── tests.tsx ├── tsconfig.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_BACKEND_URL="http://localhost:8002" 2 | NEXT_PUBLIC_WEBSOCKET_URL="ws://localhost:8002" 3 | NEXTAUTH_SECRET="" #https://generate-secret.vercel.app/32 4 | NEXTAUTH_URL="http://localhost:3000" 5 | 6 | NEXT_PUBLIC_USAGE_DASHBOARD_ID="" 7 | NEXT_PUBLIC_USAGE_DASHBOARD_DOMAIN="" 8 | NEXT_PUBLIC_DALGO_WHITELIST_IPS=",," 9 | NEXT_PUBLIC_DEMO_ACCOUNT_DEST_SCHEMA=demostaging 10 | 11 | CYPRESS_BASE_URL="http://localhost:3000" 12 | 13 | NEXT_PUBLIC_DEMO_WALKTHROUGH_ENABLED=true 14 | NEXT_PUBLIC_SHOW_ELEMENTARY_MENU=false 15 | NEXT_PUBLIC_SHOW_DATA_INSIGHTS_TAB=false 16 | NEXT_PUBLIC_DEFAULT_LOAD_MORE_LIMIT=3 17 | 18 | NEXT_PUBLIC_AIRBYTE_URL="https://.org" -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended"], 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": ["@typescript-eslint"], 5 | "rules": { 6 | "@typescript-eslint/no-empty-function": "warn", 7 | "@typescript-eslint/no-unused-vars": "warn" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/c4gt.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: C4GT 3 | about: C4GT Community Issues Template 4 | title: "[C4GT] Button for likes" 5 | labels: C4GT Community 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Description 11 | [Provide a brief description of the feature, including why it is needed and what it will accomplish. You can skip any of Goals, Expected Outcome, Implementation Details, Mockups / Wireframes if they are irrelevant. Please note that this section of the ticket is suggestive & you can structure it as per your prerogative.] 12 | 13 | ## Goals 14 | - [ ] [Goal 1] 15 | - [ ] [Goal 2] 16 | - [ ] [Goal 3] 17 | - [ ] [Goal 4] 18 | - [ ] [Goal 5] 19 | 20 | ## Expected Outcome 21 | [Describe in detail what the final product or result should look like and how it should behave.] 22 | 23 | ## Acceptance Criteria 24 | - [ ] [Criteria 1] 25 | - [ ] [Criteria 2] 26 | - [ ] [Criteria 3] 27 | - [ ] [Criteria 4] 28 | - [ ] [Criteria 5] 29 | 30 | ## Implementation Details 31 | [List any technical details about the proposed implementation, including any specific technologies that will be used.] 32 | 33 | ## Mockups / Wireframes 34 | [Include links to any visual aids, mockups, wireframes, or diagrams that help illustrate what the final product should look like. This is not always necessary, but can be very helpful in many cases.] 35 | 36 | 37 | ### Product Name 38 | Dalgo 39 | 40 | 41 | ### Project Name 42 | Dalgo 43 | 44 | 45 | ### Organization Name: 46 | Project Tech4Dev 47 | 48 | ### Domain 49 | 30 Others 50 | 51 | ### Tech Skills Needed: 52 | [Required technical skills for the project] 53 | 54 | ### Mentor(s) 55 | [@Mentor1] [@Mentor2] [@Mentor3] 56 | 57 | ### Complexity 58 | Pick one of [High]/[Medium]/[Low] 59 | 60 | ### Category 61 | Pick one or more of [CI/CD], [Integrations], [Performance Improvement], [Security], [UI/UX/Design], [Bug], [Feature], [Documentation], [Deployment], [Test], [PoC] 62 | 63 | ### Sub Category 64 | Pick one or more of [API], [Database], [Analytics], [Refactoring], [Data Science], [Machine Learning], [Accessibility], [Internationalization], [Localization], [Frontend], [Backend], [Mobile], [SEO], [Configuration], [Deprecation], [Breaking Change], [Maintenance], [Support], [Question], [Technical Debt], [Beginner friendly], [Research], [Reproducible], [Needs Reproduction]. 65 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/task.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Task 3 | about: Task that needs to be done 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the task** 11 | A clear and concise description of the task 12 | 13 | 14 | **Expected behavior** 15 | A clear and concise description of what needs to be done. 16 | 17 | 18 | **References** 19 | Specify any reference material / links 20 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 13 | 14 | ## Summary 15 | 16 | 17 | 18 | ## Test Plan 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/dalgo-cd.yml: -------------------------------------------------------------------------------- 1 | name: Dalgo CD 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Deploy code to EC2 server 12 | uses: appleboy/ssh-action@v1.2.0 13 | with: 14 | host: ${{ secrets.SSH_HOST }} 15 | username: ${{ secrets.SSH_USER }} 16 | key: ${{ secrets.SSH_PRIVATE_KEY }} 17 | port: 22 18 | command_timeout: 500s 19 | script: | 20 | set -e 21 | source ~/.nvm/nvm.sh 22 | cd /home/ddp/webapp 23 | current_branch=$(git rev-parse --abbrev-ref HEAD) 24 | if [ "$current_branch" != "main" ]; then 25 | echo "Error: You are not on the main branch. Current branch is $current_branch." 26 | exit 1 27 | fi 28 | git pull 29 | yarn install 30 | yarn build 31 | /home/ddp/.yarn/bin/pm2 restart ddp-webapp 32 | -------------------------------------------------------------------------------- /.github/workflows/dalgo-ci.yml: -------------------------------------------------------------------------------- 1 | name: Dalgo CI 2 | on: 3 | push: 4 | branches: [main] 5 | pull_request: 6 | branches: [main] 7 | 8 | jobs: 9 | checks: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: actions/setup-node@v4 14 | with: 15 | node-version: '20' 16 | cache: 'yarn' 17 | 18 | - name: Install Dependencies 19 | run: yarn install --frozen-lockfile 20 | 21 | - name: Run Prettier 22 | run: yarn run format:check 23 | 24 | - name: Run linter 25 | run: yarn run eslint 'src/**/*.{js,ts,tsx}' --quiet 26 | 27 | - name: Run test and generate coverage report 28 | run: yarn test:coverage 29 | 30 | - name: Upload coverage to Codecov 31 | uses: codecov/codecov-action@v4.2.0 32 | env: 33 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/dalgo-docker-release.yml: -------------------------------------------------------------------------------- 1 | # This workflow builds and pushes a new docker image 2 | # to dockerhub whenever a new release is published 3 | 4 | name: Publish Docker image 5 | 6 | on: 7 | release: 8 | types: [published] 9 | 10 | jobs: 11 | push_image_to_registry: 12 | name: Push Docker image to docker hub 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Check out the repo 16 | uses: actions/checkout@v4 17 | 18 | - name: Set build date 19 | run: echo "BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_ENV 20 | 21 | - name: Set release number 22 | run: echo "RELEASE_NUMBER=${{ github.event.release.tag_name }}" >> $GITHUB_ENV 23 | 24 | - name: Set up QEMU 25 | uses: docker/setup-qemu-action@v3 26 | 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v3 29 | 30 | - name: Login to docker registry 31 | uses: docker/login-action@v3 32 | with: 33 | username: ${{secrets.DOCKERHUB_USERNAME}} 34 | password: ${{secrets.DOCKERHUB_PASSWORD}} 35 | 36 | - name: Build and push docker image to registry 37 | uses: docker/build-push-action@v5 38 | with: 39 | context: . 40 | file: ./Docker/Dockerfile 41 | target: deps 42 | platforms: linux/amd64,linux/arm64 43 | build-args: | 44 | BUILD_DATE=${{ env.BUILD_DATE }} 45 | push: true 46 | tags: | 47 | tech4dev/dalgo_frontend:${{ env.RELEASE_NUMBER }} 48 | tech4dev/dalgo_frontend:latest 49 | cache-from: type=registry,ref=tech4dev/dalgo_frontend:latest 50 | cache-to: type=inline 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .env 39 | 40 | .vscode 41 | 42 | cypress.env.json 43 | 44 | cloudbuild.yaml 45 | 46 | .yarn/* 47 | .yarnrc.yml 48 | 49 | # Sentry Config File 50 | .env.sentry-build-plugin 51 | .qodo 52 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | yarn run format:write -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "printWidth": 100, 4 | "singleQuote": true, 5 | "tabWidth": 2, 6 | "trailingComma": "es5", 7 | "endOfLine": "auto", 8 | "bracketSpacing": true 9 | } 10 | -------------------------------------------------------------------------------- /Docker/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | .dockerignore 3 | node_modules 4 | npm-debug.log 5 | README.md 6 | .next 7 | 8 | .gcloudignore 9 | 10 | # Exclude git history and configuration. 11 | .gitignore -------------------------------------------------------------------------------- /Docker/.env.example: -------------------------------------------------------------------------------- 1 | 2 | NEXT_PUBLIC_BACKEND_URL="http://localhost:8002" 3 | NEXTAUTH_SECRET="" #https://generate-secret.vercel.app/32 4 | NEXTAUTH_URL="http://localhost:3000" 5 | NEXT_PUBLIC_WEBSOCKET_URL="ws://localhost:8002" 6 | NEXT_PUBLIC_USAGE_DASHBOARD_ID="" 7 | NEXT_PUBLIC_USAGE_DASHBOARD_DOMAIN="" 8 | NEXT_PUBLIC_DEMO_ACCOUNT_DEST_SCHEMA="demostaging" 9 | NEXT_PUBLIC_DEMO_WALKTHROUGH_ENABLED=false 10 | NEXT_PUBLIC_SHOW_ELEMENTARY_MENU=true 11 | NEXT_PUBLIC_SHOW_DATA_INSIGHTS_TAB=true 12 | NEXT_PUBLIC_SHOW_DATA_ANALYSIS_TAB=true 13 | NEXT_PUBLIC_SHOW_SUPERSET_USAGE_TAB=true 14 | NEXT_PUBLIC_SHOW_SUPERSET_ANALYSIS_TAB=true 15 | 16 | NEXT_PUBLIC_SENTRY_DSN="" 17 | NEXT_PUBLIC_AMPLITUDE_ENV="" 18 | NEXT_PUBLIC_AIRBYTE_URL="" 19 | 20 | NEXT_PUBLIC_DALGO_WHITELIST_IPS=",," -------------------------------------------------------------------------------- /Docker/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM node:23-alpine 2 | 3 | # Set working directory 4 | WORKDIR /app 5 | 6 | # Install necessary packages 7 | RUN apk add --no-cache libc6-compat bash 8 | 9 | # Copy only package files first (for caching) 10 | COPY package.json yarn.lock ./ 11 | 12 | # Install dependencies 13 | RUN yarn install --frozen-lockfile --network-timeout 1000000 14 | 15 | # Copy the rest of your app source code 16 | COPY . . 17 | 18 | # Expose port 3000 19 | EXPOSE 3000 20 | 21 | # Run next dev for hot reload 22 | CMD ["yarn", "dev"] 23 | -------------------------------------------------------------------------------- /Docker/docker-compose.dev.yaml: -------------------------------------------------------------------------------- 1 | version: '3.8' 2 | 3 | services: 4 | app: 5 | image: image_name:tag #replace the value with proper tag 6 | build: 7 | context: ../ 8 | dockerfile: Docker/Dockerfile.dev 9 | container_name: container_name #replace the value with container name 10 | restart: always 11 | environment: 12 | - NEXT_PUBLIC_BACKEND_URL=${NEXT_PUBLIC_BACKEND_URL} 13 | - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} 14 | - NEXTAUTH_URL=${NEXTAUTH_URL} 15 | - NEXT_PUBLIC_WEBSOCKET_URL=${NEXT_PUBLIC_WEBSOCKET_URL} 16 | - NEXT_PUBLIC_USAGE_DASHBOARD_ID=${NEXT_PUBLIC_USAGE_DASHBOARD_ID} 17 | - NEXT_PUBLIC_USAGE_DASHBOARD_DOMAIN=${NEXT_PUBLIC_USAGE_DASHBOARD_DOMAIN} 18 | - NEXT_PUBLIC_DEMO_ACCOUNT_DEST_SCHEMA=${NEXT_PUBLIC_DEMO_ACCOUNT_DEST_SCHEMA} 19 | - CYPRESS_BASE_URL=${CYPRESS_BASE_URL} 20 | - NEXT_PUBLIC_DEMO_WALKTHROUGH_ENABLED=${NEXT_PUBLIC_DEMO_WALKTHROUGH_ENABLED} 21 | - NEXT_PUBLIC_SHOW_ELEMENTARY_MENU=${NEXT_PUBLIC_SHOW_ELEMENTARY_MENU} 22 | - NEXT_PUBLIC_SHOW_DATA_INSIGHTS_TAB=${NEXT_PUBLIC_SHOW_DATA_INSIGHTS_TAB} 23 | - NEXT_PUBLIC_SHOW_DATA_ANALYSIS_TAB=${NEXT_PUBLIC_SHOW_DATA_ANALYSIS_TAB} 24 | - NEXT_PUBLIC_SHOW_SUPERSET_USAGE_TAB=${NEXT_PUBLIC_SHOW_SUPERSET_USAGE_TAB} 25 | - NEXT_PUBLIC_SHOW_SUPERSET_ANALYSIS_TAB=${NEXT_PUBLIC_SHOW_SUPERSET_ANALYSIS_TAB} 26 | - NEXT_PUBLIC_AIRBYTE_URL=${NEXT_PUBLIC_AIRBYTE_URL} 27 | - NEXT_PUBLIC_DALGO_WHITELIST_IPS=${NEXT_PUBLIC_DALGO_WHITELIST_IPS} 28 | ports: 29 | - '3000:3000' 30 | volumes: 31 | - ../:/app 32 | - /app/node_modules 33 | networks: 34 | - dalgo-network 35 | extra_hosts: 36 | - 'host.docker.internal:host-gateway' 37 | 38 | networks: 39 | dalgo-network: 40 | driver: bridge 41 | -------------------------------------------------------------------------------- /Docker/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | image: image_name:tagname #replace with the correct tag 4 | restart: always 5 | environment: 6 | - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} 7 | - NEXTAUTH_URL=${NEXTAUTH_URL} 8 | ports: 9 | - '3000:3000' 10 | networks: 11 | - dalgo-network 12 | extra_hosts: 13 | - 'host.docker.internal:host-gateway' 14 | 15 | networks: 16 | dalgo-network: 17 | driver: bridge 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Dalgo frontend 2 | 3 | [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](https://www.gnu.org/licenses/agpl-3.0) 4 | [![Code coverage badge](https://img.shields.io/codecov/c/github/DalgoT4D/webapp/main.svg)](https://codecov.io/gh/DalgoT4D/webapp/branch/main) 5 | 6 | ## Installation instructions 7 | 8 | ```bash 9 | yarn install 10 | ``` 11 | 12 | ## Formatter 13 | 14 | This project uses `Prettier` for code formatting to maintain consistent style across all JavaScript and TypeScript files. 15 | `Husky` is used as a pre-commit hook. It automatically formats the code and adds the changes to the commit if any formatting inconsistencies are found. 16 | 17 | 18 | ## Run the development server 19 | 20 | You will need to run the [Django backend](https://github.com/DalgoT4D/DDP_backend). Once that is running, specify its URL in the `.env` under 21 | 22 | ``` 23 | NEXT_PUBLIC_BACKEND_URL= 24 | ``` 25 | 26 | Next, generate a security secret using `https://generate-secret.vercel.app/32` and set it in 27 | 28 | ``` 29 | NEXTAUTH_SECRET= 30 | ``` 31 | 32 | Finally, select an available port on your system and define the URL for this frontend 33 | 34 | ``` 35 | NEXTAUTH_URL=http://localhost: 36 | ``` 37 | 38 | Now you can start the application 39 | 40 | ```bash 41 | yarn dev 42 | ``` 43 | 44 | Open `http://localhost:` with your browser to see the result. 45 | 46 | ## Development convention 47 | 48 | Refer to this [guide](https://github.com/airbnb/javascript/tree/master/react) 49 | 50 | ## Using Docker on Dev 51 | 52 | Make sure you have docker and docker compose installed. 53 | 54 | - Install [docker](https://docs.docker.com/engine/install/) 55 | - Install [docker compose](https://docs.docker.com/compose/install/) 56 | 57 | ### Step 1: Copy .env file to Docker folder 58 | 59 | ### Step 2: Build the Docker image 60 | 61 | The env varibales will be picked up from the .env file during the yarn build stage. This will copy all the env variables starting with NEXT_PUBLIC_ (that get embedd into the javascript code). The env varibales starting without NEXT_PUBLIC_ will be picked up during run time. 62 | Run the script: 63 | ```bash 64 | bash docker-build.sh "image_name:tag" 65 | ``` 66 | Once the image is built, add that image_name:tag to the Docker/docker-compose.yaml file 67 | 68 | ### Step 3: Start the application 69 | Run the script: 70 | ```bash 71 | bash docker-compose.sh 72 | ``` 73 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | setupNodeEvents(on, config) { 6 | // implement node event listeners here 7 | }, 8 | video: false, 9 | screenshotOnRunFailure: false, 10 | }, 11 | 12 | component: { 13 | devServer: { 14 | framework: 'next', 15 | bundler: 'webpack', 16 | }, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /cypress.env.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "base_url": "", 3 | "username": "", 4 | "password": "", 5 | "signupcode": "", 6 | "SRC_KOBO_USERNAME": "", 7 | "SRC_KOBO_PASSWORD": "", 8 | "SRC_KOBO_BASE_URL": "" 9 | } 10 | -------------------------------------------------------------------------------- /cypress/e2e/Notifications/Notifications.cy.ts: -------------------------------------------------------------------------------- 1 | describe('Notifications', () => { 2 | beforeEach(() => { 3 | cy.login('Admin'); 4 | }); 5 | 6 | it('renders the notifications if any and mark them read or unread', () => { 7 | cy.get('.MuiBox-root.css-9ys6qa') // Locate the element with the specified class 8 | .find('button') // Find the button inside that element 9 | .click(); 10 | 11 | // intercepting 12 | cy.get('[role="tab"][tabindex="0"]').should('contain', 'all'); 13 | cy.get('[role="tab"][tabindex="-1"]').eq(0).should('contain', 'read'); 14 | cy.get('[role="tab"][tabindex="-1"]').eq(1).should('contain', 'unread'); 15 | 16 | cy.intercept('GET', '/api/notifications/v1?limit=10&page=1').as('getNotifications'); 17 | 18 | //checking read notifications 19 | cy.get('[role="tab"][tabindex="-1"]').eq(0).should('contain', 'read').click(); 20 | cy.contains('p', 'Showing 0 of 0 notifications').should('be.visible'); 21 | 22 | cy.get('[role="tab"][tabindex="-1"]').eq(1).should('contain', 'unread').click(); 23 | //making all unread notifications as read 24 | cy.get('[data-testid="select-all-checkbox"]').click(); 25 | cy.contains('button', 'Mark as read').click(); 26 | 27 | //again going to read 28 | cy.get('[role="tab"][tabindex="-1"]').eq(1).should('contain', 'read').click(); 29 | cy.contains('p', 'Showing 10 of 10 notifications').should('be.visible'); 30 | 31 | //selecting and making them read. 32 | cy.get('[data-testid="select-all-checkbox"]').click(); 33 | cy.contains('button', 'Mark as unread').click(); 34 | cy.contains('p', 'Showing 0 of 0 notifications').should('be.visible'); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /cypress/e2e/Settings/UserManagement.cy.ts: -------------------------------------------------------------------------------- 1 | describe('User management', () => { 2 | beforeEach(() => { 3 | cy.login('Admin'); 4 | }); 5 | it('Invites a new user', () => { 6 | // Go to user management 7 | cy.get('[data-testid="side-menu"]') 8 | .find('li') 9 | .eq(4) 10 | .within(() => { 11 | cy.get('[data-testid="listButton"]').find('button').click(); 12 | }); 13 | cy.get('[data-testid="menu-item-4.1"]').click(); 14 | cy.get('[role="tab"][tabindex="0"]').should('contain', 'Users'); 15 | 16 | //inviting users 17 | 18 | cy.contains('button', 'Invite user').click(); 19 | cy.get('[name="invited_email"]') 20 | .type('dummy@gmail.com') 21 | .should('have.value', 'dummy@gmail.com'); 22 | 23 | cy.contains('label', 'Role*').parent().find('.MuiInputBase-formControl').click(); 24 | cy.get('.MuiMenu-list') // Escape special characters like ":" in the ID 25 | .should('have.attr', 'role', 'listbox'); // Optional: Validate attributes 26 | cy.contains('li', 'Super User').click(); 27 | 28 | //sending invitations. 29 | cy.contains('button', 'Send invitation').click(); 30 | 31 | //clicking on pending invitations and checking and deleting 32 | cy.get('[role="tab"][tabindex="-1"]').should('contain', 'Pending Invitations').click(); 33 | cy.contains('td', 'dummy@gmail.com').should('be.visible'); 34 | 35 | //deleting the invite 36 | cy.get('[data-testid="MoreHorizIcon"]').click(); 37 | cy.get('[data-testid="deletetestid"]').click(); 38 | cy.contains('button', 'I Understand the consequences, confirm').click(); 39 | 40 | //deletd 41 | cy.contains('td', 'dummy@gmail.com').should('not.exist'); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /cypress/e2e/auth/Logout.cy.ts: -------------------------------------------------------------------------------- 1 | describe('Logout', () => { 2 | it('should logout correctly', () => { 3 | // login with the creds 4 | cy.login('Admin'); 5 | // click the profile icon on top right 6 | cy.get('[alt="profile icon"]').click(); 7 | // select the logout item and click 8 | cy.get('[alt="logout icon"]').click(); 9 | // should see the login page 10 | cy.get('h5').should('contain', 'Log In'); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /cypress/e2e/auth/Signup.cy.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | 3 | describe('Sigup Page', () => { 4 | beforeEach(() => { 5 | cy.visit('/signup'); 6 | }); 7 | 8 | it('Load the signup page', () => { 9 | cy.get('h5').should('contain', 'Create an account'); 10 | }); 11 | 12 | it('Validations', () => { 13 | cy.get('[data-testid="submitbutton"]').click(); 14 | cy.get('p').should('contain', 'Password is required'); 15 | cy.get('p').should('contain', 'Business email is required'); 16 | cy.get('p').should('contain', 'Confirm Password is required'); 17 | cy.get('p').should('contain', 'Signup code is required'); 18 | }); 19 | 20 | it('Redirect to Login page', () => { 21 | cy.contains('Log in').click(); 22 | cy.get('h5').should('contain', 'Log In'); 23 | }); 24 | 25 | it('Confirm password and password do not match', () => { 26 | cy.get('[data-testid="username"]').type('testuser@gmail.com'); 27 | cy.get('[data-testid="password"]').type('password'); 28 | cy.get('[data-testid="confirmpassword"]').type('confirmpassword'); 29 | cy.get('[data-testid="signupcode"]').type('random_code'); 30 | cy.get('[data-testid="submitbutton"]').click(); 31 | cy.get('p').should('contain', 'Passwords do not match'); 32 | }); 33 | 34 | it('Invalid signup code', () => { 35 | cy.get('[data-testid="username"]').type(`cypress_${uuidv4()}@gmail.com`); 36 | cy.get('[data-testid="password"]').type('password'); 37 | cy.get('[data-testid="confirmpassword"]').type('password'); 38 | cy.get('[data-testid="signupcode"]').type('random_code'); 39 | cy.get('[data-testid="submitbutton"]').click(); 40 | cy.get('div').should('contain', 'That is not the right signup code'); 41 | }); 42 | 43 | it('Successfully signup', () => { 44 | cy.get('[data-testid="username"]').type(`cypress_${uuidv4()}@gmail.com`); 45 | cy.get('[data-testid="password"]').type('password'); 46 | cy.get('[data-testid="confirmpassword"]').type('password'); 47 | cy.get('[data-testid="signupcode"]').type(Cypress.env('signupcode')); 48 | cy.get('[data-testid="submitbutton"]').click(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /cypress/e2e/auth/login.cy.ts: -------------------------------------------------------------------------------- 1 | describe('Login Page', () => { 2 | beforeEach(() => { 3 | cy.visit('https://staging.dalgo.org/login'); 4 | }); 5 | 6 | it('Load the login page', () => { 7 | cy.get('h5').should('contain', 'Log In'); 8 | }); 9 | 10 | it('Validations', () => { 11 | cy.get('[data-testid="submitbutton"]').click(); 12 | cy.get('p').should('contain', 'Password is required'); 13 | cy.get('p').should('contain', 'Business email is required'); 14 | }); 15 | 16 | it('Redirect to forgot password page', () => { 17 | cy.contains('Forgot password?').click(); 18 | cy.get('h5').should('contain', 'Forgot password'); 19 | }); 20 | 21 | it('Redirect to Sign up', () => { 22 | cy.contains('Sign Up').click(); 23 | cy.get('h5').should('contain', 'Create an account'); 24 | }); 25 | 26 | it('Failure login', () => { 27 | cy.get('[data-testid="username"]').type('randomeusername'); 28 | cy.get('[data-testid="password"]').type('randompassword'); 29 | cy.get('[data-testid="submitbutton"]').click(); 30 | cy.get('div').should('contain', 'Please check your credentials'); 31 | }); 32 | 33 | it('Successfully login', () => { 34 | cy.get('[data-testid="username"]').type(Cypress.env('admin_username')); 35 | cy.get('[data-testid="password"]').type(Cypress.env('admin_password')); 36 | cy.get('[data-testid="submitbutton"]').click(); 37 | cy.get('div').should('contain', 'User logged in successfully'); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /cypress/e2e/ingest/Connection.cy.ts: -------------------------------------------------------------------------------- 1 | describe('Add Connection', () => { 2 | beforeEach(() => { 3 | cy.login('Admin'); 4 | cy.intercept('/api/dashboard/v1').as('dashboard'); 5 | cy.wait(['@dashboard']); 6 | }); 7 | 8 | it('Add new Connection', () => { 9 | cy.get('[data-testid="side-menu"]') 10 | .find('li') 11 | .eq(1) 12 | .within(() => { 13 | cy.get('[data-testid="listButton"]').find('button').click(); 14 | }); 15 | cy.get('[data-testid="menu-item-1.1"]').click(); 16 | cy.get('[role="tab"][tabindex="0"]').should('contain', 'Connections'); 17 | cy.get('[data-testid="add-new-connection"]').contains('+ New Connection').click(); 18 | cy.get('[role="dialog"]').contains('Add a new connection'); 19 | cy.wait(1000); 20 | // Fill in the name field 21 | cy.contains('label', 'Name*').parent().find('input').type('cypress test con'); 22 | 23 | // Open the dropdown 24 | 25 | cy.intercept('GET', 'api/airbyte/sources/*/schema_catalog').as('schemaCatalog'); 26 | cy.contains('label', 'Select source').parent().find('.MuiAutocomplete-popupIndicator').click(); 27 | // Wait for the dropdown options to be visible, then click the item 28 | cy.get('.MuiAutocomplete-listbox').should('be.visible'); 29 | cy.contains('.MuiAutocomplete-option', 'cypress test src').click(); 30 | 31 | cy.wait('@schemaCatalog').then((intercept) => { 32 | expect(intercept.response.statusCode).to.eq(200); 33 | cy.get('[data-testid="stream-sync-0"]').click(); 34 | }); 35 | 36 | //intercept the api call here. 37 | cy.intercept('POST', 'api/airbyte/v1/connections/').as('createConnection'); 38 | cy.get('[type="submit"]').should('contain', 'Connect').should('not.be.disabled').click(); 39 | cy.wait('@createConnection').then((intercept) => { 40 | expect(intercept.response.statusCode).to.eq(200); 41 | cy.contains('td', 'cypress test con'); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /cypress/e2e/ingest/Ingest.cy.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | 3 | describe('Ingest', () => { 4 | beforeEach(() => { 5 | cy.login('Admin'); 6 | cy.get('[data-testid="side-menu"]') 7 | .find('li') 8 | .eq(1) 9 | .within(() => { 10 | cy.get('[data-testid="listButton"]').find('button').click(); 11 | }); 12 | cy.get('[data-testid="menu-item-1.1"]').click(); 13 | }); 14 | 15 | it('Render ingest sub menu item', () => { 16 | // three tabs should be visible 17 | cy.get('[role="tab"]').should('have.length', 3); 18 | cy.get('[role="tab"]').eq(0).should('contain', 'Connections'); 19 | cy.get('[role="tab"]').eq(1).should('contain', 'Sources'); 20 | cy.get('[role="tab"]').eq(2).should('contain', 'Your Warehouse'); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /cypress/fixtures/profile.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 8739, 3 | "name": "Jane", 4 | "email": "jane@example.com" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare namespace Cypress { 3 | interface Chainable { 4 | login(role): Chainable; 5 | } 6 | } 7 | 8 | // *********************************************** 9 | // This example commands.ts shows you how to 10 | // create various custom commands and overwrite 11 | // existing commands. 12 | // 13 | // For more comprehensive examples of custom 14 | // commands please read more here: 15 | // https://on.cypress.io/custom-commands 16 | // *********************************************** 17 | // 18 | // 19 | // -- This is a parent command -- 20 | // Cypress.Commands.add('login', (email, password) => { ... }) 21 | // 22 | // 23 | // -- This is a child command -- 24 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 25 | // 26 | // 27 | // -- This is a dual command -- 28 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 29 | // 30 | // 31 | // -- This will overwrite an existing command -- 32 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 33 | // 34 | // declare global { 35 | // namespace Cypress { 36 | // interface Chainable { 37 | // login(email: string, password: string): Chainable 38 | // drag(subject: string, options?: Partial): Chainable 39 | // dismiss(subject: string, options?: Partial): Chainable 40 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 41 | // } 42 | // } 43 | // } 44 | Cypress.Commands.add('login', (role) => { 45 | const Role = { 46 | Admin: { 47 | email: Cypress.env('admin_username'), 48 | password: Cypress.env('admin_password'), 49 | }, 50 | Pipeline_Manager: { 51 | email: Cypress.env('pipeline_manager_username'), 52 | password: Cypress.env('pipeline_manager_password'), 53 | }, 54 | }; 55 | const currentRole = Role[role]; 56 | cy.visit('https://staging.dalgo.org/login'); 57 | cy.get('[data-testid="username"]').type(currentRole.email); 58 | cy.get('[data-testid="password"]').type(currentRole.password); 59 | cy.get('[data-testid="submitbutton"]').click(); 60 | cy.get('div').should('contain', 'User logged in successfully'); 61 | }); 62 | -------------------------------------------------------------------------------- /cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 |
10 | 11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /cypress/support/component.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/component.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | import { mount } from 'cypress/react18'; 23 | 24 | // Augment the Cypress namespace to include type definitions for 25 | // your custom command. 26 | // Alternatively, can be defined in cypress/support/component.d.ts 27 | // with a at the top of your spec. 28 | declare global { 29 | namespace Cypress { 30 | interface Chainable { 31 | mount: typeof mount; 32 | } 33 | } 34 | } 35 | 36 | Cypress.Commands.add('mount', mount); 37 | 38 | // Example use: 39 | // cy.mount() 40 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.ts is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands'; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | 22 | const baseUrl = Cypress.env('base_url'); 23 | 24 | // reset baseUrl 25 | Cypress.config('baseUrl', baseUrl); 26 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es5", "dom", "es2017"], 5 | "types": ["cypress", "jest"] 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /docker-build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | if [ -z "$1" ]; then 6 | echo "Usage: $0 " 7 | exit 1 8 | fi 9 | 10 | TAG=$1 11 | ENV_FILE="Docker/.env" 12 | 13 | # Check if .env file exists 14 | if [ ! -f "$ENV_FILE" ]; then 15 | echo "Error: .env file not found" 16 | exit 1 17 | fi 18 | 19 | # Source the .env file to get exact values 20 | source "$ENV_FILE" 21 | 22 | # Build with all environment variables from Dockerfile ARGs 23 | docker build \ 24 | --build-arg NEXT_PUBLIC_BACKEND_URL="${NEXT_PUBLIC_BACKEND_URL}" \ 25 | --build-arg NEXT_PUBLIC_USAGE_DASHBOARD_ID="${NEXT_PUBLIC_USAGE_DASHBOARD_ID}" \ 26 | --build-arg NEXT_PUBLIC_USAGE_DASHBOARD_DOMAIN="${NEXT_PUBLIC_USAGE_DASHBOARD_DOMAIN}" \ 27 | --build-arg NEXT_PUBLIC_DEMO_ACCOUNT_DEST_SCHEMA="${NEXT_PUBLIC_DEMO_ACCOUNT_DEST_SCHEMA}" \ 28 | --build-arg NEXT_PUBLIC_DEMO_WALKTHROUGH_ENABLED="${NEXT_PUBLIC_DEMO_WALKTHROUGH_ENABLED}" \ 29 | --build-arg NEXT_PUBLIC_WEBSOCKET_URL="${NEXT_PUBLIC_WEBSOCKET_URL}" \ 30 | --build-arg NEXT_PUBLIC_SHOW_ELEMENTARY_MENU="${NEXT_PUBLIC_SHOW_ELEMENTARY_MENU}" \ 31 | --build-arg NEXT_PUBLIC_SHOW_DATA_INSIGHTS_TAB="${NEXT_PUBLIC_SHOW_DATA_INSIGHTS_TAB}" \ 32 | --build-arg NEXT_PUBLIC_SHOW_DATA_ANALYSIS_TAB="${NEXT_PUBLIC_SHOW_DATA_ANALYSIS_TAB}" \ 33 | --build-arg NEXT_PUBLIC_SHOW_SUPERSET_USAGE_TAB="${NEXT_PUBLIC_SHOW_SUPERSET_USAGE_TAB}" \ 34 | --build-arg NEXT_PUBLIC_SHOW_SUPERSET_ANALYSIS_TAB="${NEXT_PUBLIC_SHOW_SUPERSET_ANALYSIS_TAB}" \ 35 | --build-arg NEXT_PUBLIC_SENTRY_DSN="${NEXT_PUBLIC_SENTRY_DSN}" \ 36 | --build-arg NEXT_PUBLIC_AMPLITUDE_ENV="${NEXT_PUBLIC_AMPLITUDE_ENV}" \ 37 | --build-arg NEXT_PUBLIC_DALGO_WHITELIST_IPS="${NEXT_PUBLIC_DALGO_WHITELIST_IPS}" \ 38 | --build-arg NEXT_PUBLIC_AIRBYTE_URL="${NEXT_PUBLIC_AIRBYTE_URL}" \ 39 | --build-arg BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \ 40 | -f Docker/Dockerfile \ 41 | -t "$TAG" . 42 | -------------------------------------------------------------------------------- /docker-compose.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check if the docker-compose.yaml file exists 4 | if [ ! -f Docker/docker-compose.yaml ]; then 5 | echo "Docker Compose file Docker/docker-compose.yaml not found!" 6 | exit 1 7 | fi 8 | 9 | # Run docker compose up 10 | docker compose -f Docker/docker-compose.yaml up -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'jest'; 2 | import nextJest from 'next/jest.js'; 3 | 4 | const createJestConfig = nextJest({ 5 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 6 | dir: './', 7 | }); 8 | 9 | // Add any custom config to be passed to Jest 10 | const config: Config = { 11 | moduleNameMapper: { 12 | '^d3$': '/node_modules/d3/dist/d3.min.js', 13 | '^@/(.*)$': '/src/$1' 14 | }, 15 | // Add more setup options before each test is run 16 | setupFilesAfterEnv: ['/jest.setup.ts'], 17 | collectCoverageFrom: ['src/components/**/*.{ts,tsx}', '!**/node_modules/**'], 18 | testEnvironment: 'jsdom', 19 | testPathIgnorePatterns: ['/__tests__/helpers.ts'], 20 | }; 21 | 22 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async 23 | export default createJestConfig(config); 24 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | output: 'standalone' 5 | } 6 | 7 | module.exports = nextConfig 8 | 9 | 10 | // Injected content via Sentry wizard below 11 | 12 | const { withSentryConfig } = require("@sentry/nextjs"); 13 | 14 | module.exports = withSentryConfig( 15 | module.exports, 16 | { 17 | // For all available options, see: 18 | // https://github.com/getsentry/sentry-webpack-plugin#options 19 | 20 | org: "dalgo", 21 | project: "javascript-nextjs", 22 | 23 | // Only print logs for uploading source maps in CI 24 | silent: !process.env.CI, 25 | 26 | // For all available options, see: 27 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ 28 | 29 | // Upload a larger set of source maps for prettier stack traces (increases build time) 30 | widenClientFileUpload: true, 31 | 32 | // Uncomment to route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. 33 | // This can increase your server load as well as your hosting bill. 34 | // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- 35 | // side errors will fail. 36 | // tunnelRoute: "/monitoring", 37 | 38 | // Hides source maps from generated client bundles 39 | hideSourceMaps: true, 40 | 41 | // Automatically tree-shake Sentry logger statements to reduce bundle size 42 | disableLogger: true, 43 | 44 | // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) 45 | // See the following for more information: 46 | // https://docs.sentry.io/product/crons/ 47 | // https://vercel.com/docs/cron-jobs 48 | automaticVercelMonitors: true, 49 | } 50 | ); 51 | -------------------------------------------------------------------------------- /public/favicon_new.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DalgoT4D/webapp/8cd127e9d0827d41b4cb07734fb8c89065d401bc/public/favicon_new.png -------------------------------------------------------------------------------- /public/loginbanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DalgoT4D/webapp/8cd127e9d0827d41b4cb07734fb8c89065d401bc/public/loginbanner.png -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /sentry.client.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the client. 2 | // The config you add here will be used whenever a users loads a page in their browser. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | Sentry.init({ 8 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, 9 | 10 | // Adjust this value in production, or use tracesSampler for greater control 11 | tracesSampleRate: 1, 12 | 13 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 14 | debug: false, 15 | 16 | replaysOnErrorSampleRate: 1.0, 17 | 18 | autoSessionTracking: true, 19 | 20 | // This sets the sample rate to be 10%. You may want this to be 100% while 21 | // in development and sample at a lower rate in production 22 | replaysSessionSampleRate: 0.1, 23 | 24 | // You can remove this option if you're not planning to use the Sentry Session Replay feature: 25 | integrations: [ 26 | Sentry.replayIntegration({ 27 | // Additional Replay configuration goes in here, for example: 28 | maskAllText: true, 29 | blockAllMedia: true, 30 | }), 31 | ], 32 | }); 33 | -------------------------------------------------------------------------------- /sentry.edge.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). 2 | // The config you add here will be used whenever one of the edge features is loaded. 3 | // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. 4 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 5 | 6 | import * as Sentry from "@sentry/nextjs"; 7 | 8 | Sentry.init({ 9 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, 10 | 11 | // Adjust this value in production, or use tracesSampler for greater control 12 | tracesSampleRate: 1, 13 | autoSessionTracking: true, 14 | 15 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 16 | debug: false, 17 | }); 18 | -------------------------------------------------------------------------------- /sentry.server.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the server. 2 | // The config you add here will be used whenever the server handles a request. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | Sentry.init({ 8 | dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, 9 | 10 | // Adjust this value in production, or use tracesSampler for greater control 11 | tracesSampleRate: 1, 12 | autoSessionTracking: true, 13 | 14 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 15 | debug: false, 16 | 17 | // Uncomment the line below to enable Spotlight (https://spotlightjs.com) 18 | // spotlight: process.env.NODE_ENV === 'development', 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /src/assets/icons/UI4T/aggregate.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/UI4T/case.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/assets/icons/UI4T/coalesce.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/assets/icons/UI4T/concat.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/UI4T/drop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/UI4T/filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/icons/UI4T/flatten.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/icons/UI4T/groupby.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/icons/UI4T/join.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/icons/UI4T/pivot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/UI4T/rename.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/UI4T/replace.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/icons/UI4T/union.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/UI4T/unpivot.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/analysis.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | const Analysis = (props: any) => ( 3 | 4 | 8 | 12 | 13 | ); 14 | export default Analysis; 15 | -------------------------------------------------------------------------------- /src/assets/icons/arrow_back_ios.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/icons/cancel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | const CancelIcon = (props: any) => ( 3 | 11 | 15 | 16 | ); 17 | export default CancelIcon; 18 | -------------------------------------------------------------------------------- /src/assets/icons/check-large.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/icons/connection.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/assets/icons/content_copy.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/dalgoIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/icons/datatable.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/icons/delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DalgoT4D/webapp/8cd127e9d0827d41b4cb07734fb8c89065d401bc/src/assets/icons/delete.png -------------------------------------------------------------------------------- /src/assets/icons/delete.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/icons/drag.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/icons/elementary.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/assets/icons/explore.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | const SvgComponent = (props: any) => ( 3 | 4 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | ); 25 | export default SvgComponent; 26 | -------------------------------------------------------------------------------- /src/assets/icons/folder.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/hamburger.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/assets/icons/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/icons/logout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/notifications.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | const Notifications = (props: any) => ( 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | ); 16 | export default Notifications; 17 | -------------------------------------------------------------------------------- /src/assets/icons/notifications_unread.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | const Unread_Notifications = (props: any) => ( 3 | 4 | 5 | 9 | 13 | 14 | ); 15 | export default Unread_Notifications; 16 | -------------------------------------------------------------------------------- /src/assets/icons/orchestrate.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | const SvgComponent = (props: any) => ( 3 | 4 | 10 | 16 | 22 | 23 | ); 24 | export default SvgComponent; 25 | -------------------------------------------------------------------------------- /src/assets/icons/pipeline.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | const SvgComponent = (props: any) => ( 3 | 4 | 10 | 11 | ); 12 | export default SvgComponent; 13 | -------------------------------------------------------------------------------- /src/assets/icons/settings.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | const Settings = (props: any) => ( 3 | 11 | 15 | 19 | 20 | ); 21 | export default Settings; 22 | -------------------------------------------------------------------------------- /src/assets/icons/success.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/assets/icons/switch-chart.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/icons/switch-filter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/assets/icons/sync.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icons/thumb_up (1).svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/thumb_up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/assets/icons/transform.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | const SvgComponent = (props: any) => ( 3 | 4 | 8 | 9 | ); 10 | export default SvgComponent; 11 | -------------------------------------------------------------------------------- /src/assets/images/airbyte.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DalgoT4D/webapp/8cd127e9d0827d41b4cb07734fb8c89065d401bc/src/assets/images/airbyte.ico -------------------------------------------------------------------------------- /src/assets/images/airbytelogo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DalgoT4D/webapp/8cd127e9d0827d41b4cb07734fb8c89065d401bc/src/assets/images/airbytelogo.webp -------------------------------------------------------------------------------- /src/assets/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DalgoT4D/webapp/8cd127e9d0827d41b4cb07734fb8c89065d401bc/src/assets/images/banner.png -------------------------------------------------------------------------------- /src/assets/images/dbt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DalgoT4D/webapp/8cd127e9d0827d41b4cb07734fb8c89065d401bc/src/assets/images/dbt.png -------------------------------------------------------------------------------- /src/assets/images/github_transform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DalgoT4D/webapp/8cd127e9d0827d41b4cb07734fb8c89065d401bc/src/assets/images/github_transform.png -------------------------------------------------------------------------------- /src/assets/images/pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DalgoT4D/webapp/8cd127e9d0827d41b4cb07734fb8c89065d401bc/src/assets/images/pattern.png -------------------------------------------------------------------------------- /src/assets/images/prefect-logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DalgoT4D/webapp/8cd127e9d0827d41b4cb07734fb8c89065d401bc/src/assets/images/prefect-logo-black.png -------------------------------------------------------------------------------- /src/assets/images/superset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DalgoT4D/webapp/8cd127e9d0827d41b4cb07734fb8c89065d401bc/src/assets/images/superset.png -------------------------------------------------------------------------------- /src/assets/images/supersetlogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DalgoT4D/webapp/8cd127e9d0827d41b4cb07734fb8c89065d401bc/src/assets/images/supersetlogo.png -------------------------------------------------------------------------------- /src/assets/images/ui_transform.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DalgoT4D/webapp/8cd127e9d0827d41b4cb07734fb8c89065d401bc/src/assets/images/ui_transform.png -------------------------------------------------------------------------------- /src/components/Charts/StringInsights.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import switchIcon from '@/assets/icons/switch-chart.svg'; 3 | import { BarChart } from './BarChart'; 4 | import { Box } from '@mui/material'; 5 | import Image from 'next/image'; 6 | import RangeChart, { CharacterData } from './RangeChart'; 7 | import { DataProps, StatsChart } from './StatsChart'; 8 | 9 | type StringInsightsProps = { 10 | data: CharacterData[]; 11 | statsData: DataProps; 12 | }; 13 | 14 | export const StringInsights: React.FC = ({ data, statsData }) => { 15 | const [chartType, setChartType] = useState<'chart' | 'bars' | 'stats'>('chart'); 16 | 17 | return ( 18 | 25 | {chartType === 'chart' && } 26 | 27 | {chartType === 'bars' && ( 28 | ({ 30 | label: bar.name, 31 | value: bar.count, 32 | barTopLabel: `${bar.count} | ${bar.percentage}%`, 33 | }))} 34 | /> 35 | )} 36 | 37 | {chartType === 'stats' && 38 | (statsData.minimum === statsData.maximum ? ( 39 | All entries in this column are identical in length 40 | ) : ( 41 | 42 | 43 | 51 | String length distribution 52 | 53 | 54 | ))} 55 | 56 | 60 | setChartType(chartType === 'chart' ? 'bars' : chartType === 'bars' ? 'stats' : 'chart') 61 | } 62 | alt="switch icon" 63 | /> 64 | 65 | 66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /src/components/Charts/__tests__/BarChart.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { fireEvent, render, screen } from '@testing-library/react'; 3 | import { BarChart, BarChartProps } from '../BarChart'; 4 | import * as d3 from 'd3'; 5 | 6 | describe('BarChart', () => { 7 | const data: BarChartProps['data'] = [ 8 | { label: 'January', value: 30 }, 9 | { label: 'February', value: 10 }, 10 | { label: 'March', value: 50, barTopLabel: 'High' }, 11 | { label: 'April', value: 20 }, 12 | { label: 'May', value: 60 }, 13 | ]; 14 | 15 | it('renders the bar-chart correctly', () => { 16 | render(); 17 | const svgElement = screen.getByTestId('barchart-svg'); 18 | expect(svgElement).toBeInTheDocument(); 19 | }); 20 | 21 | it('renders correct number of bars', () => { 22 | render(); 23 | const svgElement = screen.getByTestId('barchart-svg'); 24 | const bars = d3.select(svgElement).selectAll('.bar'); 25 | expect(bars.size()).toBe(data.length); 26 | }); 27 | 28 | it('renders correct values on bars', () => { 29 | render(); 30 | const svgElement = screen.getByTestId('barchart-svg'); 31 | const barLabels = d3.select(svgElement).selectAll('g text').nodes(); 32 | 33 | data.forEach((d, i) => { 34 | const expectedText = d.barTopLabel ? d.barTopLabel : d.value.toString(); 35 | const actualText = d3.select(barLabels[i + data.length]).text(); 36 | expect(actualText).toBe(expectedText); 37 | }); 38 | }); 39 | 40 | it('trims long labels and shows tooltip on hover', async () => { 41 | render(); 42 | const svgElement = screen.getByTestId('barchart-svg'); 43 | const ticks = d3.select(svgElement).selectAll('.tick text').nodes(); 44 | 45 | //trim label 46 | ticks.forEach((node, i) => { 47 | const originalLabel = data[i].label; 48 | const expectedLabel = 49 | originalLabel.length > 10 ? `${originalLabel.substring(0, 10)}...` : originalLabel; 50 | expect(d3.select(node).text()).toBe(expectedLabel); 51 | }); 52 | 53 | const longLabelNode = ticks.find((node) => d3.select(node).text().endsWith('...')); 54 | if (longLabelNode) { 55 | fireEvent.mouseOver(longLabelNode as HTMLElement); 56 | const tooltip = d3.select('body').select('.tooltip'); 57 | expect(tooltip.style('opacity')).toBe('0.9'); 58 | } 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/components/Charts/__tests__/RangeChart.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent, waitFor } from '@testing-library/react'; 3 | import { RangeChart, CharacterData } from '../RangeChart'; 4 | import * as d3 from 'd3'; 5 | 6 | describe('RangeChart', () => { 7 | const data: CharacterData[] = [ 8 | { name: 'Character 1', percentage: '10', count: 100 }, 9 | { name: 'Character 2', percentage: '20', count: 200 }, 10 | { name: 'Character 3', percentage: '30', count: 300 }, 11 | { name: 'Character 4', percentage: '40', count: 400 }, 12 | { name: 'Character 5', percentage: '50', count: 500 }, 13 | { name: 'Character 6', percentage: '60', count: 600 }, 14 | ]; 15 | 16 | it('renders without crashing', () => { 17 | render(); 18 | const container = screen.getByTestId('range-chart-container'); 19 | expect(container).toBeInTheDocument(); 20 | }); 21 | 22 | it('renders correct number of bars', () => { 23 | render(); 24 | const container = screen.getByTestId('range-chart-container'); 25 | const bars = d3.select(container).selectAll('rect').nodes(); 26 | expect(bars.length).toBe(data.length * 2); // Considering both the main bars and the legend bars 27 | }); 28 | 29 | it('trims long labels and shows tooltip on hover', async () => { 30 | render(); 31 | const container = screen.getByTestId('range-chart-container'); 32 | const legendTexts = d3.select(container).selectAll('g text').nodes(); 33 | 34 | data.forEach((d, i) => { 35 | const originalLabel = d.name; 36 | const expectedLabel = 37 | originalLabel.length > 10 ? `${originalLabel.substring(0, 10)}...` : originalLabel; 38 | const actualText = d3.select(legendTexts[i]).text(); 39 | expect(actualText).toBe(expectedLabel); 40 | }); 41 | 42 | const longLabelNode = legendTexts.find((node) => d3.select(node).text().endsWith('...')); 43 | if (longLabelNode) { 44 | fireEvent.mouseOver(longLabelNode as HTMLElement); 45 | const tooltip = d3.select('body').select('.tooltip'); 46 | 47 | await waitFor(() => { 48 | expect(tooltip.style('opacity')).toBe('0.9'); 49 | }); 50 | } 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/components/Charts/__tests__/StatsChart.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { fireEvent, render, screen, waitFor } from '@testing-library/react'; 3 | import { StatsChart } from '../StatsChart'; 4 | 5 | describe('StatsChart', () => { 6 | const data = { 7 | minimum: 10, 8 | maximum: 90, 9 | mean: 50, 10 | median: 45, 11 | mode: 40, 12 | otherModes: [35, 60], 13 | }; 14 | 15 | beforeEach(() => { 16 | render(); 17 | }); 18 | it('renders svg statcharts correctly', () => { 19 | const svg = screen.getByTestId('svg-container'); 20 | expect(svg).toBeInTheDocument(); 21 | 22 | expect(screen.getByText('Min: 10')).toBeInTheDocument(); 23 | expect(screen.getByText('Max: 90')).toBeInTheDocument(); 24 | expect(screen.getByText('Mean: 50')).toBeInTheDocument(); 25 | expect(screen.getByText('Median: 45')).toBeInTheDocument(); 26 | expect(screen.getByText('Mode: 40')).toBeInTheDocument(); 27 | }); 28 | 29 | it('checks if mouseover works correctly', async () => { 30 | const modeLabel = screen.getByText('Mode: 40'); 31 | fireEvent.mouseOver(modeLabel); 32 | 33 | const tooltip = screen.getByText('Other modes: 35, 60'); 34 | expect(tooltip).toBeInTheDocument(); 35 | await waitFor(() => { 36 | expect(tooltip).toHaveStyle('opacity: 0.9'); 37 | }); 38 | fireEvent.mouseLeave(modeLabel); 39 | await waitFor(() => { 40 | expect(tooltip).toHaveStyle('opacity: 0'); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/components/DBT/DBTDocs.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import { useSession } from 'next-auth/react'; 3 | import { backendUrl } from '@/config/constant'; 4 | import { Card, Typography } from '@mui/material'; 5 | import { httpPost } from '@/helpers/http'; 6 | 7 | export const DBTDocs = () => { 8 | const [dbtDocsToken, setDbtDocsToken] = useState(''); 9 | const { data: session }: any = useSession(); 10 | 11 | const fetchDbtDocsToken = async () => { 12 | if (!session) return; 13 | try { 14 | const response = await httpPost(session, 'dbt/makedocs/', {}); 15 | if (response.token) { 16 | setDbtDocsToken(response.token); 17 | } 18 | } catch (err: any) { 19 | console.error(err); 20 | // don't show errorToast 21 | } 22 | }; 23 | 24 | useEffect(() => { 25 | fetchDbtDocsToken(); 26 | }, []); 27 | 28 | return ( 29 | <> 30 | {dbtDocsToken && ( 31 | 32 | 38 | 39 | )} 40 | {!dbtDocsToken && ( 41 | 51 | 52 | Please go to the setup tab and select the function DBT docs-generate 53 | 54 | 55 | )} 56 | 57 | ); 58 | }; 59 | -------------------------------------------------------------------------------- /src/components/DBT/__tests__/DBTDocs.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, waitFor } from '@testing-library/react'; 3 | import { useSession } from 'next-auth/react'; 4 | import { DBTDocs } from '../DBTDocs'; 5 | 6 | // Mock the useSession hook 7 | jest.mock('next-auth/react', () => ({ 8 | useSession: jest.fn(), 9 | })); 10 | 11 | const mockSession = { 12 | expires: '1', 13 | user: { email: 'a', name: 'Delta', image: 'c', token: 'test-token' }, 14 | }; 15 | 16 | describe('DBTDocs Component', () => { 17 | beforeEach(() => { 18 | jest.clearAllMocks(); 19 | }); 20 | 21 | it('should display the loading message when there is no token', () => { 22 | (useSession as jest.Mock).mockReturnValueOnce({ data: null, status: 'loading' }); 23 | 24 | render(); 25 | 26 | expect( 27 | screen.getByText(/Please go to the setup tab and select the function DBT docs-generate/i) 28 | ).toBeInTheDocument(); 29 | }); 30 | 31 | it('should fetch and display the token when session is available', async () => { 32 | (useSession as jest.Mock).mockReturnValueOnce({ 33 | data: mockSession, 34 | status: 'authenticated', 35 | }); 36 | 37 | global.fetch = jest.fn(() => 38 | Promise.resolve({ 39 | json: () => Promise.resolve({ token: 'dbt-docs-token' }), 40 | }) 41 | ) as jest.Mock; 42 | 43 | render(); 44 | 45 | await waitFor(() => { 46 | expect(global.fetch).toHaveBeenCalled(); 47 | }); 48 | }); 49 | 50 | it('should display error message if fetching token fails', async () => { 51 | (useSession as jest.Mock).mockReturnValueOnce({ 52 | data: mockSession, 53 | status: 'authenticated', 54 | }); 55 | 56 | global.fetch = jest.fn(() => Promise.reject(new Error('Failed to fetch token'))) as jest.Mock; 57 | 58 | render(); 59 | 60 | await waitFor(() => { 61 | expect(global.fetch).toHaveBeenCalled(); 62 | }); 63 | 64 | await waitFor(() => { 65 | expect( 66 | screen.getByText(/Please go to the setup tab and select the function DBT docs-generate/i) 67 | ).toBeInTheDocument(); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/components/DataAnalysis/__tests__/TopBar.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, fireEvent } from '@testing-library/react'; 2 | import { TopBar } from '../TopBar'; 3 | 4 | describe('TopBar Component', () => { 5 | const mockHandleOpenSavedSession = jest.fn(); 6 | const mockHandleNewSession = jest.fn(); 7 | 8 | const defaultProps = { 9 | handleOpenSavedSession: mockHandleOpenSavedSession, 10 | handleNewSession: mockHandleNewSession, 11 | }; 12 | 13 | beforeEach(() => { 14 | jest.clearAllMocks(); 15 | }); 16 | 17 | test('renders TopBar component with correct buttons and tooltip', () => { 18 | render(); 19 | 20 | expect(screen.getByText('Parameters')).toBeInTheDocument(); 21 | 22 | // Check if "Saved Sessions" button is rendered 23 | expect(screen.getByText('Saved Sessions')).toBeInTheDocument(); 24 | 25 | // Check if "+ New" button is rendered 26 | expect(screen.getByText('+ New')).toBeInTheDocument(); 27 | }); 28 | 29 | test('calls handleOpenSavedSession when "Saved Sessions" button is clicked', () => { 30 | render(); 31 | 32 | const savedSessionsButton = screen.getByText('Saved Sessions'); 33 | fireEvent.click(savedSessionsButton); 34 | 35 | expect(mockHandleOpenSavedSession).toHaveBeenCalledTimes(1); 36 | }); 37 | 38 | test('calls handleNewSession when "+ New" button is clicked', () => { 39 | render(); 40 | 41 | const newButton = screen.getByText('+ New'); 42 | fireEvent.click(newButton); 43 | 44 | expect(mockHandleNewSession).toHaveBeenCalledTimes(1); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/components/Dialog/CustomDialog.tsx: -------------------------------------------------------------------------------- 1 | import { Close } from '@mui/icons-material'; 2 | import { 3 | Backdrop, 4 | Box, 5 | CircularProgress, 6 | Dialog, 7 | DialogActions, 8 | DialogContent, 9 | DialogTitle, 10 | IconButton, 11 | Typography, 12 | } from '@mui/material'; 13 | import React from 'react'; 14 | 15 | interface CustomDialogProps { 16 | title: string; 17 | show: boolean; 18 | handleClose: (...args: any) => any; 19 | handleSubmit: (...args: any) => any; 20 | subTitle?: string; 21 | formContent: any; 22 | formActions: any; 23 | 24 | loading?: boolean; 25 | [propName: string]: any; 26 | } 27 | 28 | const CustomDialog: React.FC = ({ 29 | formContent, 30 | formActions, 31 | title, 32 | show, 33 | subTitle, 34 | handleClose, 35 | handleSubmit, 36 | loading, 37 | ...rest 38 | }) => { 39 | return ( 40 | 48 | 49 | 50 | {title} 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 |
59 | 60 | {formContent} 61 | {subTitle && ( 62 | 69 | {subTitle} 70 | 71 | )} 72 | 73 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | {formActions} 86 | 87 |
88 |
89 | ); 90 | }; 91 | 92 | export default CustomDialog; 93 | -------------------------------------------------------------------------------- /src/components/Explore/__tests__/Explore.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, waitFor } from '@testing-library/react'; 2 | import { SessionProvider } from 'next-auth/react'; 3 | import { Session } from 'next-auth'; 4 | import { Explore } from '../Explore'; 5 | import '@testing-library/jest-dom'; 6 | 7 | jest.mock('next/router', () => ({ 8 | useRouter() { 9 | return { 10 | push: jest.fn(), 11 | }; 12 | }, 13 | })); 14 | 15 | window.ResizeObserver = 16 | window.ResizeObserver || 17 | jest.fn().mockImplementation(() => ({ 18 | disconnect: jest.fn(), 19 | observe: jest.fn(), 20 | unobserve: jest.fn(), 21 | })); 22 | 23 | const mockSession: Session = { 24 | expires: 'false', 25 | user: { email: 'a' }, 26 | }; 27 | 28 | const mockedFetch = jest.fn().mockResolvedValueOnce({ 29 | ok: true, 30 | json: jest.fn().mockResolvedValueOnce([]), 31 | }); 32 | (global as any).fetch = mockedFetch; 33 | 34 | it('renders the explore page with preview and data statistics tab', async () => { 35 | render( 36 | 37 | 38 | 39 | ); 40 | 41 | await waitFor(() => { 42 | expect(screen.getByText('Preview')).toBeInTheDocument(); 43 | expect(screen.getByText('Data statistics')).toBeInTheDocument(); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /src/components/Flows/__tests__/SingleFlowRunHistory.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, act } from '@testing-library/react'; 2 | import { SessionProvider } from 'next-auth/react'; 3 | import { Session } from 'next-auth'; 4 | import { SingleFlowRunHistory } from '../SingleFlowRunHistory'; 5 | import '@testing-library/jest-dom'; 6 | 7 | jest.mock('next/router', () => ({ 8 | useRouter() { 9 | return { 10 | push: jest.fn(), 11 | }; 12 | }, 13 | })); 14 | 15 | describe('Flow Creation', () => { 16 | const mockSession: Session = { 17 | expires: 'false', 18 | user: { email: 'a' }, 19 | }; 20 | 21 | // ================================================================================ 22 | it('renders the form', async () => { 23 | (global as any).fetch = jest.fn().mockResolvedValueOnce({ 24 | ok: true, 25 | json: jest.fn().mockResolvedValueOnce({ 26 | logs: { 27 | logs: [{ message: 'log-0-0' }, { message: 'log-0-1' }], 28 | }, 29 | }), 30 | }); 31 | 32 | await act(async () => { 33 | render( 34 | 35 | 45 | 46 | ); 47 | }); 48 | 49 | const logmessages = screen.getByTestId('logmessages'); 50 | expect(logmessages).toBeInTheDocument(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/components/Flows/__tests__/TaskSequence.test.tsx: -------------------------------------------------------------------------------- 1 | // TaskSequence.test.js 2 | import React from 'react'; 3 | import { render, screen, fireEvent } from '@testing-library/react'; 4 | 5 | import { TaskSequence } from '../TaskSequence'; 6 | import { TransformTask } from '../../DBT/DBTTarget'; 7 | import { ControllerRenderProps } from 'react-hook-form'; 8 | 9 | // Mock data for testing 10 | const mockTasks: TransformTask[] = [ 11 | { 12 | uuid: '1', 13 | command: 'task 1', 14 | order: 1, 15 | generated_by: 'system', 16 | }, 17 | { 18 | uuid: '2', 19 | command: 'task 2', 20 | order: 2, 21 | generated_by: 'user', 22 | }, 23 | { 24 | uuid: '3', 25 | command: 'task 3', 26 | order: 3, 27 | generated_by: 'user', 28 | }, 29 | ]; 30 | 31 | const mockField: ControllerRenderProps = { 32 | onChange: jest.fn(), 33 | onBlur: jest.fn(), 34 | value: [mockTasks[0], mockTasks[1]], 35 | name: 'taskSequence', 36 | ref: jest.fn(), 37 | }; 38 | 39 | describe('TaskSequence Component', () => { 40 | beforeEach(() => { 41 | jest.clearAllMocks(); 42 | }); 43 | 44 | it('renders correctly with form elements and actions', () => { 45 | render(); 46 | 47 | expect(screen.getByTestId('tasksequence')).toBeInTheDocument(); 48 | expect(screen.getByText('Reset to default')).toBeInTheDocument(); 49 | expect( 50 | screen.getByText( 51 | "These are your default transformation tasks. Most users don't need to change this list" 52 | ) 53 | ).toBeInTheDocument(); 54 | }); 55 | 56 | it('handles removing a task', () => { 57 | render(); 58 | 59 | // Simulate removing the second task 60 | const deleteButtons = screen.getAllByAltText('delete icon'); 61 | fireEvent.click(deleteButtons[1]); 62 | 63 | expect(mockField.onChange).toHaveBeenCalledWith([mockTasks[0]]); 64 | }); 65 | 66 | it('handles resetting tasks to default', () => { 67 | render(); 68 | 69 | const resetButton = screen.getByText('Reset to default'); 70 | fireEvent.click(resetButton); 71 | 72 | expect(mockField.onChange).toHaveBeenCalledWith( 73 | mockTasks.filter((task) => task.generated_by === 'system' && task.pipeline_default) 74 | ); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /src/components/Header/Header.module.css: -------------------------------------------------------------------------------- 1 | .Header { 2 | display: flex; 3 | position: fixed; 4 | z-index: 1201; 5 | width: 100%; 6 | height: 56px; 7 | box-shadow: 0px 6px 11px rgba(64, 68, 77, 0.06); 8 | } 9 | 10 | .Paper { 11 | box-shadow: unset; 12 | border: 0.5px solid rgba(15, 36, 64, 0.54); 13 | border-top-right-radius: unset; 14 | overflow: visible; 15 | } 16 | 17 | .Paper::after { 18 | content: ''; 19 | position: absolute; 20 | width: 0; 21 | height: 0; 22 | border-bottom: 15px solid #fff; 23 | border-left: 15px solid transparent; 24 | top: -15px; 25 | right: 0px; 26 | } 27 | 28 | .Paper::before { 29 | content: ''; 30 | position: absolute; 31 | width: 0; 32 | height: 0; 33 | border-bottom: 17px solid rgba(15, 36, 64, 0.54); 34 | border-left: 16px solid transparent; 35 | top: -17px; 36 | right: -0.5px; 37 | } 38 | -------------------------------------------------------------------------------- /src/components/Layouts/Auth.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Grid, Paper, Typography } from '@mui/material'; 2 | import Image from 'next/image'; 3 | import Banner from '@/assets/images/banner.png'; 4 | import Logo from '@/assets/images/logo.svg'; 5 | import { ReactNode } from 'react'; 6 | import moment from 'moment'; 7 | 8 | type AuthProps = { 9 | children: ReactNode; 10 | heading: string; 11 | subHeading?: string; 12 | }; 13 | export const Auth: React.FC = ({ heading, subHeading, children }) => { 14 | return ( 15 | 16 | 17 | 26 | 27 | dalgo logo 28 | 36 | 37 | 38 | {heading} 39 | 40 | {subHeading && ( 41 | 42 | {subHeading} 43 | 44 | )} 45 | 46 | {children} 47 | 48 | 49 | 50 | Privacy Policy 51 | 52 | 53 | 54 | 55 | 56 | {moment().year()}, DALGO ALL RIGHTS RESERVED 57 | 58 | 59 | 60 | 61 | 62 | 70 | 71 | 72 | 73 | ); 74 | }; 75 | 76 | export default Auth; 77 | -------------------------------------------------------------------------------- /src/components/Layouts/__tests__/Main.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, waitFor } from '@testing-library/react'; 2 | import { SessionProvider } from 'next-auth/react'; 3 | import { Main } from '../Main'; 4 | 5 | jest.mock('next/navigation'); 6 | 7 | jest.mock('next/router', () => ({ 8 | useRouter() { 9 | return { push: jest.fn() }; 10 | }, 11 | })); 12 | 13 | // mock Header and SideDrawer 14 | jest.mock('../../SideDrawer/SideDrawer', () => { 15 | const MockSideDrawer = () => { 16 | return
; 17 | }; 18 | 19 | MockSideDrawer.displayName = 'MockSideDrawer'; 20 | 21 | return MockSideDrawer; 22 | }); 23 | jest.mock('../../Header/Header', () => { 24 | const MockHeader = () => { 25 | return
; 26 | }; 27 | 28 | MockHeader.displayName = 'MockHeader'; 29 | 30 | return MockHeader; 31 | }); 32 | 33 | export function mockFetch(data: any) { 34 | return jest.fn().mockImplementation(() => 35 | Promise.resolve({ 36 | ok: true, 37 | json: () => data, 38 | }) 39 | ); 40 | } 41 | 42 | describe('no token', () => { 43 | const mockSession = { 44 | expires: '1', 45 | user: { name: '' }, 46 | }; 47 | 48 | window.fetch = mockFetch([{ org: { slug: 'test-org' } }]); 49 | 50 | it('renders the component', async () => { 51 | render( 52 | 53 |
54 |
55 |
56 |
57 | ); 58 | 59 | const notLoggedIn = screen.getByTestId('not-logged-in'); 60 | await waitFor(() => { 61 | expect(notLoggedIn).toBeInTheDocument(); 62 | }); 63 | }); 64 | }); 65 | 66 | describe('token and normal flow', () => { 67 | const mockSession: any = { 68 | expires: '1', 69 | user: { token: 'token', email_verified: true }, 70 | }; 71 | 72 | it('renders the header and sidedrawer', async () => { 73 | render( 74 | 75 |
76 |
77 |
78 |
79 | ); 80 | 81 | await waitFor(() => {}); 82 | 83 | // TODO: rewrite test cases for this component - logic has been changed 84 | // const normalFlow = screen.getByTestId('normal-flow'); 85 | // expect(normalFlow).toBeInTheDocument(); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /src/components/List/__tests__/List.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen, fireEvent } from '@testing-library/react'; 3 | import { List } from '../List'; 4 | 5 | const headers = { 6 | values: ['Name', 'Type'], 7 | sortable: [true, false], 8 | }; 9 | 10 | const rows = [ 11 | [Source 1, Type 1, ], 12 | [Source 2, Type 2, ], 13 | ]; 14 | 15 | const rowValues = [ 16 | ['Source 1', 'Type 1'], 17 | ['Source 2', 'Type 2'], 18 | ]; 19 | 20 | const mockOpenDialog = jest.fn(); 21 | 22 | describe('List component', () => { 23 | test('renders List component and displays rows', () => { 24 | render( 25 | 32 | ); 33 | 34 | expect(screen.getByTestId('add-new-source')).toBeInTheDocument(); 35 | 36 | expect(screen.getByText('Source 1')).toBeInTheDocument(); 37 | expect(screen.getByText('Source 2')).toBeInTheDocument(); 38 | }); 39 | 40 | test('handles sorting', () => { 41 | render( 42 | 49 | ); 50 | 51 | const sortLabel = screen.getByText('Name'); 52 | expect(sortLabel).toBeInTheDocument(); 53 | 54 | fireEvent.click(sortLabel); 55 | 56 | const firstRowName = screen.getAllByText(/Source/)[1]; 57 | //ignoring the actual first row which will show the +New source button. 58 | expect(firstRowName).toHaveTextContent('Source 1'); 59 | 60 | fireEvent.click(sortLabel); 61 | 62 | const firstRowNameDesc = screen.getAllByText(/Source/)[1]; 63 | expect(firstRowNameDesc).toHaveTextContent('Source 2'); 64 | }); 65 | 66 | test('displays message when no rows are present', () => { 67 | render( 68 | 69 | ); 70 | 71 | expect(screen.getByText('No source found. Please create one')).toBeInTheDocument(); 72 | }); 73 | 74 | test('opens dialog when add new button is clicked', () => { 75 | render( 76 | 83 | ); 84 | 85 | fireEvent.click(screen.getByTestId('add-new-source')); 86 | expect(mockOpenDialog).toHaveBeenCalledTimes(1); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/components/Logs/LogCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { Box, Button, Card, CardActions, CardContent, Collapse, IconButton } from '@mui/material'; 3 | import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; 4 | 5 | export type FlowRunLogMessage = { 6 | message: string; 7 | }; 8 | 9 | interface LogCardProps { 10 | expand: boolean; 11 | logs: Array; 12 | setExpand: (...args: any) => any; 13 | fetchMore?: boolean; 14 | fetchMoreLogs?: () => void; 15 | } 16 | 17 | export const LogCard = ({ 18 | expand, 19 | setExpand, 20 | logs, 21 | fetchMore = false, 22 | fetchMoreLogs = () => {}, 23 | }: LogCardProps) => { 24 | return ( 25 | 41 | 42 | Logs 43 | setExpand(!expand)}> 44 | 49 | 50 | 51 | 52 | { 53 | 54 | {logs?.map((log: any, idx) => ( 55 | 56 | - {log?.message || log} 57 | 58 | ))} 59 | {fetchMore && ( 60 | 63 | )} 64 | 65 | } 66 | 67 | 68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /src/components/Logs/LogSummaryCard.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@mui/material'; 2 | import { LogSummaryBlock } from './LogSummaryBlock'; 3 | 4 | export type LogSummary = { 5 | task_name: string; 6 | status: string; 7 | pattern?: string; 8 | log_lines: Array; 9 | errors?: number; 10 | passed?: number; 11 | skipped?: number; 12 | warnings?: number; 13 | tests: Array; 14 | }; 15 | 16 | interface LogSummaryCardProps { 17 | logsummary: Array; 18 | setLogsummaryLogs: any; 19 | } 20 | 21 | export const LogSummaryCard = ({ logsummary, setLogsummaryLogs }: LogSummaryCardProps) => { 22 | return ( 23 | 24 | {logsummary.map((log: LogSummary, index: number) => ( 25 | 26 | ))} 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/MultiTagInput.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen, within, fireEvent } from '@testing-library/react'; 2 | import '@testing-library/jest-dom'; 3 | import { Controller, useForm } from 'react-hook-form'; 4 | import MultiTagInput from './MultiTagInput'; 5 | 6 | const MultiTagBoundToFormControl = () => { 7 | const { setValue, control } = useForm({}); 8 | return ( 9 | ( 13 | 20 | )} 21 | /> 22 | ); 23 | }; 24 | 25 | describe('setup for multi tag component', () => { 26 | it('render & delete the tags', () => { 27 | render(); 28 | 29 | // it renders with empty stack i.e. no tags added 30 | const multiTagStack = screen.getByTestId('test-multi-tag-stack'); 31 | expect(multiTagStack.childElementCount).toBe(0); 32 | 33 | // Add a tag 34 | const multiTag: any = screen.getByTestId('test-multi-tag'); 35 | const multiTagInput: HTMLInputElement = within(multiTag).getByRole('textbox'); 36 | fireEvent.change(multiTagInput, { target: { value: 'tag1' } }); 37 | // expect text box to have the input 38 | expect(multiTagInput.value).toBe('tag1'); 39 | 40 | // Save the tag and check the count of tags in input 41 | fireEvent.keyDown(multiTagInput, { key: 'Enter' }); 42 | expect(multiTagStack.childElementCount).toBe(1); 43 | 44 | // Add another tag 45 | fireEvent.change(multiTagInput, { target: { value: 'tag2' } }); 46 | expect(multiTagInput.value).toBe('tag2'); 47 | fireEvent.keyDown(multiTagInput, { key: 'Enter' }); 48 | expect(multiTagStack.childElementCount).toBe(2); 49 | 50 | // Check the tags text value 51 | const tags = within(multiTagStack).getAllByRole('button'); 52 | expect(tags[0].children[0].innerHTML).toBe('tag1'); 53 | expect(tags[1].children[0].innerHTML).toBe('tag2'); 54 | 55 | // Delete a tag 56 | let tagCloseButton = tags[0].children[1]; 57 | fireEvent.click(tagCloseButton); 58 | expect(multiTagStack.childElementCount).toBe(1); 59 | 60 | // Delete another tag. No tags should be left 61 | tagCloseButton = tags[0].children[1]; 62 | fireEvent.click(tagCloseButton); 63 | expect(multiTagStack.childElementCount).toBe(0); 64 | }); 65 | }); 66 | -------------------------------------------------------------------------------- /src/components/Notifications/Notifications.module.css: -------------------------------------------------------------------------------- 1 | .Paper { 2 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.15); 3 | overflow: visible; 4 | } 5 | 6 | .Paper::after { 7 | content: ''; 8 | position: absolute; 9 | width: 0; 10 | height: 0; 11 | border-bottom: 15px solid #fff; 12 | border-bottom: 15px solid #fff; 13 | border-left: 15px solid transparent; 14 | top: -10px; 15 | right: 165px; 16 | } 17 | 18 | .Paper::before { 19 | content: ''; 20 | position: absolute; 21 | width: 0; 22 | height: 0; 23 | border-bottom: 15px solid #fff; 24 | border-right: 15px solid transparent; 25 | top: -10px; 26 | right: 152px; 27 | } -------------------------------------------------------------------------------- /src/components/PageHead.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | 3 | export interface PageHeadProps { 4 | title: string; 5 | } 6 | 7 | export const PageHead = ({ title }: PageHeadProps) => { 8 | return ( 9 | 10 | {title} 11 | 12 | ); 13 | }; 14 | -------------------------------------------------------------------------------- /src/components/ProductWalk/ProductWalk.tsx: -------------------------------------------------------------------------------- 1 | import { primaryColor } from '@/config/theme'; 2 | import React from 'react'; 3 | import Joyride from 'react-joyride'; 4 | import { WalkThroughContent } from './WalkThroughContent'; 5 | 6 | interface StepContent { 7 | target: string; 8 | body: string; 9 | } 10 | 11 | interface ProductWalkProps { 12 | run: boolean; 13 | setRun: (...args: any) => any; 14 | steps: StepContent[]; 15 | } 16 | 17 | export const ProductWalk = ({ run, steps }: ProductWalkProps) => { 18 | const handleJoyrideCallback = () => {}; 19 | 20 | return ( 21 | ({ 28 | target: step.target, 29 | content: , 30 | placementBeacon: 'right', 31 | }))} 32 | styles={{ 33 | options: { 34 | zIndex: 10000, 35 | primaryColor: primaryColor, 36 | width: '300px', 37 | }, 38 | }} 39 | /> 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/components/ProductWalk/WalkThroughContent.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@mui/material'; 2 | import React from 'react'; 3 | 4 | interface WalkThroughContentProps { 5 | body: string; 6 | } 7 | 8 | export const WalkThroughContent = ({ body }: WalkThroughContentProps) => { 9 | // Your component logic here 10 | 11 | return {body}; 12 | }; 13 | -------------------------------------------------------------------------------- /src/components/SideDrawer/SideDrawer.teststofix.tsx_: -------------------------------------------------------------------------------- 1 | import { render, screen, waitFor } from '@testing-library/react'; 2 | import { SideDrawer } from './SideDrawer'; 3 | import userEvent from '@testing-library/user-event'; 4 | import '@testing-library/jest-dom'; 5 | const user = userEvent.setup(); 6 | 7 | const pushMock = jest.fn(); 8 | jest.mock('next/router', () => ({ 9 | useRouter() { 10 | return { 11 | push: pushMock, 12 | }; 13 | }, 14 | })); 15 | 16 | describe('Side drawer', () => { 17 | it('renders ', () => { 18 | render(); 19 | expect(screen.getAllByTestId('listButton').at(0)).toHaveTextContent( 20 | 'Analysis' 21 | ); 22 | }); 23 | 24 | it('renders ', async () => { 25 | render(); 26 | const button = screen.getAllByTestId('listButton').at(0); 27 | user.click(button); 28 | await waitFor(() => { 29 | expect(pushMock).toHaveBeenCalledWith('/analysis'); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/components/ToastMessage/ToastHelper.ts: -------------------------------------------------------------------------------- 1 | export const successToast = (message = '', messages: Array = [], context: any) => { 2 | context?.Toast?.dispatch({ 3 | type: 'new', 4 | toastState: { 5 | open: true, 6 | severity: 'success', 7 | seconds: 3, 8 | message: message, 9 | messages: messages, 10 | }, 11 | }); 12 | }; 13 | 14 | export const errorToast = (message = '', messages: Array = [], context: any) => { 15 | context?.Toast?.dispatch({ 16 | type: 'new', 17 | toastState: { 18 | open: true, 19 | severity: 'error', 20 | seconds: 3, 21 | message: message, 22 | messages: messages, 23 | }, 24 | }); 25 | }; 26 | -------------------------------------------------------------------------------- /src/components/ToastMessage/ToastMessage.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, Snackbar, Typography } from '@mui/material'; 2 | import React from 'react'; 3 | import { ToastStateInterface } from '@/contexts/reducers/ToastReducer'; 4 | 5 | const ToastMessage = ({ 6 | open, 7 | severity, 8 | seconds, 9 | message, 10 | messages, 11 | handleClose, 12 | }: ToastStateInterface) => { 13 | const handleCloseButton = () => { 14 | handleClose(); 15 | }; 16 | return ( 17 | 26 | 27 | {messages?.length 28 | ? messages.map((msg: string, idx: number) => ( 29 | 30 | {msg} 31 | 32 | )) 33 | : message} 34 | 35 | 36 | ); 37 | }; 38 | 39 | export default ToastMessage; 40 | -------------------------------------------------------------------------------- /src/components/ToastMessage/__tests__/ToastMessage.test.tsx: -------------------------------------------------------------------------------- 1 | // ToastMessage.test.js 2 | import React from 'react'; 3 | import { render, screen, fireEvent } from '@testing-library/react'; 4 | import ToastMessage from '../ToastMessage'; 5 | import { ToastStateInterface } from '@/contexts/reducers/ToastReducer'; 6 | 7 | describe('ToastMessage Component', () => { 8 | const mockHandleClose = jest.fn(); 9 | 10 | const singleMessageProps: ToastStateInterface = { 11 | open: true, 12 | severity: 'success', 13 | seconds: 3, 14 | message: 'This is a single message', 15 | messages: [], 16 | handleClose: mockHandleClose, 17 | }; 18 | 19 | const multipleMessagesProps: ToastStateInterface = { 20 | open: true, 21 | severity: 'error', 22 | seconds: 3, 23 | message: '', 24 | messages: ['First message', 'Second message', 'Third message'], 25 | handleClose: mockHandleClose, 26 | }; 27 | 28 | it('renders correctly with a single message', () => { 29 | render(); 30 | 31 | expect(screen.getByText('This is a single message')).toBeInTheDocument(); 32 | }); 33 | 34 | it('renders correctly with multiple messages', () => { 35 | render(); 36 | 37 | expect(screen.getByText('First message')).toBeInTheDocument(); 38 | expect(screen.getByText('Second message')).toBeInTheDocument(); 39 | expect(screen.getByText('Third message')).toBeInTheDocument(); 40 | }); 41 | 42 | it('calls handleClose when close button is clicked', () => { 43 | render(); 44 | 45 | fireEvent.click(screen.getByRole('button', { name: /close/i })); 46 | expect(mockHandleClose).toHaveBeenCalled(); 47 | }); 48 | 49 | it('calls handleClose after autoHideDuration', () => { 50 | jest.useFakeTimers(); 51 | render(); 52 | 53 | jest.advanceTimersByTime(singleMessageProps.seconds * 1000); 54 | expect(mockHandleClose).toHaveBeenCalled(); 55 | jest.useRealTimers(); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /src/components/TransformWorkflow/FlowEditor/Components/InfoBox.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Typography } from '@mui/material'; 2 | 3 | const InfoBox = ({ text }: { text: string }) => { 4 | return ( 5 | 15 | 16 | 17 | 18 | 24 | 25 | 26 | 27 | 36 | NOTE 37 | 38 | 39 | 40 | 41 | 45 | 46 | 47 | 48 | 49 | 50 | {text} 51 | 52 | 53 | 54 | ); 55 | }; 56 | 57 | export default InfoBox; 58 | -------------------------------------------------------------------------------- /src/components/TransformWorkflow/FlowEditor/Components/LowerSectionTabs/__tests__/LogsPane.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import { LogsPane } from '../LogsPane'; 4 | 5 | jest.mock('moment', () => { 6 | return () => jest.requireActual('moment')('2023-07-30T12:00:00Z'); 7 | }); 8 | 9 | describe('LogsPane Component', () => { 10 | const defaultProps = { 11 | height: 500, 12 | dbtRunLogs: [], 13 | finalLockCanvas: false, 14 | }; 15 | 16 | it('renders the table with logs when dbtRunLogs is not empty', () => { 17 | const logs = [ 18 | { timestamp: '2023-07-30T12:00:00Z', message: 'Log message 1' }, 19 | { timestamp: '2023-07-30T13:00:00Z', message: 'Log message 2' }, 20 | ]; 21 | render(); 22 | 23 | expect(screen.getByText('Last Run')).toBeInTheDocument(); 24 | expect(screen.getByText('Description')).toBeInTheDocument(); 25 | 26 | // Check if the log messages are present 27 | logs.forEach((log) => { 28 | // expect(screen.getByText(moment(log.timestamp).format('YYYY/MM/DD'))).toBeInTheDocument(); 29 | // expect(screen.getByText(moment(log.timestamp).format('hh:mm:ss A '))).toBeInTheDocument(); 30 | expect(screen.getByText(log.message)).toBeInTheDocument(); 31 | }); 32 | }); 33 | 34 | it('renders the loading spinner when workflowInProgress is true and dbtRunLogs is empty', () => { 35 | render(); 36 | 37 | expect(screen.getByTestId('progressbar')).toBeInTheDocument(); 38 | }); 39 | 40 | it('renders the "Please press run" message when dbtRunLogs is empty and workflowInProgress is false', () => { 41 | render(); 42 | 43 | expect(screen.getByText('Please press run')).toBeInTheDocument(); 44 | }); 45 | 46 | it('does not render the loading spinner when workflowInProgress is false', () => { 47 | render(); 48 | 49 | expect(screen.queryByTestId('progressbar')).not.toBeInTheDocument(); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /src/components/TransformWorkflow/FlowEditor/Components/OperationPanel/CreateTableOrAddFunction.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button } from '@mui/material'; 2 | import React from 'react'; 3 | 4 | interface CreateTableAddFunctionProps { 5 | clickCreateTable: () => void; 6 | clickAddFunction: () => void; 7 | showAddFunction: boolean; 8 | } 9 | 10 | const CreateTableOrAddFunction = ({ 11 | clickCreateTable, 12 | clickAddFunction, 13 | showAddFunction, 14 | }: CreateTableAddFunctionProps) => { 15 | return ( 16 | 25 | 28 | 29 | {showAddFunction && ( 30 | 33 | )} 34 | 35 | ); 36 | }; 37 | 38 | export default CreateTableOrAddFunction; 39 | -------------------------------------------------------------------------------- /src/components/TransformWorkflow/FlowEditor/Components/OperationPanel/Forms/__tests__/CreateTableForm.test.tsx: -------------------------------------------------------------------------------- 1 | // CreateTableForm.test.js 2 | import React from 'react'; 3 | import { render, screen, fireEvent, waitFor } from '@testing-library/react'; 4 | import CreateTableForm from '../CreateTableForm'; 5 | import { useSession } from 'next-auth/react'; 6 | import { httpPost } from '@/helpers/http'; 7 | import { useCanvasAction, useCanvasNode } from '@/contexts/FlowEditorCanvasContext'; 8 | 9 | jest.mock('next-auth/react'); 10 | jest.mock('@/helpers/http'); 11 | jest.mock('@/contexts/FlowEditorCanvasContext'); 12 | 13 | const mockSetCanvasAction = jest.fn(); 14 | const mockClearAndClosePanel = jest.fn(); 15 | 16 | jest.mock('react-hook-form', () => ({ 17 | useForm: () => ({ 18 | control: {}, 19 | handleSubmit: jest.fn((fn) => (data) => fn(data)), 20 | reset: jest.fn(), 21 | register: jest.fn(), 22 | }), 23 | Controller: ({ render }) => render({ field: {}, fieldState: {} }), 24 | })); 25 | 26 | describe('CreateTableForm', () => { 27 | beforeEach(() => { 28 | useSession.mockReturnValue({ data: { session: 'mockSession' } }); 29 | useCanvasNode.mockReturnValue({ 30 | canvasNode: { type: 'operation_node', data: { target_model_id: '123' } }, 31 | }); 32 | useCanvasAction.mockReturnValue({ setCanvasAction: mockSetCanvasAction }); 33 | httpPost.mockResolvedValue({}); 34 | }); 35 | 36 | afterEach(() => { 37 | jest.clearAllMocks(); 38 | }); 39 | 40 | const renderComponent = (props = {}) => 41 | render(); 42 | 43 | it('renders the form', () => { 44 | renderComponent(); 45 | 46 | expect(screen.getByText(/Output Name/i)).toBeInTheDocument(); 47 | expect(screen.getByText(/Output Schema Name/i)).toBeInTheDocument(); 48 | }); 49 | 50 | //write test for form submission. 51 | it('calls clearAndClosePanel if provided', async () => { 52 | renderComponent({ clearAndClosePanel: mockClearAndClosePanel }); 53 | 54 | fireEvent.change(screen.getByLabelText(/Output Name/i), { 55 | target: { value: 'Test Table' }, 56 | }); 57 | 58 | fireEvent.click(screen.getByTestId('savebutton')); 59 | 60 | await waitFor(() => { 61 | expect(mockClearAndClosePanel).toHaveBeenCalled(); 62 | }); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /src/components/TransformWorkflow/FlowEditor/Components/dummynodes.ts: -------------------------------------------------------------------------------- 1 | import { DbtSourceModel, UIOperationType } from './Canvas'; 2 | import { OPERATION_NODE, SRC_MODEL_NODE } from '../constant'; 3 | import { getNextNodePosition } from '@/utils/editor'; 4 | 5 | export const generateDummySrcModelNode = (node: any, model: DbtSourceModel, height = 200) => { 6 | const { x: xnew, y: ynew } = getNextNodePosition([ 7 | { 8 | position: { x: node?.xPos, y: node?.yPos }, 9 | height: height, 10 | }, 11 | ]); 12 | 13 | const dummyNode: any = { 14 | id: model.id, 15 | type: SRC_MODEL_NODE, 16 | data: {}, 17 | position: { 18 | x: xnew, 19 | y: ynew, 20 | }, 21 | }; 22 | 23 | dummyNode.data = { ...model, isDummy: true, parentNode: node }; 24 | 25 | return dummyNode; 26 | }; 27 | 28 | export const generateDummyOperationlNode = (node: any, op: UIOperationType, height = 200) => { 29 | const nodeId = String(Date.now()); //this is the node.id, that is used to hit backend. 30 | const { x: xnew, y: ynew } = getNextNodePosition([ 31 | { position: { x: node?.xPos, y: node?.yPos }, height: height }, 32 | ]); 33 | const dummyTargetNodeData: any = { 34 | id: nodeId, 35 | selected: true, 36 | type: OPERATION_NODE, 37 | data: { 38 | id: nodeId, 39 | type: OPERATION_NODE, 40 | output_cols: [], 41 | target_model_id: '', 42 | target_model_name: '', 43 | target_model_schema: '', 44 | config: { type: op.slug }, 45 | isDummy: true, 46 | parentNode: node, //added to check parent node. 47 | }, 48 | position: { 49 | x: xnew, 50 | y: ynew, 51 | }, 52 | }; 53 | return dummyTargetNodeData; 54 | }; 55 | -------------------------------------------------------------------------------- /src/components/UI/Autocomplete/Autocomplete.tsx: -------------------------------------------------------------------------------- 1 | import Input from '@/components/UI/Input/Input'; 2 | import { 3 | Autocomplete as AutocompleteElement, 4 | AutocompleteProps as AutocompleteElementProps, 5 | Popper, 6 | styled, 7 | } from '@mui/material'; 8 | import { forwardRef } from 'react'; 9 | 10 | const CustomPopper = styled(Popper)({ 11 | '.MuiAutocomplete-paper': { 12 | width: 'auto', 13 | minWidth: '100%', 14 | overflowX: 'auto', 15 | }, 16 | '.MuiAutocomplete-listbox': { 17 | display: 'inline-block', 18 | whiteSpace: 'nowrap', 19 | minWidth: '100%', 20 | }, 21 | }); 22 | 23 | interface AutocompleteProps 24 | extends Omit< 25 | AutocompleteElementProps, 26 | 'renderInput' | 'onChange' 27 | > { 28 | label?: string; 29 | fieldStyle?: 'normal' | 'transformation' | 'none'; 30 | error?: boolean; 31 | helperText?: string; 32 | name?: string; 33 | onChange: any; 34 | } 35 | 36 | export const Autocomplete = forwardRef(function Autocomplete( 37 | { 38 | placeholder, 39 | fieldStyle = 'normal', 40 | label, 41 | error, 42 | helperText, 43 | name, 44 | onChange, 45 | ...rest 46 | }: AutocompleteProps, 47 | ref 48 | ) { 49 | return ( 50 | { 55 | onChange(data); 56 | }} 57 | PopperComponent={(props) => } 58 | renderInput={(params) => ( 59 | 68 | )} 69 | /> 70 | ); 71 | }); 72 | -------------------------------------------------------------------------------- /src/components/UI/Tooltip/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Tooltip from '@mui/material/Tooltip'; 3 | import InfoIcon from '@mui/icons-material/InfoOutlined'; 4 | import Typography from '@mui/material/Typography'; 5 | 6 | interface InfoTooltipProps { 7 | title: string | any; 8 | placement?: any; 9 | } 10 | 11 | function InfoTooltip({ title, placement = 'bottom' }: InfoTooltipProps) { 12 | return ( 13 | {title}} 16 | > 17 | 22 | 23 | ); 24 | } 25 | 26 | export default InfoTooltip; 27 | -------------------------------------------------------------------------------- /src/components/Workflow/Editor.tsx: -------------------------------------------------------------------------------- 1 | import 'react'; 2 | import { PageHead } from '@/components/PageHead'; 3 | import styles from '@/styles/Home.module.css'; 4 | import FlowEditor from '@/components/TransformWorkflow/FlowEditor/FlowEditor'; 5 | import { DbtRunLogsProvider } from '@/contexts/DbtRunLogsContext'; 6 | import { CanvasActionProvider, CanvasNodeProvider } from '@/contexts/FlowEditorCanvasContext'; 7 | import { PreviewActionProvider } from '@/contexts/FlowEditorPreviewContext'; 8 | 9 | export default function WorkflowEditor() { 10 | return ( 11 | <> 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/config/constant.ts: -------------------------------------------------------------------------------- 1 | export const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL; 2 | 3 | export const websocketUrl = process.env.NEXT_PUBLIC_WEBSOCKET_URL; 4 | export const showElementaryMenu = process.env.NEXT_PUBLIC_SHOW_ELEMENTARY_MENU === 'true'; // Data quality 5 | export const showDataInsightsTab = process.env.NEXT_PUBLIC_SHOW_DATA_INSIGHTS_TAB === 'true'; //Data insights in UI4T lower pane. 6 | export const showDataAnalysisTab = process.env.NEXT_PUBLIC_SHOW_DATA_ANALYSIS_TAB === 'true'; // Data Analysis 7 | export const showSupersetUsageTab = process.env.NEXT_PUBLIC_SHOW_SUPERSET_USAGE_TAB === 'true'; // Usage 8 | export const showSupersetAnalysisTab = 9 | process.env.NEXT_PUBLIC_SHOW_SUPERSET_ANALYSIS_TAB === 'true'; // Analysis. 10 | export const defaultLoadMoreLimit = parseInt( 11 | process.env.NEXT_PUBLIC_DEFAULT_LOAD_MORE_LIMIT || '3' 12 | ); 13 | export const dalgoWhitelistIps = process.env.NEXT_PUBLIC_DALGO_WHITELIST_IPS?.split(',') || []; 14 | 15 | export const flowRunLogsOffsetLimit = 200; 16 | 17 | export const usageDashboardId = process.env.NEXT_PUBLIC_USAGE_DASHBOARD_ID; 18 | 19 | export const usageDashboardDomain = process.env.NEXT_PUBLIC_USAGE_DASHBOARD_DOMAIN; 20 | 21 | export const airbyteUrl = process.env.NEXT_PUBLIC_AIRBYTE_URL; 22 | 23 | // Master task slugs 24 | export const TASK_DBTRUN = 'dbt-run'; 25 | export const TASK_DBTTEST = 'dbt-test'; 26 | export const TASK_DBTCLEAN = 'dbt-clean'; 27 | export const TASK_DBTDEPS = 'dbt-deps'; 28 | export const TASK_GITPULL = 'git-pull'; 29 | export const TASK_DOCSGENERATE = 'dbt-docs-generate'; 30 | export const TASK_DBTCLOUD_JOB = 'dbt-cloud-job'; 31 | 32 | // Demo account 33 | export const demoAccDestSchema = process.env.NEXT_PUBLIC_DEMO_ACCOUNT_DEST_SCHEMA; 34 | 35 | // Product walkthrough for demo account 36 | export const demoProductWalkthrough = process.env.NEXT_PUBLIC_DEMO_WALKTHROUGH_ENABLED || false; 37 | 38 | // alpha features 39 | export const enableLogSummaries = process.env.NEXT_PUBLIC_ENABLE_LOG_SUMMARIES; 40 | -------------------------------------------------------------------------------- /src/contexts/ConnectionSyncLogsContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, Dispatch, SetStateAction } from 'react'; 2 | 3 | const ConnectionSyncLogsContext = React.createContext([]); 4 | const ConnectionSyncLogsUpdateContext = React.createContext>>( 5 | (() => {}) as Dispatch> 6 | ); 7 | 8 | export const useConnSyncLogs = () => { 9 | return useContext(ConnectionSyncLogsContext); 10 | }; 11 | 12 | export const useConnSyncLogsUpdate = () => { 13 | return useContext(ConnectionSyncLogsUpdateContext); 14 | }; 15 | 16 | export const ConnectionSyncLogsProvider = ({ children }: any) => { 17 | const [syncLogs, setSyncLogs] = useState([]); 18 | 19 | return ( 20 | 21 | 22 | {children} 23 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/contexts/DbtRunLogsContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, Dispatch, SetStateAction } from 'react'; 2 | 3 | import { TaskProgressLog } from '@/components/TransformWorkflow/FlowEditor/FlowEditor'; 4 | 5 | export const DbtRunLogsContext = React.createContext([]); 6 | export const DbtRunLogsUpdateContext = React.createContext< 7 | Dispatch> 8 | >((() => {}) as Dispatch>); 9 | 10 | export const useDbtRunLogs = () => { 11 | return useContext(DbtRunLogsContext); 12 | }; 13 | 14 | export const useDbtRunLogsUpdate = () => { 15 | return useContext(DbtRunLogsUpdateContext); 16 | }; 17 | 18 | export const DbtRunLogsProvider = ({ children }: any) => { 19 | const [dbtRunLogs, setDbtRunLogs] = useState([]); 20 | 21 | // Todo: Make them in same context object 22 | return ( 23 | 24 | 25 | {children} 26 | 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /src/contexts/FlowEditorCanvasContext.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | OperationNodeType, 3 | SrcModelNodeType, 4 | } from '@/components/TransformWorkflow/FlowEditor/Components/Canvas'; 5 | import React, { useState, useContext, Dispatch, SetStateAction } from 'react'; 6 | 7 | ///////////////////////////// Canvas Node //////////////////////////////// 8 | 9 | interface CanvasNodeContext { 10 | canvasNode: SrcModelNodeType | OperationNodeType | null | undefined; 11 | setCanvasNode: Dispatch>; 12 | } 13 | 14 | export const CanvasNodeContext = React.createContext({ 15 | canvasNode: null, 16 | setCanvasNode: () => {}, 17 | }); 18 | 19 | export const CanvasNodeProvider = ({ children }: any) => { 20 | const [canvasNode, setCanvasNode] = useState< 21 | SrcModelNodeType | OperationNodeType | null | undefined 22 | >(null); 23 | 24 | return ( 25 | 26 | {children} 27 | 28 | ); 29 | }; 30 | 31 | export const useCanvasNode = () => { 32 | return useContext(CanvasNodeContext); 33 | }; 34 | 35 | ///////////////////////////// Canvas Action //////////////////////////////// 36 | 37 | interface Action { 38 | type: 39 | | 'add-srcmodel-node' 40 | | 'delete-node' 41 | | 'delete-source-tree-node' 42 | | 'refresh-canvas' 43 | | 'open-opconfig-panel' 44 | | 'close-reset-opconfig-panel' 45 | | 'sync-sources' 46 | | 'run-workflow' 47 | | 'update-canvas-node' 48 | | '' 49 | | undefined 50 | | null; 51 | data: any; 52 | } 53 | 54 | interface CanvasActionContext { 55 | canvasAction: Action; 56 | setCanvasAction: Dispatch>; 57 | } 58 | 59 | export const CanvasActionContext = React.createContext({ 60 | canvasAction: { type: '', data: null }, 61 | setCanvasAction: () => {}, 62 | }); 63 | 64 | export const CanvasActionProvider = ({ children }: any) => { 65 | const [canvasAction, setCanvasAction] = useState({ 66 | type: '', 67 | data: null, 68 | }); 69 | 70 | return ( 71 | 72 | {children} 73 | 74 | ); 75 | }; 76 | 77 | export const useCanvasAction = () => { 78 | return useContext(CanvasActionContext); 79 | }; 80 | -------------------------------------------------------------------------------- /src/contexts/FlowEditorPreviewContext.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useContext, Dispatch, SetStateAction } from 'react'; 2 | 3 | ///////////////////////////// Preview Action //////////////////////////////// 4 | 5 | interface Action { 6 | type: 'preview' | 'clear-preview' | '' | undefined | null; 7 | data: any; 8 | } 9 | 10 | interface PreviewActionContext { 11 | previewAction: Action; 12 | setPreviewAction: Dispatch>; 13 | } 14 | 15 | const PreviewActionContext = React.createContext({ 16 | previewAction: { type: '', data: null }, 17 | setPreviewAction: () => {}, 18 | }); 19 | 20 | export const PreviewActionProvider = ({ children }: any) => { 21 | const [previewAction, setPreviewAction] = useState({ 22 | type: '', 23 | data: null, 24 | }); 25 | 26 | return ( 27 | 28 | {children} 29 | 30 | ); 31 | }; 32 | 33 | export const usePreviewAction = () => { 34 | return useContext(PreviewActionContext); 35 | }; 36 | -------------------------------------------------------------------------------- /src/contexts/reducers/CurrentOrgReducer.ts: -------------------------------------------------------------------------------- 1 | // The reducer here will update the global state of the current selected org component 2 | 3 | export interface CurrentOrgStateInterface { 4 | slug: string; 5 | name: string; 6 | airbyte_workspace_id: string; 7 | viz_url: string; 8 | viz_login_type: string; 9 | wtype: string; 10 | is_demo: boolean; 11 | } 12 | 13 | interface Action { 14 | type: 'new'; 15 | orgState: CurrentOrgStateInterface; 16 | } 17 | 18 | export const initialCurrentOrgState = { 19 | slug: '', 20 | name: '', 21 | airbyte_workspace_id: '', 22 | viz_url: '', 23 | viz_login_type: '', 24 | wtype: '', 25 | is_demo: false, 26 | }; 27 | 28 | export const CurrentOrgReducer = (state: CurrentOrgStateInterface, updateAction: Action) => { 29 | switch (updateAction?.type) { 30 | case 'new': 31 | return updateAction.orgState; 32 | default: 33 | return state; 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /src/contexts/reducers/OrgUsersReducer.ts: -------------------------------------------------------------------------------- 1 | // The reducer here will update the global state of the current selected org component 2 | import { CurrentOrgStateInterface } from './CurrentOrgReducer'; 3 | 4 | export interface OrgUserStateInterface { 5 | email: string; 6 | active: boolean; 7 | role: number; 8 | role_slug: string; 9 | org: CurrentOrgStateInterface; 10 | wtype: string; 11 | } 12 | 13 | interface Action { 14 | type: 'new'; 15 | orgUsersState: Array; 16 | } 17 | 18 | export const initialOrgUsersState = []; 19 | 20 | export const OrgUsersReducer = (state: Array, updateAction: Action) => { 21 | switch (updateAction?.type) { 22 | case 'new': 23 | return updateAction.orgUsersState; 24 | default: 25 | return state; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /src/contexts/reducers/ToastReducer.ts: -------------------------------------------------------------------------------- 1 | // The reducer here will update the global state of the toast component 2 | type severity = 'success' | 'error' | 'info' | 'warning'; 3 | 4 | export interface ToastStateInterface { 5 | open: boolean; 6 | severity: severity; 7 | message: string; 8 | messages?: Array; 9 | seconds: number; 10 | handleClose: (...args: any) => any; 11 | } 12 | 13 | interface ToastAction { 14 | type: 'close' | 'new'; 15 | toastState: ToastStateInterface; 16 | } 17 | 18 | interface PermissionAction { 19 | type: 'add'; 20 | permissionState: string[]; 21 | } 22 | 23 | export const initialToastState = { 24 | open: true, 25 | severity: 'error', 26 | message: 'initial state of toast message', 27 | seconds: 0, 28 | messages: [], 29 | }; 30 | 31 | export const initialPermissionState = []; 32 | 33 | export const PermissionReducer = (state: string[], updateAction: PermissionAction) => { 34 | switch (updateAction?.type) { 35 | case 'add': 36 | return updateAction.permissionState; 37 | 38 | default: 39 | return state; 40 | } 41 | }; 42 | 43 | export const ToastReducer = (state: ToastStateInterface, updateAction: ToastAction) => { 44 | switch (updateAction?.type) { 45 | case 'close': 46 | return { ...state, open: false }; 47 | 48 | case 'new': 49 | return updateAction?.toastState; 50 | default: 51 | return state; 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /src/contexts/reducers/UnsavedChangesReducer.ts: -------------------------------------------------------------------------------- 1 | // UnsavedChangesReducer.js 2 | 3 | // Initial state for unsaved changes 4 | export const initialUnsavedChangesState = false; 5 | 6 | // Reducer for handling unsaved changes 7 | export const UnsavedChangesReducer = (state: any, action: any) => { 8 | switch (action.type) { 9 | case 'SET_UNSAVED_CHANGES': 10 | return true; 11 | case 'CLEAR_UNSAVED_CHANGES': 12 | return false; 13 | default: 14 | return state; 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /src/customHooks/useLockCanvas.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export const useLockCanvas = (lockUpperSection: boolean) => { 4 | const [tempLockCanvas, setTempLockCanvas] = useState(false); 5 | const finalLockCanvas = tempLockCanvas || lockUpperSection; 6 | 7 | return { finalLockCanvas, setTempLockCanvas }; 8 | }; 9 | -------------------------------------------------------------------------------- /src/customHooks/useOpForm.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | DbtSourceModel, 3 | OperationNodeData, 4 | } from '@/components/TransformWorkflow/FlowEditor/Components/Canvas'; 5 | import { OperationFormProps } from '@/components/TransformWorkflow/FlowEditor/Components/OperationConfigLayout'; 6 | import { OPERATION_NODE, SRC_MODEL_NODE } from '@/components/TransformWorkflow/FlowEditor/constant'; 7 | 8 | export const useOpForm = ({ props }: { props: OperationFormProps | any }) => { 9 | //parent node from which dummy node is being created. 10 | //it can be operational node or source node. 11 | 12 | const { node } = props; 13 | const nodeData: any = 14 | node?.type === SRC_MODEL_NODE 15 | ? (node?.data as DbtSourceModel) 16 | : node?.type === OPERATION_NODE 17 | ? (node?.data as OperationNodeData) 18 | : {}; 19 | const parentNode = node?.data?.parentNode; //parentNode inside Node 20 | return { nodeData, parentNode }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/customHooks/useQueryParams.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import { useEffect, useState } from 'react'; 3 | 4 | export const useQueryParams = ({ 5 | tabsObj, 6 | basePath, 7 | defaultTab, 8 | }: { 9 | tabsObj: { [key: string]: number }; 10 | basePath: string; 11 | defaultTab: string; 12 | }) => { 13 | const router = useRouter(); 14 | 15 | const { tab }: any = router.query; 16 | const currentTab = 17 | tab && typeof tab === 'string' && tab in tabsObj ? +tabsObj[tab] : +tabsObj[defaultTab]; 18 | 19 | const reverseTabsObj = Object.entries(tabsObj).reduce( 20 | (acc, [key, value]) => ({ ...acc, [value]: key }), 21 | {} as { [key: number]: string } 22 | ); 23 | 24 | const [value, setValue] = useState(currentTab); 25 | 26 | useEffect(() => { 27 | if (!router.isReady) return; 28 | 29 | if (!tab || !(tab in tabsObj)) { 30 | //check for the first render and also if user put wrong query params 31 | router.replace(`${basePath}?tab=${defaultTab}`, undefined, { shallow: true }); 32 | } else { 33 | setValue(currentTab); 34 | } 35 | }, [router.isReady, tab, tabsObj, currentTab, basePath, defaultTab]); 36 | 37 | const handleChange = (event: React.SyntheticEvent, newValue: number) => { 38 | const tabName = reverseTabsObj[newValue]; 39 | if (tabName) { 40 | setValue(newValue); 41 | router.push(`${basePath}?tab=${tabName}`, undefined, { shallow: true }); 42 | } 43 | }; 44 | return { value, handleChange }; 45 | }; 46 | -------------------------------------------------------------------------------- /src/customHooks/useSyncLock.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useRef, useState } from 'react'; 2 | 3 | type LockStatus = 'running' | 'queued' | 'locked' | null; 4 | export const useSyncLock = (lock: any) => { 5 | const [tempSyncState, setTempSyncState] = useState(false); //on polling it will set to false automatically. //local state of each button. 6 | //side Effect is because the lock state keeps on changing during polling. 7 | const lockLastStateRef = useRef(null); 8 | //using ref because the value of lock is null both in starting and end. 9 | //so to store the value before it becomes null again. 10 | useEffect(() => { 11 | if (lock) { 12 | if (lock.status === 'running') { 13 | lockLastStateRef.current = 'running'; 14 | } else if (lock.status === 'queued') { 15 | lockLastStateRef.current = 'queued'; 16 | } else if (lock.status === 'locked' || lock?.status === 'complete') { 17 | lockLastStateRef.current = 'locked'; 18 | } 19 | } 20 | 21 | //when polling ends, resets state. 22 | if (!lock && lockLastStateRef.current && tempSyncState) { 23 | setTempSyncState(false); 24 | 25 | lockLastStateRef.current = null; 26 | } 27 | }, [lock]); 28 | 29 | return { tempSyncState, setTempSyncState }; 30 | }; 31 | -------------------------------------------------------------------------------- /src/helpers/http.tsx: -------------------------------------------------------------------------------- 1 | import { backendUrl } from '@/config/constant'; 2 | // import { useSession } from 'next-auth/react'; 3 | import { getOrgHeaderValue } from '@/utils/common'; 4 | 5 | export async function httpGet(session: any, path: string, isJson = true) { 6 | const response = await fetch(`${backendUrl}/api/${path}`, { 7 | method: 'GET', 8 | headers: { 9 | Authorization: `Bearer ${session?.user.token}`, 10 | 'x-dalgo-org': getOrgHeaderValue('GET', path), 11 | }, 12 | }); 13 | 14 | if (response.ok) { 15 | const message = isJson ? await response.json() : response; 16 | return message; 17 | } else { 18 | const error = await response.json(); 19 | throw new Error(error?.detail ? error.detail : 'error', { cause: error }); 20 | } 21 | } 22 | 23 | export async function httpPost(session: any, path: string, payload: object) { 24 | const response = await fetch(`${backendUrl}/api/${path}`, { 25 | method: 'POST', 26 | headers: { 27 | Authorization: `Bearer ${session?.user.token}`, 28 | 'x-dalgo-org': getOrgHeaderValue('POST', path), 29 | }, 30 | body: JSON.stringify(payload), 31 | }); 32 | 33 | if (response.ok) { 34 | const message = await response.json(); 35 | return message; 36 | } else { 37 | const error = await response.json(); 38 | throw new Error(error?.detail ? error.detail : 'error', { cause: error }); 39 | } 40 | } 41 | 42 | export async function httpPut(session: any, path: string, payload: object) { 43 | const response = await fetch(`${backendUrl}/api/${path}`, { 44 | method: 'PUT', 45 | headers: { 46 | Authorization: `Bearer ${session?.user.token}`, 47 | 'x-dalgo-org': getOrgHeaderValue('PUT', path), 48 | }, 49 | body: JSON.stringify(payload), 50 | }); 51 | 52 | if (response.ok) { 53 | const message = await response.json(); 54 | return message; 55 | } else { 56 | const error = await response.json(); 57 | throw new Error(error?.detail ? error.detail : 'error', { cause: error }); 58 | } 59 | } 60 | 61 | export async function httpDelete(session: any, path: string) { 62 | const response = await fetch(`${backendUrl}/api/${path}`, { 63 | method: 'DELETE', 64 | headers: { 65 | Authorization: `Bearer ${session?.user.token}`, 66 | 'x-dalgo-org': getOrgHeaderValue('DELETE', path), 67 | }, 68 | }); 69 | 70 | if (response.ok) { 71 | const message = await response.json(); 72 | return message; 73 | } else { 74 | const error = await response.json(); 75 | throw new Error(error?.detail ? error.detail : 'error', { cause: error }); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/helpers/websocket.tsx: -------------------------------------------------------------------------------- 1 | import { websocketUrl } from '@/config/constant'; 2 | import { getOrgHeaderValue } from '@/utils/common'; 3 | 4 | export const generateWebsocketUrl = (relative_url: string, session: any) => { 5 | const queryParams: { 6 | token: string; 7 | orgslug: string; 8 | [key: string]: string; 9 | } = { 10 | token: session?.user.token, 11 | orgslug: getOrgHeaderValue('GET', relative_url), 12 | }; 13 | const queryString = Object.keys(queryParams) 14 | .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`) 15 | .join('&'); 16 | const urlWithParams = `${websocketUrl}/wss/${relative_url}?${queryString}`; 17 | 18 | return urlWithParams; 19 | }; 20 | -------------------------------------------------------------------------------- /src/instrumentation.ts: -------------------------------------------------------------------------------- 1 | export async function register() { 2 | if (process.env.NEXT_RUNTIME === 'nodejs') { 3 | await import('../sentry.server.config'); 4 | } 5 | 6 | if (process.env.NEXT_RUNTIME === 'edge') { 7 | await import('../sentry.edge.config'); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { withAuth } from 'next-auth/middleware'; 2 | 3 | // More on how NextAuth.js middleware works: https://next-auth.js.org/configuration/nextjs#middleware 4 | export default withAuth({ 5 | callbacks: { 6 | authorized({ token }) { 7 | // `/admin` requires admin role 8 | // if (req.nextUrl.pathname === '/admin') { 9 | // return token?.userRole === 'admin'; 10 | // } 11 | // `/account` only requires the user to be logged in 12 | return !!token; 13 | }, 14 | }, 15 | }); 16 | 17 | export const config = { matcher: ['/', '/pipeline/:path*', '/analysis'] }; 18 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@/styles/globals.css'; 2 | import type { AppProps } from 'next/app'; 3 | import { rajdhani } from '@/config/theme'; 4 | import CssBaseline from '@mui/material/CssBaseline'; 5 | import { ThemeProvider } from '@mui/material/styles'; 6 | import theme from '@/config/theme'; 7 | import { SessionProvider } from 'next-auth/react'; 8 | import { StyledEngineProvider } from '@mui/material/styles'; 9 | import { Main } from '@/components/Layouts/Main'; 10 | import ContextProvider from '@/contexts/ContextProvider'; 11 | import { TrackingProvider } from '@/contexts/TrackingContext'; 12 | 13 | export default function App({ Component, pageProps: { session, ...pageProps } }: AppProps) { 14 | return ( 15 | 16 |
17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 |
26 |
27 |
28 |
29 |
30 |
31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document'; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/pages/_error.jsx: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/nextjs'; 2 | import Error from 'next/error'; 3 | 4 | const CustomErrorComponent = (props) => { 5 | return ; 6 | }; 7 | 8 | CustomErrorComponent.getInitialProps = async (contextData) => { 9 | // In case this is running in a serverless function, await this in order to give Sentry 10 | // time to send the error before the lambda exits 11 | await Sentry.captureUnderscoreErrorException(contextData); 12 | 13 | // This will contain the status code of the response 14 | return Error.getInitialProps(contextData); 15 | }; 16 | 17 | export default CustomErrorComponent; 18 | -------------------------------------------------------------------------------- /src/pages/data-quality.tsx: -------------------------------------------------------------------------------- 1 | import { Elementary } from '@/components/DBT/Elementary'; 2 | import { PageHead } from '@/components/PageHead'; 3 | 4 | export default function DataQualityPage() { 5 | return ( 6 | <> 7 | 8 | ; 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/explore/index.tsx: -------------------------------------------------------------------------------- 1 | import { Explore } from '@/components/Explore/Explore'; 2 | import { PreviewActionProvider } from '@/contexts/FlowEditorPreviewContext'; 3 | 4 | export default function ExplorePage() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/forgotpassword/index.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, Typography } from '@mui/material'; 2 | import { useSession } from 'next-auth/react'; 3 | import { useForm } from 'react-hook-form'; 4 | 5 | import styles from '@/styles/Login.module.css'; 6 | import { useContext, useState } from 'react'; 7 | import { GlobalContext } from '@/contexts/ContextProvider'; 8 | import { errorToast, successToast } from '@/components/ToastMessage/ToastHelper'; 9 | import Input from '@/components/UI/Input/Input'; 10 | import Auth from '@/components/Layouts/Auth'; 11 | import { httpPost } from '../../helpers/http'; 12 | import { PageHead } from '@/components/PageHead'; 13 | 14 | export const ForgotPassword = () => { 15 | const { register, handleSubmit } = useForm(); 16 | const { data: session }: any = useSession(); 17 | const toastContext = useContext(GlobalContext); 18 | const [emailSent, setEmailSent] = useState(false); 19 | 20 | const onSubmit = async (reqData: any) => { 21 | try { 22 | await httpPost(session, 'users/forgot_password/', { 23 | email: reqData.email, 24 | }); 25 | setEmailSent(true); 26 | successToast('Please check your email', [], toastContext); 27 | } catch (error) { 28 | errorToast('Something went wrong...', [], toastContext); 29 | } 30 | }; 31 | 32 | if (emailSent) { 33 | return ( 34 | 35 | 36 | 37 | Please check your mailbox for a reset link 38 | 39 | 40 | 41 | ); 42 | } else { 43 | return ( 44 | <> 45 | 46 | 47 |
48 | 49 | 59 | 60 | 68 | 69 |
70 |
71 | 72 | ); 73 | } 74 | }; 75 | 76 | export default ForgotPassword; 77 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/navigation'; 2 | import { useEffect } from 'react'; 3 | 4 | export default function Home() { 5 | const { push } = useRouter(); 6 | 7 | useEffect(() => { 8 | push('/pipeline'); 9 | }, []); 10 | 11 | return null; 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/pipeline/ingest.tsx: -------------------------------------------------------------------------------- 1 | // pages/ingest.tsx (or /pages/ingest/index.tsx) 2 | import styles from '@/styles/Home.module.css'; 3 | import { Box, Tab, Tabs, Typography } from '@mui/material'; 4 | import React from 'react'; 5 | import { Connections } from '@/components/Connections/Connections'; 6 | import { Sources } from '@/components/Sources/Sources'; 7 | import { Destinations } from '@/components/Destinations/Destinations'; 8 | import { PageHead } from '@/components/PageHead'; 9 | import { ConnectionSyncLogsProvider } from '@/contexts/ConnectionSyncLogsContext'; 10 | import { useQueryParams } from '@/customHooks/useQueryParams'; 11 | 12 | interface TabPanelProps { 13 | children?: React.ReactNode; 14 | index: number; 15 | value: number; 16 | } 17 | 18 | function TabPanel(props: TabPanelProps) { 19 | const { children, value, index, ...other } = props; 20 | 21 | return ( 22 | 31 | ); 32 | } 33 | 34 | export default function Ingest() { 35 | const tabsObj: { [key: string]: number } = { 36 | connections: 0, 37 | sources: 1, 38 | warehouse: 2, 39 | }; 40 | 41 | const { value, handleChange } = useQueryParams({ 42 | tabsObj, 43 | basePath: '/pipeline/ingest', 44 | defaultTab: 'connections', 45 | }); 46 | 47 | return ( 48 | <> 49 | 50 |
51 | 52 | Ingest 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 |
73 | 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/pages/resetpassword/index.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button } from '@mui/material'; 2 | import { useSession } from 'next-auth/react'; 3 | import { useForm } from 'react-hook-form'; 4 | import { useRouter } from 'next/router'; 5 | import styles from '@/styles/Login.module.css'; 6 | import { useContext, useEffect, useState } from 'react'; 7 | import { GlobalContext } from '@/contexts/ContextProvider'; 8 | import { errorToast } from '@/components/ToastMessage/ToastHelper'; 9 | import Input from '@/components/UI/Input/Input'; 10 | import Auth from '@/components/Layouts/Auth'; 11 | import { httpPost } from '../../helpers/http'; 12 | import { PageHead } from '@/components/PageHead'; 13 | 14 | export const ResetPassword = () => { 15 | const { register, handleSubmit } = useForm(); 16 | const { data: session }: any = useSession(); 17 | const toastContext = useContext(GlobalContext); 18 | const [token, setToken] = useState(null); 19 | 20 | const router = useRouter(); 21 | 22 | useEffect(() => { 23 | const searchParams = new URLSearchParams(window.location.search); 24 | const urlToken: string | null = searchParams.get('token'); 25 | setToken(urlToken); 26 | }, []); 27 | 28 | const onSubmit = async (reqData: any) => { 29 | try { 30 | if (!token) { 31 | errorToast('This password reset link is invalid', [], toastContext); 32 | return; 33 | } 34 | await httpPost(session, 'users/reset_password/', { 35 | token: token, 36 | password: reqData.password, 37 | }); 38 | router.push('/login'); 39 | } catch (error: any) { 40 | errorToast(error.cause.detail[0].msg, [], toastContext); 41 | } 42 | }; 43 | 44 | return ( 45 | <> 46 | 47 | 48 |
49 | 50 | 60 | 61 | 69 | 70 |
71 |
72 | 73 | ); 74 | }; 75 | 76 | export default ResetPassword; 77 | -------------------------------------------------------------------------------- /src/pages/settings/ai-settings.tsx: -------------------------------------------------------------------------------- 1 | import { PageHead } from '@/components/PageHead'; 2 | import { Typography } from '@mui/material'; 3 | import styles from '@/styles/Home.module.css'; 4 | import { AIEnablePanel } from '@/components/Settings/AI_settings/AiEnablePanel'; 5 | 6 | const AISettings = () => { 7 | return ( 8 | <> 9 | 10 |
11 | 12 | AI Settings 13 | 14 | 15 |
16 | 17 | ); 18 | }; 19 | export default AISettings; 20 | -------------------------------------------------------------------------------- /src/pages/settings/index.tsx: -------------------------------------------------------------------------------- 1 | import { PageHead } from '@/components/PageHead'; 2 | import { Box, List, ListItem, ListItemIcon, ListItemText, Typography } from '@mui/material'; 3 | import styles from '@/styles/Home.module.css'; 4 | import { ServicesInfo } from '@/components/Settings/ServicesInfo'; 5 | import { SubscriptionInfo } from '@/components/Settings/SubscriptionInfo'; 6 | import { dalgoWhitelistIps } from '@/config/constant'; 7 | import { Circle } from '@mui/icons-material'; 8 | 9 | const Settings = () => { 10 | return ( 11 | <> 12 | 13 |
14 | 15 | Settings 16 | 17 | 18 | {dalgoWhitelistIps?.length > 0 && ( 19 | 29 | 35 | Dalgo runs on the following IP addresses, please whitelist these in your firewall if 36 | you need to: 37 | 38 | 39 | {dalgoWhitelistIps?.map((ip: string, index: number) => ( 40 | 41 | 42 | 43 | 44 | 54 | 55 | ))} 56 | 57 | 58 | )} 59 | 60 | 61 |
62 | 63 | ); 64 | }; 65 | export default Settings; 66 | -------------------------------------------------------------------------------- /src/pages/verifyemail/index.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button } from '@mui/material'; 2 | import { useSession, signOut } from 'next-auth/react'; 3 | import { useForm } from 'react-hook-form'; 4 | import styles from '@/styles/Login.module.css'; 5 | import { useContext } from 'react'; 6 | import { GlobalContext } from '@/contexts/ContextProvider'; 7 | import { useSearchParams } from 'next/navigation'; 8 | import { errorToast } from '@/components/ToastMessage/ToastHelper'; 9 | import Auth from '@/components/Layouts/Auth'; 10 | import { httpPost } from '../../helpers/http'; 11 | import { PageHead } from '@/components/PageHead'; 12 | 13 | export const VerifyEmail = () => { 14 | const { handleSubmit } = useForm(); 15 | const { data: session }: any = useSession(); 16 | const toastContext = useContext(GlobalContext); 17 | 18 | const searchParams = useSearchParams(); 19 | const token = searchParams.get('token'); 20 | 21 | const onSubmit = async () => { 22 | try { 23 | await httpPost(session, 'users/verify_email/', { 24 | token: token, 25 | }); 26 | signOut({ callbackUrl: '/login' }); 27 | } catch (error: any) { 28 | errorToast(error.cause.detail, [], toastContext); 29 | } 30 | }; 31 | 32 | return ( 33 | <> 34 | 35 | 36 | 37 |
38 | 39 | 47 | 48 |
49 |
50 | 51 | ); 52 | }; 53 | 54 | export default VerifyEmail; 55 | -------------------------------------------------------------------------------- /src/pages/verifyemail/resend.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, Link } from '@mui/material'; 2 | import { signOut, useSession } from 'next-auth/react'; 3 | import { useForm } from 'react-hook-form'; 4 | import styles from '@/styles/Login.module.css'; 5 | import { useContext } from 'react'; 6 | import { GlobalContext } from '@/contexts/ContextProvider'; 7 | import LogoutIcon from '@/assets/icons/logout.svg'; 8 | import { errorToast, successToast } from '@/components/ToastMessage/ToastHelper'; 9 | import Auth from '@/components/Layouts/Auth'; 10 | import { httpGet } from '../../helpers/http'; 11 | import Image from 'next/image'; 12 | import { PageHead } from '@/components/PageHead'; 13 | 14 | export const VerifyEmailResend = () => { 15 | const { handleSubmit } = useForm(); 16 | const { data: session }: any = useSession(); 17 | const globalContext = useContext(GlobalContext); 18 | 19 | const handleSignout = () => { 20 | // Hit backend api to invalidate the token 21 | localStorage.clear(); 22 | signOut({ callbackUrl: '/login' }); 23 | }; 24 | 25 | const onSubmit = async () => { 26 | try { 27 | await httpGet(session, 'users/verify_email/resend'); 28 | successToast('Verification email sent to your inbox', [], globalContext); 29 | } catch (error: any) { 30 | console.log(error); 31 | errorToast(error.cause.detail, [], globalContext); 32 | } 33 | }; 34 | 35 | return ( 36 | <> 37 | 38 | 39 | 40 |
41 | 42 | 50 | 51 |
52 | handleSignout()} 62 | > 63 | logout icon 64 | Logout 65 | 66 |
67 | 68 | ); 69 | }; 70 | 71 | export default VerifyEmailResend; 72 | -------------------------------------------------------------------------------- /src/pages/wren/index.tsx: -------------------------------------------------------------------------------- 1 | import { errorToast } from '@/components/ToastMessage/ToastHelper'; 2 | import { GlobalContext } from '@/contexts/ContextProvider'; 3 | import { httpGet } from '@/helpers/http'; 4 | import { Box, Typography } from '@mui/material'; 5 | import { useSession } from 'next-auth/react'; 6 | import React, { useContext, useEffect, useState } from 'react'; 7 | const MyIFrame = () => { 8 | const globalContext = useContext(GlobalContext); 9 | const { data: session } = useSession(); 10 | const [wrenUrl, setWrenUrl] = useState(''); 11 | 12 | const fetchWrenUrl = async () => { 13 | try { 14 | const response = await httpGet(session, `organizations/wren`); 15 | if (!response.wren_url) { 16 | errorToast('No hostname found for Wren', [], globalContext); 17 | } 18 | const { wren_url } = response; 19 | setWrenUrl(wren_url); 20 | } catch (error: any) { 21 | errorToast(error.message, [], globalContext); 22 | console.error(error, 'error'); 23 | } 24 | }; 25 | useEffect(() => { 26 | if (session) { 27 | fetchWrenUrl(); 28 | } 29 | }, [session]); 30 | return ( 31 | 32 |
33 | {wrenUrl ? ( 34 |