├── .dockerignore ├── .editorconfig ├── .env.sample ├── .env.test.sample ├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md ├── pr-labeler.yml └── workflows │ ├── cypress.yml │ ├── docker-publish.yml │ ├── docker-publish │ ├── docker-setup.mjs │ └── test-image.mjs │ ├── label-pr.yml │ ├── reviewdog.yml │ ├── stale.yml │ └── trigger-deploy.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE.MD ├── README.md ├── components ├── AnalyticsScripts.tsx ├── AuthLayout.tsx ├── Authenticated.tsx ├── ColumnListItem.tsx ├── DashedCreateBox.tsx ├── DataSourcesSidebar.tsx ├── DragIcon.tsx ├── ErrorMessage.tsx ├── ErrorWrapper.tsx ├── FeedbackPanel.tsx ├── FeedbackSidebarItem.tsx ├── GoogleSheetsSetup.tsx ├── HeadSection.tsx ├── Layout.tsx ├── LoadingComponent.tsx ├── LoadingOverlay.tsx ├── OrganizationSidebar.tsx ├── PageWrapper.tsx ├── PublicLayout.tsx ├── Shimmer.tsx ├── ShimmerOrCount.tsx ├── ShowErrorMessages.tsx ├── Sidebar.tsx ├── SidebarItem.tsx ├── TinyLabel.tsx ├── UnauthenticatedView.tsx └── svg │ ├── AlignLeftIcon.tsx │ ├── BracketsCurlyIcon.tsx │ ├── QuestionIcon.tsx │ ├── Spinner.tsx │ └── TextIcon.tsx ├── cypress.env.json ├── cypress.json ├── cypress ├── fixtures │ └── user.json ├── integration │ ├── 0-auth │ │ ├── check_login.js │ │ └── login_spec.js │ └── 1-generic │ │ └── data_sources_spec.js ├── plugins │ └── index.js └── support │ ├── commands.js │ └── index.js ├── deploy └── docker │ ├── .env.sample │ └── docker-compose.yml ├── features ├── activity │ ├── components │ │ └── ActivityItem.tsx │ └── types.d.ts ├── api │ ├── ApiResponse.ts │ ├── ApiService.ts │ ├── cookies.ts │ ├── index.ts │ ├── middleware.ts │ ├── middlewares │ │ ├── BelongsToOrganization.ts │ │ ├── HasAccessToDashboard.ts │ │ ├── IsSignedIn.ts │ │ ├── OwnsDataSource.ts │ │ └── VerifyKeepAlive.ts │ ├── types.d.ts │ └── urls.ts ├── app │ ├── api-slice.ts │ └── state-slice.ts ├── auth │ ├── index.ts │ ├── signinSchema.ts │ └── signupSchema.ts ├── authorization │ └── hooks.ts ├── cache │ └── index.ts ├── code │ └── evaluator.ts ├── dashboards │ ├── api-slice.ts │ ├── components │ │ ├── DashboardEditDataSourceInfo.tsx │ │ ├── DashboardEditName.tsx │ │ ├── DashboardEditVisibility.tsx │ │ ├── DashboardEditWidgets.tsx │ │ ├── DashboardPage.tsx │ │ ├── DashboardsSidebarSection.tsx │ │ ├── Divider.tsx │ │ ├── GenericCodeOption.tsx │ │ ├── GenericTextOption.tsx │ │ ├── Widget.tsx │ │ └── WidgetEditor.tsx │ ├── hooks.ts │ ├── index.ts │ ├── schema.ts │ ├── server-helpers.ts │ └── types.d.ts ├── data-sources │ ├── api-slice.ts │ ├── components │ │ ├── DataSourceEditName.tsx │ │ ├── DataSourceTileCreate.tsx │ │ ├── DataSourcesBlock.tsx │ │ ├── DataSourcesEditLayout.tsx │ │ ├── DummyDataSources.tsx │ │ ├── NewDataSourceForm.tsx │ │ └── OptionWrapper.tsx │ ├── hooks.ts │ ├── index.ts │ └── types.d.ts ├── fields │ ├── api-slice.ts │ ├── components │ │ ├── BooleanCheck.tsx │ │ ├── DummyField.tsx │ │ ├── EmptyDash.tsx │ │ ├── FieldWrapper │ │ │ ├── EditFieldWrapper.tsx │ │ │ ├── IndexFieldWrapper.tsx │ │ │ └── ShowFieldWrapper.tsx │ │ ├── GoToRecordLink.tsx │ │ └── MenuItem.tsx │ ├── factory.ts │ ├── getColumns.ts │ ├── hooks.ts │ ├── index.ts │ └── types.d.ts ├── options │ └── index.ts ├── organizations │ ├── api-slice.ts │ ├── components │ │ └── OrganizationsBlock.tsx │ └── invitationsSchema.ts ├── records │ ├── api-slice.ts │ ├── clientHelpers.ts │ ├── components │ │ ├── BackButton.tsx │ │ ├── EditRecord.tsx │ │ ├── Form.tsx │ │ ├── NewRecord.tsx │ │ ├── RecordsIndexPage.tsx │ │ └── ShowRecord.tsx │ ├── convertToBaseFilters.ts │ ├── hooks.ts │ ├── index.ts │ ├── state-slice.ts │ └── types.d.ts ├── roles │ ├── AccessControlService.ts │ ├── api-slice.ts │ ├── index.ts │ └── schema.ts ├── tables │ ├── api-slice.ts │ ├── components │ │ ├── BooleanConditionComponent.tsx │ │ ├── BulkDeleteButton.tsx │ │ ├── Cell.tsx │ │ ├── CheckboxColumnCell.tsx │ │ ├── ConditionComponent.tsx │ │ ├── ConditionSelect.tsx │ │ ├── CursorPagination.tsx │ │ ├── DateConditionComponent.tsx │ │ ├── Filter.tsx │ │ ├── FilterTrashIcon.tsx │ │ ├── FiltersButton.tsx │ │ ├── FiltersPanel.tsx │ │ ├── GroupFiltersPanel.tsx │ │ ├── IntConditionComponent.tsx │ │ ├── ItemControls.tsx │ │ ├── ItemControlsCell.tsx │ │ ├── MobileRow.tsx │ │ ├── OffsetPagination.tsx │ │ ├── OffsetPaginationComponent.tsx │ │ ├── RecordRow.tsx │ │ ├── RecordsTable.tsx │ │ ├── SelectConditionComponent.tsx │ │ ├── StringConditionComponent.tsx │ │ ├── TablesSidebarSection.tsx │ │ └── VerbComponent.tsx │ ├── hooks.ts │ ├── index.ts │ └── types.d.ts └── views │ ├── api-slice.ts │ ├── components │ ├── CompactFiltersView.tsx │ ├── FieldEditor.tsx │ ├── FieldTypeOption.tsx │ ├── GenericBooleanOption.tsx │ ├── GenericCodeOption.tsx │ ├── GenericSelectOption.tsx │ ├── GenericTextOption.tsx │ ├── NullableOption.tsx │ ├── OptionWrapper.tsx │ ├── ViewEditColumns.tsx │ ├── ViewEditDataSourceInfo.tsx │ ├── ViewEditFilters.tsx │ ├── ViewEditName.tsx │ ├── ViewEditOrder.tsx │ ├── ViewEditVisibility.tsx │ ├── ViewsSidebarSection.tsx │ └── VisibilityOption.tsx │ ├── hooks.ts │ ├── schema.ts │ └── types.d.ts ├── hooks └── index.ts ├── lib ├── ItemTypes.ts ├── chakra.ts ├── colors.js ├── constants.ts ├── crypto.ts ├── email.ts ├── encoding.ts ├── environment.ts ├── errors.ts ├── fonts.css ├── globals.css ├── gtag.ts ├── helpers.ts ├── humanize.ts ├── logger.ts ├── messages.ts ├── redis.ts ├── services.ts ├── siteMeta.ts ├── store.ts ├── time.ts └── track.ts ├── next-env.d.ts ├── next.config.js ├── package.json ├── pages ├── 404.tsx ├── _app.tsx ├── _error.tsx ├── api │ ├── auth │ │ ├── [...nextauth].ts │ │ └── register.ts │ ├── columns.ts │ ├── dashboards │ │ ├── [dashboardId].ts │ │ ├── [dashboardId] │ │ │ └── widgets │ │ │ │ ├── order.ts │ │ │ │ └── values.ts │ │ └── index.ts │ ├── data-sources │ │ ├── [dataSourceId].ts │ │ ├── [dataSourceId] │ │ │ ├── query.ts │ │ │ ├── tables.ts │ │ │ └── tables │ │ │ │ └── [tableName] │ │ │ │ ├── records.ts │ │ │ │ └── records │ │ │ │ ├── [recordId].ts │ │ │ │ └── bulk.ts │ │ ├── check-connection.ts │ │ ├── google-sheets │ │ │ ├── [dataSourceId] │ │ │ │ └── sheets.ts │ │ │ ├── auth-url.ts │ │ │ └── callback.ts │ │ └── index.ts │ ├── feedback.ts │ ├── hello.ts │ ├── organization-users │ │ └── [organizationUserId] │ │ │ └── roles.ts │ ├── organizations │ │ ├── [organizationId].ts │ │ ├── [organizationId] │ │ │ ├── activities.ts │ │ │ ├── invitations.ts │ │ │ ├── roles.ts │ │ │ ├── roles │ │ │ │ └── [roleId].ts │ │ │ └── users │ │ │ │ └── [organizationUserId].ts │ │ └── index.ts │ ├── profile.ts │ ├── records.ts │ ├── records │ │ └── [recordId].ts │ ├── test │ │ └── seed.ts │ ├── views │ │ ├── [viewId].ts │ │ ├── [viewId] │ │ │ ├── columns.ts │ │ │ └── columns │ │ │ │ ├── [columnName].ts │ │ │ │ └── order.ts │ │ └── index.ts │ └── widgets │ │ ├── [widgetId].ts │ │ ├── [widgetId] │ │ └── value.ts │ │ └── index.ts ├── auth │ ├── login.tsx │ └── register.tsx ├── beta.tsx ├── dashboards │ ├── [dashboardId].tsx │ └── [dashboardId] │ │ └── edit.tsx ├── data-sources │ ├── [dataSourceId].tsx │ ├── [dataSourceId] │ │ ├── edit.tsx │ │ └── tables │ │ │ ├── [tableName].tsx │ │ │ └── [tableName] │ │ │ ├── [recordId].tsx │ │ │ ├── [recordId] │ │ │ └── edit.tsx │ │ │ └── new.tsx │ ├── google-sheets │ │ └── new.tsx │ ├── maria_db │ │ └── new.tsx │ ├── mssql │ │ └── new.tsx │ ├── mysql │ │ └── new.tsx │ ├── new.tsx │ ├── postgresql │ │ └── new.tsx │ └── stripe │ │ └── new.tsx ├── index.tsx ├── organization-invitations │ └── [uuid].tsx ├── organizations │ ├── [organizationSlug].tsx │ └── [organizationSlug] │ │ ├── activity.tsx │ │ ├── members.tsx │ │ └── roles.tsx ├── profile.tsx └── views │ ├── [viewId].tsx │ ├── [viewId] │ ├── edit.tsx │ └── records │ │ ├── [recordId].tsx │ │ ├── [recordId] │ │ └── edit.tsx │ │ └── new.tsx │ └── new.tsx ├── plugins ├── data-sources │ ├── ConnectionPooler.ts │ ├── QueryServiceWrapper.ts │ ├── abstract-sql-query-service │ │ ├── AbstractQueryService.ts │ │ ├── doInitialScan.ts │ │ ├── getKnexClient.ts │ │ ├── index.ts │ │ └── types.d.ts │ ├── enums.ts │ ├── getDataSourceInfo.ts │ ├── getSchema.ts │ ├── google-sheets │ │ ├── GoogleDriveService.ts │ │ ├── QueryService.ts │ │ ├── constants.ts │ │ ├── index.ts │ │ └── types.d.ts │ ├── index.ts │ ├── maria_db │ │ ├── index.ts │ │ └── schema.ts │ ├── mssql │ │ ├── QueryService.ts │ │ ├── index.ts │ │ └── schema.ts │ ├── mysql │ │ ├── QueryService.ts │ │ ├── index.ts │ │ ├── schema.ts │ │ └── types.d.ts │ ├── postgresql │ │ ├── QueryService.ts │ │ ├── index.ts │ │ ├── schema.ts │ │ └── types.d.ts │ ├── serverHelpers.ts │ ├── stripe │ │ ├── QueryService.ts │ │ ├── index.ts │ │ └── schema.ts │ └── types.d.ts └── fields │ ├── Association │ ├── Edit.tsx │ ├── Index.tsx │ ├── Inspector.tsx │ ├── Show.tsx │ ├── fieldOptions.ts │ └── schema.ts │ ├── Boolean │ ├── Edit.tsx │ ├── Index.tsx │ └── Show.tsx │ ├── DateTime │ ├── Edit.tsx │ ├── Index.tsx │ ├── Inspector.tsx │ ├── Show.tsx │ ├── fieldOptions.ts │ ├── parsedValue.ts │ ├── schema.ts │ └── types.d.ts │ ├── Gravatar │ ├── Index.tsx │ ├── Inspector.tsx │ ├── Show.tsx │ ├── fieldOptions.ts │ ├── schema.ts │ └── types.d.ts │ ├── Id │ ├── Edit.tsx │ ├── Index.tsx │ └── Show.tsx │ ├── Json │ ├── Edit.tsx │ ├── Index.tsx │ ├── Show.tsx │ └── schema.ts │ ├── LinkTo │ ├── Index.tsx │ ├── Inspector.tsx │ ├── Show.tsx │ ├── fieldOptions.ts │ ├── schema.ts │ └── types.d.ts │ ├── Number │ ├── Edit.tsx │ ├── Index.tsx │ ├── Show.tsx │ ├── options.ts │ └── schema.ts │ ├── ProgressBar │ ├── Edit.tsx │ ├── Index.tsx │ ├── Inspector.tsx │ ├── Show.tsx │ ├── fieldOptions.ts │ ├── schema.ts │ └── types.d.ts │ ├── Select │ ├── Edit.tsx │ ├── Index.tsx │ ├── Inspector.tsx │ ├── Show.tsx │ ├── fieldOptions.ts │ ├── schema.ts │ └── types.d.ts │ ├── Text │ ├── Edit.tsx │ ├── Index.tsx │ ├── Inspector.tsx │ ├── Show.tsx │ ├── fieldOptions.ts │ ├── schema.ts │ └── types.d.ts │ └── Textarea │ ├── Edit.tsx │ ├── Index.tsx │ ├── Inspector.tsx │ ├── Show.tsx │ ├── fieldOptions.ts │ ├── schema.ts │ └── types.d.ts ├── postcss.config.js ├── prisma ├── index.ts ├── migrations │ ├── 20210827073855_initial_migration │ │ └── migration.sql │ ├── 20210902112607_add_encrypted_credentials_to_data_sources │ │ └── migration.sql │ ├── 20210908205601_add_roles │ │ └── migration.sql │ ├── 20210909110755_add_options_to_roles │ │ └── migration.sql │ ├── 20210923094700_add_ogranization_membership_invite │ │ └── migration.sql │ ├── 20210923095550_refactor_invite_table │ │ └── migration.sql │ ├── 20210923111111_invitations_have_uuid_primary_keys │ │ └── migration.sql │ ├── 20210923132123_cascade_user_deletion_to_invites │ │ └── migration.sql │ ├── 20210924130927_add_last_logged_in_at_to_users │ │ └── migration.sql │ ├── 20211015102604_add_views │ │ └── migration.sql │ ├── 20211015103805_change_filters_and_order_rules_to_arrays_in_views │ │ └── migration.sql │ ├── 20211015104016_delete_order_rules_from_views │ │ └── migration.sql │ ├── 20211015150345_add_datasource_id_to_views │ │ └── migration.sql │ ├── 20211015152114_rename_datasource_views │ │ └── migration.sql │ ├── 20211020084717_split_indexes_for_view │ │ └── migration.sql │ ├── 20211020141810_add_encrypted_ssh_credentials │ │ └── migration.sql │ ├── 20211022134622_add_order_rule_to_views │ │ └── migration.sql │ ├── 20211025070704_chnage_name_of_order_rule_in_views │ │ └── migration.sql │ ├── 20211105155559_add_activities │ │ └── migration.sql │ ├── 20211107151703_add_columns_to_views │ │ └── migration.sql │ ├── 20211108135526_add_action_to_activity │ │ └── migration.sql │ ├── 20211108210747_change_user_id_to_user_email_in_activity │ │ └── migration.sql │ ├── 20211109092051_change_back_to_user_id_activity │ │ └── migration.sql │ ├── 20211109114014_changes_to_array_acitivity │ │ └── migration.sql │ ├── 20211109164431_add_index_user_id_activity │ │ └── migration.sql │ ├── 20211110105430_update_view_filters_to_array │ │ └── migration.sql │ ├── 20211110111825_add_last_known_timezone_to_users │ │ └── migration.sql │ ├── 20211111151715_view_columns_to_array │ │ └── migration.sql │ ├── 20211119102418_add_table_meta_data_to_datasource │ │ └── migration.sql │ ├── 20211209121519_add_dashboard │ │ └── migration.sql │ ├── 20211210093006_add_datasource_to_dashboard │ │ └── migration.sql │ ├── 20211210142704_add_dashboard_item │ │ └── migration.sql │ ├── 20211214134027_rename_dashboard_item_to_widget │ │ └── migration.sql │ └── migration_lock.toml ├── sample-seed.sql ├── schema.prisma ├── seed-script.ts └── seed.ts ├── public ├── favicon.ico ├── favicons │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest ├── fonts │ ├── nunito-v16-latin-700.eot │ ├── nunito-v16-latin-700.svg │ ├── nunito-v16-latin-700.ttf │ ├── nunito-v16-latin-700.woff │ ├── nunito-v16-latin-700.woff2 │ ├── nunito-v16-latin-regular.eot │ ├── nunito-v16-latin-regular.svg │ ├── nunito-v16-latin-regular.ttf │ ├── nunito-v16-latin-regular.woff │ └── nunito-v16-latin-regular.woff2 └── img │ ├── 404_cat.jpg │ ├── bg.svg │ ├── cover.jpg │ ├── heading.png │ ├── illustration.svg │ ├── logo_text_black.png │ └── logos │ ├── airtable.png │ ├── amazon_redshift.png │ ├── github.png │ ├── google-sheets.png │ ├── intercom.png │ ├── maria_db.png │ ├── mssql.png │ ├── mysql.png │ ├── postgresql.png │ ├── redis.png │ ├── salesforce.png │ ├── shopify.png │ ├── stripe.png │ ├── swagger.png │ └── zendesk.png ├── scripts ├── docker-start.mjs ├── start-cypress.mjs └── test:migrate.mjs ├── sentry.client.config.js ├── sentry.properties ├── sentry.server.config.js ├── tailwind.config.js ├── tsconfig.json ├── types ├── index.d.ts ├── next-auth.d.ts └── react-table-config.d.ts └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | /.git 2 | /.github 3 | /.vscode 4 | .idea 5 | docker/ 6 | 7 | # Dockerfile 8 | deploy/docker 9 | final.Dockerfile 10 | docker-compose.yml 11 | .dockerignore 12 | node_modules 13 | npm-debug.log 14 | README.md 15 | .next 16 | 17 | pages/api/lol.ts 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | # The port on which to run the app in the container 2 | PORT=3000 3 | # URL without the http(s) protocol 4 | BASE_URL=localhost:${PORT} 5 | # URL with the http(s) protocol 6 | NEXTAUTH_URL=http://${BASE_URL} 7 | 8 | DATABASE_URL=postgresql://YOUR_USERNAME@127.0.0.1/basetool 9 | 10 | # generate a secret using `openssl rand -hex 32` 11 | SECRET= 12 | 13 | EMAIL_FROM="Basetool " 14 | SMTP_HOST= 15 | SMTP_PORT=587 16 | SMTP_USER= 17 | SMTP_PASSWORD= 18 | 19 | # Set up both analytics properties 20 | # https://developers.google.com/analytics/devguides/collection/ga4/basic-tag?technology=gtagjs 21 | # universal analytics (old) 22 | NEXT_PUBLIC_GOOGLE_ANALYTICS_UA= 23 | # GA 4 analytics (new) 24 | NEXT_PUBLIC_GOOGLE_ANALYTICS= 25 | 26 | # Google Sheets integration 27 | # https://console.cloud.google.com/apis/credentials/consent/edit 28 | GSHEETS_CLIENT_ID= 29 | GSHEETS_CLIENT_SECRET= 30 | GSHEETS_REDIRECT_URI= 31 | 32 | REDIS_URL= 33 | 34 | SENTRY_SERVER_INIT_PATH=.next/server/sentry/initServerSDK.js 35 | SENTRY_DSN= 36 | 37 | NEXT_PUBLIC_SENTRY_DSN= 38 | 39 | SENTRY_ORG=basetool 40 | SENTRY_PROJECT=basetool 41 | SENTRY_AUTH_TOKEN= 42 | 43 | INTERCOM_SECRET= 44 | INTERCOM_ACCESS_TOKEN= 45 | NEXT_PUBLIC_INTERCOM_APP_ID= 46 | 47 | TZ=UTC 48 | 49 | NEXT_PUBLIC_SEGMENT_PUBLIC_KEY= 50 | SEGMENT_WRITE_KEY= 51 | 52 | AWS_S3_DS_KEYS_ACCESS_KEY_ID= 53 | AWS_S3_DS_KEYS_SECRET_ACCESS_KEY= 54 | AWS_S3_DS_KEYS_REGION=us-east-1 55 | 56 | NEXT_PUBLIC_ENABLE_FULLSTORY=0 57 | 58 | SLACK_GROWTH_CHANNEL_WEBHOOK= 59 | -------------------------------------------------------------------------------- /.env.test.sample: -------------------------------------------------------------------------------- 1 | BASE_URL=http://localhost:4099 2 | API_URL=${BASE_URL}/api 3 | NEXTAUTH_URL=${BASE_URL} 4 | 5 | NEXT_PUBLIC_BASE_URL=${BASE_URL} 6 | NEXT_PUBLIC_API_URL=${API_URL} 7 | 8 | DATABASE_URL=postgresql://YOUR_USERNAME@127.0.0.1/basetool_test 9 | DATABASE_TEST_CREDENTIALS=postgresql://YOUR_USERNAME@127.0.0.1/avodemo_development 10 | 11 | SECRET= 12 | 13 | EMAIL_SERVER= 14 | EMAIL_FROM=avo@avohq.io 15 | 16 | SENTRY_SERVER_INIT_PATH=.next/server/sentry/initServerSDK.js 17 | SENTRY_DSN= 18 | NEXT_PUBLIC_SENTRY_DSN= 19 | SENTRY_ORG=basetool 20 | SENTRY_PROJECT=basetool 21 | SENTRY_AUTH_TOKEN= 22 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | dist 5 | # don't lint nyc coverage output 6 | coverage 7 | 8 | .next 9 | 10 | prisma/seed.ts 11 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [adrianthedev] 4 | -------------------------------------------------------------------------------- /.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/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/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | Fixes # (issue) 6 | 7 | ## Type of change 8 | 9 | - [ ] Bug fix (non-breaking change which fixes an issue) 10 | - [ ] New feature (non-breaking change which adds functionality) 11 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 12 | - [ ] This change requires a documentation update 13 | 14 | # How Has This Been Tested? 15 | 16 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 17 | 18 | # Checklist: 19 | 20 | - [ ] My code follows the style guidelines of this project 21 | - [ ] I have performed a self-review of my own code 22 | - [ ] I have commented my code, particularly in hard-to-understand areas 23 | - [ ] I have made corresponding changes to the documentation 24 | - [ ] My changes generate no new warnings 25 | - [ ] I have added tests that prove my fix is effective or that my feature works 26 | - [ ] New and existing unit tests pass locally with my changes 27 | - [ ] Any dependent changes have been merged and published in downstream modules 28 | -------------------------------------------------------------------------------- /.github/pr-labeler.yml: -------------------------------------------------------------------------------- 1 | feature: ['feature/*', 'feat/*'] 2 | fix: fix/* 3 | chore: chore/* 4 | test: test/* 5 | docs: docs/* 6 | refactor: [refactor/*, 'refact/*'] 7 | style: style/* 8 | ci: ci/* 9 | perf: perf/* 10 | fixed-branch: fixed-branch-name 11 | -------------------------------------------------------------------------------- /.github/workflows/cypress.yml: -------------------------------------------------------------------------------- 1 | name: Cypress tests 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | Test: 13 | env: 14 | ENV: test 15 | PGHOST: localhost 16 | PGUSER: postgres 17 | PGPORT: 5432 18 | DATABASE_URL: postgresql://postgres@127.0.0.1/basetool 19 | 20 | runs-on: ubuntu-latest 21 | 22 | services: 23 | postgres: 24 | image: postgres:11.5 25 | ports: ["5432:5432"] 26 | options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 27 | 28 | steps: 29 | - uses: actions/checkout@v2 30 | 31 | - name: Install deps 32 | run: yarn install 33 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish/docker-setup.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | const YAML = require("yaml"); 3 | 4 | const file = fs.readFileSync( 5 | "./../../../deploy/docker/docker-compose.yml", 6 | "utf8" 7 | ); 8 | const parsed = YAML.parse(file); 9 | 10 | parsed.services.app.image = `${process.env.DOCKER_IMAGE_NAME}:${process.env.DOCKER_IMAGE_TAG}`; 11 | parsed.services.database = { 12 | image: "postgres", 13 | ports: ["5432:5432"], 14 | environment: { 15 | POSTGRES_USER: "basetool", 16 | POSTGRES_PASSWORD: "basetool", 17 | POSTGRES_DB: "basetool", 18 | }, 19 | }; 20 | 21 | fs.writeFileSync( 22 | "./../../../deploy/docker/docker-compose.yml", 23 | YAML.stringify(parsed) 24 | ); 25 | 26 | console.log(`The docker-compose.yml file has been updated 🎉`); 27 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish/test-image.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | 3 | const port = process.env.PORT; 4 | const url = `http://localhost:${port}/api/hello`; 5 | const ipReq = await fetch(url); 6 | const response = await ipReq.json(); 7 | 8 | if (!response || response.hi !== "there") { 9 | console.log(`The reponse is not valid (${JSON.stringify(response)}).`); 10 | process.exit(1); 11 | } 12 | 13 | console.log(`The reponse is valid 🎉.`); 14 | -------------------------------------------------------------------------------- /.github/workflows/label-pr.yml: -------------------------------------------------------------------------------- 1 | name: PR Labeler 2 | 3 | on: 4 | pull_request: 5 | types: [opened] 6 | 7 | jobs: 8 | pr-labeler: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: TimonVS/pr-labeler-action@v3 12 | env: 13 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 14 | -------------------------------------------------------------------------------- /.github/workflows/reviewdog.yml: -------------------------------------------------------------------------------- 1 | name: Reviewdog 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | eslint: 7 | name: runner / eslint 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Get yarn cache directory path 12 | id: yarn-cache-dir-path 13 | run: echo "::set-output name=dir::$(yarn cache dir)" 14 | - uses: actions/cache@v1 15 | id: yarn-cache 16 | with: 17 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 18 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 19 | restore-keys: | 20 | ${{ runner.os }}-yarn 21 | - name: Install deps 22 | run: yarn 23 | - name: eslint 24 | uses: reviewdog/action-eslint@v1 25 | with: 26 | reporter: github-pr-review 27 | level: error 28 | eslint_flags: --quiet 29 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | 3 | on: 4 | schedule: 5 | - cron: '39 1 * * *' 6 | 7 | permissions: 8 | issues: write 9 | pull-requests: write 10 | 11 | jobs: 12 | stale: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/stale@v4 16 | with: 17 | stale-issue-message: This issue hasn't been updated in 60 days. We'll mark it as stale. Would you mind letting us know if you've got updates on it and you want to re-open it? Thanks 🙏 18 | stale-pr-message: This PR hasn't been updated in 60 days. We'll mark it as stale. Would you mind letting us know if you've got updates on it and you want to re-open it? Thanks 🙏 19 | close-issue-message: This issue hasn't been updated in 60 days. We'll close it for now. Would you mind letting us know if you've got updates on it and you want to re-open it? Thanks 🙏 20 | close-pr-message: This PR hasn't been updated in 60 days. We'll close it for now. Would you mind letting us know if you've got updates on it and you want to re-open it? Thanks 🙏 21 | -------------------------------------------------------------------------------- /.github/workflows/trigger-deploy.yml: -------------------------------------------------------------------------------- 1 | name: VPC Deploy 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Trigger deploy 13 | run: | 14 | curl -XPOST -u "${{ secrets.BASETOOL_IO_DEPLOY_USERNAME}}:${{secrets.BASETOOL_IO_DEPLOY_SECRET}}" -H "Accept: application/vnd.github.everest-preview+json" -H "Content-Type: application/json" https://api.github.com/repos/${{secrets.BASETOOL_IO_DEPLOY_REPO}}/dispatches --data '{"event_type": "deploy_application"}' 15 | -------------------------------------------------------------------------------- /.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 | 27 | # local env files 28 | .env 29 | .env.local 30 | .env.development.local 31 | .env.test 32 | .env.test.local 33 | .env.production.local 34 | 35 | # vercel 36 | .vercel 37 | 38 | cypress/screenshots 39 | cypress/videos 40 | 41 | #amplify-do-not-edit-begin 42 | amplify/\#current-cloud-backend 43 | amplify/.config/local-* 44 | amplify/logs 45 | amplify/mock-data 46 | amplify/backend/amplify-meta.json 47 | amplify/backend/awscloudformation 48 | amplify/backend/.temp 49 | build/ 50 | dist/ 51 | node_modules/ 52 | aws-exports.js 53 | awsconfiguration.json 54 | amplifyconfiguration.json 55 | amplifyconfiguration.dart 56 | amplify-build-config.json 57 | amplify-gradle-config.json 58 | amplifytools.xcconfig 59 | .secret-* 60 | **.sample 61 | #amplify-do-not-edit-end 62 | 63 | # Sentry 64 | .sentryclirc 65 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributing 3 | 4 | If you happened to come across a bug or want to suggest a feature, feel free to [say something](https://github.com/basetool-io/basetool/issues/new)! 5 | 6 | If you'd like to contribute code, the steps below will help you get up and running. 7 | 8 | ## Forking & branches 9 | 10 | Please fork Basetool and create a descriptive branch for your feature or fix. 11 | 12 | ## Getting your local environment set up 13 | 14 | ### Install 15 | 16 | ```bash 17 | yarn install 18 | cp .env.sample .env 19 | # edit your .env and 20 | # set your DATABASE_URL 21 | # generate a SECRET with openssl rand -hex 32 22 | yarn prisma migrate dev 23 | # seed 24 | SEED_PASSWORD=secret yarn prisma db seed 25 | ``` 26 | 27 | You may now log in with `ted.lasso@apple.com` and password `secret`. The seed script will not seed a datasource. Only the user and it's organization. 28 | 29 | There's also a `prisma/sample-seed.sql` file that you can use to create a sample database. 30 | 31 | ### Run 32 | 33 | ```bash 34 | yarn dev 35 | ``` 36 | 37 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 38 | 39 | ### Emails 40 | 41 | Your `.env` file uld have the `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER` and `SMTP_PASSWORD` variables filled in. For development and staging we can use [mailtrap](https://mailtrap.io/). On production we use AWS SES. 42 | 43 | ## Development 44 | 45 | We're using [google/zx](https://github.com/google/zx) to help us run scripts. 46 | 47 | ### Timezones 48 | 49 | `.env` holds the `TZ=UTC` entry to simulate server conditions (`TZ=UTC`). 50 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Install dependencies only when needed 2 | FROM node:14-alpine AS deps 3 | # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. 4 | RUN apk add --no-cache libc6-compat python3 py3-pip make g++ 5 | WORKDIR /app 6 | COPY package.json yarn.lock ./ 7 | COPY prisma ./ 8 | RUN yarn install --frozen-lockfile 9 | 10 | # Rebuild the source code only when needed 11 | FROM node:14-alpine AS builder 12 | WORKDIR /app 13 | COPY . . 14 | COPY --from=deps /app/node_modules ./node_modules 15 | RUN yarn build && yarn install --production --ignore-scripts --prefer-offline 16 | 17 | # Production image, copy all the files and run next 18 | FROM node:14-alpine AS runner 19 | WORKDIR /app 20 | 21 | ENV NODE_ENV production 22 | 23 | RUN addgroup -g 1001 -S nodejs 24 | RUN adduser -S nextjs -u 1001 25 | 26 | # You only need to copy next.config.js if you are NOT using the default configuration 27 | COPY --from=builder /app/next.config.js ./ 28 | COPY --from=builder /app/public ./public 29 | COPY --from=builder /app/prisma ./prisma 30 | COPY --from=builder /app/scripts ./scripts 31 | COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next 32 | COPY --from=builder /app/node_modules ./node_modules 33 | COPY --from=builder /app/package.json ./package.json 34 | 35 | USER nextjs 36 | 37 | EXPOSE 3000 38 | 39 | # Next.js collects completely anonymous telemetry data about general usage. 40 | # Learn more here: https://nextjs.org/telemetry 41 | # Uncomment the following line in case you want to disable telemetry. 42 | # ENV NEXT_TELEMETRY_DISABLED 1 43 | 44 | CMD ["yarn", "docker-start"] 45 | -------------------------------------------------------------------------------- /components/AuthLayout.tsx: -------------------------------------------------------------------------------- 1 | import HeadSection from "./HeadSection"; 2 | import Image from "next/image"; 3 | import React, { ReactNode } from "react"; 4 | 5 | function AuthLayout({ children }: { children: ReactNode }) { 6 | return ( 7 | <> 8 | 9 | 10 |
11 |
12 |
13 | {children} 14 |
15 |
16 |
17 |
18 | View and manage all your data in one place 25 |
26 |
27 | View and manage all your data in one place 34 |
35 |
36 |
37 |
38 | 39 | {/*
40 |
41 |
*/} 42 |
43 | 44 | ); 45 | } 46 | 47 | export default AuthLayout; 48 | -------------------------------------------------------------------------------- /components/Authenticated.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from "react"; 2 | import { useSession } from "next-auth/client"; 3 | import UnauthenticatedView from "./UnauthenticatedView" 4 | 5 | function Authenticated({ children }: { children: ReactElement}): ReactElement { 6 | const [session, isLoading] = useSession(); 7 | 8 | if (isLoading)
Loading...
9 | if (!isLoading && !session) return 10 | 11 | return children; 12 | } 13 | 14 | export default Authenticated; 15 | -------------------------------------------------------------------------------- /components/DashedCreateBox.tsx: -------------------------------------------------------------------------------- 1 | import { PlusIcon } from "@heroicons/react/outline"; 2 | import React, { ReactNode } from "react"; 3 | 4 | function DashedCreateBox({ children }: { children: string | ReactNode }) { 5 | return ( 6 |
7 |
8 | 9 | {children} 10 |
11 |
12 | ); 13 | } 14 | 15 | export default DashedCreateBox; 16 | -------------------------------------------------------------------------------- /components/DragIcon.tsx: -------------------------------------------------------------------------------- 1 | import { DotsVerticalIcon } from "@heroicons/react/outline"; 2 | import React from "react"; 3 | import classNames from "classnames"; 4 | 5 | function DragIcon({ className }: { className?: string }) { 6 | return ( 7 |
13 | 14 | 15 |
16 | ); 17 | } 18 | 19 | export default DragIcon; 20 | -------------------------------------------------------------------------------- /components/ErrorMessage.tsx: -------------------------------------------------------------------------------- 1 | import { memo } from "react"; 2 | 3 | const ErrorMessage = ({ 4 | message = "Oh no, There's been an error...", 5 | error, 6 | }: { 7 | message?: string; 8 | error?: string; 9 | }) => { 10 | return ( 11 |
12 |
13 |
{message}
14 | {error &&
{error}
} 15 |
16 |
17 | ); 18 | }; 19 | 20 | export default memo(ErrorMessage); 21 | -------------------------------------------------------------------------------- /components/FeedbackSidebarItem.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | ChatAltIcon, 3 | } from "@heroicons/react/outline"; 4 | import { DataSourceItem } from "./DataSourcesSidebar"; 5 | import { useBoolean, useClickAway } from "react-use"; 6 | import FeedbackPanel from "./FeedbackPanel"; 7 | import React, { memo, useRef } from "react"; 8 | 9 | const FeedbackSidebarItem = () => { 10 | const [feedbackPanelVisible, toggleFeedbackPanelVisible] = useBoolean(false); 11 | const feedbackButton = useRef(null); 12 | const feedbackPanel = useRef(null); 13 | 14 | useClickAway(feedbackPanel, (e) => { 15 | // When a user click the filters button to close the filters panel, the button is still outside, 16 | // so the action triggers twice closing and opening the filters panel. 17 | if ( 18 | feedbackButton?.current && 19 | !(feedbackButton?.current as any)?.contains(e.target) 20 | ) { 21 | toggleFeedbackPanelVisible(false); 22 | } 23 | }); 24 | 25 | const handleShowFeedbackPanelClick = () => { 26 | toggleFeedbackPanelVisible(); 27 | }; 28 | 29 | return ( 30 | <> 31 |
32 | } 35 | label={`Share any feedback or ideas`} 36 | onClick={handleShowFeedbackPanelClick} 37 | /> 38 |
39 | 40 | {feedbackPanelVisible && ( 41 |
45 | toggleFeedbackPanelVisible(false)} /> 46 |
47 | )} 48 | 49 | ); 50 | }; 51 | 52 | export default memo(FeedbackSidebarItem); 53 | -------------------------------------------------------------------------------- /components/LoadingOverlay.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, memo } from "react" 2 | import LoadingComponent from "./LoadingComponent"; 3 | import classNames from "classnames"; 4 | 5 | const LoadingOverlay = ({ 6 | label, 7 | subTitle, 8 | transparent = false, 9 | inPageWrapper, 10 | children, 11 | className 12 | }: { 13 | label?: string; 14 | subTitle?: string | boolean; 15 | transparent?: boolean; 16 | inPageWrapper?: boolean; 17 | children?: ReactNode 18 | className?: string; 19 | }) => { 20 | return ( 21 |
29 | {children} 30 |
31 | ); 32 | }; 33 | 34 | export default memo(LoadingOverlay); 35 | -------------------------------------------------------------------------------- /components/PublicLayout.tsx: -------------------------------------------------------------------------------- 1 | // import { inProduction } from "@/lib/environment"; 2 | // import { intercomAppId } from "@/lib/services" 3 | // import { useIntercom } from "react-use-intercom"; 4 | // import { useSession } from "next-auth/client"; 5 | import HeadSection from "./HeadSection"; 6 | import React, { ReactNode } from "react"; 7 | import classNames from "classnames"; 8 | 9 | function Layout({ children }: { children: ReactNode }) { 10 | // const [session, sessionIsLoading] = useSession(); 11 | // const { boot, update } = useIntercom(); 12 | 13 | // useEffect(() => { 14 | // // Boot up the Intercom widget 15 | // if (inProduction && intercomAppId) boot(); 16 | // }, []); 17 | 18 | // useEffect(() => { 19 | // // Update Intercom with the user's info 20 | // if (inProduction && !sessionIsLoading && session && intercomAppId) { 21 | // update({ 22 | // name: session?.user?.name, 23 | // email: session?.user?.email, 24 | // createdAt: session?.user?.createdAt?.toString(), 25 | // userHash: session?.user?.intercomUserHash, 26 | // }); 27 | // } 28 | // }, [sessionIsLoading, session]); 29 | 30 | return ( 31 | <> 32 | 33 |
34 |
39 |
40 |
41 | {children} 42 |
43 |
44 |
45 |
46 | 47 | ); 48 | } 49 | 50 | export default Layout; 51 | -------------------------------------------------------------------------------- /components/Shimmer.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import classNames from "classnames"; 3 | 4 | function Shimmer({ 5 | width = "100%", 6 | height = "auto", 7 | className = "", 8 | }: { 9 | width?: string | number; 10 | height?: string | number; 11 | className?: string; 12 | }) { 13 | const styles = { 14 | width, 15 | height, 16 | }; 17 | 18 | return ( 19 |
23 | ); 24 | } 25 | 26 | export default memo(Shimmer); 27 | -------------------------------------------------------------------------------- /components/ShimmerOrCount.tsx: -------------------------------------------------------------------------------- 1 | import React, { memo } from "react"; 2 | import Shimmer from "@/components/Shimmer"; 3 | import pluralize from "pluralize"; 4 | 5 | const ShimmerOrCount = ({ 6 | item, 7 | count, 8 | isLoading, 9 | }: { 10 | item: string; 11 | count: number; 12 | isLoading: boolean; 13 | }) => { 14 | return ( 15 | <> 16 | {isLoading && ( 17 | 18 | )} 19 | {isLoading || ( 20 | <> 21 | {count} {pluralize(item, count)} 22 | 23 | )} 24 | 25 | ); 26 | }; 27 | 28 | export default memo(ShimmerOrCount); 29 | -------------------------------------------------------------------------------- /components/ShowErrorMessages.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect } from "react" 2 | import { toast } from "react-toastify" 3 | import { useRouter } from "next/router" 4 | 5 | const ShowErrorMessages = ({ children }: { children: ReactNode }) => { 6 | const router = useRouter(); 7 | 8 | useEffect(() => { 9 | if (router.query.errorMessage) toast.error(router.query.errorMessage); 10 | }, [router.query.errorMessage]); 11 | 12 | return <>{children}; 13 | }; 14 | 15 | export default ShowErrorMessages 16 | -------------------------------------------------------------------------------- /components/SidebarItem.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React, { memo } from "react"; 3 | import classNames from "classnames"; 4 | 5 | function SidebarItem({ 6 | active, 7 | label, 8 | link, 9 | ...rest 10 | }: { 11 | active?: boolean; 12 | label: string; 13 | link: string; 14 | onClick?: () => void; 15 | [name: string]: any; 16 | }) { 17 | return ( 18 | 19 | 27 | {label} 28 | 29 | 30 | ); 31 | } 32 | 33 | export default memo(SidebarItem); 34 | -------------------------------------------------------------------------------- /components/TinyLabel.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | import classNames from "classnames"; 3 | 4 | const TinyLabel = ({ 5 | className = "", 6 | children, 7 | }: { 8 | className?: string; 9 | children: ReactNode | string; 10 | }) => ( 11 | 17 | {children} 18 | 19 | ); 20 | 21 | export default TinyLabel; 22 | -------------------------------------------------------------------------------- /components/UnauthenticatedView.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import PageWrapper from "./PageWrapper" 3 | import React, { memo, useEffect } from "react"; 4 | 5 | function UnauthenticatedView() { 6 | const router = useRouter(); 7 | useEffect(() => { 8 | router.push("/auth/login"); 9 | }, []); 10 | 11 | return ( 12 |
13 | 14 | <> 1 15 | 16 |
17 | ); 18 | } 19 | 20 | export default memo(UnauthenticatedView); 21 | -------------------------------------------------------------------------------- /components/svg/AlignLeftIcon.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import React from 'react' 3 | 4 | function Icon(props: any) { 5 | return () 6 | } 7 | 8 | export default Icon 9 | -------------------------------------------------------------------------------- /components/svg/BracketsCurlyIcon.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import React from 'react' 3 | 4 | function Icon(props: any) { 5 | return () 6 | } 7 | 8 | export default Icon 9 | -------------------------------------------------------------------------------- /components/svg/QuestionIcon.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import React from 'react' 3 | 4 | function Icon(props: any) { 5 | return () 6 | } 7 | 8 | export default Icon 9 | -------------------------------------------------------------------------------- /components/svg/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | function Spinner(props: any) { 4 | return ( 5 | 6 | 7 | 8 | 9 | ) 10 | } 11 | 12 | export default Spinner 13 | -------------------------------------------------------------------------------- /components/svg/TextIcon.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | import React from 'react' 3 | 4 | function Icon(props: any) { 5 | return () 6 | } 7 | 8 | export default Icon 9 | -------------------------------------------------------------------------------- /cypress.env.json: -------------------------------------------------------------------------------- 1 | { 2 | "COOKIE_NAME": "next-auth.session-token" 3 | } 4 | -------------------------------------------------------------------------------- /cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "baseUrl": "http://localhost:4099", 3 | "defaultCommandTimeout": 6000 4 | } 5 | -------------------------------------------------------------------------------- /cypress/fixtures/user.json: -------------------------------------------------------------------------------- 1 | { 2 | "email": "ted.lasso@apple.com", 3 | "password": "secret" 4 | } 5 | -------------------------------------------------------------------------------- /cypress/integration/0-auth/check_login.js: -------------------------------------------------------------------------------- 1 | describe("Check login", () => { 2 | before(() => { 3 | cy.seed() 4 | cy.login(); 5 | }) 6 | 7 | it("should login", () => { 8 | cy.visit("/"); 9 | cy.url().should("eql", `${Cypress.config('baseUrl')}/`); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /cypress/integration/0-auth/login_spec.js: -------------------------------------------------------------------------------- 1 | describe("Login Test", () => { 2 | before(() => { 3 | cy.seed() 4 | }) 5 | 6 | it("should take user to login page", () => { 7 | cy.visit("/auth/login"); 8 | cy.get("h2.mt-6.text-center.text-3xl.font-extrabold.text-gray-900").should( 9 | "have.text", 10 | "Sign in to your account" 11 | ); 12 | }); 13 | 14 | it("should redirect unauthenticated user to login page", () => { 15 | cy.visit("/"); 16 | cy.url().should("contain", "/auth/login"); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /cypress/integration/1-generic/data_sources_spec.js: -------------------------------------------------------------------------------- 1 | describe("Data Sources", () => { 2 | before(() => { 3 | Cypress.Cookies.defaults({ 4 | preserve: Cypress.env("COOKIE_NAME"), 5 | }); 6 | 7 | cy.seed(); 8 | cy.login(); 9 | }); 10 | 11 | it("adds a new datasource", () => { 12 | cy.visit("/data-sources/postgresql/new"); 13 | 14 | // Set up interceptor for login set up 15 | cy.intercept("POST", "/api/data-source?").as("create"); 16 | 17 | const credUrl = "postgresql://adrian@127.0.0.1/avodemo_development"; 18 | 19 | cy.get("[name=name]").type("Demo DB"); 20 | cy.get("[name='credentials.url']").type(credUrl); 21 | cy.get("#credentials_useSsl-label").click(); 22 | cy.get('button[type="submit"]') 23 | .should("be.visible") 24 | .click() 25 | .then((el) => { 26 | cy.get(el[0]).click() 27 | }); 28 | 29 | cy.wait("@create"); 30 | cy.get("@create").then((xhr) => { 31 | expect(xhr.response.statusCode).to.eq(200); 32 | cy.url().should( 33 | "eql", 34 | `${Cypress.config("baseUrl")}/data-sources/${xhr.response.body.data.id}` 35 | ); 36 | }); 37 | 38 | cy.contains('Data source created 🚀') 39 | }); 40 | 41 | it.only("can see a datasource", () => { 42 | cy.seedDataSource() 43 | cy.visit("/"); 44 | 45 | cy.contains("Avo Demo").click() 46 | cy.contains("Select a table to get started.") 47 | cy.contains(/^users$/).click() 48 | cy.contains("Browse records") 49 | cy.contains("Filters") 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | // eslint-disable-next-line no-unused-vars 19 | module.exports = (on, config) => { 20 | // `on` is used to hook into various events Cypress emits 21 | // `config` is the resolved Cypress config 22 | } 23 | -------------------------------------------------------------------------------- /cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js 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 | -------------------------------------------------------------------------------- /deploy/docker/.env.sample: -------------------------------------------------------------------------------- 1 | # The port on which to run the app in the container 2 | PORT=7654 3 | # URL without the http(s) protocol 4 | BASE_URL=localhost:${PORT} 5 | # URL with the http(s) protocol 6 | NEXTAUTH_URL=http://${BASE_URL} 7 | 8 | # Change this for production 9 | NEXT_PUBLIC_APP_ENV=development 10 | 11 | # Generate a secret using `openssl rand -hex 32` 12 | SECRET= 13 | 14 | TZ=UTC 15 | 16 | # host.docker.internal uses your localhost as db host 17 | DATABASE_URL=postgresql://PG_USER:PG_PASSWORD@host.docker.internal/basetool 18 | 19 | # Required to store the data source SSH keys 20 | AWS_S3_DS_KEYS_ACCESS_KEY_ID= 21 | AWS_S3_DS_KEYS_SECRET_ACCESS_KEY= 22 | AWS_S3_DS_KEYS_REGION= 23 | 24 | # Required to send emails 25 | EMAIL_FROM="Basetool install " 26 | SMTP_HOST=smtp.mailtrap.io 27 | SMTP_PORT=587 28 | SMTP_USER= 29 | SMTP_PASSWORD= 30 | 31 | # Set up both analytics properties 32 | # https://developers.google.com/analytics/devguides/collection/ga4/basic-tag?technology=gtagjs 33 | NEXT_PUBLIC_GOOGLE_ANALYTICS_UA= 34 | NEXT_PUBLIC_GOOGLE_ANALYTICS= 35 | 36 | # Google Sheets integration 37 | # Required if you plan to use the Google Sheets integration 38 | # https://console.cloud.google.com/apis/credentials/consent/edit?authuser=1&supportedpurview=project 39 | GSHEETS_CLIENT_ID= 40 | GSHEETS_CLIENT_SECRET= 41 | GSHEETS_REDIRECT_URI= 42 | 43 | # Required when using SSH keys and Google Sheets integration 44 | REDIS_URL=redis://redis:6379 45 | 46 | # Report errors to your account 47 | SENTRY_SERVER_INIT_PATH=.next/server/sentry/initServerSDK.js 48 | SENTRY_ORG= 49 | SENTRY_PROJECT= 50 | SENTRY_AUTH_TOKEN= 51 | 52 | SENTRY_DSN= 53 | # Same as above 54 | NEXT_PUBLIC_SENTRY_DSN= 55 | -------------------------------------------------------------------------------- /deploy/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.9" 2 | 3 | services: 4 | app: 5 | image: docker.io/basetool/basetool:latest 6 | env_file: .env 7 | ports: 8 | - "7654:7654" 9 | links: 10 | - redis 11 | redis: 12 | image: redis 13 | -------------------------------------------------------------------------------- /features/activity/types.d.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Activity as ActivityTypePrisma, 3 | DataSource, 4 | User, 5 | View, 6 | } from "@prisma/client"; 7 | 8 | export type ActivityType = ActivityTypePrisma & { 9 | dataSource?: DataSource; 10 | view?: View; 11 | user: User; 12 | }; 13 | -------------------------------------------------------------------------------- /features/api/cookies.ts: -------------------------------------------------------------------------------- 1 | import { CookieSerializeOptions, serialize } from 'cookie' 2 | import { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | /** 5 | * This sets `cookie` using the `res` object 6 | */ 7 | export const setCookie = ( 8 | res: NextApiResponse, 9 | name: string, 10 | value: unknown, 11 | options: CookieSerializeOptions = {}, 12 | ) => { 13 | const stringValue = typeof value === 'object' ? `j:${JSON.stringify(value)}` : String(value) 14 | 15 | if ('maxAge' in options && options?.maxAge) { 16 | options.expires = new Date(Date.now() + options?.maxAge) 17 | options.maxAge /= 1000 18 | } 19 | 20 | res.setHeader('Set-Cookie', serialize(name, String(stringValue), options)) 21 | } 22 | 23 | // Get the value of a cookie from a request object 24 | export const getCookie = ( 25 | req: NextApiRequest, 26 | name: string, 27 | ) => { 28 | if (req.cookies[name]) { 29 | if (req.cookies[name].includes('j:')) { 30 | return JSON.parse(req.cookies[name].replace('j:', '')) 31 | } 32 | 33 | return req.cookies[name] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /features/api/middlewares/BelongsToOrganization.ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler, NextApiRequest, NextApiResponse } from "next"; 2 | import { OrganizationUser, User } from "@prisma/client"; 3 | import { getUserFromRequest } from "@/features/api"; 4 | import isUndefined from "lodash/isUndefined"; 5 | 6 | const BelongsToOrganization = 7 | (handler: NextApiHandler) => 8 | async (req: NextApiRequest, res: NextApiResponse) => { 9 | const user = (await getUserFromRequest(req, { 10 | include: { 11 | organizations: true, 12 | }, 13 | })) as User & { 14 | organizations: OrganizationUser[]; 15 | }; 16 | 17 | // Check that the organizationId is in the users organizations 18 | const foundOrg = user?.organizations?.find( 19 | (org) => 20 | org.organizationId === 21 | parseInt( 22 | (req.query.organizationId || req.body.organizationId) as string 23 | ) 24 | ); 25 | if (isUndefined(foundOrg)) return res.status(404).send(""); 26 | 27 | return handler(req, res); 28 | }; 29 | 30 | export default BelongsToOrganization; 31 | -------------------------------------------------------------------------------- /features/api/middlewares/HasAccessToDashboard.ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler, NextApiRequest, NextApiResponse } from "next"; 2 | import { User } from "@prisma/client"; 3 | import { getDashboardFromRequest, getUserFromRequest } from ".."; 4 | 5 | const HasAccessToDashboard = 6 | (handler: NextApiHandler) => 7 | async (req: NextApiRequest, res: NextApiResponse) => { 8 | const dashboard = await getDashboardFromRequest(req); 9 | 10 | if (!dashboard) { 11 | return res.status(404).send(""); 12 | } 13 | 14 | const user = (await getUserFromRequest(req, { 15 | select: { 16 | id: true, 17 | organizations: { 18 | select: { 19 | organizationId: true, 20 | }, 21 | }, 22 | }, 23 | })) as User & { 24 | organizations: { 25 | organizationId: number; 26 | }[]; 27 | }; 28 | 29 | const organizationIds = user.organizations.map( 30 | ({ organizationId }) => organizationId 31 | ); 32 | 33 | if (!organizationIds.includes(dashboard?.organizationId)) { 34 | return res.status(404).send(""); 35 | } 36 | 37 | return handler(req, res); 38 | }; 39 | 40 | export default HasAccessToDashboard; 41 | -------------------------------------------------------------------------------- /features/api/middlewares/IsSignedIn.ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler, NextApiRequest, NextApiResponse } from 'next' 2 | import { getSession } from 'next-auth/client' 3 | 4 | const IsSignedIn = (handler: NextApiHandler) => async (req: NextApiRequest, res: NextApiResponse) => { 5 | const session = await getSession({ req }) 6 | 7 | if (!session) { 8 | return res.status(404).send('') 9 | } 10 | 11 | return handler(req, res) 12 | } 13 | 14 | export default IsSignedIn 15 | -------------------------------------------------------------------------------- /features/api/middlewares/OwnsDataSource.ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler, NextApiRequest, NextApiResponse } from "next"; 2 | import { User } from "@prisma/client"; 3 | import { getDataSourceFromRequest, getUserFromRequest } from "@/features/api"; 4 | 5 | const OwnsDataSource = 6 | (handler: NextApiHandler) => 7 | async (req: NextApiRequest, res: NextApiResponse) => { 8 | const user = (await getUserFromRequest(req, { 9 | include: { 10 | organizations: { 11 | select: { 12 | organizationId: true, 13 | }, 14 | }, 15 | }, 16 | })) as User & { 17 | organizations: { organizationId: number }[]; 18 | }; 19 | const dataSource = await getDataSourceFromRequest(req); 20 | 21 | if ( 22 | dataSource?.organizationId && 23 | user && 24 | user.organizations && 25 | // if data source organization is in one of the user's organization 26 | user.organizations.map(({ organizationId }) => organizationId).includes(dataSource.organizationId) 27 | ) { 28 | return handler(req, res); 29 | } 30 | 31 | return res.status(404).send(""); 32 | }; 33 | 34 | export default OwnsDataSource; 35 | -------------------------------------------------------------------------------- /features/api/middlewares/VerifyKeepAlive.ts: -------------------------------------------------------------------------------- 1 | import { NextApiHandler, NextApiRequest, NextApiResponse } from "next"; 2 | 3 | const VerifyKeepAlive = 4 | (handler: NextApiHandler) => 5 | async (req: NextApiRequest, res: NextApiResponse) => { 6 | if (req.query.keep_alive) return res.send('OK') 7 | 8 | return handler(req, res); 9 | }; 10 | 11 | export default VerifyKeepAlive; 12 | -------------------------------------------------------------------------------- /features/api/types.d.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest } from "next" 2 | 3 | export type BasetoolApiRequest = NextApiRequest & { 4 | subdomain?: string 5 | } 6 | -------------------------------------------------------------------------------- /features/api/urls.ts: -------------------------------------------------------------------------------- 1 | export const urls = () => { 2 | let baseUrl = ""; 3 | 4 | if (typeof window !== "undefined" && window?.location?.origin) { 5 | // If we can get it from the window object, use that. 6 | baseUrl = window.location.origin; 7 | } else { 8 | const prefix = (process.env.BASE_URL as string).includes("localhost") 9 | ? "http://" 10 | : "https://"; 11 | // By default we should use the BASE_URL. 12 | baseUrl = `${prefix}${process.env.BASE_URL}`; 13 | 14 | // If we're on vercel and not production, we should read the custom deployment url. 15 | if (process.env.VERCEL && process.env.VERCEL_ENV !== "production") { 16 | if (process.env.NEXT_PUBLIC_VERCEL_URL) { 17 | baseUrl = `${prefix}${process.env.NEXT_PUBLIC_VERCEL_URL}`; 18 | } else { 19 | baseUrl = `${prefix}${process.env.VERCEL_URL}`; 20 | } 21 | } 22 | } 23 | 24 | const apiUrl = `${baseUrl}/api`; 25 | 26 | return { baseUrl, apiUrl }; 27 | }; 28 | 29 | const { baseUrl, apiUrl } = urls(); 30 | 31 | export { baseUrl, apiUrl }; 32 | -------------------------------------------------------------------------------- /features/app/api-slice.ts: -------------------------------------------------------------------------------- 1 | import { apiUrl } from "../api/urls"; 2 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 3 | import ApiResponse from "../api/ApiResponse"; 4 | 5 | export const profileApiSlice = createApi({ 6 | reducerPath: "profileApi", 7 | baseQuery: fetchBaseQuery({ 8 | baseUrl: apiUrl, 9 | }), 10 | tagTypes: ["Profile"], 11 | endpoints(builder) { 12 | return { 13 | getProfile: builder.query({ 14 | query: () => `/profile`, 15 | providesTags: (response) => { 16 | // is result available? 17 | if (response && response?.data) { 18 | // successful query 19 | return [{ type: "Profile", id: response?.data?.user?.email }]; 20 | } 21 | 22 | return []; 23 | }, 24 | }), 25 | sendFeedback: builder.mutation< 26 | ApiResponse, 27 | Partial<{ 28 | body: { note: string, emotion: string, url: string }; 29 | }> 30 | >({ 31 | query: ({ body }) => ({ 32 | url: `${apiUrl}/feedback`, 33 | method: "POST", 34 | body, 35 | }), 36 | }), 37 | }; 38 | }, 39 | }); 40 | 41 | export const { useGetProfileQuery, useSendFeedbackMutation } = profileApiSlice; 42 | -------------------------------------------------------------------------------- /features/app/state-slice.ts: -------------------------------------------------------------------------------- 1 | import { PayloadAction, createSlice } from "@reduxjs/toolkit"; 2 | 3 | interface AppState { 4 | sidebarsVisible: boolean; 5 | 6 | dataSourceId: string; 7 | tableName: string; 8 | } 9 | 10 | const initialState: AppState = { 11 | sidebarsVisible: false, 12 | 13 | dataSourceId: "", 14 | tableName: "", 15 | }; 16 | 17 | const appStateSlice = createSlice({ 18 | name: "appState", 19 | initialState, 20 | reducers: { 21 | resetState() { 22 | return initialState; 23 | }, 24 | 25 | /* Sidebars */ 26 | setSidebarVisibile(state, action: PayloadAction) { 27 | state.sidebarsVisible = action.payload; 28 | }, 29 | 30 | /* Data source context */ 31 | setDataSourceId(state, action: PayloadAction) { 32 | state.dataSourceId = action.payload; 33 | }, 34 | setTableName(state, action: PayloadAction) { 35 | state.tableName = action.payload; 36 | }, 37 | }, 38 | }); 39 | 40 | export const sidebarsVisibleSelector = ({ appState }: { appState: AppState }) => 41 | appState.sidebarsVisible; 42 | 43 | export const dataSourceIdSelector = ({ appState }: { appState: AppState }) => 44 | appState.dataSourceId; 45 | export const tableNameSelector = ({ appState }: { appState: AppState }) => 46 | appState.tableName; 47 | 48 | export const { resetState, setSidebarVisibile, setDataSourceId, setTableName } = 49 | appStateSlice.actions; 50 | 51 | export default appStateSlice.reducer; 52 | -------------------------------------------------------------------------------- /features/auth/signinSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | export const schema = Joi.object({ 4 | email: Joi.string() 5 | .label("Email") 6 | .email({ tlds: { allow: false } }) 7 | .required(), 8 | password: Joi.string().label("Password").min(6).required(), 9 | }); 10 | -------------------------------------------------------------------------------- /features/auth/signupSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | export const schema = Joi.object({ 4 | csrfToken: Joi.string() 5 | .label("CSRF Token") 6 | .required(), 7 | email: Joi.string() 8 | .label("Email") 9 | .email({ tlds: { allow: false } }) 10 | .required(), 11 | password: Joi.string().label("Password").min(6).required(), 12 | organization: Joi.string().label("Organization").required(), 13 | firstName: Joi.string().label("First name").allow(""), 14 | lastName: Joi.string().label("Last name").allow(""), 15 | lastKnownTimezone: Joi.string().label("Last known timezone").allow(""), 16 | }); 17 | -------------------------------------------------------------------------------- /features/code/evaluator.ts: -------------------------------------------------------------------------------- 1 | const curlyRegex = /{{(.*)}}/i; 2 | 3 | /** 4 | * This method gets two params. The context and the binding. 5 | * 6 | * Example: 7 | * evaluateBinding({ 8 | * context: { record: field.record, value: field.value }, 9 | * input: field.column.baseOptions.backgroundColor, // {{ value.toLowerCase().includes('z') ? 'yellow' : 'green' }} 10 | * }); 11 | */ 12 | 13 | export const evaluateBinding = ({ 14 | context, 15 | input, 16 | }: { 17 | context: Record; 18 | input: string; 19 | }): string | undefined => { 20 | if (!input || !context) return; 21 | 22 | // assign params and values 23 | const contextParams = Object.keys(context); 24 | const contextValues = Object.values(context); 25 | 26 | // fetch the contents between the curly braces 27 | const matches = input.match(curlyRegex); 28 | 29 | if (!matches || matches.length < 2) return; 30 | 31 | // Add return if missing 32 | const statement = matches[1].startsWith("return") 33 | ? matches[1] 34 | : `return ${matches[1]}`; 35 | 36 | // create the evaluation function 37 | const evalFn = new Function(...contextParams, statement); 38 | // evaluate the context 39 | try { 40 | return evalFn(...contextValues); 41 | } catch (error) { 42 | console.error("Evaluation error:", error); 43 | } 44 | }; 45 | -------------------------------------------------------------------------------- /features/dashboards/components/DashboardEditDataSourceInfo.tsx: -------------------------------------------------------------------------------- 1 | import { useDataSourceContext } from "@/hooks"; 2 | import { useDataSourceResponse } from "@/features/data-sources/hooks" 3 | import React from "react"; 4 | import Shimmer from "@/components/Shimmer"; 5 | import TinyLabel from "@/components/TinyLabel"; 6 | 7 | function DashboardEditDataSourceInfo() { 8 | const { dataSourceId } = useDataSourceContext(); 9 | const { dataSource, isLoading: dataSourceIsLoading } = 10 | useDataSourceResponse(dataSourceId); 11 | 12 | return ( 13 |
14 |
15 | DataSource 16 |
17 | {dataSourceIsLoading && ( 18 | 19 | )} 20 | {!dataSourceIsLoading && dataSource?.name} 21 |
22 |
23 |
24 | ); 25 | } 26 | 27 | export default DashboardEditDataSourceInfo; 28 | -------------------------------------------------------------------------------- /features/dashboards/components/DashboardEditVisibility.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox } from "@chakra-ui/react"; 2 | import { useDashboardResponse } from "../hooks"; 3 | import { useDataSourceContext } from "@/hooks"; 4 | import React from "react"; 5 | import Shimmer from "@/components/Shimmer"; 6 | import TinyLabel from "@/components/TinyLabel"; 7 | 8 | function DashboardEditVisibility({ 9 | updateVisibility, 10 | }: { 11 | updateVisibility: (visible: boolean) => void; 12 | }) { 13 | const { dashboardId } = useDataSourceContext(); 14 | const { dashboard, isLoading: dashboardIsLoading } = useDashboardResponse(dashboardId); 15 | 16 | return ( 17 |
18 | Visibility 19 |
20 | {dashboardIsLoading && ( 21 |
22 | {" "} 23 | 24 |
25 | )} 26 | {!dashboardIsLoading && ( 27 | updateVisibility(e.currentTarget.checked)} 31 | > 32 | Visible to all members 33 | 34 | )} 35 |
36 |
37 | ); 38 | } 39 | 40 | export default DashboardEditVisibility; 41 | -------------------------------------------------------------------------------- /features/dashboards/components/Divider.tsx: -------------------------------------------------------------------------------- 1 | import { Widget } from "@prisma/client"; 2 | import { activeWidgetIdSelector } from "@/features/records/state-slice"; 3 | import { useAppSelector } from "@/hooks"; 4 | import React, { memo, useMemo } from "react"; 5 | import classNames from "classnames"; 6 | 7 | 8 | function Divider({ widget }: { widget: Widget }) { 9 | const activeWidgetId = useAppSelector(activeWidgetIdSelector); 10 | 11 | const widgetIsActive = useMemo( 12 | () => activeWidgetId === widget.id, 13 | [activeWidgetId, widget.id] 14 | ); 15 | 16 | return ( 17 |
25 | {widget.name} 26 |
27 | ); 28 | } 29 | 30 | export default memo(Divider); 31 | -------------------------------------------------------------------------------- /features/dashboards/index.ts: -------------------------------------------------------------------------------- 1 | import { ElementType } from "react"; 2 | import { MinusIcon, VariableIcon } from "@heroicons/react/outline"; 3 | import { Widget } from "@prisma/client"; 4 | 5 | export const iconForWidget = (widget: Widget): ElementType => { 6 | switch (widget.type) { 7 | default: 8 | case "metric": 9 | return VariableIcon; 10 | case "divider": 11 | return MinusIcon; 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /features/dashboards/schema.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | export const schema = Joi.object({ 4 | name: Joi.string().min(3).required(), 5 | isPublic: Joi.boolean().required(), 6 | dataSourceId: Joi.number().required(), 7 | }); 8 | -------------------------------------------------------------------------------- /features/dashboards/server-helpers.ts: -------------------------------------------------------------------------------- 1 | import { DataSource, Widget } from "@prisma/client"; 2 | import { WidgetValue } from "@/features/dashboards/types"; 3 | import { runQuery } from "@/plugins/data-sources/serverHelpers"; 4 | 5 | export const getValueForWidget = async ( 6 | widget: Pick, 7 | dataSource: DataSource 8 | ) => { 9 | let response: WidgetValue; 10 | 11 | try { 12 | let queryValue = { 13 | value: "", 14 | }; 15 | 16 | if (widget.type === "metric") { 17 | queryValue = await runQuery(dataSource, "runRawQuery", { 18 | query: widget.query, 19 | }); 20 | } 21 | 22 | response = { 23 | id: widget.id, 24 | value: queryValue.value, 25 | }; 26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 27 | } catch (e: any) { 28 | response = { 29 | id: widget.id, 30 | error: e.message, 31 | }; 32 | } 33 | 34 | return response; 35 | }; 36 | -------------------------------------------------------------------------------- /features/dashboards/types.d.ts: -------------------------------------------------------------------------------- 1 | export type WidgetValue = { 2 | id: number; 3 | value?: string; 4 | error?: string; 5 | }; 6 | 7 | export type WidgetOptions = { 8 | prefix: string; 9 | suffix: string; 10 | }; 11 | -------------------------------------------------------------------------------- /features/data-sources/components/OptionWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, memo } from "react"; 2 | import classNames from "classnames"; 3 | 4 | const OptionWrapper = ({ 5 | helpText, 6 | children, 7 | fullWidth = false, 8 | }: { 9 | helpText?: string | ReactNode; 10 | children: ReactNode; 11 | fullWidth?: boolean; 12 | }) => { 13 | return ( 14 |
15 |
16 | {children} 17 |
18 | {!fullWidth && ( 19 |
20 | {helpText && ( 21 |
22 | {helpText} 23 |
24 | )} 25 |
26 | )} 27 |
28 | ); 29 | }; 30 | 31 | export default memo(OptionWrapper); 32 | -------------------------------------------------------------------------------- /features/data-sources/hooks.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from "@prisma/client"; 2 | import { useGetDataSourceQuery } from "./api-slice"; 3 | import { useMemo } from "react"; 4 | 5 | export const useDataSourceResponse = (dataSourceId: string) => { 6 | const { 7 | data: response, 8 | isLoading, 9 | isFetching, 10 | refetch, 11 | error, 12 | } = useGetDataSourceQuery({ dataSourceId }, { skip: !dataSourceId }); 13 | 14 | const dataSource: DataSource | undefined = useMemo( 15 | () => response?.ok && response.data, 16 | [response] 17 | ); 18 | 19 | const info = useMemo(() => { 20 | if (response && response.ok) { 21 | return response.meta?.dataSourceInfo; 22 | } 23 | 24 | return {}; 25 | }, [response]); 26 | 27 | return { 28 | dataSource, 29 | response, 30 | isLoading, 31 | isFetching, 32 | refetch, 33 | error, 34 | info, 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /features/data-sources/index.ts: -------------------------------------------------------------------------------- 1 | import { FieldType } from "../fields/types" 2 | import { S3_SSH_KEYS_BUCKET_PREFIX } from "@/lib/constants"; 3 | import { inProduction } from "@/lib/environment"; 4 | 5 | export const s3KeysBucket = () => { 6 | return `${S3_SSH_KEYS_BUCKET_PREFIX}${ 7 | inProduction ? "production" : "staging" 8 | }`; 9 | }; 10 | 11 | export const INITIAL_NEW_COLUMN = { 12 | name: "computed_field", 13 | label: "Computed field", 14 | primaryKey: false, 15 | baseOptions: { 16 | visibleOnIndex: true, 17 | visibleOnShow: true, 18 | required: false, 19 | nullable: false, 20 | nullValues: [], 21 | readonly: false, 22 | placeholder: "", 23 | help: "", 24 | label: "", 25 | disconnected: false, 26 | defaultValue: "", 27 | computed: true, 28 | }, 29 | fieldType: "Text" as FieldType, 30 | fieldOptions: { 31 | value: "", 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /features/data-sources/types.d.ts: -------------------------------------------------------------------------------- 1 | import { DataQuery as PrismaDataQuery } from "@prisma/client"; 2 | 3 | interface DataQuery extends PrismaDataQuery { 4 | options: { 5 | query: string; 6 | runOnPageLoad: boolean; 7 | }; 8 | data: [] | Record; 9 | isLoading: boolean; 10 | type: string; // one of DataSourceTypes 11 | } 12 | 13 | export default DataQuery; 14 | 15 | export type TableMetaData = { 16 | name: string; 17 | idColumn: string; 18 | nameColumn: string; 19 | createdAtColumn?: string; 20 | updatedAtColumn?: string; 21 | }; 22 | -------------------------------------------------------------------------------- /features/fields/api-slice.ts: -------------------------------------------------------------------------------- 1 | import { apiUrl } from "../api/urls"; 2 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 3 | import ApiResponse from "../api/ApiResponse"; 4 | import URI from "urijs"; 5 | 6 | export const api = createApi({ 7 | reducerPath: "fields", 8 | baseQuery: fetchBaseQuery({ 9 | baseUrl: `${apiUrl}`, 10 | }), 11 | tagTypes: ["ViewColumns", "TableColumns"], 12 | endpoints(builder) { 13 | return { 14 | getColumns: builder.query< 15 | ApiResponse, 16 | { dataSourceId?: string; tableName?: string; viewId?: string } 17 | >({ 18 | query({ dataSourceId, tableName, viewId }) { 19 | const queryParams = URI() 20 | .query({ 21 | dataSourceId, 22 | tableName, 23 | viewId, 24 | }) 25 | .query() 26 | .toString(); 27 | 28 | return `/columns?${queryParams}`; 29 | }, 30 | providesTags: (response, error, { tableName, viewId }) => { 31 | if (viewId) return [{ type: "ViewColumns", id: tableName }]; 32 | 33 | if (tableName) return [{ type: "TableColumns", id: tableName }]; 34 | 35 | return []; 36 | }, 37 | }), 38 | }; 39 | }, 40 | }); 41 | 42 | export const { useGetColumnsQuery, usePrefetch } = api; 43 | -------------------------------------------------------------------------------- /features/fields/components/BooleanCheck.tsx: -------------------------------------------------------------------------------- 1 | import { CheckCircleIcon, XCircleIcon } from '@heroicons/react/outline' 2 | import React, { memo, useMemo } from 'react' 3 | 4 | function BooleanCheck({ checked } : {checked: boolean}) { 5 | const classes = useMemo(() => `h-5 ${checked ? 'text-green-600' : 'text-red-600'}`, [checked]) 6 | 7 | return ( 8 | <> 9 | {checked && } 10 | {!checked && } 11 | 12 | ) 13 | } 14 | 15 | export default memo(BooleanCheck) 16 | -------------------------------------------------------------------------------- /features/fields/components/DummyField.tsx: -------------------------------------------------------------------------------- 1 | const DummyField = () => null; 2 | 3 | export default DummyField; 4 | -------------------------------------------------------------------------------- /features/fields/components/EmptyDash.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { memo } from "react"; 3 | 4 | const EmptyDash = () => ; 5 | 6 | export default memo(EmptyDash); 7 | -------------------------------------------------------------------------------- /features/fields/components/FieldWrapper/IndexFieldWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { Field } from "../../types"; 2 | import { ReactNode, memo } from "react"; 3 | import { bgColors } from "@/lib/colors"; 4 | import { evaluateBinding } from "@/features/code/evaluator"; 5 | import classNames from "classnames"; 6 | 7 | const IndexFieldWrapper = ({ 8 | field, 9 | children, 10 | flush = false, 11 | }: { 12 | field: Field; 13 | children: ReactNode; 14 | flush?: boolean; 15 | }) => { 16 | const styles: Record = {}; 17 | const classes: string[] = []; 18 | 19 | // Apply the background color if set 20 | if (field.column.baseOptions.backgroundColor) { 21 | let color 22 | try { 23 | color = evaluateBinding({ 24 | context: { record: field.record, value: field.value }, 25 | input: field.column.baseOptions.backgroundColor, 26 | }); 27 | } catch (error) { 28 | // eslint-disable-next-line no-console 29 | console.log('There was an error computing the background color: ', error.message) 30 | } 31 | 32 | if (color) { 33 | if (bgColors.includes(color)) { 34 | classes.push(`bg-${color}-200`); 35 | } else { 36 | styles.backgroundColor = color; 37 | } 38 | } 39 | } 40 | 41 | return ( 42 |
53 |
{children}
54 |
55 | ); 56 | }; 57 | 58 | export default memo(IndexFieldWrapper); 59 | -------------------------------------------------------------------------------- /features/fields/components/GoToRecordLink.tsx: -------------------------------------------------------------------------------- 1 | import { ArrowRightIcon } from "@heroicons/react/outline"; 2 | import { Tooltip } from "@chakra-ui/react"; 3 | import Link from "next/link"; 4 | import React, { memo } from "react"; 5 | 6 | function GoToRecord({ 7 | href, 8 | label = "Go to record", 9 | }: { 10 | href: string; 11 | label?: string; 12 | }) { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | } 25 | 26 | export default memo(GoToRecord); 27 | -------------------------------------------------------------------------------- /features/fields/components/MenuItem.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, memo } from "react"; 2 | 3 | const MenuItem = ({ 4 | children, 5 | ...rest 6 | }: { 7 | children: ReactNode; 8 | [rest: string]: any; 9 | }) => ( 10 | 14 | {children} 15 | 16 | ); 17 | 18 | export default memo(MenuItem); 19 | -------------------------------------------------------------------------------- /features/fields/getColumns.ts: -------------------------------------------------------------------------------- 1 | import { Column } from "react-table"; 2 | import { DataSource } from "@prisma/client"; 3 | import { hydrateColumns } from "../records"; 4 | import { runQuery } from "@/plugins/data-sources/serverHelpers"; 5 | 6 | export const getColumns = async ({ 7 | dataSource, 8 | tableName, 9 | storedColumns = [], 10 | }: { 11 | dataSource: DataSource; 12 | tableName: string; 13 | storedColumns: []; 14 | }): Promise => { 15 | let columns = await runQuery(dataSource, "getColumns", { 16 | tableName: tableName, 17 | storedColumns, 18 | }); 19 | 20 | columns = hydrateColumns(columns, storedColumns); 21 | 22 | return columns; 23 | }; 24 | -------------------------------------------------------------------------------- /features/fields/hooks.ts: -------------------------------------------------------------------------------- 1 | import { Column } from "./types"; 2 | import { useGetColumnsQuery } from "./api-slice"; 3 | import { useMemo } from "react"; 4 | 5 | export const useColumnsResponse = ({ 6 | dataSourceId, 7 | tableName, 8 | }: { 9 | dataSourceId: string; 10 | tableName: string; 11 | }) => { 12 | const { 13 | data: response, 14 | isLoading, 15 | isFetching, 16 | refetch, 17 | error, 18 | } = useGetColumnsQuery( 19 | { dataSourceId, tableName }, 20 | { skip: !dataSourceId || !tableName } 21 | ); 22 | 23 | const columns: Column[] = useMemo( 24 | () => (response?.ok ? response.data : []), 25 | [response] 26 | ); 27 | 28 | return { 29 | columns, 30 | response, 31 | isLoading, 32 | isFetching, 33 | refetch, 34 | error, 35 | }; 36 | }; 37 | -------------------------------------------------------------------------------- /features/options/index.ts: -------------------------------------------------------------------------------- 1 | type PossibleValues = string | boolean | number | null | undefined; 2 | 3 | const OPTIONS: Record = { 4 | runInProxy: false, 5 | }; 6 | 7 | const optionsHandler = { 8 | get: async (key: string): Promise => { 9 | return OPTIONS[key]; 10 | }, 11 | }; 12 | 13 | export default optionsHandler; 14 | -------------------------------------------------------------------------------- /features/organizations/invitationsSchema.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi" 2 | 3 | export const schema = Joi.object({ 4 | email: Joi.string() 5 | .email({ tlds: { allow: false } }) 6 | .required(), 7 | firstName: Joi.string().required(), 8 | lastName: Joi.string().required(), 9 | password: Joi.string().pattern(new RegExp("^[a-zA-Z0-9@$!%*?&]{6,30}$")), 10 | passwordConfirmation: Joi.ref("password"), 11 | }); 12 | -------------------------------------------------------------------------------- /features/records/clientHelpers.ts: -------------------------------------------------------------------------------- 1 | import { BasetoolRecord, PossibleRecordValues } from "@/features/records/types"; 2 | import { Column } from "../fields/types" 3 | import { IFilter } from "../tables/types"; 4 | import { isArray, isString } from "lodash"; 5 | 6 | export const filtersForHasMany = ( 7 | columnName: string, 8 | ids: string | number[] 9 | ): IFilter[] => { 10 | let value = ""; 11 | 12 | if (isArray(ids)) { 13 | value = ids.join(","); 14 | } else if (isString(ids)) { 15 | value = ids; 16 | } 17 | 18 | return [ 19 | { 20 | column: {} as Column, 21 | columnName, 22 | condition: "is_in", 23 | value, 24 | verb: "and", 25 | }, 26 | ]; 27 | }; 28 | 29 | /** 30 | * This method tries to extract a pretty name from a record 31 | */ 32 | export const getPrettyName = ( 33 | record: BasetoolRecord, 34 | field?: string | undefined 35 | ): string => { 36 | // See if we have some `name` column set in the DB 37 | let prettyName: PossibleRecordValues = ""; 38 | 39 | // Use the `nameColumn` attribute 40 | if (field) { 41 | prettyName = record[field]; 42 | } else { 43 | // Try and find a common `name` columns 44 | if (record.url) prettyName = record.url; 45 | if (record.email) prettyName = record.email; 46 | if (record.first_name) prettyName = record.first_name; 47 | if (record.firstName) prettyName = record.firstName; 48 | if (record.title) prettyName = record.title; 49 | if (record.name) prettyName = record.name; 50 | } 51 | 52 | if (prettyName) return prettyName.toString(); 53 | 54 | if (record && record?.id) return record.id.toString(); 55 | 56 | return ""; 57 | }; 58 | -------------------------------------------------------------------------------- /features/records/components/BackButton.tsx: -------------------------------------------------------------------------------- 1 | import { BackspaceIcon } from "@heroicons/react/outline"; 2 | import { Button } from "@chakra-ui/react"; 3 | import Link from "next/link"; 4 | import React, { ReactElement, memo } from "react"; 5 | 6 | function BackButton({ href, children = 'Back' }: { href: string, children?: ReactElement | string }) { 7 | return ( 8 | 9 | 18 | 19 | ); 20 | } 21 | 22 | export default memo(BackButton); 23 | -------------------------------------------------------------------------------- /features/records/components/NewRecord.tsx: -------------------------------------------------------------------------------- 1 | import { Column } from "@/features/fields/types"; 2 | import { getVisibleColumns } from "@/features/fields"; 3 | import { isEmpty } from "lodash"; 4 | import { useDataSourceContext } from "@/hooks"; 5 | import { useGetColumnsQuery } from "@/features/fields/api-slice"; 6 | import ErrorMessage from "@/components/ErrorMessage"; 7 | import Form from "@/features/records/components/Form"; 8 | import Layout from "@/components/Layout"; 9 | import LoadingOverlay from "@/components/LoadingOverlay"; 10 | import React, { memo, useMemo } from "react"; 11 | 12 | const NewRecord = () => { 13 | const { dataSourceId, tableName, viewId } = useDataSourceContext(); 14 | const { 15 | data: columnsResponse, 16 | isLoading, 17 | error, 18 | } = useGetColumnsQuery( 19 | { 20 | dataSourceId, 21 | tableName, 22 | viewId, 23 | }, 24 | { skip: !dataSourceId || !tableName } 25 | ); 26 | 27 | const columns = useMemo( 28 | () => 29 | getVisibleColumns(columnsResponse?.data, "new").filter( 30 | (column: Column) => !column.primaryKey 31 | ), 32 | [columnsResponse?.data] 33 | ); 34 | 35 | return ( 36 | 37 | {isLoading && ( 38 | 39 | )} 40 | {error && "data" in error && ( 41 | 44 | )} 45 | {!isLoading && columnsResponse?.ok && ( 46 |
47 | )} 48 | 49 | ); 50 | }; 51 | 52 | export default memo(NewRecord); 53 | -------------------------------------------------------------------------------- /features/records/convertToBaseFilters.ts: -------------------------------------------------------------------------------- 1 | import { FilterOrFilterGroup } from "../tables/types"; 2 | import { isArray, isNull } from "lodash"; 3 | 4 | export const convertToBaseFilters = ( 5 | filters: FilterOrFilterGroup[] | null | undefined 6 | ): FilterOrFilterGroup[] => { 7 | if (!filters || isNull(filters) || !isArray(filters)) return []; 8 | 9 | return filters.map((filter: FilterOrFilterGroup) => ({ 10 | ...filter, 11 | isBase: true, 12 | })); 13 | }; 14 | -------------------------------------------------------------------------------- /features/records/types.d.ts: -------------------------------------------------------------------------------- 1 | export type PossibleRecordValues = string | number | boolean | null | undefined; 2 | 3 | export type BasetoolRecord = Record 4 | -------------------------------------------------------------------------------- /features/roles/index.ts: -------------------------------------------------------------------------------- 1 | const OWNER_ROLE = "Owner"; 2 | const MEMBER_ROLE = "Member"; 3 | 4 | export const defaultAbilities = [ 5 | { 6 | id: "can_read", 7 | label: "Can view records", 8 | }, 9 | { 10 | id: "can_create", 11 | label: "Can create records", 12 | }, 13 | { 14 | id: "can_update", 15 | label: "Can edit records", 16 | }, 17 | { 18 | id: "can_delete", 19 | label: "Can delete records", 20 | }, 21 | ]; 22 | 23 | export { OWNER_ROLE, MEMBER_ROLE }; 24 | -------------------------------------------------------------------------------- /features/roles/schema.ts: -------------------------------------------------------------------------------- 1 | import { defaultAbilities } from "@/features/roles" 2 | import Joi from "joi"; 3 | 4 | const abilities = defaultAbilities.map(({id}) => id) 5 | 6 | export const schema = Joi.object({ 7 | name: Joi.string().min(3).required(), 8 | options: Joi.object({ 9 | abilities: Joi.array().items(...abilities.map((ability) => Joi.string().valid(ability))), 10 | }), 11 | }); 12 | -------------------------------------------------------------------------------- /features/tables/api-slice.ts: -------------------------------------------------------------------------------- 1 | import { apiUrl } from "../api/urls"; 2 | import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; 3 | import ApiResponse from "../api/ApiResponse"; 4 | 5 | export const tablesApiSlice = createApi({ 6 | reducerPath: "tables", 7 | baseQuery: fetchBaseQuery({ 8 | baseUrl: `${apiUrl}`, 9 | }), 10 | tagTypes: ["Table"], 11 | endpoints(builder) { 12 | return { 13 | getTables: builder.query({ 14 | query({ dataSourceId }) { 15 | return `/data-sources/${dataSourceId}/tables`; 16 | }, 17 | providesTags: (response, error, { dataSourceId }) => [ 18 | { type: "Table", id: dataSourceId }, 19 | ], 20 | }), 21 | }; 22 | }, 23 | }); 24 | 25 | export const { 26 | useGetTablesQuery, 27 | usePrefetch, 28 | } = tablesApiSlice; 29 | -------------------------------------------------------------------------------- /features/tables/components/BooleanConditionComponent.tsx: -------------------------------------------------------------------------------- 1 | import { BooleanFilterConditions } from ".."; 2 | import { IFilter } from "../types"; 3 | import ConditionSelect from "./ConditionSelect"; 4 | import React from "react"; 5 | 6 | function BooleanConditionComponent({ 7 | filter, 8 | onChange, 9 | }: { 10 | filter: IFilter; 11 | onChange: (condition: BooleanFilterConditions) => void; 12 | }) { 13 | return ( 14 | onChange(value as BooleanFilterConditions)} 18 | /> 19 | ); 20 | } 21 | 22 | export default BooleanConditionComponent; 23 | -------------------------------------------------------------------------------- /features/tables/components/Cell.tsx: -------------------------------------------------------------------------------- 1 | import { Column } from "@/features/fields/types"; 2 | import { Row } from "react-table"; 3 | import { getField } from "@/features/fields/factory"; 4 | import { makeField } from "@/features/fields"; 5 | 6 | import React, { memo } from "react"; 7 | 8 | const Cell = memo( 9 | ({ 10 | row, 11 | column, 12 | tableName, 13 | }: { 14 | row: Row; 15 | column: { meta: Column }; 16 | tableName: string; 17 | }) => { 18 | const field = makeField({ 19 | record: row.original, 20 | column: column?.meta, 21 | tableName, 22 | }); 23 | const Element = getField(column.meta, "index"); 24 | 25 | return ; 26 | } 27 | ); 28 | 29 | export default memo(Cell); 30 | -------------------------------------------------------------------------------- /features/tables/components/CheckboxColumnCell.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox } from "@chakra-ui/react"; 2 | import { isUndefined } from "lodash"; 3 | import { useSelectRecords } from "@/features/records/hooks"; 4 | import React, { memo } from "react"; 5 | 6 | const CheckboxColumnCell = ({ id }: { id: number }) => { 7 | const { selectedRecords, toggleRecordSelection } = useSelectRecords(); 8 | 9 | if (isUndefined(id)) return null; 10 | 11 | return ( 12 |
13 | toggleRecordSelection(id)} 17 | /> 18 |
19 | ); 20 | }; 21 | 22 | export default memo(CheckboxColumnCell); 23 | -------------------------------------------------------------------------------- /features/tables/components/ConditionComponent.tsx: -------------------------------------------------------------------------------- 1 | import { FilterConditions, IFilter } from "../types"; 2 | import BooleanConditionComponent from "./BooleanConditionComponent"; 3 | import DateConditionComponent from "./DateConditionComponent"; 4 | import IntConditionComponent from "./IntConditionComponent"; 5 | import SelectConditionComponent from "./SelectConditionComponent"; 6 | import StringConditionComponent from "./StringConditionComponent"; 7 | 8 | function ConditionComponent({ 9 | filter, 10 | onChange, 11 | ...rest 12 | }: { 13 | filter: IFilter; 14 | onChange: (condition: FilterConditions) => void; 15 | }) { 16 | let Component; 17 | const { column } = filter; 18 | 19 | switch (column.fieldType) { 20 | case "Id": 21 | case "Number": 22 | case "Association": 23 | case "ProgressBar": 24 | Component = IntConditionComponent; 25 | break; 26 | case "Boolean": 27 | Component = BooleanConditionComponent; 28 | break; 29 | case "DateTime": 30 | Component = DateConditionComponent; 31 | break; 32 | case "Select": 33 | Component = SelectConditionComponent; 34 | break; 35 | default: 36 | case "Text": 37 | Component = StringConditionComponent; 38 | break; 39 | } 40 | 41 | return ; 42 | } 43 | 44 | export default ConditionComponent; 45 | -------------------------------------------------------------------------------- /features/tables/components/ConditionSelect.tsx: -------------------------------------------------------------------------------- 1 | import { Select } from "@chakra-ui/react"; 2 | import React, { memo } from "react"; 3 | 4 | function ConditionSelect({ 5 | value, 6 | options, 7 | onChange, 8 | }: { 9 | value: string; 10 | options: [string, string][]; 11 | onChange: (value: unknown) => void; 12 | }) { 13 | return ( 14 | 25 | ); 26 | } 27 | 28 | export default memo(ConditionSelect); 29 | -------------------------------------------------------------------------------- /features/tables/components/DateConditionComponent.tsx: -------------------------------------------------------------------------------- 1 | import { DateFilterConditions } from ".."; 2 | import { IFilter } from "../types"; 3 | import ConditionSelect from "./ConditionSelect"; 4 | import React from "react"; 5 | 6 | function DateConditionComponent({ 7 | filter, 8 | onChange, 9 | }: { 10 | filter: IFilter; 11 | onChange: (condition: DateFilterConditions) => void; 12 | }) { 13 | return ( 14 | onChange(value as DateFilterConditions)} 18 | /> 19 | ); 20 | } 21 | 22 | export default DateConditionComponent; 23 | -------------------------------------------------------------------------------- /features/tables/components/FilterTrashIcon.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Tooltip } from "@chakra-ui/react"; 2 | import { TrashIcon } from "@heroicons/react/outline"; 3 | import React, { memo } from "react"; 4 | 5 | const FilterTrashIcon = ({ onClick }: { onClick: () => void }) => ( 6 |
7 | 8 | 11 | 12 |
13 | ); 14 | 15 | export default memo(FilterTrashIcon); 16 | -------------------------------------------------------------------------------- /features/tables/components/IntConditionComponent.tsx: -------------------------------------------------------------------------------- 1 | import { IFilter } from "../types"; 2 | import { IntFilterConditions } from ".."; 3 | import ConditionSelect from "./ConditionSelect"; 4 | import React from "react"; 5 | 6 | const options = { 7 | is: "=", 8 | is_not: "!=", 9 | gt: ">", 10 | gte: ">=", 11 | lt: "<", 12 | lte: "<=", 13 | is_null: "is_null", 14 | is_not_null: "is_not_null", 15 | is_in: "is_in", 16 | is_not_in: "is_not_in", 17 | }; 18 | 19 | function IntConditionComponent({ 20 | filter, 21 | onChange, 22 | }: { 23 | filter: IFilter; 24 | onChange: (condition: IntFilterConditions) => void; 25 | }) { 26 | return ( 27 | onChange(value as IntFilterConditions)} 31 | /> 32 | ); 33 | } 34 | 35 | export default IntConditionComponent; 36 | -------------------------------------------------------------------------------- /features/tables/components/ItemControlsCell.tsx: -------------------------------------------------------------------------------- 1 | import { Row } from "react-table"; 2 | import { isUndefined } from "lodash"; 3 | import ItemControls from "./ItemControls"; 4 | import React, { memo } from "react"; 5 | 6 | const ItemControlsCell = ({ row }: { row: Row }) => { 7 | if (isUndefined(row?.original?.id)) return null; 8 | 9 | return
10 | 11 |
12 | }; 13 | 14 | export default memo(ItemControlsCell); 15 | -------------------------------------------------------------------------------- /features/tables/components/OffsetPagination.tsx: -------------------------------------------------------------------------------- 1 | import { usePagination } from "@/features/records/hooks"; 2 | import OffsetPaginationComponent from "./OffsetPaginationComponent"; 3 | import React, { memo } from "react"; 4 | 5 | const OffsetPagination = () => { 6 | const { 7 | page, 8 | perPage, 9 | offset, 10 | nextPage, 11 | previousPage, 12 | maxPages, 13 | canPreviousPage, 14 | canNextPage, 15 | recordsCount, 16 | } = usePagination(); 17 | 18 | return ( 19 | 30 | ); 31 | }; 32 | 33 | export default memo(OffsetPagination); 34 | -------------------------------------------------------------------------------- /features/tables/components/RecordRow.tsx: -------------------------------------------------------------------------------- 1 | import { Row } from "react-table"; 2 | import { columnWidthsSelector } from "@/features/records/state-slice"; 3 | import { useAppSelector } from "@/hooks"; 4 | import React, { memo } from "react"; 5 | import classNames from "classnames"; 6 | 7 | const RecordRow = ({ row }: { row: Row }) => { 8 | useAppSelector(columnWidthsSelector); // keep this so the columnWidths will trigger a change 9 | 10 | return ( 11 |
15 | {row.cells.map((cell) => ( 16 |
17 | {cell.render("Cell")} 18 |
19 | ))} 20 |
21 | ); 22 | }; 23 | 24 | export default memo(RecordRow); 25 | -------------------------------------------------------------------------------- /features/tables/components/SelectConditionComponent.tsx: -------------------------------------------------------------------------------- 1 | import { IFilter } from "../types"; 2 | import { SelectFilterConditions } from ".."; 3 | import ConditionSelect from "./ConditionSelect"; 4 | import React from "react"; 5 | 6 | function SelectConditionComponent({ 7 | filter, 8 | onChange, 9 | }: { 10 | filter: IFilter; 11 | onChange: (condition: SelectFilterConditions) => void; 12 | }) { 13 | return ( 14 | onChange(value as SelectFilterConditions)} 18 | /> 19 | ); 20 | } 21 | 22 | export default SelectConditionComponent; 23 | -------------------------------------------------------------------------------- /features/tables/components/StringConditionComponent.tsx: -------------------------------------------------------------------------------- 1 | import { IFilter } from "../types"; 2 | import { StringFilterConditions } from ".."; 3 | import ConditionSelect from "./ConditionSelect"; 4 | import React from "react"; 5 | 6 | function StringConditionComponent({ 7 | filter, 8 | onChange, 9 | }: { 10 | filter: IFilter; 11 | onChange: (condition: StringFilterConditions) => void; 12 | }) { 13 | return ( 14 | onChange(value as StringFilterConditions)} 18 | /> 19 | ); 20 | } 21 | 22 | export default StringConditionComponent; 23 | -------------------------------------------------------------------------------- /features/tables/components/VerbComponent.tsx: -------------------------------------------------------------------------------- 1 | import { FilterVerbs } from ".."; 2 | import { FormControl, Select } from "@chakra-ui/react"; 3 | import React, { memo, useMemo } from "react"; 4 | 5 | function VerbComponent({ 6 | idx, 7 | verb, 8 | onChange, 9 | }: { 10 | idx: number; 11 | verb: FilterVerbs; 12 | onChange: (verb: FilterVerbs) => void; 13 | }) { 14 | const isFirst = useMemo(() => idx === 0, [idx]); 15 | const isSecond = useMemo(() => idx === 1, [idx]); 16 | const isMoreThanSecond = useMemo(() => idx > 1, [idx]); 17 | 18 | return ( 19 | <> 20 |
21 | {isFirst && "where"} 22 | {isMoreThanSecond && verb} 23 |
24 | {isSecond && ( 25 | 26 | 37 | 38 | )} 39 | 40 | ); 41 | } 42 | 43 | export default memo(VerbComponent); 44 | -------------------------------------------------------------------------------- /features/tables/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TableResponse } from "./types"; 2 | import { useGetTablesQuery } from "./api-slice"; 3 | import { useMemo } from "react"; 4 | 5 | export const useTablesResponse = ({ 6 | dataSourceId, 7 | }: { 8 | dataSourceId: string; 9 | }) => { 10 | const { 11 | data: response, 12 | isLoading, 13 | isFetching, 14 | refetch, 15 | error, 16 | } = useGetTablesQuery({ dataSourceId }, { skip: !dataSourceId }); 17 | 18 | const tables: TableResponse[] = useMemo( 19 | () => (response?.ok ? response.data : []), 20 | [response] 21 | ); 22 | 23 | return { 24 | tables, 25 | response, 26 | isLoading, 27 | isFetching, 28 | refetch, 29 | error, 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /features/tables/types.d.ts: -------------------------------------------------------------------------------- 1 | import type { Column } from "@/features/fields/types"; 2 | 3 | export type Table = { 4 | columns?: { 5 | [columnName: string]: Column; 6 | }; 7 | }; 8 | 9 | export type Tables = { 10 | [tableName: string]: Table; 11 | }; 12 | 13 | export type TableResponse = { 14 | name: string; 15 | }; 16 | 17 | export type OrderDirection = "" | "asc" | "desc"; 18 | 19 | export type FilterConditions = 20 | | IntFilterConditions 21 | | StringFilterConditions 22 | | BooleanFilterConditions 23 | | DateFilterConditions 24 | | SelectFilterConditions; 25 | 26 | export type IFilter = { 27 | column: Column; 28 | columnName: string; 29 | verb: FilterVerb; 30 | condition: FilterConditions; 31 | option?: string; 32 | value?: string; 33 | isBase?: boolean; 34 | }; 35 | 36 | export type IFilterGroup = { 37 | isGroup: boolean; 38 | verb: FilterVerb; 39 | filters: IFilter[]; 40 | isBase?: boolean; 41 | }; 42 | 43 | export type FilterOrFilterGroup = IFilter | IFilterGroup; 44 | -------------------------------------------------------------------------------- /features/views/components/FieldTypeOption.tsx: -------------------------------------------------------------------------------- 1 | import { FieldType } from "@/features/fields/types"; 2 | import { Select } from "@chakra-ui/react"; 3 | import { useSegment } from "@/hooks"; 4 | import { useUpdateColumn } from "../hooks"; 5 | import OptionWrapper from "@/features/views/components/OptionWrapper"; 6 | import React from "react"; 7 | 8 | export const FieldTypeOption = () => { 9 | const track = useSegment(); 10 | 11 | const { column, columnOptions, setColumnOptions } = useUpdateColumn(); 12 | 13 | if (!column) return null; 14 | 15 | const handleOnChange = (e: any) => { 16 | setColumnOptions(column.name, { 17 | fieldType: e.currentTarget.value as FieldType, 18 | }); 19 | 20 | track("Changed the field type selector", { 21 | type: e.currentTarget.value, 22 | }); 23 | }; 24 | 25 | return ( 26 | 33 | 49 | 50 | ); 51 | }; 52 | 53 | export default FieldTypeOption; 54 | -------------------------------------------------------------------------------- /features/views/components/OptionWrapper.tsx: -------------------------------------------------------------------------------- 1 | import { FormControl, Tooltip } from "@chakra-ui/react"; 2 | import { InformationCircleIcon } from "@heroicons/react/outline"; 3 | import { isString, snakeCase } from "lodash"; 4 | import React, { ReactNode, memo } from "react"; 5 | 6 | const OptionWrapper = ({ 7 | helpText, 8 | label, 9 | id, 10 | children, 11 | }: { 12 | helpText?: string | ReactNode; 13 | label?: string | ReactNode; 14 | id?: string; 15 | children: ReactNode; 16 | }) => { 17 | if (isString(label)) id ||= snakeCase(label.toLowerCase()); 18 | 19 | return ( 20 |
21 | 22 |
23 | 29 |
30 | 31 |
32 | 33 |
34 |
35 |
36 |
37 |
{children}
38 |
39 |
40 | ); 41 | }; 42 | 43 | export default memo(OptionWrapper); 44 | -------------------------------------------------------------------------------- /features/views/components/ViewEditDataSourceInfo.tsx: -------------------------------------------------------------------------------- 1 | import { useDataSourceContext } from "@/hooks"; 2 | import { useDataSourceResponse } from "@/features/data-sources/hooks" 3 | import { useViewResponse } from "../hooks"; 4 | import React from "react"; 5 | import Shimmer from "@/components/Shimmer"; 6 | import TinyLabel from "@/components/TinyLabel"; 7 | 8 | function ViewEditDataSourceInfo() { 9 | const { viewId, dataSourceId } = useDataSourceContext(); 10 | const { view, isLoading: viewIsLoading } = useViewResponse(viewId); 11 | const { dataSource, isLoading: dataSourceIsLoading } = 12 | useDataSourceResponse(dataSourceId); 13 | 14 | return ( 15 |
16 |
17 | DataSource 18 |
19 | {dataSourceIsLoading && ( 20 | 21 | )} 22 | {!dataSourceIsLoading && dataSource?.name} 23 |
24 |
25 |
26 | Table name 27 |
28 | {viewIsLoading && ( 29 | 30 | )} 31 | {!viewIsLoading && view?.tableName} 32 |
33 |
34 |
35 | ); 36 | } 37 | 38 | export default ViewEditDataSourceInfo; 39 | -------------------------------------------------------------------------------- /features/views/components/ViewEditVisibility.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox } from "@chakra-ui/react"; 2 | import { useDataSourceContext } from "@/hooks"; 3 | import { useViewResponse } from "../hooks"; 4 | import React from "react"; 5 | import Shimmer from "@/components/Shimmer"; 6 | import TinyLabel from "@/components/TinyLabel"; 7 | 8 | function ViewEditVisibility({ 9 | updateVisibility, 10 | }: { 11 | updateVisibility: (visible: boolean) => void; 12 | }) { 13 | const { viewId } = useDataSourceContext(); 14 | const { view, isLoading: viewIsLoading } = useViewResponse(viewId); 15 | 16 | return ( 17 |
18 | Visibility 19 |
20 | {viewIsLoading && ( 21 |
22 | {" "} 23 | 24 |
25 | )} 26 | {!viewIsLoading && ( 27 | updateVisibility(e.currentTarget.checked)} 31 | > 32 | Visible to all members 33 | 34 | )} 35 |
36 |
37 | ); 38 | } 39 | 40 | export default ViewEditVisibility; 41 | -------------------------------------------------------------------------------- /features/views/schema.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | export const schema = Joi.object({ 4 | name: Joi.string().min(3).required(), 5 | public: Joi.boolean().required(), 6 | dataSourceId: Joi.number().required(), 7 | tableName: Joi.string().required(), 8 | filters: Joi.array().items(Joi.object()), 9 | defaultOrder: Joi.array().items(Joi.object()), 10 | }); 11 | -------------------------------------------------------------------------------- /features/views/types.d.ts: -------------------------------------------------------------------------------- 1 | export type DecoratedView = View & { 2 | defaultOrder: OrderParams[]; 3 | }; 4 | 5 | export type OrderParams = { columnName?: string; direction?: OrderDirection }; 6 | -------------------------------------------------------------------------------- /lib/ItemTypes.ts: -------------------------------------------------------------------------------- 1 | export const ItemTypes = { 2 | COLUMN: 'column', 3 | TABLE: 'table', 4 | WIDGET: 'widget', 5 | }; 6 | -------------------------------------------------------------------------------- /lib/chakra.ts: -------------------------------------------------------------------------------- 1 | import { createBreakpoints } from "@chakra-ui/theme-tools"; 2 | import { extendTheme } from "@chakra-ui/react"; 3 | 4 | export const breakpoints = { 5 | sm: "640px", 6 | md: "768px", 7 | lg: "1024px", 8 | xl: "1280px", 9 | "2xl": "1536px", 10 | }; 11 | 12 | export default function getChakraTheme() { 13 | // 1. Update the breakpoints to match Tailwind 14 | // 2. Extend the theme 15 | const theme = extendTheme({ 16 | breakpoints: createBreakpoints(breakpoints), 17 | fonts: { 18 | heading: "Nunito", 19 | body: "Nunito", 20 | }, 21 | colors: { 22 | blue: { 23 | "50": "#108105100", 24 | "100": "#f1f6fe", 25 | "200": "#bed5f9", 26 | "300": "#8bb5f4", 27 | "400": "#5c97ef", 28 | "500": "#2976ea", 29 | "600": "#145cc8", 30 | "700": "#0f4495", 31 | "800": "#0a2d62", 32 | "900": "#05152e" 33 | }, 34 | } 35 | }); 36 | 37 | return theme; 38 | } 39 | -------------------------------------------------------------------------------- /lib/colors.js: -------------------------------------------------------------------------------- 1 | exports.bgColors = [ 2 | "blue", 3 | "red", 4 | "green", 5 | "yellow", 6 | "orange", 7 | "pink", 8 | "purple", 9 | "gray", 10 | ]; 11 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | export const LOCAL_STORAGE_PREFIX = "basetool"; 2 | export const LOCALHOST = "127.0.0.1"; 3 | 4 | export const WHITELISTED_IP_ADDRESS = "34.238.175.176"; 5 | 6 | /** 7 | * Columns 8 | */ 9 | export const MINIMUM_VIEW_NAME_LENGTH = 2 10 | 11 | /** 12 | * Tables 13 | */ 14 | export const DEFAULT_COLUMN_WIDTH = 150; 15 | export const MIN_COLUMN_WIDTH = 30; 16 | export const MAX_COLUMN_WIDTH = 800; 17 | export const DEFAULT_PER_PAGE = 24; 18 | 19 | /** 20 | * AWS 21 | */ 22 | export const S3_SSH_KEYS_BUCKET_PREFIX = "basetool-ds-keys-"; 23 | 24 | /** 25 | * Redis 26 | */ 27 | export const REDIS_CACHE_DB = 2; 28 | export const REDIS_OPTIONS_DB = 5; 29 | 30 | /** 31 | * SQL Connection Pooler 32 | */ 33 | export const POOLER_CONNECTION_TIMEOUT = 3 * 60 * 1000; // 3 minutes 34 | export const POOLER_MAX_DB_CONNECTIONS = 3; 35 | 36 | /** 37 | * Cookies 38 | */ 39 | export const COOKIES_FROM_TOOL_NEW = "basetool:from-tool.new" 40 | 41 | /** 42 | * Widgets 43 | */ 44 | export const MINIMUM_WIDGET_NAME_LENGTH = 3 45 | -------------------------------------------------------------------------------- /lib/crypto.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import isString from 'lodash/isString' 3 | 4 | const algorithm = 'aes-256-ctr' 5 | const ENCRYPTION_KEY = (process.env.SECRET as string) 6 | const IV_LENGTH = 16 7 | 8 | export const encrypt = (text: string) => { 9 | const iv = crypto.randomBytes(IV_LENGTH) 10 | const cipher = crypto.createCipheriv(algorithm, Buffer.from(ENCRYPTION_KEY, 'hex'), iv) 11 | let encrypted = cipher.update(text) 12 | encrypted = Buffer.concat([encrypted, cipher.final()]) 13 | 14 | return `${iv.toString('hex')}:${encrypted.toString('hex')}` 15 | } 16 | 17 | export const decrypt = (text: string): string | undefined => { 18 | const textParts = text.split(':') 19 | const textIv = textParts[0] 20 | 21 | if (!textIv || !isString(textIv)) return '' 22 | 23 | const iv = Buffer.from(textIv, 'hex') 24 | const encryptedText = Buffer.from(textParts[1], 'hex') 25 | const decipher = crypto.createDecipheriv(algorithm, Buffer.from(ENCRYPTION_KEY, 'hex'), iv) 26 | let decrypted = decipher.update(encryptedText) 27 | decrypted = Buffer.concat([decrypted, decipher.final()]) 28 | 29 | return decrypted.toString() 30 | } 31 | -------------------------------------------------------------------------------- /lib/encoding.ts: -------------------------------------------------------------------------------- 1 | import { ArrayOrObject } from '@/types' 2 | import { isString } from 'lodash' 3 | import isObject from 'lodash/isObject' 4 | 5 | export const encodeObject = (payload: ArrayOrObject | undefined): string => { 6 | let toEncode = '' 7 | if (isObject(payload)) toEncode = JSON.stringify(payload) 8 | 9 | return Buffer.from(toEncode).toString('base64') 10 | } 11 | 12 | export const decodeObject = (text: string): ArrayOrObject | undefined => { 13 | if (!isString(text)) return 14 | 15 | const decodedString = Buffer.from(text, 'base64').toString() 16 | 17 | try { 18 | return JSON.parse(decodedString) 19 | } catch (error) { 20 | return 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/environment.ts: -------------------------------------------------------------------------------- 1 | export const inStaging = process.env.NEXT_PUBLIC_APP_ENV === "staging"; 2 | export const inProduction = process.env.NEXT_PUBLIC_APP_ENV === "production"; 3 | export const inDevelopment = process.env.NEXT_PUBLIC_APP_ENV === "development"; 4 | -------------------------------------------------------------------------------- /lib/errors.ts: -------------------------------------------------------------------------------- 1 | class SQLError extends Error {} 2 | 3 | class SSHConnectionError extends Error {} 4 | 5 | class BasetoolError extends Error { 6 | public links?: string[]; 7 | 8 | constructor(message: string, data?: { links?: string[] }) { 9 | super(message); 10 | this.links = data?.links; 11 | } 12 | } 13 | 14 | export { SQLError, SSHConnectionError, BasetoolError }; 15 | -------------------------------------------------------------------------------- /lib/fonts.css: -------------------------------------------------------------------------------- 1 | /* nunito-regular - latin */ 2 | @font-face { 3 | font-family: 'Nunito'; 4 | font-style: normal; 5 | font-weight: 400; 6 | src: url('/fonts/nunito-v16-latin-regular.eot'); /* IE9 Compat Modes */ 7 | src: local(''), 8 | url('/fonts/nunito-v16-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 9 | url('/fonts/nunito-v16-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */ 10 | url('/fonts/nunito-v16-latin-regular.woff') format('woff'), /* Modern Browsers */ 11 | url('/fonts/nunito-v16-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */ 12 | url('/fonts/nunito-v16-latin-regular.svg#Nunito') format('svg'); /* Legacy iOS */ 13 | } 14 | /* nunito-700 - latin */ 15 | @font-face { 16 | font-family: 'Nunito'; 17 | font-style: normal; 18 | font-weight: 700; 19 | src: url('/fonts/nunito-v16-latin-700.eot'); /* IE9 Compat Modes */ 20 | src: local(''), 21 | url('/fonts/nunito-v16-latin-700.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 22 | url('/fonts/nunito-v16-latin-700.woff2') format('woff2'), /* Super Modern Browsers */ 23 | url('/fonts/nunito-v16-latin-700.woff') format('woff'), /* Modern Browsers */ 24 | url('/fonts/nunito-v16-latin-700.ttf') format('truetype'), /* Safari, Android, iOS */ 25 | url('/fonts/nunito-v16-latin-700.svg#Nunito') format('svg'); /* Legacy iOS */ 26 | } 27 | -------------------------------------------------------------------------------- /lib/gtag.ts: -------------------------------------------------------------------------------- 1 | import { googleAnalyticsUACode } from "./services"; 2 | 3 | export const pageview = (url: string) => { 4 | if (!(window as any)?.gtag) return; 5 | 6 | (window as any).gtag("config", googleAnalyticsUACode, { 7 | page_path: url, 8 | }); 9 | }; 10 | 11 | // https://developers.google.com/analytics/devguides/collection/gtagjs/events 12 | export const event = ({ action, category, label, value }: any) => { 13 | if (!(window as any)?.gtag) return; 14 | 15 | (window as any).gtag("event", action, { 16 | event_category: category, 17 | event_label: label, 18 | value: value, 19 | }); 20 | }; 21 | -------------------------------------------------------------------------------- /lib/humanize.ts: -------------------------------------------------------------------------------- 1 | import { 2 | capitalize, snakeCase, trim, 3 | } from 'lodash' 4 | 5 | export const humanize = (str: string) => capitalize(trim(snakeCase(str).replace(/_id$/, '').replace(/_/g, ' '))) 6 | -------------------------------------------------------------------------------- /lib/logger.ts: -------------------------------------------------------------------------------- 1 | // import { logflarePinoVercel } from 'pino-logflare' 2 | import pino from "pino"; 3 | 4 | // create pino-logflare console stream for serverless functions and send function for browser logs 5 | // Browser logs are going to: https://logflare.app/sources/13989 6 | // Vercel log drain was setup to send logs here: https://logflare.app/sources/13830 7 | 8 | // const { stream, send } = logflarePinoVercel({ 9 | // apiKey: 'eA_3wro12LpZ', 10 | // sourceToken: 'eb1d841a-e0e4-4d23-af61-84465c808157', 11 | // }) 12 | 13 | // create pino loggger 14 | const logger = pino({ 15 | // browser: { 16 | // transmit: { 17 | // level: 'info', 18 | // // send, 19 | // }, 20 | // }, 21 | level: "debug", 22 | base: { 23 | env: process.env.NODE_ENV, 24 | revision: process.env.VERCEL_GITHUB_COMMIT_SHA, 25 | }, 26 | }); 27 | 28 | export default logger; 29 | -------------------------------------------------------------------------------- /lib/messages.ts: -------------------------------------------------------------------------------- 1 | export const errorResponse = "Apologies about this issue. However, the founding engineering team has been notified, and they are on it to fix it." 2 | 3 | export const funnyLoadingMessages = [ 4 | "Vegetables give you +5 points. Fast food gives you +10 points, but depletes your health daily by -2.", 5 | "Some avocado trees take up to 14 years to bear fruit.", 6 | "Glaciers and ice sheets hold about 69 percent of the world's freshwater.", 7 | "The best place in the world to see rainbows is in Hawaii.", 8 | "North Korea and Cuba are the only places you can't buy Coca-Cola.", 9 | 'A cow-bison hybrid is called a "beefalo".', 10 | "Octopuses lay 56,000 eggs at a time.", 11 | "Cats have fewer toes on their back paws", 12 | "Only a quarter of the Sahara Desert is sandy", 13 | "Dogs sniff good smells with their left nostril", 14 | "Bats are the only mammal that can actually fly", 15 | "Elephants can’t jump", 16 | "Octopuses have three hearts.", 17 | "Polar bears have black skin", 18 | "Tigers' skin is actually striped, just like their fur", 19 | "Flamingos are only pink because of chemicals in their food", 20 | "There are no muscles in your fingers", 21 | "It’s impossible to hum while holding your nose (just try it!)", 22 | "Skin is the body’s largest organ", 23 | "Hawaiian pizza was created in Ontario, Canada, by Greek immigrant Sam Panopoulos in 1962", 24 | ] 25 | -------------------------------------------------------------------------------- /lib/redis.ts: -------------------------------------------------------------------------------- 1 | import Redis from "ioredis"; 2 | 3 | const getRedisClient = (db: number) => { 4 | const options = { 5 | keyPrefix: `basetool_${process.env.NEXT_PUBLIC_APP_ENV}:`, 6 | lazyConnect: true, 7 | db, 8 | }; 9 | const client = new Redis(process.env.REDIS_URL, options); 10 | 11 | return client; 12 | }; 13 | 14 | export { getRedisClient }; 15 | -------------------------------------------------------------------------------- /lib/services.ts: -------------------------------------------------------------------------------- 1 | export const googleAnalyticsUACode = 2 | process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS_UA; 3 | export const googleAnalytics4Code = process.env.NEXT_PUBLIC_GOOGLE_ANALYTICS; 4 | 5 | export const intercomAppId = process.env.NEXT_PUBLIC_INTERCOM_APP_ID; 6 | export const intercomSecret = process.env.INTERCOM_SECRET; 7 | export const intercomAccessToken = process.env.INTERCOM_ACCESS_TOKEN; 8 | 9 | export const segmentPublicKey = process.env.NEXT_PUBLIC_SEGMENT_PUBLIC_KEY; 10 | export const segmentWriteKey = process.env.SEGMENT_WRITE_KEY; 11 | 12 | export const slackGrowthChannelWebhook = process.env.SLACK_GROWTH_CHANNEL_WEBHOOK; 13 | -------------------------------------------------------------------------------- /lib/siteMeta.ts: -------------------------------------------------------------------------------- 1 | const name = "basetool"; 2 | const description = "View and manage all your data in one place like a pro"; 3 | const separator = "·"; 4 | const title = `${name} ${separator} ${description}`; 5 | const url = "https://basetool.io"; 6 | const image = "img/cover.jpg"; 7 | const imagePath = `${url}/${image}`; 8 | const meta = { 9 | title, 10 | name, 11 | separator, 12 | description, 13 | url, 14 | image, 15 | imagePath, 16 | twitter: { 17 | handle: "@basetool", 18 | }, 19 | }; 20 | 21 | export default meta; 22 | -------------------------------------------------------------------------------- /lib/time.ts: -------------------------------------------------------------------------------- 1 | import { DateTimeFieldOptions } from "@/plugins/fields/DateTime/types"; 2 | 3 | export const getBrowserTimezone = () => 4 | window ? window.Intl.DateTimeFormat().resolvedOptions().timeZone : "UTC"; 5 | export const getFormatFormFieldOptions = ( 6 | fieldOptions: DateTimeFieldOptions 7 | ): string => { 8 | if (fieldOptions.showDate) { 9 | if (fieldOptions.showTime) { 10 | return dateTimeFormat; 11 | } else { 12 | return dateFormat; 13 | } 14 | } else if (fieldOptions.showTime) { 15 | return timeFormat; 16 | } else { 17 | // Fallback to dateTime 18 | return dateTimeFormat; 19 | } 20 | }; 21 | export const timeFormat = "HH:mm:ss"; 22 | export const dateFormat = "yyyy/LL/dd"; 23 | export const dateTimeFormat = "yyyy/LL/dd HH:mm:ss"; 24 | -------------------------------------------------------------------------------- /lib/track.ts: -------------------------------------------------------------------------------- 1 | import { inProduction } from "./environment"; 2 | import { segmentWriteKey } from "./services"; 3 | import Analytics from "analytics-node"; 4 | import isUndefined from "lodash/isUndefined"; 5 | 6 | export const segment = () => { 7 | if (!isUndefined(window) && window?.analytics) { 8 | return window?.analytics; 9 | } 10 | 11 | return { 12 | page: () => undefined, 13 | identify: () => undefined, 14 | track: () => undefined, 15 | }; 16 | }; 17 | 18 | export const serverSegment = () => { 19 | let segment: any; 20 | 21 | if (inProduction) { 22 | segment = new Analytics(segmentWriteKey as string); 23 | } else { 24 | segment = { 25 | track: () => undefined, 26 | }; 27 | } 28 | 29 | return { 30 | track: (args: any) => { 31 | try { 32 | if (args?.userId && args?.email) { 33 | segment.identify({ 34 | userId: args.userId, 35 | traits: { email: args.email }, 36 | }); 37 | } 38 | segment.track(args); 39 | } catch (error) {} 40 | }, 41 | }; 42 | }; 43 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /pages/404.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import Link from "next/link" 3 | import PageWrapper from "@/components/PageWrapper"; 4 | import PublicLayout from "@/components/PublicLayout"; 5 | import React from "react"; 6 | 7 | function Custom404() { 8 | return ( 9 | 10 |
11 | 12 |
13 |
14 |

404 - Missing page. Maybe the cat has something to do with it?

15 |
16 | Take me home 17 |
18 |
19 | 404 cat 26 |
27 |
28 |
29 |
30 |
31 | ); 32 | } 33 | 34 | export default Custom404; 35 | -------------------------------------------------------------------------------- /pages/api/data-sources/[dataSourceId]/query.ts: -------------------------------------------------------------------------------- 1 | import { getDataSourceFromRequest } from "@/features/api"; 2 | import { withMiddlewares } from "@/features/api/middleware"; 3 | import ApiResponse from "@/features/api/ApiResponse"; 4 | import pooler from "@/plugins/data-sources/ConnectionPooler" 5 | import type { NextApiRequest, NextApiResponse } from "next"; 6 | 7 | const handler = async ( 8 | req: NextApiRequest, 9 | res: NextApiResponse 10 | ): Promise => { 11 | switch (req.method) { 12 | case "POST": 13 | return handlePOST(req, res); 14 | default: 15 | return res.status(404).send(""); 16 | } 17 | }; 18 | 19 | async function handlePOST(req: NextApiRequest, res: NextApiResponse) { 20 | if (process.env.PROXY_SECRET !== req?.body?.secret) 21 | return res.status(404).send(""); 22 | if (!req?.body?.queries) 23 | return res.send(ApiResponse.withError("No queries sent.")); 24 | 25 | const dataSource = await getDataSourceFromRequest(req); 26 | 27 | if (!dataSource) 28 | return res.send(ApiResponse.withError("Invalid data source.")); 29 | 30 | const service = await pooler.getConnection(dataSource); 31 | 32 | try { 33 | return res.send(await service.runQueries(req?.body?.queries)); 34 | } catch (error) { 35 | return res.status(500).send({ 36 | error: true, 37 | type: error.constructor.name, 38 | message: error.message, 39 | stack: error.stack, 40 | }); 41 | } 42 | } 43 | 44 | export default withMiddlewares(handler, { middlewares: [] }); 45 | -------------------------------------------------------------------------------- /pages/api/feedback.ts: -------------------------------------------------------------------------------- 1 | import { captureMessage } from "@sentry/nextjs"; 2 | import { getUserFromRequest } from "@/features/api"; 3 | import { withMiddlewares } from "@/features/api/middleware"; 4 | import ApiResponse from "@/features/api/ApiResponse"; 5 | import IsSignedIn from "@/features/api/middlewares/IsSignedIn"; 6 | import email from "@/lib/email"; 7 | import logger from "@/lib/logger"; 8 | import type { NextApiRequest, NextApiResponse } from "next"; 9 | 10 | const handler = async ( 11 | req: NextApiRequest, 12 | res: NextApiResponse 13 | ): Promise => { 14 | switch (req.method) { 15 | case "POST": 16 | return handlePOST(req, res); 17 | default: 18 | return res.status(404).send(""); 19 | } 20 | }; 21 | 22 | async function handlePOST(req: NextApiRequest, res: NextApiResponse) { 23 | 24 | const user = await getUserFromRequest(req); 25 | 26 | const emailData: any = { 27 | to: "hi@basetool.io", 28 | subject: `New feedback message from ${user?.email || ""}`, 29 | html: `
30 |

From: ${user?.email || ""}

31 |

Message: ${req.body.note}

32 |

Emotion: ${req.body.emotion}

33 |

Posted from URL: ${req.body.url}

34 |
`, 35 | text: `From: ${user?.email || ""}; Message: ${req.body.note}; Emotion: ${req.body.emotion}; Posted from URL: ${req.body.url};` 36 | }; 37 | 38 | try { 39 | await email.send(emailData); 40 | } catch (error: any) { 41 | logger.debug(error); 42 | captureMessage(`Failed to send email ${error.message}. Message: ${emailData.text}`); 43 | } 44 | 45 | return res.json(ApiResponse.withMessage("🙏🏼 Thank you!")); 46 | } 47 | 48 | export default withMiddlewares(handler, { 49 | middlewares: [[IsSignedIn, {}]], 50 | }); 51 | -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | import { withMiddlewares } from "@/features/api/middleware"; 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | 4 | const handler = async (req: NextApiRequest, res: NextApiResponse) => { 5 | res.status(200).json({ hi: "there" }); 6 | }; 7 | 8 | export default withMiddlewares(handler); 9 | -------------------------------------------------------------------------------- /pages/api/organizations/index.ts: -------------------------------------------------------------------------------- 1 | import { Organization, OrganizationUser, User } from "@prisma/client"; 2 | import { getUserFromRequest } from "@/features/api"; 3 | import { withMiddlewares } from "@/features/api/middleware" 4 | import ApiResponse from "@/features/api/ApiResponse"; 5 | import IsSignedIn from "@/features/api/middlewares/IsSignedIn"; 6 | import type { NextApiRequest, NextApiResponse } from "next"; 7 | 8 | const handler = async ( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ): Promise => { 12 | switch (req.method) { 13 | case "GET": 14 | return handleGET(req, res); 15 | default: 16 | return res.status(404).send(""); 17 | } 18 | }; 19 | 20 | async function handleGET(req: NextApiRequest, res: NextApiResponse) { 21 | const user = (await getUserFromRequest(req, { 22 | include: { 23 | organizations: { 24 | include: { 25 | organization: { 26 | select: { 27 | id: true, 28 | name: true 29 | } 30 | }, 31 | }, 32 | }, 33 | }, 34 | })) as User & { 35 | organizations: Array; 36 | }; 37 | 38 | const organizations = user?.organizations.map( 39 | ({ organization }) => organization 40 | ); 41 | 42 | res.json(ApiResponse.withData(organizations || [])); 43 | } 44 | 45 | export default withMiddlewares(handler, { 46 | middlewares: [ 47 | [IsSignedIn, {}], 48 | ], 49 | }); 50 | -------------------------------------------------------------------------------- /pages/api/test/seed.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import { seed, seedDataSource } from "@/prisma/seed-script"; 3 | import type { NextApiRequest, NextApiResponse } from "next"; 4 | 5 | export default async function handler( 6 | req: NextApiRequest, 7 | res: NextApiResponse 8 | ) { 9 | if (process.env.ENV !== "test") return res.status(404).send(""); 10 | 11 | switch (req.method) { 12 | case "POST": 13 | return handlePOST(req, res); 14 | default: 15 | return res.status(404).send(""); 16 | } 17 | } 18 | 19 | async function handlePOST(req: NextApiRequest, res: NextApiResponse) { 20 | const user = req.body.user; 21 | 22 | // Seed user & organization 23 | const { organization } = await seed({ user }); 24 | 25 | await seedDataSource({ 26 | organizationId: organization.id, 27 | }); 28 | 29 | res.status(200).json({ name: "John Doe" }); 30 | } 31 | -------------------------------------------------------------------------------- /pages/data-sources/[dataSourceId]/edit.tsx: -------------------------------------------------------------------------------- 1 | import DataSourcesEditLayout from "@/features/data-sources/components/DataSourcesEditLayout" 2 | 3 | export default DataSourcesEditLayout; 4 | -------------------------------------------------------------------------------- /pages/data-sources/[dataSourceId]/tables/[tableName]/[recordId].tsx: -------------------------------------------------------------------------------- 1 | import ShowRecord from "@/features/records/components/ShowRecord"; 2 | 3 | export default ShowRecord; 4 | -------------------------------------------------------------------------------- /pages/data-sources/[dataSourceId]/tables/[tableName]/[recordId]/edit.tsx: -------------------------------------------------------------------------------- 1 | import EditRecord from "@/features/records/components/EditRecord"; 2 | 3 | export default EditRecord; 4 | -------------------------------------------------------------------------------- /pages/data-sources/[dataSourceId]/tables/[tableName]/new.tsx: -------------------------------------------------------------------------------- 1 | import NewRecord from "@/features/records/components/NewRecord"; 2 | 3 | export default NewRecord; 4 | -------------------------------------------------------------------------------- /pages/data-sources/google-sheets/new.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@chakra-ui/react"; 2 | import { useGetAuthUrlQuery } from "@/features/data-sources/api-slice"; 3 | import BackButton from "@/features/records/components/BackButton"; 4 | import Layout from "@/components/Layout"; 5 | import Link from "next/link"; 6 | import PageWrapper from "@/components/PageWrapper"; 7 | import React, { useMemo } from "react"; 8 | 9 | function New() { 10 | const { data: authUrlResponse, isLoading } = useGetAuthUrlQuery({ 11 | dataSourceName: "google-sheets", 12 | }); 13 | const authUrl = useMemo( 14 | () => (authUrlResponse?.ok ? authUrlResponse?.data?.url : "/"), 15 | [authUrlResponse] 16 | ); 17 | 18 | return ( 19 | 20 | } 23 | > 24 | <> 25 | You will be prompted to share read-write permissions so you can access 26 | & update your data. 27 |
28 | {isLoading && ( 29 | 32 | )} 33 | {!isLoading && authUrl && ( 34 | 35 | 36 | 37 | )} 38 |
39 | 40 |
41 |
42 | ); 43 | } 44 | 45 | export default New; 46 | -------------------------------------------------------------------------------- /pages/data-sources/maria_db/new.tsx: -------------------------------------------------------------------------------- 1 | import NewDataSourceForm from "@/features/data-sources/components/NewDataSourceForm"; 2 | 3 | const New = () => ( 4 | 15 | ); 16 | 17 | export default New; 18 | -------------------------------------------------------------------------------- /pages/data-sources/mssql/new.tsx: -------------------------------------------------------------------------------- 1 | import NewDataSourceForm from "@/features/data-sources/components/NewDataSourceForm"; 2 | 3 | const New = () => ( 4 | 15 | ); 16 | 17 | export default New; 18 | -------------------------------------------------------------------------------- /pages/data-sources/mysql/new.tsx: -------------------------------------------------------------------------------- 1 | import NewDataSourceForm from "@/features/data-sources/components/NewDataSourceForm"; 2 | 3 | const New = () => ( 4 | 15 | ); 16 | 17 | export default New; 18 | -------------------------------------------------------------------------------- /pages/data-sources/postgresql/new.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from "next/router"; 2 | import NewDataSourceForm, { DefaultValueCredentials } from "@/features/data-sources/components/NewDataSourceForm"; 3 | import URI from "urijs"; 4 | 5 | const New = () => { 6 | const router = useRouter(); 7 | 8 | let credentials: DefaultValueCredentials = { port: 5432 }; 9 | if (router.query.credentials) { 10 | const uri = URI(router.query.credentials); 11 | credentials = { 12 | host: uri.hostname(), 13 | port: parseInt(uri.port()), 14 | database: uri.path().replace("/", ""), 15 | user: uri.username(), 16 | password: uri.password(), 17 | useSsl: true, 18 | }; 19 | } 20 | 21 | let name: string | undefined; 22 | if (router.query.name) { 23 | name = router.query.name as string; 24 | } 25 | 26 | return ( 27 | 37 | ); 38 | }; 39 | 40 | export default New; 41 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { COOKIES_FROM_TOOL_NEW } from "@/lib/constants" 2 | import { useCookie } from "react-use"; 3 | import { useProfile } from "@/hooks"; 4 | import { useRouter } from "next/router"; 5 | import DataSourcesBlock from "@/features/data-sources/components/DataSourcesBlock"; 6 | import DummyDataSources from "@/features/data-sources/components/DummyDataSources"; 7 | import Layout from "@/components/Layout"; 8 | import OrganizationsBlock from "@/features/organizations/components/OrganizationsBlock"; 9 | import PageWrapper from "@/components/PageWrapper"; 10 | import React, { useEffect } from "react"; 11 | 12 | function Index() { 13 | const router = useRouter(); 14 | const { user, isLoading: profileIsLoading } = useProfile(); 15 | const [cookie, _, deleteCookie] = useCookie(COOKIES_FROM_TOOL_NEW); 16 | 17 | useEffect(() => { 18 | if (cookie === "1") { 19 | deleteCookie(); 20 | router.push("/data-sources/new"); 21 | } 22 | }, [cookie]); 23 | 24 | return ( 25 | 26 | 29 |
30 | 31 | 32 | 33 |
34 |
35 |
36 | ); 37 | } 38 | 39 | export default Index; 40 | -------------------------------------------------------------------------------- /pages/organizations/[organizationSlug].tsx: -------------------------------------------------------------------------------- 1 | import { useOrganizationFromProfile } from "@/hooks"; 2 | import { useRouter } from "next/router"; 3 | import DataSourcesBlock from "@/features/data-sources/components/DataSourcesBlock" 4 | import Layout from "@/components/Layout"; 5 | import PageWrapper from "@/components/PageWrapper"; 6 | import React from "react"; 7 | 8 | function OrganizationShow() { 9 | const router = useRouter(); 10 | const organization = useOrganizationFromProfile({ 11 | slug: router.query.organizationSlug as string, 12 | }); 13 | 14 | return ( 15 | 16 | 17 | <> 18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | export default OrganizationShow; 26 | -------------------------------------------------------------------------------- /pages/views/[viewId]/records/[recordId].tsx: -------------------------------------------------------------------------------- 1 | import ShowRecord from "@/features/records/components/ShowRecord"; 2 | 3 | export default ShowRecord; 4 | -------------------------------------------------------------------------------- /pages/views/[viewId]/records/[recordId]/edit.tsx: -------------------------------------------------------------------------------- 1 | import EditRecord from "@/features/records/components/EditRecord"; 2 | 3 | export default EditRecord; 4 | -------------------------------------------------------------------------------- /pages/views/[viewId]/records/new.tsx: -------------------------------------------------------------------------------- 1 | import NewRecord from "@/features/records/components/NewRecord"; 2 | 3 | export default NewRecord; 4 | -------------------------------------------------------------------------------- /plugins/data-sources/abstract-sql-query-service/getKnexClient.ts: -------------------------------------------------------------------------------- 1 | import { ClientOverrides, SQLDataSourceTypes } from "./types"; 2 | import { MysqlCredentials } from "../mysql/types"; 3 | import { PgCredentials } from "../postgresql/types"; 4 | import knex from "knex"; 5 | import type { Knex } from "knex"; 6 | 7 | export const getKnexClient = ( 8 | type: "pg" | SQLDataSourceTypes, 9 | credentials: PgCredentials | MysqlCredentials, 10 | overrides?: ClientOverrides 11 | ) => { 12 | // Doing this transformation because knex expects `pg` and we have it as `postgresql` 13 | const knexClientType = type === "postgresql" ? "pg" : type; 14 | 15 | // Initial config 16 | const connection: Knex.StaticConnectionConfig = { 17 | host: credentials.host, 18 | port: credentials.port, 19 | database: credentials.database, 20 | user: credentials.user, 21 | password: credentials.password, 22 | }; 23 | 24 | // Initial if use SSL is checked 25 | if (credentials.useSsl) { 26 | connection.ssl = { rejectUnauthorized: false }; 27 | } 28 | 29 | // We might need to override some things when on SSH 30 | if (overrides) { 31 | connection.host = overrides.host; 32 | connection.port = overrides.port; 33 | } 34 | 35 | // If the type of connection is MSSQL, we need to add the `encrypt` option for azure connections. 36 | if (type === "mssql" && connection.host && connection.host.includes("database.windows.net")) { 37 | (connection as Knex.MsSqlConnectionConfig).options = { encrypt: true }; 38 | } 39 | 40 | const client = knex({ 41 | client: knexClientType, 42 | connection, 43 | debug: false, 44 | }); 45 | 46 | return client; 47 | }; 48 | -------------------------------------------------------------------------------- /plugins/data-sources/abstract-sql-query-service/index.ts: -------------------------------------------------------------------------------- 1 | import { DataSourceInfo } from "../types"; 2 | 3 | export const defaultSettings: Omit< 4 | DataSourceInfo, 5 | "id" | "name" | "description" 6 | > = { 7 | readOnly: false, 8 | pagination: "offset", 9 | supports: { 10 | filters: true, 11 | columnsRequest: true, 12 | views: true, 13 | dashboards: true, 14 | }, 15 | runsInProxy: true, 16 | }; 17 | -------------------------------------------------------------------------------- /plugins/data-sources/enums.ts: -------------------------------------------------------------------------------- 1 | export enum DataSourceTypes { 2 | postgresql = "postgresql", 3 | } 4 | -------------------------------------------------------------------------------- /plugins/data-sources/getDataSourceInfo.ts: -------------------------------------------------------------------------------- 1 | import { DataSourceInfo } from "./types" 2 | 3 | const getDataSourceInfo = async (id: string): Promise => { 4 | try { 5 | return ( 6 | await import(`@/plugins/data-sources/${id}/index.ts`) 7 | ).default; 8 | } catch (error) { 9 | } 10 | }; 11 | 12 | export default getDataSourceInfo; 13 | -------------------------------------------------------------------------------- /plugins/data-sources/getSchema.ts: -------------------------------------------------------------------------------- 1 | import { AnySchema } from "joi"; 2 | import { schema as mysqlSchema } from "./mysql/schema"; 3 | import { schema as postgresqlSchema } from "./postgresql/schema"; 4 | import { schema as stripeSchema } from "./stripe/schema"; 5 | 6 | const getSchema = (id: string): AnySchema => { 7 | switch (id) { 8 | case "stripe": 9 | return stripeSchema; 10 | case "mysql": 11 | case "maria_db": 12 | return mysqlSchema; 13 | case "postgresql": 14 | default: 15 | return postgresqlSchema; 16 | } 17 | }; 18 | 19 | export default getSchema; 20 | -------------------------------------------------------------------------------- /plugins/data-sources/google-sheets/constants.ts: -------------------------------------------------------------------------------- 1 | export const OAUTH_USER_ID_COOKIE = 'google-sheets:oauth:callback_id' 2 | export const COOKIE_KEYS = 'basetool app dash admin' 3 | -------------------------------------------------------------------------------- /plugins/data-sources/google-sheets/index.ts: -------------------------------------------------------------------------------- 1 | import { DataSourceInfo } from "../types"; 2 | import { defaultSettings } from "../abstract-sql-query-service"; 3 | 4 | interface GoogleSheetsDataSourceInfo extends DataSourceInfo { 5 | oauthScopes: string[]; 6 | } 7 | 8 | const info: GoogleSheetsDataSourceInfo = { 9 | ...defaultSettings, 10 | id: "googleSheets", 11 | name: "Google sheets", 12 | description: "Google sheets", 13 | oauthScopes: [ 14 | "https://www.googleapis.com/auth/spreadsheets", 15 | "https://www.googleapis.com/auth/drive", 16 | "https://www.googleapis.com/auth/drive.file", 17 | // 'https://www.googleapis.com/auth/drive.readonly', 18 | ], 19 | runsInProxy: false, 20 | supports: { 21 | ...defaultSettings.supports, 22 | views: false, 23 | dashboards: false, 24 | } 25 | }; 26 | 27 | export default info; 28 | -------------------------------------------------------------------------------- /plugins/data-sources/google-sheets/types.d.ts: -------------------------------------------------------------------------------- 1 | import { DataSource } from '@prisma/client' 2 | 3 | export type GoogleSheetsDataSourceOptions = { 4 | spreadsheetId: string | null 5 | } 6 | 7 | export type GoogleSheetsCredentials = { 8 | tokens: { 9 | refresh_token: string, 10 | access_token: string, 11 | scope: string, 12 | token_type: 'Bearer', 13 | expiry_date: number 14 | } 15 | }; 16 | 17 | export interface GoogleSheetsDataSource extends DataSource implements DataSource { 18 | options: GoogleSheetsDataSourceOptions 19 | encryptedCredentials: string 20 | } 21 | -------------------------------------------------------------------------------- /plugins/data-sources/maria_db/index.ts: -------------------------------------------------------------------------------- 1 | import { DataSourceInfo } from "../types"; 2 | import mysqlInfo from "./../mysql"; 3 | 4 | const info: DataSourceInfo = { 5 | ...mysqlInfo, 6 | id: "maria_db", 7 | name: "Maria DB", 8 | description: "Maria DB data source", 9 | }; 10 | 11 | export default info; 12 | -------------------------------------------------------------------------------- /plugins/data-sources/maria_db/schema.ts: -------------------------------------------------------------------------------- 1 | export { schema } from "../mysql/schema"; 2 | -------------------------------------------------------------------------------- /plugins/data-sources/mssql/index.ts: -------------------------------------------------------------------------------- 1 | import { DataSourceInfo } from "../types"; 2 | import { defaultSettings } from "../abstract-sql-query-service" 3 | 4 | const info: DataSourceInfo = { 5 | ...defaultSettings, 6 | id: "mssql", 7 | name: "SQL Server", 8 | description: "Microsoft SQL server data source", 9 | }; 10 | 11 | export default info; 12 | -------------------------------------------------------------------------------- /plugins/data-sources/mssql/schema.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | export const schema = Joi.object({ 4 | name: Joi.string().min(3).required(), 5 | type: Joi.string().allow("mssql").required(), 6 | credentials: Joi.object({ 7 | host: Joi.string().required(), 8 | port: Joi.number().required(), 9 | database: Joi.string().required(), 10 | user: Joi.string().required(), 11 | password: Joi.string().allow(""), 12 | useSsl: Joi.boolean(), 13 | }), 14 | organizationId: Joi.string().required(), 15 | }); 16 | -------------------------------------------------------------------------------- /plugins/data-sources/mysql/index.ts: -------------------------------------------------------------------------------- 1 | import { DataSourceInfo } from "../types"; 2 | import { defaultSettings } from "../abstract-sql-query-service" 3 | 4 | const info: DataSourceInfo = { 5 | ...defaultSettings, 6 | id: "mysql", 7 | name: "MySQL", 8 | description: "MySQL data source", 9 | }; 10 | 11 | export default info; 12 | -------------------------------------------------------------------------------- /plugins/data-sources/mysql/schema.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | export const schema = Joi.object({ 4 | name: Joi.string().min(3).required(), 5 | type: Joi.string().allow("mysql").required(), 6 | options: Joi.object({ 7 | connectsWithSSH: Joi.boolean(), 8 | connectsWithSSHKey: Joi.boolean(), 9 | }), 10 | credentials: Joi.object({ 11 | host: Joi.string().required(), 12 | port: Joi.number().required(), 13 | database: Joi.string().required(), 14 | user: Joi.string().required(), 15 | password: Joi.string().allow(""), 16 | useSsl: Joi.boolean(), 17 | }), 18 | ssh: Joi.object({ 19 | host: Joi.string().allow(""), 20 | port: Joi.number().allow(""), 21 | user: Joi.string().allow(""), 22 | password: Joi.string().allow(""), 23 | key: Joi.any(), 24 | passphrase: Joi.string().allow(""), 25 | }), 26 | organizationId: Joi.number().required() 27 | }); 28 | -------------------------------------------------------------------------------- /plugins/data-sources/mysql/types.d.ts: -------------------------------------------------------------------------------- 1 | export type MysqlCredentials = { 2 | host: string; 3 | port: number; 4 | database: string; 5 | user: string; 6 | password?: string; 7 | useSsl: boolean; 8 | }; 9 | -------------------------------------------------------------------------------- /plugins/data-sources/postgresql/index.ts: -------------------------------------------------------------------------------- 1 | import { DataSourceInfo } from "../types"; 2 | import { defaultSettings } from "../abstract-sql-query-service" 3 | 4 | const info: DataSourceInfo = { 5 | ...defaultSettings, 6 | id: "postgresql", 7 | name: "PostgreSQL", 8 | description: "PostgreSQL data source", 9 | }; 10 | 11 | export default info; 12 | -------------------------------------------------------------------------------- /plugins/data-sources/postgresql/schema.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | export const schema = Joi.object({ 4 | name: Joi.string().min(3).required(), 5 | type: Joi.string().allow('postgresql').required(), 6 | options: Joi.object({ 7 | connectsWithSSH: Joi.boolean(), 8 | connectsWithSSHKey: Joi.boolean(), 9 | }), 10 | credentials: Joi.object({ 11 | host: Joi.string().required(), 12 | port: Joi.number().required(), 13 | database: Joi.string().required(), 14 | user: Joi.string().required(), 15 | password: Joi.string().allow(""), 16 | useSsl: Joi.boolean(), 17 | }), 18 | ssh: Joi.object({ 19 | host: Joi.string().allow(""), 20 | port: Joi.number().allow(""), 21 | user: Joi.string().allow(""), 22 | password: Joi.string().allow(""), 23 | key: Joi.any(), 24 | passphrase: Joi.string().allow(""), 25 | }), 26 | organizationId: Joi.number().required() 27 | }); 28 | -------------------------------------------------------------------------------- /plugins/data-sources/postgresql/types.d.ts: -------------------------------------------------------------------------------- 1 | import { Column } from "@/features/fields/types"; 2 | import { DataSource } from "@prisma/client"; 3 | import { SqlColumnOptions, Tables } from "../abstract-sql-query-service/types"; 4 | 5 | export type PgCredentials = { 6 | host: string; 7 | port: number; 8 | database: string; 9 | user: string; 10 | password: string; 11 | useSsl: boolean; 12 | }; 13 | 14 | export type PgLegacyCredentials = { 15 | url: string; 16 | useSsl: boolean; 17 | }; 18 | 19 | export interface PgDataSource extends DataSource { 20 | options: { 21 | url?: string; 22 | columns?: Column[]; 23 | tables?: Tables; 24 | }; 25 | } 26 | 27 | export type PgColumnOptions = SqlColumnOptions; 28 | -------------------------------------------------------------------------------- /plugins/data-sources/stripe/index.ts: -------------------------------------------------------------------------------- 1 | import { DataSourceInfo } from "../types"; 2 | 3 | const info: DataSourceInfo = { 4 | id: "stripe", 5 | name: "Stripe", 6 | description: "Stripe", 7 | readOnly: true, 8 | pagination: 'cursor', 9 | supports: { 10 | filters: false, 11 | columnsRequest: false, 12 | views: false, 13 | dashboards: false, 14 | }, 15 | runsInProxy: false, 16 | }; 17 | 18 | export default info; 19 | -------------------------------------------------------------------------------- /plugins/data-sources/stripe/schema.ts: -------------------------------------------------------------------------------- 1 | import Joi from "joi"; 2 | 3 | export const schema = Joi.object({ 4 | name: Joi.string().min(3).required(), 5 | type: Joi.string().allow("stripe").required(), 6 | credentials: Joi.object({ 7 | secretKey: Joi.string().required(), 8 | }), 9 | organizationId: Joi.number().required(), 10 | }); 11 | -------------------------------------------------------------------------------- /plugins/fields/Association/Show.tsx: -------------------------------------------------------------------------------- 1 | import { Field, RecordAssociationValue } from "@/features/fields/types"; 2 | import GoToRecordLink from "@/features/fields/components/GoToRecordLink"; 3 | import React, { memo } from "react"; 4 | import ShowFieldWrapper from "@/features/fields/components/FieldWrapper/ShowFieldWrapper"; 5 | 6 | const Show = ({ field }: { field: Field }) => { 7 | let value, dataSourceId, foreignTable, foreignId; 8 | 9 | if (field.value) { 10 | value = field.value.value 11 | dataSourceId = field.value.dataSourceId 12 | foreignTable = field.value.foreignTable 13 | foreignId = field.value.foreignId 14 | } 15 | 16 | return ( 17 | 18 | {value} 19 | {foreignId && } 22 | 23 | ); 24 | }; 25 | 26 | export default memo(Show); 27 | -------------------------------------------------------------------------------- /plugins/fields/Association/fieldOptions.ts: -------------------------------------------------------------------------------- 1 | const fieldOptions = { 2 | nameColumn: '', 3 | }; 4 | export default fieldOptions; 5 | -------------------------------------------------------------------------------- /plugins/fields/Association/schema.ts: -------------------------------------------------------------------------------- 1 | import { Column } from "@/features/fields/types"; 2 | import Joi from "joi"; 3 | import type { BasetoolRecord } from "@/features/records/types"; 4 | 5 | const schema = (record: BasetoolRecord, column: Column) => { 6 | let rule = Joi.alternatives(Joi.string(), Joi.number()) 7 | 8 | if (column.baseOptions.required) { 9 | rule = rule.required(); 10 | } else { 11 | rule = rule.allow("", null); 12 | } 13 | 14 | return rule; 15 | }; 16 | 17 | export default schema; 18 | -------------------------------------------------------------------------------- /plugins/fields/Boolean/Index.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from "@chakra-ui/layout"; 2 | import { Field } from "@/features/fields/types"; 3 | import { isNull } from "lodash"; 4 | import BooleanCheck from "@/features/fields/components/BooleanCheck"; 5 | import IndexFieldWrapper from "@/features/fields/components/FieldWrapper/IndexFieldWrapper" 6 | import React, { memo, useMemo } from "react"; 7 | 8 | const Index = ({ field }: { field: Field }) => { 9 | const isTruthy = useMemo( 10 | () => field.value === true || field.value === 1, 11 | [field.value] 12 | ); 13 | 14 | return ( 15 | 16 | {isNull(field.value) && null} 17 | {!isNull(field.value) && } 18 | 19 | ); 20 | }; 21 | 22 | export default memo(Index); 23 | -------------------------------------------------------------------------------- /plugins/fields/Boolean/Show.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from "@chakra-ui/layout"; 2 | import { Field } from "@/features/fields/types"; 3 | import { isNull } from "lodash"; 4 | import BooleanCheck from "@/features/fields/components/BooleanCheck"; 5 | import React, { memo, useMemo } from "react"; 6 | import ShowFieldWrapper from "@/features/fields/components/FieldWrapper/ShowFieldWrapper"; 7 | 8 | const Show = ({ field }: { field: Field }) => { 9 | const isTruthy = useMemo( 10 | () => field.value === true || field.value === 1, 11 | [field.value] 12 | ); 13 | 14 | return ( 15 | 16 | {!isNull(field.value) && } 17 | {isNull(field.value) && null} 18 | 19 | ); 20 | }; 21 | 22 | export default memo(Show); 23 | -------------------------------------------------------------------------------- /plugins/fields/DateTime/Index.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from "@chakra-ui/layout"; 2 | import { Field } from "@/features/fields/types"; 3 | import { getBrowserTimezone, getFormatFormFieldOptions } from "@/lib/time"; 4 | import { isNull } from "lodash"; 5 | import { parsed } from "./parsedValue"; 6 | import IndexFieldWrapper from "@/features/fields/components/FieldWrapper/IndexFieldWrapper"; 7 | import React, { memo } from "react"; 8 | 9 | const Show = ({ field }: { field: Field }) => { 10 | const format = getFormatFormFieldOptions(field.column.fieldOptions); 11 | 12 | const formattedDate = parsed(field.value) 13 | .setZone(getBrowserTimezone()) 14 | .toFormat(format); 15 | 16 | return ( 17 | 18 | {isNull(field.value) ? null : formattedDate} 19 | 20 | ); 21 | }; 22 | 23 | export default memo(Show); 24 | -------------------------------------------------------------------------------- /plugins/fields/DateTime/Show.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from "@chakra-ui/layout"; 2 | import { Field } from "@/features/fields/types"; 3 | import { getBrowserTimezone, getFormatFormFieldOptions } from "@/lib/time"; 4 | import { isNull } from "lodash"; 5 | import { parsed } from "./parsedValue"; 6 | import React, { memo } from "react"; 7 | import ShowFieldWrapper from "@/features/fields/components/FieldWrapper/ShowFieldWrapper"; 8 | 9 | const Show = ({ field }: { field: Field }) => { 10 | const format = getFormatFormFieldOptions(field.column.fieldOptions); 11 | 12 | const formattedDate = parsed(field.value) 13 | .setZone(getBrowserTimezone()) 14 | .toFormat(format); 15 | 16 | return ( 17 | 18 | {isNull(field.value) ? null : formattedDate} 19 | 20 | ); 21 | }; 22 | 23 | export default memo(Show); 24 | -------------------------------------------------------------------------------- /plugins/fields/DateTime/fieldOptions.ts: -------------------------------------------------------------------------------- 1 | const fieldOptions = { 2 | showDate: false, 3 | showTime: false, 4 | }; 5 | 6 | export default fieldOptions; 7 | -------------------------------------------------------------------------------- /plugins/fields/DateTime/parsedValue.ts: -------------------------------------------------------------------------------- 1 | import { DateTime } from "luxon"; 2 | import isNull from "lodash/isNull"; 3 | 4 | export const parsed = (value: unknown): DateTime => { 5 | let parsed = DateTime.fromISO(value as string); 6 | 7 | // If the format is not ISO, try timestamp 8 | if (!isNull(value) && !parsed.isValid) { 9 | try { 10 | parsed = DateTime.fromSeconds(value as number); 11 | } catch (error) {} 12 | } 13 | 14 | return parsed; 15 | }; 16 | -------------------------------------------------------------------------------- /plugins/fields/DateTime/schema.ts: -------------------------------------------------------------------------------- 1 | import { Column } from "@/features/fields/types"; 2 | import Joi from "joi"; 3 | import type { BasetoolRecord } from "@/features/records/types"; 4 | 5 | const schema = (record: BasetoolRecord, column: Column) => { 6 | let rule = Joi.any(); 7 | 8 | if (column.baseOptions.required || !column?.dataSourceInfo?.nullable) { 9 | rule = rule.required(); 10 | } else { 11 | rule = rule.allow("", null); 12 | } 13 | 14 | return rule; 15 | }; 16 | 17 | export default schema; 18 | -------------------------------------------------------------------------------- /plugins/fields/DateTime/types.d.ts: -------------------------------------------------------------------------------- 1 | export type DateTimeFieldOptions = { 2 | showDate?: boolean; 3 | showTime?: boolean; 4 | }; 5 | -------------------------------------------------------------------------------- /plugins/fields/Gravatar/Index.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from "@chakra-ui/layout"; 2 | import { Field } from "@/features/fields/types"; 3 | import { isNull, merge } from "lodash"; 4 | import Image from "next/image"; 5 | import IndexFieldWrapper from "@/features/fields/components/FieldWrapper/IndexFieldWrapper"; 6 | import React, { memo } from "react"; 7 | import classNames from "classnames"; 8 | import fieldOptions from "./fieldOptions"; 9 | import md5 from "md5"; 10 | 11 | const Index = ({ field }: { field: Field }) => { 12 | const value = field.value ? field.value.toString() : ""; 13 | const options = merge(fieldOptions, field.column.fieldOptions); 14 | const src = `https://www.gravatar.com/avatar/${md5(value)}?s=${ 15 | options.indexDimensions 16 | }`; 17 | 18 | return ( 19 | 20 | {isNull(field.value) && null} 21 | {isNull(field.value) || ( 22 |
23 | {value} 33 |
34 | )} 35 |
36 | ); 37 | }; 38 | 39 | export default memo(Index); 40 | -------------------------------------------------------------------------------- /plugins/fields/Gravatar/Show.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from "@chakra-ui/layout"; 2 | import { Field } from "@/features/fields/types"; 3 | import { GravatarFieldOptions } from "./types"; 4 | import { isNull } from "lodash"; 5 | import Image from "next/image"; 6 | import React, { memo } from "react"; 7 | import ShowFieldWrapper from "@/features/fields/components/FieldWrapper/ShowFieldWrapper"; 8 | import md5 from "md5"; 9 | 10 | const Show = ({ field }: { field: Field }) => { 11 | const value = field.value ? field.value.toString() : ""; 12 | const dimensions = 13 | (field.column.fieldOptions as GravatarFieldOptions)?.showDimensions || 340; 14 | 15 | const src = `https://www.gravatar.com/avatar/${md5(value)}?s=${dimensions}`; 16 | 17 | return ( 18 | 19 | {isNull(field.value) && null} 20 | {isNull(field.value) || ( 21 | {value} 28 | )} 29 | 30 | ); 31 | }; 32 | 33 | export default memo(Show); 34 | -------------------------------------------------------------------------------- /plugins/fields/Gravatar/fieldOptions.ts: -------------------------------------------------------------------------------- 1 | const fieldOptions = { 2 | indexDimensions: 40, 3 | showDimensions: 340, 4 | rounded: true, 5 | }; 6 | export default fieldOptions; 7 | -------------------------------------------------------------------------------- /plugins/fields/Gravatar/schema.ts: -------------------------------------------------------------------------------- 1 | import { Column } from "@/features/fields/types"; 2 | import Joi from "joi"; 3 | import type { BasetoolRecord } from "@/features/records/types"; 4 | 5 | const schema = (record: BasetoolRecord, column: Column) => { 6 | let rule = Joi.string(); 7 | 8 | if (column.baseOptions.required) { 9 | rule = rule.required(); 10 | } else { 11 | rule = rule.allow(""); 12 | } 13 | 14 | return rule; 15 | }; 16 | 17 | export default schema; 18 | -------------------------------------------------------------------------------- /plugins/fields/Gravatar/types.d.ts: -------------------------------------------------------------------------------- 1 | export type GravatarFieldOptions = { 2 | showDimensions: number; 3 | indexDimensions: number; 4 | rounded: boolean; 5 | }; 6 | -------------------------------------------------------------------------------- /plugins/fields/Id/Edit.tsx: -------------------------------------------------------------------------------- 1 | import { EditFieldProps } from "@/features/fields/types"; 2 | import EditFieldWrapper from "@/features/fields/components/FieldWrapper/EditFieldWrapper"; 3 | import React, { memo } from "react"; 4 | 5 | const Edit = ({ field }: EditFieldProps) => ( 6 | 7 |
{field.value}
8 |
9 | ); 10 | 11 | export default memo(Edit); 12 | -------------------------------------------------------------------------------- /plugins/fields/Id/Index.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from "@chakra-ui/layout"; 2 | import { Field } from "@/features/fields/types"; 3 | import { isNull } from "lodash"; 4 | import { useDataSourceContext } from "@/hooks"; 5 | import IndexFieldWrapper from "@/features/fields/components/FieldWrapper/IndexFieldWrapper"; 6 | import Link from "next/link"; 7 | import React, { memo } from "react"; 8 | 9 | const Index = ({ field }: { field: Field }) => { 10 | const { recordsPath } = useDataSourceContext(); 11 | const value = isNull(field.value) ? null : field.value; 12 | 13 | return ( 14 | 15 | 16 | {value} 17 | 18 | 19 | ); 20 | }; 21 | 22 | export default memo(Index); 23 | -------------------------------------------------------------------------------- /plugins/fields/Id/Show.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from "@chakra-ui/layout"; 2 | import { Field } from "@/features/fields/types"; 3 | import { isNull } from "lodash"; 4 | import React, { memo } from "react"; 5 | import ShowFieldWrapper from "@/features/fields/components/FieldWrapper/ShowFieldWrapper"; 6 | 7 | const Show = ({ field }: { field: Field }) => { 8 | const value = isNull(field.value) ? null : field.value; 9 | 10 | return {value} 11 | }; 12 | 13 | export default memo(Show); 14 | -------------------------------------------------------------------------------- /plugins/fields/Json/Index.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from "@chakra-ui/layout"; 2 | import { Field } from "@/features/fields/types"; 3 | import { isNull, isObjectLike, isUndefined } from "lodash"; 4 | import IndexFieldWrapper from "@/features/fields/components/FieldWrapper/IndexFieldWrapper"; 5 | import React, { memo } from "react"; 6 | 7 | const Index = ({ field }: { field: Field }) => { 8 | let value; 9 | try { 10 | value = 11 | isUndefined(field.value) || isNull(field.value) 12 | ? null 13 | : JSON.stringify(JSON.parse(field.value as string), null, 2); 14 | } catch (e) { 15 | value = field.value; 16 | } 17 | 18 | if (isObjectLike(field.value)) value = JSON.stringify(field.value); 19 | 20 | return ( 21 | 22 | {isNull(value) ? ( 23 | null 24 | ) : ( 25 | {value} 26 | )} 27 | 28 | ); 29 | }; 30 | 31 | export default memo(Index); 32 | -------------------------------------------------------------------------------- /plugins/fields/Json/Show.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from "@chakra-ui/layout"; 2 | import { Field } from "@/features/fields/types"; 3 | import { isNull, isUndefined } from "lodash"; 4 | import React, { memo } from "react"; 5 | import ShowFieldWrapper from "@/features/fields/components/FieldWrapper/ShowFieldWrapper"; 6 | import dynamic from "next/dynamic"; 7 | 8 | const DynamicReactJson = dynamic(() => import("react-json-view"), { ssr: false }); 9 | 10 | const Show = ({ field }: { field: Field }) => { 11 | let value; 12 | try { 13 | value = 14 | isUndefined(field.value) || isNull(field.value) 15 | ? null 16 | : JSON.parse(field.value as string); 17 | } catch (e) { 18 | value = field.value; 19 | } 20 | 21 | return ( 22 | 23 | {isNull(value) && null} 24 | {isNull(value) || ( 25 | 33 | )} 34 | 35 | ); 36 | }; 37 | 38 | export default memo(Show); 39 | -------------------------------------------------------------------------------- /plugins/fields/Json/schema.ts: -------------------------------------------------------------------------------- 1 | import { Column } from "@/features/fields/types"; 2 | import Joi from "joi"; 3 | import type { BasetoolRecord } from "@/features/records/types"; 4 | 5 | const schema = (record: BasetoolRecord, column: Column) => { 6 | let rule = Joi.object(); 7 | 8 | if (column.baseOptions.required) { 9 | rule = rule.required(); 10 | } else { 11 | rule = rule.allow({}, null); 12 | } 13 | 14 | return rule; 15 | }; 16 | 17 | export default schema; 18 | -------------------------------------------------------------------------------- /plugins/fields/LinkTo/Index.tsx: -------------------------------------------------------------------------------- 1 | import { Field } from "@/features/fields/types"; 2 | import { Tooltip } from "@chakra-ui/react"; 3 | import { isArray } from "lodash"; 4 | import IndexFieldWrapper from "@/features/fields/components/FieldWrapper/IndexFieldWrapper"; 5 | import React, { memo, useMemo } from "react"; 6 | import pluralize from "pluralize"; 7 | 8 | type Association = { 9 | id: string; 10 | label: string; 11 | }; 12 | 13 | const TooltipLabel = memo(({ records }: { records: Association[] }) => ( 14 |
    15 | {records.length > 0 && records.map((record) =>
  1. {record.label}
  2. )} 16 | {records.length === 0 && <>No associations} 17 |
18 | )); 19 | TooltipLabel.displayName = "TooltipLabel"; 20 | 21 | const Index = ({ field }: { field: Field }) => { 22 | const records: Association[] = useMemo( 23 | () => (isArray(field?.value) ? field.value : []), 24 | [field?.value] 25 | ); 26 | const recordsCount = useMemo(() => records.length || 0, [records]); 27 | 28 | return ( 29 | 30 | }> 31 |
32 | {recordsCount} {pluralize("record", recordsCount)} 33 |
34 |
35 |
36 | ); 37 | }; 38 | 39 | export default memo(Index); 40 | -------------------------------------------------------------------------------- /plugins/fields/LinkTo/Show.tsx: -------------------------------------------------------------------------------- 1 | import { Field } from "@/features/fields/types"; 2 | import { LinkToValueItem } from "./types"; 3 | import GoToRecord from "@/features/fields/components/GoToRecordLink"; 4 | import React, { memo, useMemo } from "react"; 5 | import ShowFieldWrapper from "@/features/fields/components/FieldWrapper/ShowFieldWrapper"; 6 | 7 | const LinkToItem = ({ record }: { record: LinkToValueItem }) => { 8 | const href = null; 9 | 10 | return ( 11 | <> 12 | {record.label} [{record.id}]{href && } 13 | 14 | ); 15 | }; 16 | 17 | const Show = ({ field }: { field: Field }) => { 18 | const hasRecords = useMemo(() => field.value?.length > 0, [field.value]); 19 | const records = field.value; 20 | 21 | return ( 22 | <> 23 | 24 |
25 | {hasRecords || "No records"} 26 | {hasRecords && ( 27 |
    28 | {records.map((record, idx: number) => ( 29 |
  1. 30 | 31 |
  2. 32 | ))} 33 |
34 | )} 35 |
36 |
37 | 38 | ); 39 | }; 40 | 41 | export default memo(Show); 42 | -------------------------------------------------------------------------------- /plugins/fields/LinkTo/fieldOptions.ts: -------------------------------------------------------------------------------- 1 | const fieldOptions = { 2 | tableName: '', 3 | columnName: '', 4 | }; 5 | 6 | export default fieldOptions; 7 | -------------------------------------------------------------------------------- /plugins/fields/LinkTo/schema.ts: -------------------------------------------------------------------------------- 1 | import { Column } from "@/features/fields/types"; 2 | import Joi from "joi"; 3 | import type { BasetoolRecord } from "@/features/records/types"; 4 | 5 | const schema = (record: BasetoolRecord, column: Column) => { 6 | let rule = Joi.alternatives(Joi.string(), Joi.number()) 7 | 8 | if (column.baseOptions.required) { 9 | rule = rule.required(); 10 | } else { 11 | rule = rule.allow("", null); 12 | } 13 | 14 | return rule; 15 | }; 16 | 17 | export default schema; 18 | -------------------------------------------------------------------------------- /plugins/fields/LinkTo/types.d.ts: -------------------------------------------------------------------------------- 1 | export type LinkToValueFieldOptions = { 2 | tableName: string; 3 | columnName: string; 4 | }; 5 | 6 | export type LinkToValueItem = { 7 | id: number; 8 | label: string; 9 | foreignId: number; 10 | foreignTable: string; 11 | dataSourceId: number; 12 | foreignColumnName: string; 13 | }; 14 | -------------------------------------------------------------------------------- /plugins/fields/Number/Index.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from "@chakra-ui/layout"; 2 | import { Field } from "@/features/fields/types"; 3 | import { isNull } from "lodash"; 4 | import IndexFieldWrapper from "@/features/fields/components/FieldWrapper/IndexFieldWrapper"; 5 | import React, { memo } from "react"; 6 | 7 | const Index = ({ field }: { field: Field }) => { 8 | const value = isNull(field.value) ? null : field.value; 9 | 10 | return {value}; 11 | }; 12 | 13 | export default memo(Index); 14 | -------------------------------------------------------------------------------- /plugins/fields/Number/Show.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from "@chakra-ui/layout"; 2 | import { Field } from "@/features/fields/types"; 3 | import { isNull } from "lodash"; 4 | import React, { memo } from "react"; 5 | import ShowFieldWrapper from "@/features/fields/components/FieldWrapper/ShowFieldWrapper"; 6 | 7 | const Show = ({ field }: { field: Field }) => { 8 | const value = isNull(field.value) ? null : field.value; 9 | 10 | return {value} 11 | }; 12 | 13 | export default memo(Show); 14 | -------------------------------------------------------------------------------- /plugins/fields/Number/options.ts: -------------------------------------------------------------------------------- 1 | const options = {}; 2 | 3 | export default options; 4 | -------------------------------------------------------------------------------- /plugins/fields/Number/schema.ts: -------------------------------------------------------------------------------- 1 | import { Column } from "@/features/fields/types"; 2 | import Joi from "joi"; 3 | import type { BasetoolRecord } from "@/features/records/types"; 4 | 5 | const schema = (record: BasetoolRecord, column: Column) => { 6 | let rule = Joi.number(); 7 | 8 | if (column.baseOptions.required) { 9 | rule = rule.required(); 10 | } else { 11 | rule = rule.allow(null, "", NaN); 12 | } 13 | 14 | return rule; 15 | }; 16 | 17 | export default schema; 18 | -------------------------------------------------------------------------------- /plugins/fields/ProgressBar/Index.tsx: -------------------------------------------------------------------------------- 1 | import { Field } from "@/features/fields/types"; 2 | import { ProgressBarFieldOptions } from "./types"; 3 | import IndexFieldWrapper from "@/features/fields/components/FieldWrapper/IndexFieldWrapper"; 4 | import React, { memo } from "react"; 5 | 6 | const Index = ({ field }: { field: Field }) => { 7 | const fieldOptions = field.column.fieldOptions as ProgressBarFieldOptions; 8 | 9 | return ( 10 | 11 | 18 | 23 | 24 | ); 25 | }; 26 | 27 | export default memo(Index); 28 | -------------------------------------------------------------------------------- /plugins/fields/ProgressBar/Show.tsx: -------------------------------------------------------------------------------- 1 | import { Field } from "@/features/fields/types"; 2 | import { ProgressBarFieldOptions } from "./types"; 3 | import React, { memo } from "react"; 4 | import ShowFieldWrapper from "@/features/fields/components/FieldWrapper/ShowFieldWrapper"; 5 | 6 | const Show = ({ field }: { field: Field }) => { 7 | const fieldOptions = field.column.fieldOptions as ProgressBarFieldOptions; 8 | 9 | return ( 10 | 11 |
14 | {fieldOptions.displayValue && <> 15 | {field.value} 16 | {fieldOptions.valueSuffix} 17 | } 18 |
19 | 24 |
25 | ); 26 | }; 27 | 28 | export default memo(Show); 29 | -------------------------------------------------------------------------------- /plugins/fields/ProgressBar/fieldOptions.ts: -------------------------------------------------------------------------------- 1 | const fieldOptions = { 2 | max: 100, 3 | valueSuffix: "", 4 | step: 1, 5 | displayValue: false, 6 | }; 7 | export default fieldOptions; 8 | -------------------------------------------------------------------------------- /plugins/fields/ProgressBar/schema.ts: -------------------------------------------------------------------------------- 1 | import { Column } from "@/features/fields/types"; 2 | import Joi from "joi"; 3 | import type { BasetoolRecord } from "@/features/records/types"; 4 | 5 | const schema = (record: BasetoolRecord, column: Column) => { 6 | const rule = Joi.number().allow(null, "", NaN); 7 | 8 | return rule; 9 | }; 10 | 11 | export default schema; 12 | -------------------------------------------------------------------------------- /plugins/fields/ProgressBar/types.d.ts: -------------------------------------------------------------------------------- 1 | export type ProgressBarFieldOptions = { 2 | max: number; 3 | valueSuffix: string; 4 | step: number; 5 | displayValue: boolean; 6 | }; 7 | -------------------------------------------------------------------------------- /plugins/fields/Select/Index.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from "@chakra-ui/layout"; 2 | import { Field } from "@/features/fields/types"; 3 | import { isNull } from "lodash"; 4 | import IndexFieldWrapper from "@/features/fields/components/FieldWrapper/IndexFieldWrapper"; 5 | import React, { memo } from "react"; 6 | 7 | const Index = ({ field }: { field: Field }) => { 8 | const value = isNull(field.value) ? null : field.value; 9 | 10 | return {value}; 11 | }; 12 | 13 | export default memo(Index); 14 | -------------------------------------------------------------------------------- /plugins/fields/Select/Inspector.tsx: -------------------------------------------------------------------------------- 1 | import { FormHelperText, Input } from "@chakra-ui/react"; 2 | import { InspectorProps } from "@/features/fields/types"; 3 | import { debounce, merge } from "lodash"; 4 | import OptionWrapper from "@/features/views/components/OptionWrapper"; 5 | import React, { useCallback } from "react"; 6 | import fieldOptions from "./fieldOptions"; 7 | 8 | function Inspector({ 9 | column, 10 | setColumnOptions, 11 | }: InspectorProps) { 12 | const options = merge(fieldOptions, column.fieldOptions); 13 | 14 | const debouncedSetColumnOptions = useCallback( 15 | debounce(setColumnOptions, 1000), 16 | [] 17 | ); 18 | 19 | const handleOnChange = (event: any) => { 20 | if (column) 21 | debouncedSetColumnOptions(column.name, { 22 | "fieldOptions.options": event.currentTarget.value, 23 | }); 24 | }; 25 | 26 | return ( 27 | 31 | 40 | Add the values comma separated. 41 | 42 | ); 43 | } 44 | 45 | export default Inspector; 46 | -------------------------------------------------------------------------------- /plugins/fields/Select/Show.tsx: -------------------------------------------------------------------------------- 1 | import { Code } from "@chakra-ui/layout"; 2 | import { Field } from "@/features/fields/types"; 3 | import { isNull } from "lodash"; 4 | import React, { memo } from "react"; 5 | import ShowFieldWrapper from "@/features/fields/components/FieldWrapper/ShowFieldWrapper"; 6 | 7 | const Show = ({ field }: { field: Field }) => { 8 | const value = isNull(field.value) ? null : field.value; 9 | 10 | return {value} 11 | }; 12 | 13 | export default memo(Show); 14 | -------------------------------------------------------------------------------- /plugins/fields/Select/fieldOptions.ts: -------------------------------------------------------------------------------- 1 | const fieldOptions = { 2 | options: "", 3 | }; 4 | export default fieldOptions; 5 | -------------------------------------------------------------------------------- /plugins/fields/Select/schema.ts: -------------------------------------------------------------------------------- 1 | import { Column } from "@/features/fields/types"; 2 | import Joi from "joi"; 3 | import type { BasetoolRecord } from "@/features/records/types"; 4 | 5 | const schema = (record: BasetoolRecord, column: Column) => { 6 | let rule = Joi.alternatives(Joi.string(), Joi.number()); 7 | 8 | if (column.baseOptions.required) { 9 | rule = rule.required(); 10 | } else { 11 | rule = rule.allow("", null); 12 | } 13 | 14 | return rule; 15 | }; 16 | 17 | export default schema; 18 | -------------------------------------------------------------------------------- /plugins/fields/Select/types.d.ts: -------------------------------------------------------------------------------- 1 | export type SelectFieldOptions = { 2 | options: string; 3 | }; 4 | -------------------------------------------------------------------------------- /plugins/fields/Text/fieldOptions.ts: -------------------------------------------------------------------------------- 1 | const fieldOptions = { 2 | displayAsLink: false, 3 | openNewTab: false, 4 | linkText: '', 5 | linkPrefix: '', 6 | displayAsImage: false, 7 | displayAsEmail: false, 8 | }; 9 | export default fieldOptions; 10 | -------------------------------------------------------------------------------- /plugins/fields/Text/schema.ts: -------------------------------------------------------------------------------- 1 | import { Column } from "@/features/fields/types"; 2 | import Joi from "joi"; 3 | import type { BasetoolRecord } from "@/features/records/types"; 4 | 5 | const schema = (record: BasetoolRecord, column: Column) => { 6 | let rule = Joi.alternatives(Joi.string(), Joi.number()); 7 | 8 | if (column.baseOptions.required) { 9 | rule = rule.required(); 10 | } else { 11 | rule = rule.allow("", null); 12 | } 13 | 14 | return rule; 15 | }; 16 | 17 | export default schema; 18 | -------------------------------------------------------------------------------- /plugins/fields/Text/types.d.ts: -------------------------------------------------------------------------------- 1 | export type SelectFieldOptions = { 2 | displayAsLink: boolean; 3 | openNewTab: boolean; 4 | linkText: string; 5 | linkPrefix: string; 6 | displayAsImage: boolean; 7 | displayAsEmail: boolean; 8 | }; 9 | -------------------------------------------------------------------------------- /plugins/fields/Textarea/Index.tsx: -------------------------------------------------------------------------------- 1 | import { Field } from "@/features/fields/types"; 2 | import IndexFieldWrapper from "@/features/fields/components/FieldWrapper/IndexFieldWrapper"; 3 | import React, { memo } from "react"; 4 | 5 | const Index = ({ field }: { field: Field }) => ( 6 | {field.value} 7 | ); 8 | 9 | export default memo(Index); 10 | -------------------------------------------------------------------------------- /plugins/fields/Textarea/Inspector.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from "@chakra-ui/react"; 2 | import { InspectorProps } from "@/features/fields/types"; 3 | import { debounce, merge } from "lodash"; 4 | import OptionWrapper from "@/features/views/components/OptionWrapper"; 5 | import React, { useCallback } from "react"; 6 | import fieldOptions from "./fieldOptions"; 7 | 8 | function Inspector({ column, setColumnOptions }: InspectorProps) { 9 | const options = merge(fieldOptions, column.fieldOptions); 10 | const debouncedSetColumnOptions = useCallback( 11 | debounce(setColumnOptions, 1000), 12 | [] 13 | ); 14 | 15 | const handleOnChange = (event: any) => { 16 | if (column) 17 | debouncedSetColumnOptions(column.name, { 18 | "fieldOptions.rows": event.currentTarget.value, 19 | }); 20 | }; 21 | 22 | return ( 23 | 24 | 32 | 33 | ); 34 | } 35 | 36 | export default Inspector; 37 | -------------------------------------------------------------------------------- /plugins/fields/Textarea/Show.tsx: -------------------------------------------------------------------------------- 1 | import { Field } from "@/features/fields/types"; 2 | import React, { memo } from "react"; 3 | import ShowFieldWrapper from "@/features/fields/components/FieldWrapper/ShowFieldWrapper"; 4 | 5 | const Show = ({ field }: { field: Field }) => ( 6 | {field.value} 7 | ); 8 | 9 | export default memo(Show); 10 | -------------------------------------------------------------------------------- /plugins/fields/Textarea/fieldOptions.ts: -------------------------------------------------------------------------------- 1 | const fieldOptions = { 2 | rows: 5, 3 | }; 4 | export default fieldOptions; 5 | -------------------------------------------------------------------------------- /plugins/fields/Textarea/schema.ts: -------------------------------------------------------------------------------- 1 | import { Column } from "@/features/fields/types"; 2 | import Joi from "joi"; 3 | import type { BasetoolRecord } from "@/features/records/types"; 4 | 5 | const schema = (record: BasetoolRecord, column: Column) => { 6 | let rule = Joi.alternatives(Joi.string(), Joi.number()); 7 | 8 | if (column.baseOptions.required) { 9 | rule = rule.required(); 10 | } else { 11 | rule = rule.allow("", null); 12 | } 13 | 14 | return rule; 15 | }; 16 | 17 | export default schema; 18 | -------------------------------------------------------------------------------- /plugins/fields/Textarea/types.d.ts: -------------------------------------------------------------------------------- 1 | export type TextareaFieldOptions = { 2 | rows: number; 3 | }; 4 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-import": {}, 4 | "tailwindcss/nesting": {}, 5 | tailwindcss: {}, 6 | autoprefixer: {}, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /prisma/index.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | 3 | // add prisma to the NodeJS global type 4 | interface CustomNodeJsGlobal { 5 | prisma: PrismaClient; 6 | } 7 | 8 | // Prevent multiple instances of Prisma Client in development 9 | declare const global: CustomNodeJsGlobal 10 | 11 | const prisma = global.prisma || new PrismaClient({ 12 | // log: ['query', 'info', 'warn', 'error'], 13 | }) 14 | 15 | if (process.env.NODE_ENV === 'development') global.prisma = prisma 16 | 17 | export default prisma 18 | -------------------------------------------------------------------------------- /prisma/migrations/20210902112607_add_encrypted_credentials_to_data_sources/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "DataSource" ADD COLUMN "encryptedCredentials" TEXT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20210908205601_add_roles/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `role` on the `OrganizationUser` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "OrganizationUser" DROP COLUMN "role", 9 | ADD COLUMN "roleId" INTEGER; 10 | 11 | -- CreateTable 12 | CREATE TABLE "Role" ( 13 | "id" SERIAL NOT NULL, 14 | "name" TEXT NOT NULL, 15 | "organizationId" INTEGER NOT NULL, 16 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 17 | "updatedAt" TIMESTAMP(3) NOT NULL, 18 | 19 | PRIMARY KEY ("id") 20 | ); 21 | 22 | -- CreateIndex 23 | CREATE INDEX "Role.organizationId_index" ON "Role"("organizationId"); 24 | 25 | -- AddForeignKey 26 | ALTER TABLE "OrganizationUser" ADD FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE SET NULL ON UPDATE CASCADE; 27 | 28 | -- AddForeignKey 29 | ALTER TABLE "Role" ADD FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; 30 | -------------------------------------------------------------------------------- /prisma/migrations/20210909110755_add_options_to_roles/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Role" ADD COLUMN "options" JSONB NOT NULL DEFAULT E'{}'; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20210923094700_add_ogranization_membership_invite/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "OrganizationMembershipInvite" ( 3 | "id" SERIAL NOT NULL, 4 | "uuid" UUID NOT NULL, 5 | "organizationUserId" INTEGER NOT NULL, 6 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | "updatedAt" TIMESTAMP(3) NOT NULL, 8 | "userId" INTEGER, 9 | "roleId" INTEGER, 10 | 11 | PRIMARY KEY ("id") 12 | ); 13 | 14 | -- CreateIndex 15 | CREATE INDEX "OrganizationMembershipInvite.organizationUserId_index" ON "OrganizationMembershipInvite"("organizationUserId"); 16 | 17 | -- AddForeignKey 18 | ALTER TABLE "OrganizationMembershipInvite" ADD FOREIGN KEY ("organizationUserId") REFERENCES "OrganizationUser"("id") ON DELETE CASCADE ON UPDATE CASCADE; 19 | 20 | -- AddForeignKey 21 | ALTER TABLE "OrganizationMembershipInvite" ADD FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; 22 | 23 | -- AddForeignKey 24 | ALTER TABLE "OrganizationMembershipInvite" ADD FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE SET NULL ON UPDATE CASCADE; 25 | -------------------------------------------------------------------------------- /prisma/migrations/20210923111111_invitations_have_uuid_primary_keys/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - The primary key for the `OrganizationInvitation` table will be changed. If it partially fails, the table could be left without primary key constraint. 5 | - You are about to drop the column `id` on the `OrganizationInvitation` table. All the data in the column will be lost. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "OrganizationInvitation" DROP CONSTRAINT "OrganizationInvitation_pkey", 10 | DROP COLUMN "id", 11 | ADD CONSTRAINT "OrganizationInvitation_pkey" PRIMARY KEY ("uuid"); 12 | -------------------------------------------------------------------------------- /prisma/migrations/20210923132123_cascade_user_deletion_to_invites/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "OrganizationInvitation" DROP CONSTRAINT "OrganizationInvitation_organizationUserId_fkey"; 3 | 4 | -- DropForeignKey 5 | ALTER TABLE "OrganizationUser" DROP CONSTRAINT "OrganizationUser_organizationId_fkey"; 6 | 7 | -- DropForeignKey 8 | ALTER TABLE "OrganizationUser" DROP CONSTRAINT "OrganizationUser_roleId_fkey"; 9 | 10 | -- DropForeignKey 11 | ALTER TABLE "OrganizationUser" DROP CONSTRAINT "OrganizationUser_userId_fkey"; 12 | 13 | -- AddForeignKey 14 | ALTER TABLE "OrganizationUser" ADD CONSTRAINT "OrganizationUser_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "OrganizationUser" ADD CONSTRAINT "OrganizationUser_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; 18 | 19 | -- AddForeignKey 20 | ALTER TABLE "OrganizationUser" ADD CONSTRAINT "OrganizationUser_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "Role"("id") ON DELETE CASCADE ON UPDATE CASCADE; 21 | 22 | -- AddForeignKey 23 | ALTER TABLE "OrganizationInvitation" ADD CONSTRAINT "OrganizationInvitation_organizationUserId_fkey" FOREIGN KEY ("organizationUserId") REFERENCES "OrganizationUser"("id") ON DELETE CASCADE ON UPDATE CASCADE; 24 | -------------------------------------------------------------------------------- /prisma/migrations/20210924130927_add_last_logged_in_at_to_users/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "lastLoggedInAt" TIMESTAMP(3); 3 | -------------------------------------------------------------------------------- /prisma/migrations/20211015102604_add_views/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "View" ( 3 | "id" SERIAL NOT NULL, 4 | "name" VARCHAR(255) NOT NULL, 5 | "public" BOOLEAN NOT NULL DEFAULT false, 6 | "createdBy" INTEGER NOT NULL, 7 | "organizationId" INTEGER NOT NULL, 8 | "tableName" VARCHAR(255) NOT NULL, 9 | "filters" JSONB NOT NULL DEFAULT E'{}', 10 | "orderRules" JSONB NOT NULL DEFAULT E'{}', 11 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 12 | "updatedAt" TIMESTAMP(3) NOT NULL, 13 | 14 | CONSTRAINT "View_pkey" PRIMARY KEY ("id") 15 | ); 16 | 17 | -- CreateIndex 18 | CREATE INDEX "View_createdBy_organizationId_idx" ON "View"("createdBy", "organizationId"); 19 | 20 | -- AddForeignKey 21 | ALTER TABLE "View" ADD CONSTRAINT "View_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 22 | 23 | -- AddForeignKey 24 | ALTER TABLE "View" ADD CONSTRAINT "View_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; 25 | -------------------------------------------------------------------------------- /prisma/migrations/20211015103805_change_filters_and_order_rules_to_arrays_in_views/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "View" ALTER COLUMN "filters" SET DEFAULT E'[]', 3 | ALTER COLUMN "orderRules" SET DEFAULT E'[]'; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20211015104016_delete_order_rules_from_views/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `orderRules` on the `View` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "View" DROP COLUMN "orderRules"; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20211015150345_add_datasource_id_to_views/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `datasourceId` to the `View` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- DropIndex 8 | DROP INDEX "View_createdBy_organizationId_idx"; 9 | 10 | -- AlterTable 11 | ALTER TABLE "View" ADD COLUMN "datasourceId" INTEGER NOT NULL; 12 | 13 | -- CreateIndex 14 | CREATE INDEX "View_createdBy_organizationId_datasourceId_idx" ON "View"("createdBy", "organizationId", "datasourceId"); 15 | 16 | -- AddForeignKey 17 | ALTER TABLE "View" ADD CONSTRAINT "View_datasourceId_fkey" FOREIGN KEY ("datasourceId") REFERENCES "DataSource"("id") ON DELETE CASCADE ON UPDATE CASCADE; 18 | -------------------------------------------------------------------------------- /prisma/migrations/20211015152114_rename_datasource_views/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `datasourceId` on the `View` table. All the data in the column will be lost. 5 | - Added the required column `dataSourceId` to the `View` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- DropForeignKey 9 | ALTER TABLE "View" DROP CONSTRAINT "View_datasourceId_fkey"; 10 | 11 | -- DropIndex 12 | DROP INDEX "View_createdBy_organizationId_datasourceId_idx"; 13 | 14 | -- AlterTable 15 | ALTER TABLE "View" DROP COLUMN "datasourceId", 16 | ADD COLUMN "dataSourceId" INTEGER NOT NULL; 17 | 18 | -- CreateIndex 19 | CREATE INDEX "View_createdBy_organizationId_dataSourceId_idx" ON "View"("createdBy", "organizationId", "dataSourceId"); 20 | 21 | -- AddForeignKey 22 | ALTER TABLE "View" ADD CONSTRAINT "View_dataSourceId_fkey" FOREIGN KEY ("dataSourceId") REFERENCES "DataSource"("id") ON DELETE CASCADE ON UPDATE CASCADE; 23 | -------------------------------------------------------------------------------- /prisma/migrations/20211020084717_split_indexes_for_view/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "View_createdBy_organizationId_dataSourceId_idx"; 3 | 4 | -- CreateIndex 5 | CREATE INDEX "View_createdBy_idx" ON "View"("createdBy"); 6 | 7 | -- CreateIndex 8 | CREATE INDEX "View_organizationId_idx" ON "View"("organizationId"); 9 | 10 | -- CreateIndex 11 | CREATE INDEX "View_dataSourceId_idx" ON "View"("dataSourceId"); 12 | -------------------------------------------------------------------------------- /prisma/migrations/20211020141810_add_encrypted_ssh_credentials/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "DataSource" ADD COLUMN "encryptedSSHCredentials" TEXT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20211022134622_add_order_rule_to_views/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "View" ADD COLUMN "orderRule" JSONB DEFAULT E'{}'; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20211025070704_chnage_name_of_order_rule_in_views/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `orderRule` on the `View` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "View" DROP COLUMN "orderRule", 9 | ADD COLUMN "defaultOrder" JSONB DEFAULT E'{}'; 10 | -------------------------------------------------------------------------------- /prisma/migrations/20211105155559_add_activities/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Activity" ( 3 | "id" SERIAL NOT NULL, 4 | "recordId" VARCHAR(255), 5 | "tableName" VARCHAR(255), 6 | "dataSourceId" INTEGER, 7 | "viewId" INTEGER, 8 | "userId" INTEGER NOT NULL, 9 | "organizationId" INTEGER NOT NULL, 10 | "changes" JSONB NOT NULL DEFAULT E'{}', 11 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 12 | 13 | CONSTRAINT "Activity_pkey" PRIMARY KEY ("id") 14 | ); 15 | 16 | -- CreateIndex 17 | CREATE INDEX "Activity_dataSourceId_idx" ON "Activity"("dataSourceId"); 18 | 19 | -- CreateIndex 20 | CREATE INDEX "Activity_viewId_idx" ON "Activity"("viewId"); 21 | 22 | -- CreateIndex 23 | CREATE INDEX "Activity_userId_idx" ON "Activity"("userId"); 24 | 25 | -- CreateIndex 26 | CREATE INDEX "Activity_organizationId_idx" ON "Activity"("organizationId"); 27 | 28 | -- AddForeignKey 29 | ALTER TABLE "Activity" ADD CONSTRAINT "Activity_dataSourceId_fkey" FOREIGN KEY ("dataSourceId") REFERENCES "DataSource"("id") ON DELETE CASCADE ON UPDATE CASCADE; 30 | 31 | -- AddForeignKey 32 | ALTER TABLE "Activity" ADD CONSTRAINT "Activity_viewId_fkey" FOREIGN KEY ("viewId") REFERENCES "View"("id") ON DELETE CASCADE ON UPDATE CASCADE; 33 | 34 | -- AddForeignKey 35 | ALTER TABLE "Activity" ADD CONSTRAINT "Activity_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 36 | 37 | -- AddForeignKey 38 | ALTER TABLE "Activity" ADD CONSTRAINT "Activity_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; 39 | -------------------------------------------------------------------------------- /prisma/migrations/20211107151703_add_columns_to_views/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "View" ADD COLUMN "columns" JSONB NOT NULL DEFAULT E'{}'; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20211108135526_add_action_to_activity/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `action` to the `Activity` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Activity" ADD COLUMN "action" VARCHAR(255) NOT NULL, 9 | ALTER COLUMN "changes" DROP NOT NULL; 10 | -------------------------------------------------------------------------------- /prisma/migrations/20211108210747_change_user_id_to_user_email_in_activity/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `userId` on the `Activity` table. All the data in the column will be lost. 5 | - Added the required column `userEmail` to the `Activity` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- DropForeignKey 9 | ALTER TABLE "Activity" DROP CONSTRAINT "Activity_userId_fkey"; 10 | 11 | -- DropIndex 12 | DROP INDEX "Activity_userId_idx"; 13 | 14 | -- AlterTable 15 | ALTER TABLE "Activity" DROP COLUMN "userId", 16 | ADD COLUMN "userEmail" TEXT NOT NULL; 17 | -------------------------------------------------------------------------------- /prisma/migrations/20211109092051_change_back_to_user_id_activity/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `userEmail` on the `Activity` table. All the data in the column will be lost. 5 | - Added the required column `userId` to the `Activity` table without a default value. This is not possible if the table is not empty. 6 | 7 | */ 8 | -- AlterTable 9 | ALTER TABLE "Activity" DROP COLUMN "userEmail", 10 | ADD COLUMN "userId" INTEGER NOT NULL; 11 | 12 | -- AddForeignKey 13 | ALTER TABLE "Activity" ADD CONSTRAINT "Activity_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 14 | -------------------------------------------------------------------------------- /prisma/migrations/20211109114014_changes_to_array_acitivity/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Activity" ALTER COLUMN "changes" SET DEFAULT E'[]'; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20211109164431_add_index_user_id_activity/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateIndex 2 | CREATE INDEX "Activity_userId_idx" ON "Activity"("userId"); 3 | -------------------------------------------------------------------------------- /prisma/migrations/20211110105430_update_view_filters_to_array/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "View" ALTER COLUMN "defaultOrder" SET DEFAULT E'[]'; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20211110111825_add_last_known_timezone_to_users/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "lastKnownTimezone" TEXT; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20211111151715_view_columns_to_array/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "View" ALTER COLUMN "columns" SET DEFAULT E'[]'; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20211119102418_add_table_meta_data_to_datasource/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "DataSource" ADD COLUMN "tablesMetaData" JSONB DEFAULT E'[]'; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20211209121519_add_dashboard/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Dashboard" ( 3 | "id" SERIAL NOT NULL, 4 | "name" VARCHAR(255) NOT NULL, 5 | "createdBy" INTEGER NOT NULL, 6 | "organizationId" INTEGER NOT NULL, 7 | "isPublic" BOOLEAN NOT NULL DEFAULT false, 8 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | "updatedAt" TIMESTAMP(3) NOT NULL, 10 | 11 | CONSTRAINT "Dashboard_pkey" PRIMARY KEY ("id") 12 | ); 13 | 14 | -- CreateIndex 15 | CREATE INDEX "Dashboard_createdBy_idx" ON "Dashboard"("createdBy"); 16 | 17 | -- CreateIndex 18 | CREATE INDEX "Dashboard_organizationId_idx" ON "Dashboard"("organizationId"); 19 | 20 | -- AddForeignKey 21 | ALTER TABLE "Dashboard" ADD CONSTRAINT "Dashboard_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 22 | 23 | -- AddForeignKey 24 | ALTER TABLE "Dashboard" ADD CONSTRAINT "Dashboard_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE; 25 | -------------------------------------------------------------------------------- /prisma/migrations/20211210093006_add_datasource_to_dashboard/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `dataSourceId` to the `Dashboard` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Dashboard" ADD COLUMN "dataSourceId" INTEGER NOT NULL; 9 | 10 | -- CreateIndex 11 | CREATE INDEX "Dashboard_dataSourceId_idx" ON "Dashboard"("dataSourceId"); 12 | 13 | -- AddForeignKey 14 | ALTER TABLE "Dashboard" ADD CONSTRAINT "Dashboard_dataSourceId_fkey" FOREIGN KEY ("dataSourceId") REFERENCES "DataSource"("id") ON DELETE CASCADE ON UPDATE CASCADE; 15 | -------------------------------------------------------------------------------- /prisma/migrations/20211210142704_add_dashboard_item/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "DashboardItem" ( 3 | "id" SERIAL NOT NULL, 4 | "name" VARCHAR(255) NOT NULL, 5 | "dashboardId" INTEGER NOT NULL, 6 | "query" TEXT NOT NULL, 7 | "type" TEXT NOT NULL, 8 | "order" INTEGER NOT NULL, 9 | "options" JSONB NOT NULL DEFAULT E'{}', 10 | "createdBy" INTEGER NOT NULL, 11 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 12 | "updatedAt" TIMESTAMP(3) NOT NULL, 13 | 14 | CONSTRAINT "DashboardItem_pkey" PRIMARY KEY ("id") 15 | ); 16 | 17 | -- CreateIndex 18 | CREATE INDEX "DashboardItem_createdBy_idx" ON "DashboardItem"("createdBy"); 19 | 20 | -- CreateIndex 21 | CREATE INDEX "DashboardItem_dashboardId_idx" ON "DashboardItem"("dashboardId"); 22 | 23 | -- AddForeignKey 24 | ALTER TABLE "DashboardItem" ADD CONSTRAINT "DashboardItem_dashboardId_fkey" FOREIGN KEY ("dashboardId") REFERENCES "Dashboard"("id") ON DELETE CASCADE ON UPDATE CASCADE; 25 | 26 | -- AddForeignKey 27 | ALTER TABLE "DashboardItem" ADD CONSTRAINT "DashboardItem_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 28 | -------------------------------------------------------------------------------- /prisma/migrations/20211214134027_rename_dashboard_item_to_widget/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `DashboardItem` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "DashboardItem" DROP CONSTRAINT "DashboardItem_createdBy_fkey"; 9 | 10 | -- DropForeignKey 11 | ALTER TABLE "DashboardItem" DROP CONSTRAINT "DashboardItem_dashboardId_fkey"; 12 | 13 | -- DropTable 14 | DROP TABLE "DashboardItem"; 15 | 16 | -- CreateTable 17 | CREATE TABLE "Widget" ( 18 | "id" SERIAL NOT NULL, 19 | "name" VARCHAR(255) NOT NULL, 20 | "dashboardId" INTEGER NOT NULL, 21 | "query" TEXT NOT NULL, 22 | "type" TEXT NOT NULL, 23 | "order" INTEGER NOT NULL, 24 | "options" JSONB NOT NULL DEFAULT E'{}', 25 | "createdBy" INTEGER NOT NULL, 26 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 27 | "updatedAt" TIMESTAMP(3) NOT NULL, 28 | 29 | CONSTRAINT "Widget_pkey" PRIMARY KEY ("id") 30 | ); 31 | 32 | -- CreateIndex 33 | CREATE INDEX "Widget_createdBy_idx" ON "Widget"("createdBy"); 34 | 35 | -- CreateIndex 36 | CREATE INDEX "Widget_dashboardId_idx" ON "Widget"("dashboardId"); 37 | 38 | -- AddForeignKey 39 | ALTER TABLE "Widget" ADD CONSTRAINT "Widget_dashboardId_fkey" FOREIGN KEY ("dashboardId") REFERENCES "Dashboard"("id") ON DELETE CASCADE ON UPDATE CASCADE; 40 | 41 | -- AddForeignKey 42 | ALTER TABLE "Widget" ADD CONSTRAINT "Widget_createdBy_fkey" FOREIGN KEY ("createdBy") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 43 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | import { seed } from "./seed-script" 3 | 4 | const prisma = new PrismaClient(); 5 | 6 | seed() 7 | .catch((e) => { 8 | console.error(e); 9 | process.exit(1); 10 | }) 11 | .finally(async () => { 12 | await prisma.$disconnect(); 13 | }); 14 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/favicon.ico -------------------------------------------------------------------------------- /public/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #3A1EF6 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/favicons/favicon.ico -------------------------------------------------------------------------------- /public/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /public/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/favicons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/favicons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /public/fonts/nunito-v16-latin-700.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/fonts/nunito-v16-latin-700.eot -------------------------------------------------------------------------------- /public/fonts/nunito-v16-latin-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/fonts/nunito-v16-latin-700.ttf -------------------------------------------------------------------------------- /public/fonts/nunito-v16-latin-700.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/fonts/nunito-v16-latin-700.woff -------------------------------------------------------------------------------- /public/fonts/nunito-v16-latin-700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/fonts/nunito-v16-latin-700.woff2 -------------------------------------------------------------------------------- /public/fonts/nunito-v16-latin-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/fonts/nunito-v16-latin-regular.eot -------------------------------------------------------------------------------- /public/fonts/nunito-v16-latin-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/fonts/nunito-v16-latin-regular.ttf -------------------------------------------------------------------------------- /public/fonts/nunito-v16-latin-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/fonts/nunito-v16-latin-regular.woff -------------------------------------------------------------------------------- /public/fonts/nunito-v16-latin-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/fonts/nunito-v16-latin-regular.woff2 -------------------------------------------------------------------------------- /public/img/404_cat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/img/404_cat.jpg -------------------------------------------------------------------------------- /public/img/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/img/cover.jpg -------------------------------------------------------------------------------- /public/img/heading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/img/heading.png -------------------------------------------------------------------------------- /public/img/logo_text_black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/img/logo_text_black.png -------------------------------------------------------------------------------- /public/img/logos/airtable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/img/logos/airtable.png -------------------------------------------------------------------------------- /public/img/logos/amazon_redshift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/img/logos/amazon_redshift.png -------------------------------------------------------------------------------- /public/img/logos/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/img/logos/github.png -------------------------------------------------------------------------------- /public/img/logos/google-sheets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/img/logos/google-sheets.png -------------------------------------------------------------------------------- /public/img/logos/intercom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/img/logos/intercom.png -------------------------------------------------------------------------------- /public/img/logos/maria_db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/img/logos/maria_db.png -------------------------------------------------------------------------------- /public/img/logos/mssql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/img/logos/mssql.png -------------------------------------------------------------------------------- /public/img/logos/mysql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/img/logos/mysql.png -------------------------------------------------------------------------------- /public/img/logos/postgresql.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/img/logos/postgresql.png -------------------------------------------------------------------------------- /public/img/logos/redis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/img/logos/redis.png -------------------------------------------------------------------------------- /public/img/logos/salesforce.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/img/logos/salesforce.png -------------------------------------------------------------------------------- /public/img/logos/shopify.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/img/logos/shopify.png -------------------------------------------------------------------------------- /public/img/logos/stripe.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/img/logos/stripe.png -------------------------------------------------------------------------------- /public/img/logos/swagger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/img/logos/swagger.png -------------------------------------------------------------------------------- /public/img/logos/zendesk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basetool-io/basetool/b5074690fa00896152fddf1083a4e20773a3368e/public/img/logos/zendesk.png -------------------------------------------------------------------------------- /scripts/docker-start.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | 3 | if (process.env.BASETOOL_TELEMETRY_DISABLED !== "1") { 4 | $.verbose = false; 5 | 6 | const ipReq = await fetch("https://api.ipify.org/?format=json"); 7 | const ip = (await ipReq.json())?.ip; 8 | 9 | try { 10 | fetch("https://api.basetool.io/api/docker/install", { 11 | method: "post", 12 | body: JSON.stringify({ 13 | ip, 14 | hostedSecret: process.env.BASETOOL_HOSTED_SECRET, 15 | }), 16 | headers: { "Content-Type": "application/json" }, 17 | }); 18 | } catch (error) {} 19 | 20 | $.verbose = true; 21 | } 22 | 23 | await $`yarn prisma migrate deploy`; 24 | -------------------------------------------------------------------------------- /scripts/start-cypress.mjs: -------------------------------------------------------------------------------- 1 | 2 | const startServer = `yarn test:start-server` 3 | const startCypress = "yarn cypress open" 4 | 5 | await $`concurrently ${startServer} ${startCypress}` 6 | -------------------------------------------------------------------------------- /scripts/test:migrate.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env zx 2 | 3 | const dotenvBin = await $`yarn bin dotenv` 4 | 5 | await $`${dotenvBin} -e .env.test -- yarn prisma migrate dev` 6 | 7 | -------------------------------------------------------------------------------- /sentry.client.config.js: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the browser. 2 | // The config you add here will be used whenever a page is visited. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from '@sentry/nextjs'; 6 | 7 | const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN; 8 | 9 | Sentry.init({ 10 | dsn: SENTRY_DSN, 11 | // Adjust this value in production, or use tracesSampler for greater control 12 | tracesSampleRate: 1.0, 13 | // ... 14 | // Note: if you want to override the automatic release value, do not set a 15 | // `release` value here - use the environment variable `SENTRY_RELEASE`, so 16 | // that it will also get attached to your source maps 17 | }); 18 | -------------------------------------------------------------------------------- /sentry.properties: -------------------------------------------------------------------------------- 1 | defaults.url=https://sentry.io/ 2 | defaults.org=basetool 3 | defaults.project=basetool 4 | cli.executable=../../.npm/_npx/89180/lib/node_modules/@sentry/wizard/node_modules/@sentry/cli/bin/sentry-cli 5 | -------------------------------------------------------------------------------- /sentry.server.config.js: -------------------------------------------------------------------------------- 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 | const SENTRY_DSN = process.env.SENTRY_DSN || process.env.NEXT_PUBLIC_SENTRY_DSN; 8 | 9 | Sentry.init({ 10 | dsn: SENTRY_DSN, 11 | // Adjust this value in production, or use tracesSampler for greater control 12 | tracesSampleRate: 1.0, 13 | // ... 14 | // Note: if you want to override the automatic release value, do not set a 15 | // `release` value here - use the environment variable `SENTRY_RELEASE`, so 16 | // that it will also get attached to your source maps 17 | }); 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext", "es5", "es2015.promise"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "baseUrl": ".", 17 | "paths": { 18 | "@/*": ["./*"], 19 | "@/styles/*": ["./styles/*"], 20 | } 21 | }, 22 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "tailwind.config.js", "postcss.config.js", "assets/colors.js"], 23 | "exclude": ["node_modules"] 24 | } 25 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | export type ArrayOrObject = [] | Record; 2 | 3 | declare global { 4 | interface Window { 5 | analytics?: { 6 | page: () => void; 7 | identify: (...args) => void; 8 | track: (...args) => void; 9 | }; 10 | FS?: any; 11 | } 12 | } 13 | 14 | export type FooterElements = { 15 | left?: ReactElement | string; 16 | center?: ReactElement | string; 17 | right?: ReactElement | string; 18 | } 19 | 20 | export type GenericEvent = { 21 | currentTarget: { 22 | value: string 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import 'next-auth' 2 | 3 | declare module "next-auth" { 4 | /** 5 | * Returned by `useSession`, `getSession` and received as a prop on the `Provider` React Context 6 | */ 7 | interface Session { 8 | user: { 9 | id: number 10 | email: string 11 | firstName: string 12 | lastName: string 13 | image: string 14 | name: string 15 | createdAt: number 16 | intercomUserHash 17 | } 18 | } 19 | } 20 | --------------------------------------------------------------------------------