From c6d65508b2d5cc1a4717aeed23077f5747889e8b Mon Sep 17 00:00:00 2001 From: Rishichandra Wawhal Date: Wed, 5 Apr 2023 14:27:19 +0530 Subject: [PATCH] [feature branch] EE Lite Trials PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8208 Co-authored-by: awjchen <13142944+awjchen@users.noreply.github.com> Co-authored-by: Vijay Prasanna <11921040+vijayprasanna13@users.noreply.github.com> Co-authored-by: Toan Nguyen <1615675+hgiasac@users.noreply.github.com> Co-authored-by: Abhijeet Khangarot <26903230+abhi40308@users.noreply.github.com> Co-authored-by: Solomon <24038+solomon-b@users.noreply.github.com> Co-authored-by: gneeri <10553562+gneeri@users.noreply.github.com> GitOrigin-RevId: 454ee0dea636da77e43810edb2f427137027956c --- cabal.project.freeze | 3 + dc-agents/dc-api-types/package.json | 2 +- dc-agents/dc-api-types/src/agent.openapi.json | 4 + dc-agents/dc-api-types/src/index.ts | 1 + .../dc-api-types/src/models/Capabilities.ts | 2 + .../dc-api-types/src/models/Licensing.ts | 7 + dc-agents/package-lock.json | 10 +- dc-agents/reference/package-lock.json | 4 +- dc-agents/reference/package.json | 2 +- dc-agents/sqlite/package-lock.json | 4 +- dc-agents/sqlite/package.json | 2 +- frontend/.vscode/settings.json | 3 +- .../dynamicDbRouting.e2e.cy.ts | 2 +- frontend/libs/console/legacy-ce/src/index.ts | 11 + .../console/legacy-ce/src/lib/Endpoints.ts | 4 + .../lib/components/Common/Common.module.scss | 2 - .../src/lib/components/Login/Login.tsx | 12 +- .../Services/Actions/Containers/Main.js | 8 +- .../Services/Actions/Landing/Main.js | 41 +- .../Services/Actions/Sidebar/LeftSidebar.js | 4 +- .../Services/AllowList/AllowListDetail.tsx | 28 +- .../GraphiQLWrapper/GraphiQLWrapper.js | 43 +- .../ApiExplorer/Security/SecurityTabs.tsx | 33 +- .../Services/ApiExplorer/TopNav.tsx | 8 +- .../components/Services/Data/DataRouter.js | 49 +- .../Data/DataSources/ConnectDatabase.tsx | 15 +- .../CreateDataSource/Neon/NeonConnect.tsx | 49 + .../CreateDataSource/Neon/index.tsx | 44 +- .../DataSources/CreateDataSource/index.tsx | 5 +- .../DataSources/DataSourceFormWrapper.tsx | 5 +- .../Data/DataSources/ReadReplicaForm.tsx | 125 ++- .../Services/Data/Schema/ManageDatabase.tsx | 337 ++++--- .../Services/Events/EventTriggers/Add/Add.tsx | 12 +- .../Events/EventTriggers/Add/CreateETForm.tsx | 6 +- .../EventTriggers/Common/AutoCleanupForm.tsx | 439 ++++---- .../Events/EventTriggers/Modify/Modify.tsx | 14 +- .../Services/Settings/About/About.stories.tsx | 71 ++ .../Services/Settings/About/About.tsx | 85 +- .../Services/Settings/About/EELicenseInfo.tsx | 110 ++ .../Services/Settings/About/LabelValue.tsx | 14 + .../Services/Settings/Sidebar.stories.tsx | 44 +- .../components/Services/Settings/Sidebar.tsx | 85 +- .../dataSources/services/cockroach/index.tsx | 2 +- .../dataSources/services/postgresql/index.tsx | 2 +- .../OASGenerator/OASGeneratorPage.tsx | 7 - .../AllowListSidebar/AllowListSidebar.tsx | 11 +- .../AllowListSidebarHeader.tsx | 11 +- .../Analytics/core/getAnalyticsAttributes.ts | 4 +- .../components/InitializeTelemetry.tsx | 16 + .../core/telemetry/htmlEvents.test.tsx | 237 +++++ .../Analytics/core/telemetry/htmlEvents.ts | 77 ++ .../src/lib/features/Analytics/index.ts | 1 + .../lib/features/ConnectDB/EditConnection.tsx | 67 +- .../ConnectDB/hooks/useAvailableDrivers.ts | 27 +- .../ConnectDatabase.route.tsx | 12 + .../ConnectDatabase.stories.tsx | 344 ++++++- .../ConnectDBRedesign/ConnectDatabase.tsx | 89 +- .../ConnectDbBody/ConnectDbBody.tsx | 44 + .../ConnectDBRedesign/ConnectDbBody/index.ts | 1 + .../ConnectDbBody/parts/Cloud.tsx | 52 + .../ConnectDbBody/parts/Oss.tsx | 6 + .../ConnectDbBody/parts/Pro.tsx | 28 + .../ConnectDbBody/parts/ProLite.tsx | 85 ++ .../ConnectDbBody/parts/index.ts | 4 + .../SelectDatabase.stories.tsx | 23 - .../ConnectDBRedesign/SelectDatabase.tsx | 76 -- .../ConnectBigQueryWidget.tsx | 84 +- .../utils/generateRequests.ts | 2 +- .../components/ConnectButton.tsx | 19 + .../components/ConnectDatabaseWrapper.tsx | 41 + .../ConnectGDCSourceWidget.tsx | 18 +- .../ConnectMssqlWidget/ConnectMssqlWidget.tsx | 127 ++- .../ConnectPostgresWidget.tsx | 229 +++-- .../DynamicDBRouting/DynamicDBRoutingForm.tsx | 2 +- .../parts/ReadReplicas.tsx | 13 +- .../ConnectUIContainer/ConnectUIContainer.tsx | 97 ++ .../components/ConnectUIContainer/index.ts | 1 + .../components/DatabaseLogo.tsx | 2 +- .../components/FancyRadioCards.stories.tsx | 56 ++ .../components/FancyRadioCards.tsx | 15 +- .../components/GraphQLCustomization/index.ts | 6 +- .../components/GraphQLCustomization/schema.ts | 4 +- .../utils/adaptResponse.ts | 4 +- .../utils/generateRequest.ts | 4 +- .../LimitedFeatureWrapper.tsx | 47 + .../ListConnectedDatabases.tsx | 10 +- .../ListConnectedDatabases/parts/Details.tsx | 2 +- .../SelectDatabase/SelectDatabase.stories.tsx | 40 - .../components/CopyableInputField.tsx | 67 -- .../components/DatabaseLogo.tsx | 17 - .../components/EETrialActive.tsx | 100 -- .../components/EETrialExpired.tsx | 31 - .../components/EETrialInactive.tsx | 35 - .../components/FancyRadioCards.tsx | 59 -- .../components/InformationCard.tsx | 25 - .../SelectDatabase/components/index.ts | 7 - .../components/SelectDatabase/databases.tsx | 66 -- .../graphics/db-logos/amazon.svg | 9 - .../graphics/db-logos/citus.svg | 9 - .../graphics/db-logos/cockroach.svg | 9 - .../graphics/db-logos/google.svg | 9 - .../graphics/db-logos/microsoft.svg | 9 - .../graphics/db-logos/postgres.svg | 9 - .../graphics/db-logos/snowflake.svg | 9 - .../components/SelectDatabase/index.ts | 1 - .../components/SelectDatabase/styles.ts | 1 - .../SetupConnector/SetupConnector.tsx | 49 + .../SetupConnector/hooks/useAgentForm.tsx | 104 ++ .../SetupConnector/hooks/useCommandForm.tsx | 52 + .../hooks/useSuperConnectorAgents.ts | 59 ++ .../parts/DockerConfigDialog.stories.tsx | 21 + .../parts/DockerConfigDialog.tsx | 68 ++ .../components/SetupConnector/utils.ts | 7 + .../ConnectDBRedesign/components/index.ts | 4 + .../features/ConnectDBRedesign/constants.ts | 39 + .../features/ConnectDBRedesign/databases.tsx | 40 - .../graphics/database-connect.svg | 0 .../graphics/db-logos/amazon.webp | Bin 0 -> 7250 bytes .../graphics/db-logos/citus.webp | Bin 0 -> 6988 bytes .../graphics/db-logos/cockroach.webp | Bin 0 -> 6892 bytes .../db-logos/default.svg} | 0 .../graphics/db-logos/google.webp | Bin 0 -> 3364 bytes .../graphics/db-logos/index.ts | 27 + .../graphics/db-logos/microsoft.webp | Bin 0 -> 890 bytes .../graphics/db-logos/mysql.webp | Bin 0 -> 6128 bytes .../graphics/db-logos/postgres.webp | Bin 0 -> 5498 bytes .../graphics/db-logos/snowflake.webp | Bin 0 -> 5092 bytes .../graphics/db-logos/sqlite.webp | Bin 0 -> 2618 bytes .../features/ConnectDBRedesign/hooks/index.ts | 6 + .../hooks/useConnectDatabaseDrivers.tsx | 68 ++ .../hooks/useEnvironmentState.ts | 31 + .../lib/features/ConnectDBRedesign/index.ts | 3 +- .../ConnectDBRedesign/mocks/data.mock.ts | 304 ++++++ .../ConnectDBRedesign/mocks/handlers.mock.ts | 214 +--- .../ConnectDatabaseSidebar.stories.tsx | 25 + .../prototypes/ConnectDatabaseSidebar.tsx | 172 ++++ .../lib/features/ConnectDBRedesign/styles.ts | 1 - .../lib/features/ConnectDBRedesign/types.d.ts | 8 + .../{components/SelectDatabase => }/utils.ts | 8 + .../src/lib/features/ControlPlane/client.ts | 2 +- .../ControlPlane/generatedGraphQLTypes.ts | 21 + .../getAllSourceKinds/getAllSourceKinds.ts | 1 + .../src/lib/features/DataSource/index.ts | 23 +- .../lib/features/DataSource/postgres/index.ts | 1 + .../src/lib/features/DataSource/types.ts | 5 +- .../EETrial/assets/eetrial-loading.svg | 69 ++ .../EETrial/assets/icon-opentelemetry.svg | 11 + .../EETrial/assets/icon-query-caching.png | Bin 0 -> 689 bytes .../features/EETrial/assets/logo-airbus.svg | 8 + .../EETrial/assets/logo-atlassian.svg | 19 + .../lib/features/EETrial/assets/logo-bbva.svg | 6 + .../features/EETrial/assets/logo-netlify.svg | 4 + .../lib/features/EETrial/assets/logo-pipe.svg | 9 + .../EETrial/assets/logo-prometheus.svg | 3 + .../assets/logo-university-virginia.svg | 7 + .../ActivateEEForm/Form/ConsentCheckbox.tsx | 72 ++ .../ActivateEEForm/Form/Form.stories.tsx | 66 ++ .../components/ActivateEEForm/Form/Form.tsx | 308 ++++++ .../components/ActivateEEForm/Form/index.ts | 2 + .../components/ActivateEEForm/Form/schema.ts | 46 + .../Form/useActivateEETrial.tsx | 73 ++ .../Form/useClientCredentialsPost.ts | 55 + .../Form/useRegisterEETrial.tsx | 84 ++ .../ActivateEEForm/FormWrapper.stories.tsx | 25 + .../components/ActivateEEForm/FormWrapper.tsx | 76 ++ .../SuccessScreen/SuccessScreen.stories.tsx | 19 + .../SuccessScreen/SuccessScreen.tsx | 74 ++ .../components/ActivateEEForm/index.ts | 1 + .../ApiSecurityTab/ApiSecuritySvg.tsx | 580 +++++++++++ .../ApiSecurityTab/ApiSecurityTab.stories.tsx | 90 ++ .../ApiSecurityTab/ApiSecurityTab.tsx | 72 ++ .../BenefitsView/BenefitsView.stories.tsx | 104 ++ .../components/BenefitsView/BenefitsView.tsx | 183 ++++ .../components/BenefitsView/ListHeader.tsx | 14 + .../components/BenefitsView/ListItem.tsx | 27 + .../BenefitsView/WithEEBenefits.stories.tsx | 44 + .../BenefitsView/WithEEBenefits.tsx | 56 ++ .../EETrial/components/BenefitsView/index.ts | 1 + .../EETrialCard/EETrialCard.stories.tsx | 187 ++++ .../components/EETrialCard/EETrialCard.tsx | 154 +++ .../ETAutoCleanupWrapper.stories.tsx | 88 ++ .../ETAutoCleanupWrapper.tsx | 44 + .../components/ETAutoCleanupWrapper/index.ts | 1 + .../EnableEEButton.stories.tsx | 24 + .../EnableEnterpriseButton/EnableEEButton.tsx | 58 ++ .../EnableEnterpriseButton/index.ts | 1 + .../ErrorMessage/ErrorMessage.stories.tsx | 13 + .../components/ErrorMessage/ErrorMessage.tsx | 23 + .../LoadingMessage/LoadingMessage.stories.tsx | 13 + .../LoadingMessage/LoadingMessage.tsx | 17 + .../MultipleAdminSecretsPage.stories.tsx | 67 ++ .../MultipleAdminSecretsPage.tsx | 70 ++ .../MultipleAdminSecretsSvg.tsx | 697 +++++++++++++ .../components/MultipleAdminSecrets/index.ts | 1 + .../MultipleJWTSecretsPage.stories.tsx | 71 ++ .../MultipleJWTSecretsPage.tsx | 70 ++ .../MultipleJWTSecretsSvg.tsx | 938 ++++++++++++++++++ .../components/MultipleJWTSecrets/index.ts | 1 + .../components/NavbarButton.stories.tsx | 106 ++ .../EETrial/components/NavbarButton.tsx | 175 ++++ .../SingleSignOn/SingleSignOnPage.stories.tsx | 63 ++ .../SingleSignOn/SingleSignOnPage.tsx | 65 ++ .../SingleSignOn/SingleSignOnSvg.tsx | 407 ++++++++ .../EETrial/components/SingleSignOn/index.ts | 1 + .../EETrial/components/WithEELiteAccess.tsx | 18 + .../EETrial/components/WithLicenseInfo.tsx | 16 + .../src/lib/features/EETrial/constants.ts | 54 + .../EETrial/hooks/useEELicenseInfo.ts | 21 + .../features/EETrial/hooks/useEELiteAccess.ts | 24 + .../src/lib/features/EETrial/index.ts | 20 + .../src/lib/features/EETrial/mocks/http.ts | 84 ++ .../EETrial/mocks/registration.mock.ts | 73 ++ .../src/lib/features/EETrial/types.ts | 60 ++ .../src/lib/features/EETrial/utils.test.ts | 77 ++ .../src/lib/features/EETrial/utils.ts | 120 +++ .../FeatureFlags/availableFeatureFlags.ts | 26 + .../ManageAgents/_test_/useAddAgent.spec.ts | 72 -- .../ManageAgents/components/AddAgentForm.tsx | 73 +- .../components/ManageAgentsTable.tsx | 2 +- .../ManageAgents/hooks/useAddAgent.ts | 190 +++- .../ManageAgents/hooks/useRemoveAgent.ts | 13 +- .../MetadataAPI/hooks/useMetadataMigration.ts | 65 +- .../OpenTelemetry/OpenTelemetry.stories.tsx | 65 +- .../OpenTelemetry/OpenTelemetry.tsx | 40 +- .../OpenTelemetryFeature.stories.tsx | 148 +++ .../OpenTelemetry/OpenTelemetryFeature.tsx | 6 +- .../OpenTelemetryProvider.tsx | 7 +- .../Prometheus/PrometheusSettings.stories.tsx | 32 +- .../PrometheusSettingsForm.stories.tsx | 27 + .../Prometheus/PrometheusSettingsForm.tsx | 44 +- .../src/lib/features/Prometheus/index.tsx | 16 +- .../QueryResponseCaching.stories.tsx | 56 ++ .../QueryResponseCaching.tsx | 87 ++ .../QueryResponseCachingSvg.tsx | 418 ++++++++ .../QueryResponseCaching/StatusBadge.tsx | 26 + .../QueryResponseCaching/StatusText.tsx | 27 + .../features/QueryResponseCaching/index.tsx | 1 + .../hasura-metadata-types/source/index.ts | 2 +- .../src/lib/graphics/HasuraEngineFlow.tsx | 903 ----------------- .../src/lib/graphics/database-connect.svg | 106 -- .../src/lib/graphics/db-logos/alloydb.svg | 1 - .../src/lib/graphics/db-logos/bigquery.svg | 1 - .../src/lib/graphics/db-logos/citus.svg | 1 - .../src/lib/graphics/db-logos/placeholder.svg | 4 - .../src/lib/graphics/db-logos/postgres.svg | 18 - .../src/lib/graphics/db-logos/sql-server.svg | 1 - .../legacy-ce/src/lib/graphics/index.ts | 1 - .../legacy-ce/src/lib/hooks/apiUtils.ts | 4 +- .../AdvancedDropDown/components/Root.tsx | 5 +- .../src/lib/new-components/Button/Button.tsx | 8 +- .../Collapsible/Collapsible.tsx | 55 +- .../Form/CopyableInputField.stories.tsx | 187 ++++ .../Form/CopyableInputField.tsx | 107 ++ .../lib/new-components/Form/FieldWrapper.tsx | 2 +- .../src/lib/new-components/Form/Input.tsx | 4 +- .../lib/new-components/Form/InputField.tsx | 22 +- .../src/lib/new-components/Form/index.ts | 3 +- .../Form/utils/filterDataAttributes.ts | 11 + .../IndicatorCard/IndicatorCard.tsx | 15 +- .../src/lib/shared/utils/sdlUtils.js | 2 +- .../legacy-ce/src/lib/telemetry/index.ts | 48 +- .../lib/utils/__tests__/proConsole.spec.ts | 4 +- .../legacy-ce/src/lib/utils/cloudConsole.ts | 4 + .../legacy-ce/src/lib/utils/getDataRoute.ts | 4 + .../console/legacy-ce/src/lib/utils/index.ts | 1 - .../legacy-ce/src/lib/utils/proConsole.ts | 26 +- .../legacy-ce/src/lib/utils/routeUtils.ts | 3 + .../legacy-ee/src/lib/components/Main/Main.js | 78 +- .../libs/console/legacy-ee/src/lib/routes.js | 25 + frontend/package-lock.json | 108 +- frontend/package.json | 7 +- frontend/tailwind.config.js | 3 + server/graphql-engine.cabal | 2 + .../src/Test/DataConnector/MetadataApiSpec.hs | 1 + .../DataConnector/API/V0/Capabilities.hs | 25 +- server/lib/dc-api/test/Main.hs | 15 +- server/lib/dc-api/test/Test/AgentClient.hs | 30 +- .../Backend/DataConnector/Mock/Server.hs | 3 +- .../test-harness/src/Harness/GraphqlEngine.hs | 2 + server/src-exec/Main.hs | 3 +- server/src-lib/Hasura/App.hs | 37 +- server/src-lib/Hasura/App/State.hs | 5 +- .../Backends/BigQuery/Instances/Transport.hs | 11 +- .../Backends/BigQuery/Instances/Types.hs | 2 + .../Backends/DataConnector/Adapter/Backend.hs | 2 + .../DataConnector/Adapter/Metadata.hs | 4 +- .../DataConnector/Adapter/Transport.hs | 76 +- .../Backends/DataConnector/Agent/Client.hs | 20 +- .../Hasura/Backends/MSSQL/Connection.hs | 8 +- .../Hasura/Backends/MSSQL/DDL/Source.hs | 4 +- .../Backends/MSSQL/Instances/Transport.hs | 11 +- .../Hasura/Backends/MSSQL/Instances/Types.hs | 2 + .../Backends/MySQL/Instances/Transport.hs | 8 +- .../Hasura/Backends/MySQL/Instances/Types.hs | 2 + .../Hasura/Backends/Postgres/Execute/Types.hs | 17 +- .../Backends/Postgres/Instances/Transport.hs | 11 +- .../Backends/Postgres/Instances/Types.hs | 2 + server/src-lib/Hasura/CredentialCache.hs | 19 + .../Hasura/GraphQL/Execute/RemoteJoin/Join.hs | 6 +- server/src-lib/Hasura/GraphQL/Explain.hs | 13 +- .../Hasura/GraphQL/Transport/Backend.hs | 5 + .../src-lib/Hasura/GraphQL/Transport/HTTP.hs | 25 +- .../Hasura/GraphQL/Transport/WSServerApp.hs | 7 +- .../Hasura/GraphQL/Transport/WebSocket.hs | 21 +- server/src-lib/Hasura/Metadata/Class.hs | 24 + .../src-lib/Hasura/RQL/DDL/DataConnector.hs | 2 +- .../src-lib/Hasura/RQL/DDL/Schema/Source.hs | 2 +- .../src-lib/Hasura/RQL/Types/CustomTypes.hs | 18 +- .../src-lib/Hasura/RQL/Types/EECredentials.hs | 60 ++ .../Hasura/RQL/Types/SourceConfiguration.hs | 7 + server/src-lib/Hasura/Server/App.hs | 52 +- server/src-lib/Hasura/Server/Auth.hs | 2 +- server/src-lib/Hasura/Server/Rest.hs | 7 +- server/src-lib/Hasura/Server/Types.hs | 2 + server/src-rsr/catalog_version.txt | 2 +- server/src-rsr/console.html | 2 +- server/src-rsr/initialise.sql | 4 +- server/src-rsr/migrations/47_to_48.sql | 3 + server/src-rsr/migrations/48_to_47.sql | 3 + .../DataConnector/API/V0/CapabilitiesSpec.hs | 4 + 320 files changed, 12275 insertions(+), 3296 deletions(-) create mode 100644 dc-agents/dc-api-types/src/models/Licensing.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataSources/CreateDataSource/Neon/NeonConnect.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/About/About.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/About/EELicenseInfo.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/About/LabelValue.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Analytics/core/telemetry/components/InitializeTelemetry.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Analytics/core/telemetry/htmlEvents.test.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/Analytics/core/telemetry/htmlEvents.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDatabase.route.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/ConnectDbBody.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/index.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/Cloud.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/Oss.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/Pro.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/ProLite.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/index.ts delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/SelectDatabase.stories.tsx delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/SelectDatabase.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectButton.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectDatabaseWrapper.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectUIContainer/index.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/FancyRadioCards.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/LimitedFeatureWrapper/LimitedFeatureWrapper.tsx delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/SelectDatabase.stories.tsx delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/CopyableInputField.tsx delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/DatabaseLogo.tsx delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/EETrialActive.tsx delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/EETrialExpired.tsx delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/EETrialInactive.tsx delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/FancyRadioCards.tsx delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/InformationCard.tsx delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/index.ts delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/databases.tsx delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/amazon.svg delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/citus.svg delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/cockroach.svg delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/google.svg delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/microsoft.svg delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/postgres.svg delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/snowflake.svg delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/index.ts delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/styles.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/SetupConnector.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/hooks/useAgentForm.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/hooks/useCommandForm.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/hooks/useSuperConnectorAgents.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/parts/DockerConfigDialog.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/parts/DockerConfigDialog.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/utils.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/index.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/constants.ts delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/databases.tsx rename frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/{components/SelectDatabase => }/graphics/database-connect.svg (100%) create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/amazon.webp create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/citus.webp create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/cockroach.webp rename frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/{components/SelectDatabase/graphics/db-logos/placeholder.svg => graphics/db-logos/default.svg} (100%) create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/google.webp create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/index.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/microsoft.webp create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/mysql.webp create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/postgres.webp create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/snowflake.webp create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/sqlite.webp create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/index.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useConnectDatabaseDrivers.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useEnvironmentState.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/mocks/data.mock.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/prototypes/ConnectDatabaseSidebar.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/prototypes/ConnectDatabaseSidebar.tsx delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/styles.ts rename frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/{components/SelectDatabase => }/utils.ts (54%) create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/eetrial-loading.svg create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/icon-opentelemetry.svg create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/icon-query-caching.png create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-airbus.svg create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-atlassian.svg create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-bbva.svg create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-netlify.svg create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-pipe.svg create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-prometheus.svg create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-university-virginia.svg create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/ConsentCheckbox.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/Form.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/Form.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/index.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/schema.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/useActivateEETrial.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/useClientCredentialsPost.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/useRegisterEETrial.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/FormWrapper.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/FormWrapper.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/SuccessScreen/SuccessScreen.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/SuccessScreen/SuccessScreen.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/index.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ApiSecurityTab/ApiSecuritySvg.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ApiSecurityTab/ApiSecurityTab.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ApiSecurityTab/ApiSecurityTab.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/BenefitsView.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/BenefitsView.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/ListHeader.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/ListItem.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/WithEEBenefits.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/WithEEBenefits.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/index.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EETrialCard/EETrialCard.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EETrialCard/EETrialCard.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ETAutoCleanupWrapper/ETAutoCleanupWrapper.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ETAutoCleanupWrapper/ETAutoCleanupWrapper.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ETAutoCleanupWrapper/index.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EnableEnterpriseButton/EnableEEButton.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EnableEnterpriseButton/EnableEEButton.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EnableEnterpriseButton/index.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ErrorMessage/ErrorMessage.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ErrorMessage/ErrorMessage.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/LoadingMessage/LoadingMessage.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/LoadingMessage/LoadingMessage.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleAdminSecrets/MultipleAdminSecretsPage.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleAdminSecrets/MultipleAdminSecretsPage.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleAdminSecrets/MultipleAdminSecretsSvg.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleAdminSecrets/index.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleJWTSecrets/MultipleJWTSecretsPage.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleJWTSecrets/MultipleJWTSecretsPage.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleJWTSecrets/MultipleJWTSecretsSvg.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleJWTSecrets/index.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/NavbarButton.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/NavbarButton.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/SingleSignOn/SingleSignOnPage.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/SingleSignOn/SingleSignOnPage.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/SingleSignOn/SingleSignOnSvg.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/SingleSignOn/index.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/WithEELiteAccess.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/WithLicenseInfo.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/constants.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/hooks/useEELicenseInfo.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/hooks/useEELiteAccess.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/index.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/mocks/http.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/mocks/registration.mock.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/types.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/utils.test.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/EETrial/utils.ts delete mode 100644 frontend/libs/console/legacy-ce/src/lib/features/ManageAgents/_test_/useAddAgent.spec.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetryFeature.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/QueryResponseCaching.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/QueryResponseCaching.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/QueryResponseCachingSvg.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/StatusBadge.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/StatusText.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/index.tsx delete mode 100644 frontend/libs/console/legacy-ce/src/lib/graphics/HasuraEngineFlow.tsx delete mode 100644 frontend/libs/console/legacy-ce/src/lib/graphics/database-connect.svg delete mode 100644 frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/alloydb.svg delete mode 100644 frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/bigquery.svg delete mode 100644 frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/citus.svg delete mode 100644 frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/placeholder.svg delete mode 100644 frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/postgres.svg delete mode 100644 frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/sql-server.svg delete mode 100644 frontend/libs/console/legacy-ce/src/lib/graphics/index.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/new-components/Form/CopyableInputField.stories.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/new-components/Form/CopyableInputField.tsx create mode 100644 frontend/libs/console/legacy-ce/src/lib/new-components/Form/utils/filterDataAttributes.ts create mode 100644 frontend/libs/console/legacy-ce/src/lib/utils/routeUtils.ts create mode 100644 server/src-lib/Hasura/CredentialCache.hs create mode 100644 server/src-lib/Hasura/RQL/Types/EECredentials.hs create mode 100644 server/src-rsr/migrations/47_to_48.sql create mode 100644 server/src-rsr/migrations/48_to_47.sql diff --git a/cabal.project.freeze b/cabal.project.freeze index e184c255da9..ff48380b457 100644 --- a/cabal.project.freeze +++ b/cabal.project.freeze @@ -91,6 +91,7 @@ constraints: any.Cabal ==3.6.3.0, any.cryptohash-md5 ==0.11.101.0, any.cryptohash-sha1 ==0.11.101.0, any.cryptonite ==0.30, + any.cryptostore ==0.3.0.0, any.data-binary-ieee754 ==0.4.4, any.data-bword ==0.1.0.2, any.data-checked ==0.3, @@ -197,6 +198,7 @@ constraints: any.Cabal ==3.6.3.0, any.iso8601-time ==0.1.5, any.isomorphism-class ==0.1.0.7, any.jose ==0.9, + any.jwt ==0.11.0, any.kan-extensions ==5.2.5, any.keys ==3.12.3, any.kriti-lang ==0.3.3, @@ -335,6 +337,7 @@ constraints: any.Cabal ==3.6.3.0, any.tasty-bench ==0.3.2, any.template-haskell ==2.18.0.0, any.template-haskell-compat-v0208 ==0.1.9.1, + any.temporary ==1.3, any.terminal-size ==0.3.3, any.terminfo ==0.4.1.5, any.text ==1.2.5.0, diff --git a/dc-agents/dc-api-types/package.json b/dc-agents/dc-api-types/package.json index 6adb17681de..73925f78c63 100644 --- a/dc-agents/dc-api-types/package.json +++ b/dc-agents/dc-api-types/package.json @@ -1,6 +1,6 @@ { "name": "@hasura/dc-api-types", - "version": "0.28.0", + "version": "0.29.0", "description": "Hasura GraphQL Engine Data Connector Agent API types", "author": "Hasura (https://github.com/hasura/graphql-engine)", "license": "Apache-2.0", diff --git a/dc-agents/dc-api-types/src/agent.openapi.json b/dc-agents/dc-api-types/src/agent.openapi.json index 2bb943bc275..1a0c43f4a2f 100644 --- a/dc-agents/dc-api-types/src/agent.openapi.json +++ b/dc-agents/dc-api-types/src/agent.openapi.json @@ -507,6 +507,9 @@ "explain": { "$ref": "#/components/schemas/ExplainCapabilities" }, + "licensing": { + "$ref": "#/components/schemas/Licensing" + }, "metrics": { "$ref": "#/components/schemas/MetricsCapabilities" }, @@ -703,6 +706,7 @@ "ExplainCapabilities": {}, "RawCapabilities": {}, "DatasetCapabilities": {}, + "Licensing": {}, "ConfigSchemaResponse": { "nullable": false, "properties": { diff --git a/dc-agents/dc-api-types/src/index.ts b/dc-agents/dc-api-types/src/index.ts index e8f98088ee1..8e37911d036 100644 --- a/dc-agents/dc-api-types/src/index.ts +++ b/dc-agents/dc-api-types/src/index.ts @@ -56,6 +56,7 @@ export type { GraphQLType } from './models/GraphQLType'; export type { InsertCapabilities } from './models/InsertCapabilities'; export type { InsertFieldSchema } from './models/InsertFieldSchema'; export type { InsertMutationOperation } from './models/InsertMutationOperation'; +export type { Licensing } from './models/Licensing'; export type { MetricsCapabilities } from './models/MetricsCapabilities'; export type { MutationCapabilities } from './models/MutationCapabilities'; export type { MutationOperation } from './models/MutationOperation'; diff --git a/dc-agents/dc-api-types/src/models/Capabilities.ts b/dc-agents/dc-api-types/src/models/Capabilities.ts index a99344399b7..7ff55d88d5c 100644 --- a/dc-agents/dc-api-types/src/models/Capabilities.ts +++ b/dc-agents/dc-api-types/src/models/Capabilities.ts @@ -6,6 +6,7 @@ import type { ComparisonCapabilities } from './ComparisonCapabilities'; import type { DataSchemaCapabilities } from './DataSchemaCapabilities'; import type { DatasetCapabilities } from './DatasetCapabilities'; import type { ExplainCapabilities } from './ExplainCapabilities'; +import type { Licensing } from './Licensing'; import type { MetricsCapabilities } from './MetricsCapabilities'; import type { MutationCapabilities } from './MutationCapabilities'; import type { QueryCapabilities } from './QueryCapabilities'; @@ -19,6 +20,7 @@ export type Capabilities = { data_schema?: DataSchemaCapabilities; datasets?: DatasetCapabilities; explain?: ExplainCapabilities; + licensing?: Licensing; metrics?: MetricsCapabilities; mutations?: MutationCapabilities; queries?: QueryCapabilities; diff --git a/dc-agents/dc-api-types/src/models/Licensing.ts b/dc-agents/dc-api-types/src/models/Licensing.ts new file mode 100644 index 00000000000..d732bd784f9 --- /dev/null +++ b/dc-agents/dc-api-types/src/models/Licensing.ts @@ -0,0 +1,7 @@ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ + +export type Licensing = { +}; + diff --git a/dc-agents/package-lock.json b/dc-agents/package-lock.json index 5906e66ea69..86024f7ef82 100644 --- a/dc-agents/package-lock.json +++ b/dc-agents/package-lock.json @@ -24,7 +24,7 @@ }, "dc-api-types": { "name": "@hasura/dc-api-types", - "version": "0.28.0", + "version": "0.29.0", "license": "Apache-2.0", "devDependencies": { "@tsconfig/node16": "^1.0.3", @@ -2227,7 +2227,7 @@ "license": "Apache-2.0", "dependencies": { "@fastify/cors": "^8.1.0", - "@hasura/dc-api-types": "0.28.0", + "@hasura/dc-api-types": "0.29.0", "fastify": "^4.13.0", "mathjs": "^11.0.0", "pino-pretty": "^8.0.0", @@ -2547,7 +2547,7 @@ "license": "Apache-2.0", "dependencies": { "@fastify/cors": "^8.1.0", - "@hasura/dc-api-types": "0.28.0", + "@hasura/dc-api-types": "0.29.0", "fastify": "^4.13.0", "fastify-metrics": "^9.2.1", "nanoid": "^3.3.4", @@ -2868,7 +2868,7 @@ "version": "file:reference", "requires": { "@fastify/cors": "^8.1.0", - "@hasura/dc-api-types": "0.28.0", + "@hasura/dc-api-types": "0.29.0", "@tsconfig/node16": "^1.0.3", "@types/node": "^16.11.49", "@types/xml2js": "^0.4.11", @@ -3080,7 +3080,7 @@ "version": "file:sqlite", "requires": { "@fastify/cors": "^8.1.0", - "@hasura/dc-api-types": "0.28.0", + "@hasura/dc-api-types": "0.29.0", "@tsconfig/node16": "^1.0.3", "@types/node": "^16.11.49", "@types/sqlite3": "^3.1.8", diff --git a/dc-agents/reference/package-lock.json b/dc-agents/reference/package-lock.json index 5a247f84a48..2aa34dd1deb 100644 --- a/dc-agents/reference/package-lock.json +++ b/dc-agents/reference/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@fastify/cors": "^8.1.0", - "@hasura/dc-api-types": "0.28.0", + "@hasura/dc-api-types": "0.29.0", "fastify": "^4.13.0", "mathjs": "^11.0.0", "pino-pretty": "^8.0.0", @@ -52,7 +52,7 @@ "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" }, "node_modules/@hasura/dc-api-types": { - "version": "0.28.0", + "version": "0.29.0", "license": "Apache-2.0", "devDependencies": { "@tsconfig/node16": "^1.0.3", diff --git a/dc-agents/reference/package.json b/dc-agents/reference/package.json index 6351d8ad4a8..d5ad82e71aa 100644 --- a/dc-agents/reference/package.json +++ b/dc-agents/reference/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@fastify/cors": "^8.1.0", - "@hasura/dc-api-types": "0.28.0", + "@hasura/dc-api-types": "0.29.0", "fastify": "^4.13.0", "mathjs": "^11.0.0", "pino-pretty": "^8.0.0", diff --git a/dc-agents/sqlite/package-lock.json b/dc-agents/sqlite/package-lock.json index 21f2385af7b..5e2c0878c7d 100644 --- a/dc-agents/sqlite/package-lock.json +++ b/dc-agents/sqlite/package-lock.json @@ -10,7 +10,7 @@ "license": "Apache-2.0", "dependencies": { "@fastify/cors": "^8.1.0", - "@hasura/dc-api-types": "0.28.0", + "@hasura/dc-api-types": "0.29.0", "fastify": "^4.13.0", "fastify-metrics": "^9.2.1", "nanoid": "^3.3.4", @@ -57,7 +57,7 @@ "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" }, "node_modules/@hasura/dc-api-types": { - "version": "0.28.0", + "version": "0.29.0", "license": "Apache-2.0", "devDependencies": { "@tsconfig/node16": "^1.0.3", diff --git a/dc-agents/sqlite/package.json b/dc-agents/sqlite/package.json index e94e1be08ec..df2b2f9355a 100644 --- a/dc-agents/sqlite/package.json +++ b/dc-agents/sqlite/package.json @@ -22,7 +22,7 @@ }, "dependencies": { "@fastify/cors": "^8.1.0", - "@hasura/dc-api-types": "0.28.0", + "@hasura/dc-api-types": "0.29.0", "fastify-metrics": "^9.2.1", "fastify": "^4.13.0", "nanoid": "^3.3.4", diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json index 985315f7abf..fa737f3bb61 100644 --- a/frontend/.vscode/settings.json +++ b/frontend/.vscode/settings.json @@ -13,5 +13,6 @@ // adding this setting per this discussion on github: // https://github.com/nrwl/nx/issues/9465#issuecomment-1080093295 - "typescript.preferences.importModuleSpecifier": "project-relative" + "typescript.preferences.importModuleSpecifier": "project-relative", + "git.ignoreLimitWarning": true } diff --git a/frontend/apps/console-ee-e2e/src/e2e/data/dynamicDbRouting/dynamicDbRouting.e2e.cy.ts b/frontend/apps/console-ee-e2e/src/e2e/data/dynamicDbRouting/dynamicDbRouting.e2e.cy.ts index 8e29a5c65ff..8556d8ba331 100644 --- a/frontend/apps/console-ee-e2e/src/e2e/data/dynamicDbRouting/dynamicDbRouting.e2e.cy.ts +++ b/frontend/apps/console-ee-e2e/src/e2e/data/dynamicDbRouting/dynamicDbRouting.e2e.cy.ts @@ -1,7 +1,7 @@ import type { Metadata } from '@hasura/console-legacy-ce'; import { readMetadata } from '../../../utils/checkMetadataPayload'; -describe('Dynamic Db Routing', () => { +xdescribe('Dynamic Db Routing', () => { before(() => { cy.visit('/data/manage/connect/'); diff --git a/frontend/libs/console/legacy-ce/src/index.ts b/frontend/libs/console/legacy-ce/src/index.ts index b526d77fab4..ee1796704e3 100644 --- a/frontend/libs/console/legacy-ce/src/index.ts +++ b/frontend/libs/console/legacy-ce/src/index.ts @@ -69,10 +69,17 @@ export { addUserProperties, programmaticallyTraceError, REDACT_EVERYTHING, + InitializeTelemetry, } from './lib/features/Analytics'; export { CloudOnboarding } from './lib/features/CloudOnboarding'; export { prefetchSurveysData } from './lib/features/Surveys'; export { prefetchOnboardingData } from './lib/features/CloudOnboarding/OnboardingWizard'; +export { + prefetchEELicenseInfo, + NavbarButton as EntepriseNavbarButton, + WithEELiteAccess, + useEELiteAccess, +} from './lib/features/EETrial'; export { default as PageNotFound } from './lib/components/Error/PageNotFound'; export * from './lib/new-components/Button/'; export * from './lib/new-components/Tooltip/'; @@ -130,6 +137,10 @@ export { export { ReactQueryProvider, reactQueryClient } from './lib/lib/reactQuery'; export { PrometheusSettings } from './lib/features/Prometheus'; +export { QueryResponseCaching } from './lib/features/QueryResponseCaching'; +export { MultipleAdminSecretsPage } from './lib/features/EETrial'; +export { MultipleJWTSecretsPage } from './lib/features/EETrial'; +export { SingleSignOnPage } from './lib/features/EETrial'; export { OpenTelemetryFeature } from './lib/features/OpenTelemetry'; diff --git a/frontend/libs/console/legacy-ce/src/lib/Endpoints.ts b/frontend/libs/console/legacy-ce/src/lib/Endpoints.ts index 6926970bdd9..10927d807c3 100644 --- a/frontend/libs/console/legacy-ce/src/lib/Endpoints.ts +++ b/frontend/libs/console/legacy-ce/src/lib/Endpoints.ts @@ -13,6 +13,8 @@ export const getEndpoints = (globals: typeof consoleGlobals) => { graphQLUrl: `${baseUrl}/v1/graphql`, relayURL: `${baseUrl}/v1beta1/relay`, query: `${baseUrl}/v2/query`, + entitlement: `${baseUrl}/v1/entitlement`, + license: `${baseUrl}/v1/entitlement/license`, metadata: `${baseUrl}/v1/metadata`, // metadata: `${baseUrl}/v1/query`, queryV2: `${baseUrl}/v2/query`, @@ -30,6 +32,8 @@ export const getEndpoints = (globals: typeof consoleGlobals) => { globals.luxDataHost }/v1/graphql`, prometheusUrl: `${baseUrl}/v1/metrics`, + registerEETrial: `https://licensing.pro.hasura.io/v1/graphql`, + // registerEETrial: `http://licensing.lux-dev.hasura.me/v1/graphql`, }; return endpoints; diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Common/Common.module.scss b/frontend/libs/console/legacy-ce/src/lib/components/Common/Common.module.scss index a5e98fd5778..85c8b7aeff6 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Common/Common.module.scss +++ b/frontend/libs/console/legacy-ce/src/lib/components/Common/Common.module.scss @@ -1561,8 +1561,6 @@ code { margin-bottom: 2px; align-self: flex-end; padding-top: 8px; - border-color: rgba(0, 0, 0, 0.1); - border-top-width: 1px; position: sticky; bottom: 0; background-color: #f8fafb; diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Login/Login.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Login/Login.tsx index 1a7275fdfe1..e708573741d 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Login/Login.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/components/Login/Login.tsx @@ -16,6 +16,7 @@ import { verifyLogin } from './Actions'; import { CLI_CONSOLE_MODE } from '../../constants'; import { getAdminSecret } from '../Services/ApiExplorer/ApiRequest/utils'; import { ConnectInjectedProps } from '../../types'; +import { isProConsole } from '../../utils/proConsole'; import hasuraLogo from './black-logo.svg'; import hasuraEELogo from './black-logo-ee.svg'; @@ -149,12 +150,11 @@ const Login: React.FC = ({ dispatch, children }) => { ); }; - const showLogo = - globals.consoleType === 'pro' || globals.consoleType === 'pro-lite' ? ( - Hasura EE - ) : ( - Hasura - ); + const showLogo = isProConsole(globals) ? ( + Hasura EE + ) : ( + Hasura + ); return (
diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Containers/Main.js b/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Containers/Main.js index 86c4354b0dc..1e23d3f868c 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Containers/Main.js +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Containers/Main.js @@ -13,7 +13,7 @@ class Container extends React.Component { render() { const { children } = this.props; - const currentLocation = location.pathname; + const currentLocation = window.location.pathname; const sidebarContent = (
    @@ -26,7 +26,11 @@ class Container extends React.Component { Manage - +
  • Actions

    {getAddBtn()} - {isImportFromOpenAPIEnabled(window.__env) && ( - + - - )} + Import from OpenAPI + + New + + +

{getIntroSection()} diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Sidebar/LeftSidebar.js b/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Sidebar/LeftSidebar.js index 9b5bb8cd551..dae46e5af4f 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Sidebar/LeftSidebar.js +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Actions/Sidebar/LeftSidebar.js @@ -6,7 +6,6 @@ import { browserHistory, Link } from 'react-router'; import LeftSubSidebar from '../../../Common/Layout/LeftSubSidebar/LeftSubSidebar'; import styles from '../../../Common/Layout/LeftSubSidebar/LeftSubSidebar.module.scss'; -import { isProConsole } from '../../../../utils'; import { Badge } from '../../../../new-components/Badge'; import globals from '../../../../Globals'; @@ -15,6 +14,7 @@ const LeftSidebar = ({ common: { currentAction }, actions, readOnlyMode, + allowOpenApiImport, }) => { const [searchText, setSearchText] = React.useState(''); @@ -109,7 +109,7 @@ const LeftSidebar = ({ addTestString={'actions-sidebar-add-table'} childListTestString={'actions-table-links'} addBtn={ - isProConsole(window.__env) ? ( + allowOpenApiImport ? (
diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/AllowList/AllowListDetail.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Services/AllowList/AllowListDetail.tsx index 6ad435eb9fe..5c0664d8e93 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/AllowList/AllowListDetail.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/AllowList/AllowListDetail.tsx @@ -13,9 +13,11 @@ import { AllowListSidebar, AllowListPermissions, } from '../../../features/AllowLists'; +import { EETrialCard, useEELiteAccess } from '../../../features/EETrial'; import PageContainer from '../../Common/Layout/PageContainer/PageContainer'; import { isProConsole } from '../../../utils/proConsole'; +import globals from '../../../Globals'; interface AllowListDetailProps { params: { @@ -33,7 +35,7 @@ export const pushUrl = (name: string, section: string) => { export const AllowListDetail: React.FC = props => { const { name, section } = props.params; - + const { access: eeLiteAccess } = useEELiteAccess(globals); const { data: queryCollections, isLoading, @@ -54,6 +56,11 @@ export const AllowListDetail: React.FC = props => { pushUrl(queryCollections[0].name, section ?? 'operations'); } + const isFeatureActive = isProConsole(globals) || eeLiteAccess === 'active'; + const isFeatureSupported = + isProConsole(globals) || eeLiteAccess !== 'forbidden'; + const isEELiteContext = eeLiteAccess !== 'forbidden'; + return (
@@ -90,7 +97,7 @@ export const AllowListDetail: React.FC = props => { />
)} - {isProConsole(window.__env) ? ( + {isFeatureSupported ? ( { @@ -111,7 +118,22 @@ export const AllowListDetail: React.FC = props => { label: 'Permissions', content: (
- + {isFeatureActive ? ( + + ) : ( + isEELiteContext && ( +
+ +
+ ) + )}
), }, diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/ApiExplorer/GraphiQLWrapper/GraphiQLWrapper.js b/frontend/libs/console/legacy-ce/src/lib/components/Services/ApiExplorer/GraphiQLWrapper/GraphiQLWrapper.js index 76ca1afe337..ae6fcee05bd 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/ApiExplorer/GraphiQLWrapper/GraphiQLWrapper.js +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/ApiExplorer/GraphiQLWrapper/GraphiQLWrapper.js @@ -25,6 +25,7 @@ import { import { showErrorNotification } from '../../Common/Notification'; import ToolTip from '../../../Common/Tooltip/Tooltip'; import { getActionsCreateRoute } from '../../../Common/utils/routesUtils'; +import { getQueryResponseCachingRoute } from '../../../../utils/routeUtils'; import { getConfirmation } from '../../../Common/utils/jsUtils'; import { setActionDefinition, @@ -33,7 +34,9 @@ import { } from '../../Actions/Add/reducer'; import { getGraphQLEndpoint } from '../utils'; import snippets from './snippets'; -import { canAccessCacheButton } from '../../../../utils/permissions'; +import globals from '../../../../Globals'; +import { WithEELiteAccess } from '../../../../features/EETrial'; +import { isProConsole } from '../../../../utils/proConsole'; import './GraphiQL.css'; import _push from '../../Data/push'; @@ -188,10 +191,18 @@ class GraphiQLWrapper extends Component { const _toggleCacheDirective = () => { trackGraphiQlToolbarButtonClick('Cache'); - const editor = graphiqlContext.getQueryEditor(); - const operationString = editor.getValue(); - const cacheToggledOperationString = toggleCacheDirective(operationString); - editor.setValue(cacheToggledOperationString); + try { + const editor = graphiqlContext.getQueryEditor(); + const operationString = editor.getValue(); + const cacheToggledOperationString = + toggleCacheDirective(operationString); + editor.setValue(cacheToggledOperationString); + } catch { + // throw a generic error + throw new Error( + 'Caching directives can only be added to valid GraphQL queries.' + ); + } }; const renderGraphiqlFooter = responseTime && @@ -237,7 +248,7 @@ class GraphiQLWrapper extends Component { } // get toolbar buttons - const getGraphiqlButtons = () => { + const getGraphiqlButtons = eeLiteAccess => { const routeToREST = createRouteToREST(graphiqlProps); const buttons = [ @@ -262,8 +273,18 @@ class GraphiQLWrapper extends Component { { label: 'Cache', title: 'Cache the response of this query', - onClick: _toggleCacheDirective, - hide: !canAccessCacheButton(), + onClick: () => { + if (eeLiteAccess === 'active' || isProConsole(globals)) { + // toggle cache directive only if it is cloud/ee-classic/ee-lite-active + _toggleCacheDirective(); + } else { + // send to the query-response-caching page if the EE lite trial isn't active + if (eeLiteAccess !== 'forbidden') { + dispatch(_push(getQueryResponseCachingRoute())); + } + } + }, + hide: !isProConsole(globals) && eeLiteAccess === 'forbidden', }, { label: 'Code Exporter', @@ -306,7 +327,11 @@ class GraphiQLWrapper extends Component { > GraphiQL - {getGraphiqlButtons()} + + {({ access: eeLiteAccess }) => { + return getGraphiqlButtons(eeLiteAccess); + }} + = ({ return ( -
- -
{children}
-
+ +
+ +
{children}
+
+
); }; diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/ApiExplorer/TopNav.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Services/ApiExplorer/TopNav.tsx index c943dcdaeb7..bf055f9d495 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/ApiExplorer/TopNav.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/ApiExplorer/TopNav.tsx @@ -1,12 +1,16 @@ import React from 'react'; import { Link, RouteComponentProps } from 'react-router'; -import { canAccessSecuritySettings } from '../../../utils/permissions'; +import { isProConsole } from '../../../utils/proConsole'; +import { useEELiteAccess } from '../../../features/EETrial'; +import globals from '../../../Globals'; type TopNavProps = { location: RouteComponentProps['location']; }; const TopNav: React.FC = ({ location }) => { + const { access: eeLiteAccess } = useEELiteAccess(globals); + const sectionsData = [ [ { @@ -32,7 +36,7 @@ const TopNav: React.FC = ({ location }) => { ], ]; - if (canAccessSecuritySettings()) { + if (isProConsole(globals) || eeLiteAccess !== 'forbidden') { sectionsData[1].push({ key: 'security', link: '/api/security/api_limits', diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataRouter.js b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataRouter.js index 7710c6b280f..a6190085d4e 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataRouter.js +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataRouter.js @@ -1,41 +1,41 @@ -import React from 'react'; -import { Route, IndexRedirect } from 'react-router'; +import { IndexRedirect, Route } from 'react-router'; -import globals from '../../../Globals'; import { SERVER_CONSOLE_MODE } from '../../../constants'; +import globals from '../../../Globals'; import { - schemaConnector, - rawSQLConnector, addExistingTableViewConnector, addTableConnector, + ConnectedCreateDataSourcePage, + ConnectedDatabaseManagePage, + dataPageConnector, + FunctionPermissions, + functionWrapperConnector, + migrationsConnector, + ModifyCustomFunction, modifyViewConnector, + permissionsConnector, + permissionsSummaryConnector, + rawSQLConnector, relationshipsConnector, relationshipsViewConnector, - permissionsConnector, - dataPageConnector, - migrationsConnector, - functionWrapperConnector, - permissionsSummaryConnector, - ModifyCustomFunction, - FunctionPermissions, - ConnectedDatabaseManagePage, - ConnectedCreateDataSourcePage, + schemaConnector, } from '.'; - +import { Connect } from '../../../features/ConnectDB'; +import { ConnectUIContainer } from '../../../features/ConnectDBRedesign'; +import { ConnectDatabaseRouteWrapper } from '../../../features/ConnectDBRedesign/ConnectDatabase.route'; +import { ManageDatabaseContainer } from '../../../features/Data'; +import { ManageTable } from '../../../features/Data/ManageTable'; +import { setDriver } from '../../../dataSources'; import { exportMetadata } from '../../../metadata/actions'; +import { getSourcesFromMetadata } from '../../../metadata/selector'; +import { UPDATE_CURRENT_DATA_SOURCE } from './DataActions'; import ConnectedDataSourceContainer from './DataSourceContainer'; import ConnectDatabase from './DataSources/ConnectDatabase'; -import { setDriver } from '../../../dataSources'; -import { UPDATE_CURRENT_DATA_SOURCE } from './DataActions'; -import { getSourcesFromMetadata } from '../../../metadata/selector'; -import { ManageDatabaseContainer } from '../../../features/Data'; -import { Connect } from '../../../features/ConnectDB'; +import { TableBrowseRowsContainer } from './TableBrowseRows/TableBrowseRowsContainer'; +import { TableEditItemContainer } from './TableEditItem/TableEditItemContainer'; import { TableInsertItemContainer } from './TableInsertItem/TableInsertItemContainer'; import { ModifyTableContainer } from './TableModify/ModifyTableContainer'; -import { TableEditItemContainer } from './TableEditItem/TableEditItemContainer'; -import { TableBrowseRowsContainer } from './TableBrowseRows/TableBrowseRowsContainer'; -import { ManageTable } from '../../../features/Data/ManageTable'; const makeDataRouter = ( connect, @@ -56,6 +56,9 @@ const makeDataRouter = ( + + + diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataSources/ConnectDatabase.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataSources/ConnectDatabase.tsx index d7a95f11f4e..28628c7ac2e 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataSources/ConnectDatabase.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataSources/ConnectDatabase.tsx @@ -7,8 +7,9 @@ import React, { useEffect, } from 'react'; import { connect, ConnectedProps } from 'react-redux'; -import { canAccessReadReplica } from '../../../../utils/permissions'; import { isPostgres } from '../../../../metadata/dataSource.utils'; +import { useEELiteAccess } from '../../../../features/EETrial'; +import globals from '../../../../Globals'; import Tabbed from './TabbedDataSourceConnection'; import { ReduxState } from '../../../../types'; @@ -39,6 +40,7 @@ import { getSupportedDrivers } from '../../../../dataSources'; import { Tabs } from '../../../../new-components/Tabs'; import { DynamicDBRouting } from '../../../../features/ConnectDBRedesign/components/ConnectPostgresWidget/parts/DynamicDBRouting'; import { isDynamicDBRoutingEnabled } from '../../../../utils/proConsole'; +import { canAccessReadReplica as isReadReplicaAccessible } from '../../../../utils'; type ConnectDatabaseProps = InjectedProps; @@ -50,6 +52,13 @@ const ConnectDatabase: React.FC = props => { getDefaultState(props) ); + const { access: eeLiteAccess } = useEELiteAccess(globals); + + // the first case is only for pro/cloud console. the second expression is for pro-lite + const canAccessReadReplica = + isReadReplicaAccessible(connectDBInputState.dbType) || + (eeLiteAccess !== 'forbidden' && connectDBInputState.dbType !== 'bigquery'); + const [connectionType, changeConnectionType] = useState( props.dbConnection.envVar ? connectionTypes.ENV_VAR @@ -404,7 +413,7 @@ const ConnectDatabase: React.FC = props => { {getSupportedDrivers('connectDbForm.read_replicas.edit').includes( connectDBInputState.dbType ) && - canAccessReadReplica(connectDBInputState.dbType) && ( + canAccessReadReplica && ( = props => { {getSupportedDrivers('connectDbForm.read_replicas.create').includes( connectDBInputState.dbType ) && - canAccessReadReplica(connectDBInputState.dbType) && ( + canAccessReadReplica && ( { + // on success, refetch queries to show neon dashboard link in connect database page, + // overriding the stale time + reactQueryClient.refetchQueries(FETCH_NEON_PROJECTS_BY_PROJECTID_QUERYKEY); + + dispatch(_push(`/data/${dataSourceName}/schema/public`)); + }; + const pushToConnectDBPage = () => { + dispatch(_push(connectDbUrl)); + }; + + const neonIntegrationStatus = useNeonIntegration( + getNeonDBName(allDatabases), + pushToDataSource, + pushToConnectDBPage, + dispatch, + 'data-manage-create' + ); + + const neonBannerProps = transformNeonIntegrationStatusToNeonBannerProps( + neonIntegrationStatus + ); + + return ; +} diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataSources/CreateDataSource/Neon/index.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataSources/CreateDataSource/Neon/index.tsx index 68acd5e75cc..d9ddcd6a400 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataSources/CreateDataSource/Neon/index.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataSources/CreateDataSource/Neon/index.tsx @@ -1,44 +1,2 @@ -import * as React from 'react'; -import { Dispatch } from '../../../../../../types'; -import { reactQueryClient } from '../../../../../../lib/reactQuery'; -import { NeonBanner } from './components/Neon/NeonBanner'; -import { - getNeonDBName, - transformNeonIntegrationStatusToNeonBannerProps, -} from './utils'; -import { useNeonIntegration } from './useNeonIntegration'; -import _push from '../../../push'; -import { FETCH_NEON_PROJECTS_BY_PROJECTID_QUERYKEY } from './components/NeonDashboardLink'; - -// This component deals with Neon DB creation on connect DB page -export function Neon(props: { allDatabases: string[]; dispatch: Dispatch }) { - const { dispatch, allDatabases } = props; - - // success callback - const pushToDatasource = (dataSourceName: string) => { - // on success, refetch queries to show neon dashboard link in connect database page, - // overriding the stale time - reactQueryClient.refetchQueries(FETCH_NEON_PROJECTS_BY_PROJECTID_QUERYKEY); - - dispatch(_push(`/data/${dataSourceName}/schema/public`)); - }; - const pushToConnectDBPage = () => { - dispatch(_push(`/data/manage/connect`)); - }; - - const neonIntegrationStatus = useNeonIntegration( - getNeonDBName(allDatabases), - pushToDatasource, - pushToConnectDBPage, - dispatch, - 'data-manage-create' - ); - - const neonBannerProps = transformNeonIntegrationStatusToNeonBannerProps( - neonIntegrationStatus - ); - - return ; -} - +export { NeonConnect } from './NeonConnect'; export { useNeonIntegration } from './useNeonIntegration'; diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataSources/CreateDataSource/index.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataSources/CreateDataSource/index.tsx index 5990c2a8871..81ad8ef2f3f 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataSources/CreateDataSource/index.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataSources/CreateDataSource/index.tsx @@ -12,7 +12,7 @@ import { mapDispatchToPropsEmpty } from '../../../../Common/utils/reactUtils'; import Tabbed from '../TabbedDataSourceConnection'; import { NotFoundError } from '../../../../Error/PageNotFound'; import { getDataSources } from '../../../../../metadata/selector'; -import { Neon } from './Neon'; +import { NeonConnect } from './Neon'; type Props = InjectedProps; @@ -28,7 +28,7 @@ const CreateDataSource: React.FC = ({ dispatch, allDataSources }) => {
- d.name)} dispatch={dispatch} /> @@ -52,3 +52,4 @@ const connector = connect(mapStateToProps, mapDispatchToPropsEmpty); type InjectedProps = ConnectedProps; const ConnectedCreateDataSourcePage = connector(CreateDataSource); export default ConnectedCreateDataSourcePage; +export { NeonConnect }; diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataSources/DataSourceFormWrapper.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataSources/DataSourceFormWrapper.tsx index cd6afb0c89a..d7050cf89ae 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataSources/DataSourceFormWrapper.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/DataSources/DataSourceFormWrapper.tsx @@ -110,7 +110,7 @@ const DataSourceFormWrapper: React.FC = props => {
{!isReadReplica && ( @@ -154,12 +154,13 @@ const DataSourceFormWrapper: React.FC = props => { {children} -
+
- - ) : ( - - )} -
- - - - -
-
+ { + // eslint-disable-next-line no-underscore-dangle + eeLiteAccess !== 'forbidden' || isProConsole(globals) ? ( + + + +
+

+ Hasura can load balance queries and subscriptions across + read replicas while sending all mutations and metadata API + calls to the master. + +

+ {readReplicaState.map((stateVar, index) => ( + + ))} + {eeLiteAccess === 'eligible' || + eeLiteAccess === 'expired' || + eeLiteAccess === 'deactivated' ? ( + + Scale your database by offloading read queries to + read-only replicas, allowing for better performance + and availability for users. + + } + buttonLabel="Enable Enterprise" + buttonType="default" + eeAccess={eeLiteAccess} + horizontal + /> + ) : null} + {(isProConsole(globals) || eeLiteAccess === 'active') && + (!isReadReplicaButtonClicked ? ( + + + + ) : ( + + ))} +
+
+
+
+ ) : null + } + ); }; diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/Schema/ManageDatabase.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/Schema/ManageDatabase.tsx index 200a21a8123..928ba9e5d67 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/Schema/ManageDatabase.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Data/Schema/ManageDatabase.tsx @@ -59,8 +59,14 @@ import { useVPCBannerVisibility, } from './utils'; import { NeonDashboardLink } from '../DataSources/CreateDataSource/Neon/components/NeonDashboardLink'; +import { getRoute } from '../../../../utils/getDataRoute'; +import { + availableFeatureFlagIds, + useIsFeatureFlagEnabled, +} from '../../../../features/FeatureFlags'; import { Collapsible } from '../../../../new-components/Collapsible'; import { IconTooltip } from '../../../../new-components/Tooltip'; +import { ListConnectedDatabases } from '../../../../features/ConnectDBRedesign'; const KNOW_MORE_PROJECT_REGION_UPDATE = 'https://hasura.io/docs/latest/projects/regions/#changing-region-of-an-existing-project'; @@ -271,8 +277,15 @@ const ManageDatabase: React.FC = ({ dataHeaders, sourcesFromMetadata, }) => { + const { enabled: isConnectDBRedesignEnabled, isLoading } = + useIsFeatureFlagEnabled(availableFeatureFlagIds.connectDBRedesign); + useEffect(() => { - if (sourcesFromMetadata.length === 0 && !autoRedirectedToConnectPage) { + if ( + sourcesFromMetadata.length === 0 && + !autoRedirectedToConnectPage && + !isLoading + ) { /** * Because the getDataSources() doesn't list the GDC sources, the Data tab will redirect to the /connect page * thinking that are no sources available in Hasura, even if there are GDC sources connected to it. Modifying getDataSources() @@ -280,10 +293,21 @@ const ManageDatabase: React.FC = ({ * So a quick workaround is to check from the actual metadata if any sources are present - * Combined with checks between getDataSources() and metadata -> we know the remaining sources are GDC sources. In such a case redirect to the manage db route */ - dispatch(_push('/data/manage/connect')); + if (isConnectDBRedesignEnabled) + dispatch(_push('/data/v2/manage/connect')); + else { + dispatch(_push('/data/manage/connect')); + } autoRedirectedToConnectPage = true; } - }, [location, dataSources, dispatch]); + }, [ + location, + dataSources, + dispatch, + sourcesFromMetadata.length, + isConnectDBRedesignEnabled, + isLoading, + ]); const { show: shouldShowVPCBanner, dismiss: dismissVPCBanner } = useVPCBannerVisibility(); @@ -323,7 +347,9 @@ const ManageDatabase: React.FC = ({ }; const onClickConnectDB = () => { - dispatch(_push('/data/manage/connect')); + isConnectDBRedesignEnabled + ? dispatch(_push(getRoute().connectDatabase())) + : dispatch(_push('/data/manage/connect')); }; const pushRoute = (route: string) => { @@ -416,6 +442,7 @@ const ManageDatabase: React.FC = ({ data-test="manage-database-section" > +

= ({ )}

-
-
-
- - - - - - - {sourcesFromMetadata.length ? ( - sourcesFromMetadata.map(source => { - if (nativeDrivers.includes(source.kind)) { - const data = dataSources.find( - s => s.name === source.name - ); - if (!data) return null; +
- return ( - - ); - } - return ( - - ); - }) - ) : ( - - )} - -
- - Database - - Connection String -
- You don't have any data sources connected, please - connect one to continue. -
+ {isConnectDBRedesignEnabled ? ( +
+
-
+ ) : ( + <> +
+
+ + + + + + + {sourcesFromMetadata.length ? ( + sourcesFromMetadata.map(source => { + if (nativeDrivers.includes(source.kind)) { + const data = dataSources.find( + s => s.name === source.name + ); + if (!data) return null; - {showCheckLatencyButton ? ( - - ) : null} - {showAccelerateProjectSection ? ( -
- -
- - Databases marked with “Elevated Latency” indicate that it - took us over 200 ms for this Hasura project to communicate - with your database. These conditions generally happen when - databases and projects are in geographically distant - regions. This can cause API and subsequently application - performance issues. We want your GraphQL APIs to be{' '} - lightning fast, therefore we recommend that you - either deploy your Hasura project in the same region as your - database or select a database instance that's closer to - where you've deployed Hasura. - - -
- - -
+ return ( + + ); + } + return ( + + ); + }) + ) : ( +
+ )} + +
+ + Database + + Connection String +
+ You don't have any data sources connected, please + connect one to continue. +
- -
- ) : null} - {showErrorIndicator ? ( -
- -
- - There was an error in fetching the latest latency data. -
{queryResponse.data}
-
-
- - -
+
+ + {showCheckLatencyButton ? ( + + ) : null} + {showAccelerateProjectSection ? ( +
+ +
+ + Databases marked with “Elevated Latency” indicate that + it took us over 200 ms for this Hasura project to + communicate with your database. These conditions + generally happen when databases and projects are in + geographically distant regions. This can cause API and + subsequently application performance issues. We want + your GraphQL APIs to be lightning fast, therefore + we recommend that you either deploy your Hasura project + in the same region as your database or select a database + instance that's closer to where you've + deployed Hasura. + + +
+ + +
+
+
-
-
- ) : null} - + ) : null} + {showErrorIndicator ? ( +
+ +
+ + There was an error in fetching the latest latency data. +
{queryResponse.data}
+
+
+ + +
+
+
+
+ ) : null} + + + )}
diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Events/EventTriggers/Add/Add.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Services/Events/EventTriggers/Add/Add.tsx index b4ec980c00d..0c1a5b7019f 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Events/EventTriggers/Add/Add.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Events/EventTriggers/Add/Add.tsx @@ -45,6 +45,8 @@ import { isEmpty } from '../../../../Common/utils/jsUtils'; import requestAction from '../../../../../utils/requestAction'; import Endpoints from '../../../../../Endpoints'; import { Button } from '../../../../../new-components/Button'; +import { useEELiteAccess } from '../../../../../features/EETrial'; +import globals from '../../../../../Globals'; import { MapStateToProps } from '../../../../../types'; import { useEventTrigger } from '../state'; import { Header } from '../../../../Common/Headers/Headers'; @@ -84,6 +86,13 @@ const Add: React.FC = props => { const [databaseInfo, setDatabaseInfo] = useState({}); + const { access: eeLiteAccess } = useEELiteAccess(globals); + + const autoCleanupSupport = + isProConsole(globals) || eeLiteAccess === 'active' + ? 'active' + : eeLiteAccess; + useEffect(() => { const driver = getSourceDriver(dataSourcesList, source); setState.operationColumns([]); @@ -303,7 +312,7 @@ const Add: React.FC = props => { const newState = { ...state }; /* don't cleanup_config if console type is oss */ - if (!isProConsole(window.__env)) { + if (autoCleanupSupport === 'active') { delete newState?.cleanupConfig; } @@ -413,6 +422,7 @@ const Add: React.FC = props => { handleHeadersChange={handleHeadersChange} handleToggleAllColumn={setState.toggleAllColumnChecked} handleAutoCleanupChange={handleAutoCleanupChange} + autoCleanupSupport={autoCleanupSupport} /> void; handleToggleAllColumn: () => void; handleAutoCleanupChange: (config: EventTriggerAutoCleanup) => void; + autoCleanupSupport: EELiteAccessStatus; }; const CreateETForm: React.FC = props => { @@ -70,6 +71,7 @@ const CreateETForm: React.FC = props => { handleHeadersChange, handleToggleAllColumn, handleAutoCleanupChange, + autoCleanupSupport, } = props; const supportedDrivers = getSupportedDrivers('events.triggers.add'); @@ -222,7 +224,7 @@ const CreateETForm: React.FC = props => {

- {isProConsole(window.__env) && ( + {autoCleanupSupport !== 'forbidden' && ( <>
diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Events/EventTriggers/Common/AutoCleanupForm.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Services/Events/EventTriggers/Common/AutoCleanupForm.tsx index 6e0e32b770a..c15f821e0f0 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Events/EventTriggers/Common/AutoCleanupForm.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Events/EventTriggers/Common/AutoCleanupForm.tsx @@ -5,8 +5,8 @@ import { Collapsible } from '../../../../../new-components/Collapsible'; import { DropdownButton } from '../../../../../new-components/DropdownButton'; import { InputSection } from '../../../../../new-components/InputSetionWithoutForm'; import { Switch } from '../../../../../new-components/Switch'; -import React from 'react'; import { EventTriggerAutoCleanup } from '../../types'; +import { ETAutoCleanupWrapper } from '../../../../../features/EETrial'; interface AutoCleanupFormProps { cleanupConfig?: EventTriggerAutoCleanup; @@ -32,230 +32,229 @@ export const AutoCleanupForm = (props: AutoCleanupFormProps) => { : !cleanupConfig?.paused; return ( -
- - - Auto-cleanup Event Logs -
- + + Auto-cleanup Event Logs +
+ + {' '} + {isCleanupConfigSet && + !( + cleanupConfig?.paused && + Object.keys(cleanupConfig).length === 1 + ) ? ( + + ) : ( + + )} + + onChange({})} + > + {isCleanupConfigSet && !( cleanupConfig?.paused && Object.keys(cleanupConfig).length === 1 ) - ? 'Auto-cleanup has been configured. After clearing/resetting, save changes to remove the configuration.' - : 'Auto-cleanup is currently not configured' - } - className="h-full flex items-center" - > - {' '} - {isCleanupConfigSet && - !( - cleanupConfig?.paused && - Object.keys(cleanupConfig).length === 1 - ) ? ( - - ) : ( - - )} - - onChange({})} - > - {isCleanupConfigSet && - !( - cleanupConfig?.paused && - Object.keys(cleanupConfig).length === 1 - ) - ? 'Clear / Reset' - : ''} - - - -
- - } - defaultOpen={!!cleanupConfig} - > -
- - { - onChange({ - ...cleanupConfig, - paused: cleanupConfig?.paused === false ? true : false, - }); - }} - /> - - Enable event log cleanup - - -
- { -
-
- - { - onChange({ - ...cleanupConfig, - clean_invocation_logs: - !cleanupConfig?.clean_invocation_logs, - }); - }} - /> - - Clean invocation logs with event logs + ? 'Clear / Reset' + : ''} - -
- - { - onChange({ - ...cleanupConfig, - clear_older_than: value ? parseInt(value, 10) : undefined, - }); - }} - /> - { - onChange({ - ...cleanupConfig, - schedule: value, - }); - }} - /> - -
- ( -
{ - onChange({ - ...cleanupConfig, - schedule: cron.value, - }); - }} - className="py-xs cursor-pointer mx-1 px-xs py-1 rounded hover:bg-gray-100" - > -

- {cron.label} -

-

{cron.value}

-
- )), - ]} - > - Frequent Frequencies -
-
- - - Advanced Settings - - } - > - { - onChange({ - ...cleanupConfig, - timeout: value ? parseInt(value, 10) : undefined, - }); - }} - /> - - { - onChange({ - ...cleanupConfig, - batch_size: value ? parseInt(value, 10) : undefined, - }); - }} - /> - - + +
- } -
- -
+ + } + defaultOpen + > + +
+
+ + { + onChange({ + ...cleanupConfig, + paused: cleanupConfig?.paused === false ? true : false, + }); + }} + /> + + Enable event log cleanup + + +
+ { +
+
+ + { + onChange({ + ...cleanupConfig, + clean_invocation_logs: + !cleanupConfig?.clean_invocation_logs, + }); + }} + /> + + Clean invocation logs with event logs + + +
+ + { + onChange({ + ...cleanupConfig, + clear_older_than: value ? parseInt(value, 10) : undefined, + }); + }} + /> + { + onChange({ + ...cleanupConfig, + schedule: value, + }); + }} + /> + +
+ ( +
{ + onChange({ + ...cleanupConfig, + schedule: cron.value, + }); + }} + className="py-xs cursor-pointer mx-1 px-xs py-1 rounded hover:bg-gray-100" + > +

+ {cron.label} +

+

{cron.value}

+
+ )), + ]} + > + Frequent Frequencies +
+
+ + + Advanced Settings + + } + > + { + onChange({ + ...cleanupConfig, + timeout: value ? parseInt(value, 10) : undefined, + }); + }} + /> + + { + onChange({ + ...cleanupConfig, + batch_size: value ? parseInt(value, 10) : undefined, + }); + }} + /> + + +
+ } +
+
+
+
); }; diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Events/EventTriggers/Modify/Modify.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Services/Events/EventTriggers/Modify/Modify.tsx index 3ad375d95bd..37fb3b5658b 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Events/EventTriggers/Modify/Modify.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Events/EventTriggers/Modify/Modify.tsx @@ -38,14 +38,16 @@ import { import ConfigureTransformation from '../../../../Common/ConfigureTransformation/ConfigureTransformation'; import requestAction from '../../../../../utils/requestAction'; import Endpoints from '../../../../../Endpoints'; +import { useEELiteAccess } from '../../../../../features/EETrial'; import { getValidateTransformOptions, parseValidateApiData, getTransformState, -} from '../../../../Common/ConfigureTransformation/utils'; -import { showErrorNotification } from '../../../Common/Notification'; +} from '../../../../../components/Common/ConfigureTransformation/utils'; +import { showErrorNotification } from '../../../../../components/Services/Common/Notification'; import { Button } from '../../../../../new-components/Button'; import { isProConsole } from '../../../../../utils/proConsole'; +import globals from '../../../../../Globals'; import { getSourceDriver } from '../../../Data/utils'; import { mapDispatchToPropsEmpty } from '../../../../Common/utils/reactUtils'; import { getEventRequestSampleInput } from '../utils'; @@ -89,6 +91,12 @@ const Modify: React.FC = props => { getEventRequestTransformDefaultState() ); + const { access: eeLiteAccess } = useEELiteAccess(globals); + const autoCleanupSupport = + isProConsole(globals) || eeLiteAccess === 'active' + ? 'active' + : eeLiteAccess; + useEffect(() => { if (currentTrigger) { const driver = getSourceDriver(dataSourcesList, currentTrigger.source); @@ -412,7 +420,7 @@ const Modify: React.FC = props => { save={saveWrapper('retry_conf')} />
- {isProConsole(window.__env) && ( + {autoCleanupSupport !== 'forbidden' && (
; + +export const LoadingServerVersion: ComponentStory = args => ( +
+ +
+); + +export const WithoutEnterpriseAccess: ComponentStory = args => ( +
+ +
+); + +export const WithoutEnterpriseLicense: ComponentStory = args => ( +
+ +
+); +WithoutEnterpriseLicense.parameters = { + msw: [eeLicenseInfo.none], + consoleType: 'pro-lite', +}; + +export const DeactivatedEnterpriseLicense: ComponentStory< + typeof About +> = args => ( +
+ +
+); +DeactivatedEnterpriseLicense.parameters = { + msw: [eeLicenseInfo.deactivated], + consoleType: 'pro-lite', +}; + +export const ExpiredEnterpriseLicense: ComponentStory = args => ( +
+ +
+); +ExpiredEnterpriseLicense.parameters = { + msw: [eeLicenseInfo.expired], + consoleType: 'pro-lite', +}; + +export const ActiveEnterpriseLicense: ComponentStory = args => ( +
+ +
+); +ActiveEnterpriseLicense.parameters = { + msw: [eeLicenseInfo.active], + consoleType: 'pro-lite', +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/About/About.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/About/About.tsx index 7550604aeb6..55b5d266d38 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/About/About.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/About/About.tsx @@ -1,71 +1,48 @@ -import React, { Component } from 'react'; +import React from 'react'; import { Connect } from 'react-redux'; import Helmet from 'react-helmet'; import { FaSpinner } from 'react-icons/fa'; + import { Analytics, REDACT_EVERYTHING } from '../../../../features/Analytics'; - +import { EELicenseInfo } from './EELicenseInfo'; +import { LabelValue } from './LabelValue'; import globals from '../../../../Globals'; +import { ReduxState } from '../../../../types'; -import { ReduxState, ConnectInjectedProps } from '../../../../types'; +export const About: React.VFC = props => { + const { serverVersion, consoleAssetVersion } = props; -type AboutState = { - consoleAssetVersion?: string; -}; + const spinner = ; -class About extends Component { - // had to add this here as the state type is not being read properly if added above. - override state: AboutState = { - consoleAssetVersion: globals.consoleAssetVersion, - }; - - override render() { - const { consoleAssetVersion } = this.state; - - const { serverVersion } = this.props; - - const spinner = ; - - const getServerVersionSection = () => { - return ( -
- Current server version: - {serverVersion || spinner} -
- ); - }; - - const getConsoleAssetVersionSection = () => { - return ( -
- Console asset version: - - {consoleAssetVersion || 'NA'} - -
- ); - }; - - return ( - -
-
- -

About

-
{getServerVersionSection()}
-
{getConsoleAssetVersionSection()}
+ return ( + +
+
+ +

About

+
+
+
+ +
+
- - ); - } -} +
+
+ ); +}; const mapStateToProps = (state: ReduxState) => { return { - dataHeaders: state.tables.dataHeaders, serverVersion: state.main.serverVersion, - source: state.tables.currentDataSource, - latestStableServerVersion: state.main.latestStableServerVersion, + consoleAssetVersion: globals.consoleAssetVersion, }; }; diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/About/EELicenseInfo.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/About/EELicenseInfo.tsx new file mode 100644 index 00000000000..e6533a148ed --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/About/EELicenseInfo.tsx @@ -0,0 +1,110 @@ +import * as React from 'react'; +import { Button } from '../../../../new-components/Button'; +import { FaExternalLinkAlt } from 'react-icons/fa'; +import moment from 'moment'; +import { LabelValue } from './LabelValue'; +import { + useEELiteAccess, + EELiteAccess, + EE_TRIAL_CONTACT_US_URL, + EETrialCard, +} from '../../../../features/EETrial'; +import globals from '../../../../Globals'; + +export const EECTAButton: React.VFC<{ + text: string; + className?: string; +}> = props => { + const { className, text } = props; + return ( + + + + ); +}; + +export const EELicenseInfo: React.VFC<{ className?: string }> = props => { + const { className } = props; + const eeLite = useEELiteAccess(globals); + + if (eeLite.access === 'forbidden') { + return null; + } + + return ( +
+ +
+ ); +}; + +export const EELicenseInfoUI: React.VFC<{ + info: EELiteAccess; +}> = props => { + const { info } = props; + switch (info.access) { + case 'eligible': { + return ( +
+
+ + } + /> +
+
+ ); + } + case 'active': + case 'expired': + const expiryDate = moment(info.license.expiry_at); + return ( +
+
+ +
+ +
+ ); + case 'deactivated': + return ( +
+
+ +
+ +
+ ); + case 'loading': + default: + return null; + } +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/About/LabelValue.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/About/LabelValue.tsx new file mode 100644 index 00000000000..857d6d7f754 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/About/LabelValue.tsx @@ -0,0 +1,14 @@ +import * as React from 'react'; + +export const LabelValue: React.VFC<{ + label: React.ReactNode; + value: React.ReactNode; +}> = props => { + const { label, value } = props; + return ( +
+ {label}: + {value} +
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/Sidebar.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/Sidebar.stories.tsx index 98d33fc608f..69881bb2fa8 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/Sidebar.stories.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/Sidebar.stories.tsx @@ -3,6 +3,8 @@ import { RouteComponentProps } from 'react-router'; import { ComponentStory, ComponentMeta } from '@storybook/react'; import { rest, DelayMode } from 'msw'; import { QueryClient, QueryClientProvider } from 'react-query'; + +import { eeLicenseInfo } from '../../../features/EETrial/mocks/http'; import Sidebar, { Metadata } from './Sidebar'; import { HasuraMetadataV3 } from '../../../metadata/types'; @@ -153,7 +155,7 @@ export const MetadataOk: ComponentStory = args => { MetadataOk.storyName = '💠 Demo Metadata Ok'; MetadataOk.args = generateArgs(); MetadataOk.parameters = { - msw: mockHandlers({}), + msw: [...mockHandlers({}), eeLicenseInfo.active], }; export const MetadataKo: ComponentStory = args => { @@ -162,7 +164,7 @@ export const MetadataKo: ComponentStory = args => { MetadataKo.storyName = '💠 Demo Metadata Ko'; MetadataKo.args = generateArgs(false); MetadataKo.parameters = { - msw: mockHandlers({}), + msw: [...mockHandlers({}), eeLicenseInfo.active], }; export const LogoutActive: ComponentStory = args => { @@ -171,7 +173,7 @@ export const LogoutActive: ComponentStory = args => { LogoutActive.storyName = '💠 Demo Pro Logout Active'; LogoutActive.args = generateArgs(); LogoutActive.parameters = { - msw: mockHandlers({}), + msw: [...mockHandlers({}), eeLicenseInfo.active], adminSecretSet: true, }; @@ -185,6 +187,19 @@ ProLiteLoading.parameters = { consoleType: 'pro-lite', }; +export const ProLitePrometheusWithoutLicense: ComponentStory< + typeof Sidebar +> = args => { + return ; +}; +ProLitePrometheusWithoutLicense.storyName = + '💠 Demo Pro Lite Prometheus Without License'; +ProLitePrometheusWithoutLicense.args = generateArgs(); +ProLitePrometheusWithoutLicense.parameters = { + msw: [...mockHandlers({ prometheusEnabled: true }), eeLicenseInfo.none], + consoleType: 'pro-lite', +}; + export const ProLitePrometheusEnabled: ComponentStory< typeof Sidebar > = args => { @@ -193,7 +208,7 @@ export const ProLitePrometheusEnabled: ComponentStory< ProLitePrometheusEnabled.storyName = '💠 Demo Pro Lite Prometheus Enabled'; ProLitePrometheusEnabled.args = generateArgs(); ProLitePrometheusEnabled.parameters = { - msw: mockHandlers({ prometheusEnabled: true }), + msw: [...mockHandlers({ prometheusEnabled: true }), eeLicenseInfo.active], consoleType: 'pro-lite', }; @@ -205,7 +220,7 @@ export const ProLitePrometheusDisabled: ComponentStory< ProLitePrometheusDisabled.storyName = '💠 Demo Pro Lite Prometheus Disabled'; ProLitePrometheusDisabled.args = generateArgs(); ProLitePrometheusDisabled.parameters = { - msw: mockHandlers({ prometheusEnabled: false }), + msw: [...mockHandlers({ prometheusEnabled: false }), eeLicenseInfo.active], consoleType: 'pro-lite', }; @@ -215,7 +230,20 @@ export const ProLiteError: ComponentStory = args => { ProLiteError.storyName = '💠 Demo Pro Lite Prometheus Error'; ProLiteError.args = generateArgs(); ProLiteError.parameters = { - msw: mockHandlers({ status: 500 }), + msw: [...mockHandlers({ status: 500 }), eeLicenseInfo.active], + consoleType: 'pro-lite', +}; + +export const ProLiteOpenTelemetryWithoutLicense: ComponentStory< + typeof Sidebar +> = args => { + return ; +}; +ProLiteOpenTelemetryWithoutLicense.storyName = + '💠 Demo Pro Lite OpenTelemetry Without License'; +ProLiteOpenTelemetryWithoutLicense.args = generateArgs(); +ProLiteOpenTelemetryWithoutLicense.parameters = { + msw: [...mockHandlers({ openTelemetryEnabled: false }), eeLicenseInfo.none], consoleType: 'pro-lite', }; @@ -228,7 +256,7 @@ ProLiteOpenTelemetryEnabled.storyName = '💠 Demo Pro Lite OpenTelemetry Enabled'; ProLiteOpenTelemetryEnabled.args = generateArgs(); ProLiteOpenTelemetryEnabled.parameters = { - msw: mockHandlers({ openTelemetryEnabled: true }), + msw: [...mockHandlers({ openTelemetryEnabled: true }), eeLicenseInfo.active], consoleType: 'pro-lite', }; @@ -241,6 +269,6 @@ ProLiteOpenTelemetryDisabled.storyName = '💠 Demo Pro Lite OpenTelemetry Disabled'; ProLiteOpenTelemetryDisabled.args = generateArgs(); ProLiteOpenTelemetryDisabled.parameters = { - msw: mockHandlers({ openTelemetryEnabled: false }), + msw: [...mockHandlers({ openTelemetryEnabled: false }), eeLicenseInfo.active], consoleType: 'pro-lite', }; diff --git a/frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/Sidebar.tsx b/frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/Sidebar.tsx index 6bee6232626..bac11f2081c 100644 --- a/frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/Sidebar.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/components/Services/Settings/Sidebar.tsx @@ -7,13 +7,15 @@ import LeftContainer from '../../Common/Layout/LeftContainer/LeftContainer'; import globals from '../../../Globals'; import { CLI_CONSOLE_MODE } from '../../../constants'; import { getAdminSecret } from '../ApiExplorer/ApiRequest/utils'; -import { isProLiteConsole } from '../../../utils/proConsole'; import { NavigationSidebar, NavigationSidebarProps, NavigationSidebarSection, } from '../../../new-components/NavigationSidebar'; +import { useEELiteAccess } from '../../../features/EETrial'; +import { getQueryResponseCachingRoute } from '../../../utils/routeUtils'; + export interface Metadata { inconsistentObjects: Record[]; inconsistentInheritedRoles: Record[]; @@ -32,10 +34,11 @@ type SectionDataKey = | 'about'; const Sidebar: React.FC = ({ location, metadata }) => { + const eeLiteAccess = useEELiteAccess(globals); + const sectionsData: Partial< Record > = {}; - sectionsData.metadata = { key: 'metadata', label: 'Metadata', @@ -105,7 +108,8 @@ const Sidebar: React.FC = ({ location, metadata }) => { const { data: openTelemetry } = useMetadata(m => m.metadata.opentelemetry); const { data: configData, isLoading, isError } = useServerConfig(); - if (isProLiteConsole(window.__env)) { + + if (eeLiteAccess.access !== 'forbidden') { sectionsData.monitoring = { key: 'monitoring', label: 'Monitoring & observability', @@ -115,13 +119,16 @@ const Sidebar: React.FC = ({ location, metadata }) => { sectionsData.monitoring.items.push({ key: 'prometheus-settings', label: 'Prometheus Metrics', - status: isLoading - ? 'loading' - : isError - ? 'error' - : configData?.is_prometheus_metrics_enabled - ? 'enabled' - : 'disabled', + status: + eeLiteAccess.access !== 'active' + ? 'disabled' + : isLoading + ? 'loading' + : isError + ? 'error' + : configData?.is_prometheus_metrics_enabled + ? 'enabled' + : 'disabled', route: '/settings/prometheus-settings', dataTestVal: 'prometheus-settings-link', }); @@ -129,16 +136,64 @@ const Sidebar: React.FC = ({ location, metadata }) => { sectionsData.monitoring.items.push({ key: 'opentelemetry-settings', label: 'OpenTelemetry Exporter (Beta)', - status: !openTelemetry - ? 'none' - : openTelemetry.status === 'enabled' - ? 'enabled' - : 'disabled', + status: + eeLiteAccess.access !== 'active' + ? 'disabled' + : !openTelemetry + ? 'none' + : openTelemetry.status === 'enabled' + ? 'enabled' + : 'disabled', route: '/settings/opentelemetry', dataTestVal: 'opentelemetry-settings-link', }); } + if (eeLiteAccess.access !== 'forbidden') { + sectionsData.security.items.push({ + key: 'multiple-admin-secrets', + label: 'Multiple admin secrets', + route: '/settings/multiple-admin-secrets', + dataTestVal: 'multiple-admin-secrets', + }); + + sectionsData.security.items.push({ + key: 'multiple-jwt-secrets', + label: 'Multiple jwt secrets', + route: '/settings/multiple-jwt-secrets', + dataTestVal: 'multiple-jwt-secrets', + }); + + // sectionsData.security.items.push({ + // key: 'single-sign-on', + // label: 'Single Sign On', + // route: '/settings/single-sign-on', + // dataTestVal: 'single-sign-on', + // }); + + sectionsData.performance = { + key: 'performance', + label: 'Performance', + items: [ + { + key: 'query-response-caching', + label: 'Query Response Caching', + // // TODO: Figure out the disabled/enabled logic + // status: + // licenseInfo?.status !== 'active' + // ? 'disabled' + // : isLoading + // ? 'loading' + // : isError + // ? 'error' + // : 'enabled', + route: getQueryResponseCachingRoute(), + dataTestVal: 'query-response-caching', + }, + ], + }; + } + sectionsData.about = { key: 'about', label: 'About', diff --git a/frontend/libs/console/legacy-ce/src/lib/dataSources/services/cockroach/index.tsx b/frontend/libs/console/legacy-ce/src/lib/dataSources/services/cockroach/index.tsx index 8e75a9a8f4c..88a73e1ecb7 100644 --- a/frontend/libs/console/legacy-ce/src/lib/dataSources/services/cockroach/index.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/dataSources/services/cockroach/index.tsx @@ -82,7 +82,7 @@ export const supportedFeatures: DeepRequired = { ssl_certificates: globals.consoleType === 'cloud' || globals.consoleType === 'pro' || - globals.consoleType === 'pro-lite', + globals.consoleType === 'pro-lite', // TODO: Should be allowed only if the license is active }, driver: { name: 'cockroach', diff --git a/frontend/libs/console/legacy-ce/src/lib/dataSources/services/postgresql/index.tsx b/frontend/libs/console/legacy-ce/src/lib/dataSources/services/postgresql/index.tsx index 0372e03a905..b2ac0bfd9b2 100644 --- a/frontend/libs/console/legacy-ce/src/lib/dataSources/services/postgresql/index.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/dataSources/services/postgresql/index.tsx @@ -765,7 +765,7 @@ export const supportedFeatures: DeepRequired = { ssl_certificates: globals.consoleType === 'cloud' || globals.consoleType === 'pro' || - globals.consoleType === 'pro-lite', + globals.consoleType === 'pro-lite', // TODO: should be enabled only when license is active }, }; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGeneratorPage.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGeneratorPage.tsx index 59ee9616cc3..91180034f97 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGeneratorPage.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/Actions/components/OASGenerator/OASGeneratorPage.tsx @@ -13,8 +13,6 @@ import { generatedActionToHasuraAction } from '../OASGenerator/utils'; import { FaAngleRight, FaFileImport, FaHome } from 'react-icons/fa'; import { z } from 'zod'; import { useQueryClient } from 'react-query'; -import { isImportFromOpenAPIEnabled } from '../../../../utils'; -import { browserHistory } from 'react-router'; import { SimpleForm } from '../../../../new-components/Form'; import { OasGeneratorForm } from './OASGeneratorForm'; import React from 'react'; @@ -111,11 +109,6 @@ export const OASGeneratorPage = () => { } }; - if (!isImportFromOpenAPIEnabled(window.__env)) { - browserHistory.push('/actions'); - return null; - } - return (
diff --git a/frontend/libs/console/legacy-ce/src/lib/features/AllowLists/components/AllowListSidebar/AllowListSidebar.tsx b/frontend/libs/console/legacy-ce/src/lib/features/AllowLists/components/AllowListSidebar/AllowListSidebar.tsx index fa6d877017b..3f40c059caa 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/AllowLists/components/AllowListSidebar/AllowListSidebar.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/AllowLists/components/AllowListSidebar/AllowListSidebar.tsx @@ -2,6 +2,9 @@ import debounce from 'lodash/debounce'; import React from 'react'; import { IndicatorCard } from '../../../../new-components/IndicatorCard'; import { useServerConfig } from '../../../../hooks'; +import globals from '../../../../Globals'; +import { isProConsole } from '../../../../utils/proConsole'; +import { useEELiteAccess } from '../../../../features/EETrial'; import { LearnMoreLink } from '../../../../new-components/LearnMoreLink'; import { AllowListSidebarHeader } from './AllowListSidebarHeader'; import { QueryCollectionList } from './QueryCollectionList'; @@ -24,6 +27,10 @@ export const AllowListSidebar: React.FC = props => { const [search, setSearch] = React.useState(''); const debouncedSearch = React.useMemo(() => debounce(setSearch, 300), []); + const { access: eeLiteAccess } = useEELiteAccess(globals); + const allowQueryCollectionsCreation = + isProConsole(globals) || eeLiteAccess === 'active'; + const { data: configData, isLoading: isConfigLoading } = useServerConfig(); const renderInstructions = @@ -32,7 +39,9 @@ export const AllowListSidebar: React.FC = props => { return (
debouncedSearch(searchString)} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/AllowLists/components/AllowListSidebar/AllowListSidebarHeader.tsx b/frontend/libs/console/legacy-ce/src/lib/features/AllowLists/components/AllowListSidebar/AllowListSidebarHeader.tsx index 65c9cd3a723..a1f4b044561 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/AllowLists/components/AllowListSidebar/AllowListSidebarHeader.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/AllowLists/components/AllowListSidebar/AllowListSidebarHeader.tsx @@ -1,13 +1,12 @@ /* eslint-disable no-underscore-dangle */ import React from 'react'; import { Button } from '../../../../new-components/Button'; -import { isProConsole } from '../../../../utils/proConsole'; import { FaFolderPlus } from 'react-icons/fa'; import { QueryCollectionCreateDialog } from './QueryCollectionCreateDialog'; import { AllowListStatus } from './AllowListStatus'; interface AllowListSidebarHeaderProps { - onQueryCollectionCreate: (name: string) => void; + onQueryCollectionCreate?: (name: string) => void; } export const AllowListSidebarHeader = (props: AllowListSidebarHeaderProps) => { @@ -17,7 +16,11 @@ export const AllowListSidebarHeader = (props: AllowListSidebarHeaderProps) => {
{isCreateModalOpen && ( { + if (onQueryCollectionCreate) { + onQueryCollectionCreate(name); + } + }} onClose={() => setIsCreateModalOpen(false)} /> )} @@ -30,7 +33,7 @@ export const AllowListSidebarHeader = (props: AllowListSidebarHeaderProps) => {
- {isProConsole(window.__env) && ( + {onQueryCollectionCreate && (
+ + ); + }; + + await render(); + + fireEvent.click(screen.getByTestId('click-me')); + + expect(mockTracker).toHaveBeenLastCalledWith('button-component', 'click'); +}); + +test('tracks nested button clicks with analytics attribute correctly', async () => { + const TestComponent = () => { + useSetupTelemetryEventListeners(); + return ( + + + + ); + }; + + await render(); + + fireEvent.click(screen.getByTestId('click-me')); + + expect(mockTracker).toHaveBeenLastCalledWith( + 'nested-button-component', + 'click' + ); +}); + +test('does not track button clicks without analytics attribute', async () => { + const TestComponent = () => { + useSetupTelemetryEventListeners(); + return ( + + ); + }; + + await render(); + + fireEvent.click(screen.getByTestId('click-me')); + + expect(mockTracker).toHaveBeenCalledTimes(0); +}); + +test('tracks onchange with of input[type=text] with analytics attribute', async () => { + const TestComponent = () => { + useSetupTelemetryEventListeners(); + const [value, setValue] = React.useState(''); + return ( + + setValue(e.target.value)} + value={value} + type="text" + data-testid="type-here" + /> + + ); + }; + + await render(); + + fireEvent.change(screen.getByTestId('type-here'), { + target: { value: 'text' }, + }); + + expect(mockTracker).toHaveBeenLastCalledWith('text-component', 'change'); +}); + +test('tracks onchange with of a nested input[type=text] with analytics attribute', async () => { + const TestComponent = () => { + useSetupTelemetryEventListeners(); + const [value, setValue] = React.useState(''); + return ( + +
+ setValue(e.target.value)} + value={value} + type="text" + data-testid="type-here" + /> +
+
+ ); + }; + + await render(); + + fireEvent.change(screen.getByTestId('type-here'), { + target: { value: 'text' }, + }); + + expect(mockTracker).toHaveBeenLastCalledWith('text-component', 'change'); +}); + +test('does not track onchange with of an input[type=text] if analytics attribute absent', async () => { + const TestComponent = () => { + useSetupTelemetryEventListeners(); + const [value, setValue] = React.useState(''); + return ( +
+ setValue(e.target.value)} + value={value} + type="text" + data-testid="type-here" + /> +
+ ); + }; + + await render(); + + fireEvent.change(screen.getByTestId('type-here'), { + target: { value: 'text' }, + }); + + expect(mockTracker).not.toHaveBeenCalled(); +}); + +test('tracks onchange with of input[type=radio] with analytics attribute', async () => { + const TestComponent = () => { + useSetupTelemetryEventListeners(); + const [checked, setChecked] = React.useState(false); + return ( + + setChecked(c => !c)} + checked={checked} + data-testid="toggle-radio" + /> + + ); + }; + + await render(); + + fireEvent.change(screen.getByTestId('toggle-radio'), { + target: { value: 'text' }, + }); + + expect(mockTracker).toHaveBeenLastCalledWith('radio-component', 'change'); +}); + +test('tracks onchange with of input[type=checkbox] with analytics attribute', async () => { + const TestComponent = () => { + useSetupTelemetryEventListeners(); + const [checked, setChecked] = React.useState(false); + return ( + + setChecked(c => !c)} + checked={checked} + data-testid="toggle-checkbox" + /> + + ); + }; + + await render(); + + fireEvent.change(screen.getByTestId('toggle-checkbox'), { + target: { value: 'text' }, + }); + + expect(mockTracker).toHaveBeenLastCalledWith('checkbox-component', 'change'); +}); + +test('tracks onchange with of setValue(e.target.value)} + value={value} + data-testid="select-option" + > + + + + + ); + }; + + await render(); + + fireEvent.change(screen.getByTestId('select-option'), { + target: { value: 'value2' }, + }); + + expect(mockTracker).toHaveBeenLastCalledWith('select-component', 'change'); +}); diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Analytics/core/telemetry/htmlEvents.ts b/frontend/libs/console/legacy-ce/src/lib/features/Analytics/core/telemetry/htmlEvents.ts new file mode 100644 index 00000000000..57f4b102a54 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/Analytics/core/telemetry/htmlEvents.ts @@ -0,0 +1,77 @@ +/** + * This file contains the core Analytics utils for reusing this Analytics module for telemetry + * It uses the common attributes exposed by the Analytics component and useGetAnalyticsAttributes hook and sets up events listeners to + * track events that have the analytics attributes. Currently, telemetry is only set up for ee-lite, but it can be extended to CE later. + */ + +import * as React from 'react'; +// import { sendTelemetryEvent, HTMLUserEvent } from '../../../../telemetry' +import { DATA_ANALYTICS_ATTRIBUTE } from '../getAnalyticsAttributes'; + +type UserEvent = 'click' | 'change'; +export type UserEventTracker = (id: string, kind: UserEvent) => void; + +// This function accepts the event identifier, constructs the telemetry payload and sends it + +const trackEvent = ( + target: HTMLElement, + kind: UserEvent, + tracker: UserEventTracker +) => { + // if the target or one of its ancestors has the data-analytics attribute, track the event with the attribute value and event kind + if (target && `closest` in target) { + const matchingTarget = target.closest(`[${DATA_ANALYTICS_ATTRIBUTE}]`); + const analyticsAttributeValue = matchingTarget?.getAttribute + ? matchingTarget.getAttribute(DATA_ANALYTICS_ATTRIBUTE) + : null; + if (analyticsAttributeValue) { + tracker(analyticsAttributeValue, kind); + } + } +}; + +const generateOnClickHandler = + (tracker: UserEventTracker) => (e: MouseEvent) => { + const eventTarget = e.target; + if (eventTarget) { + trackEvent(eventTarget as HTMLElement, 'click', tracker); + } + }; + +const generateOnChangeHandler = (tracker: UserEventTracker) => (e: Event) => { + const eventTarget = e.target; + if (eventTarget) { + trackEvent(eventTarget as HTMLElement, 'change', tracker); + } +}; + +// this function sets up event listeners on the document so that click/change events can be filtered and sent as telemetry events +// it also returns a cleaner so that the event listeners can be removed whenever needed +// the tracker is parameterised so +// a. the telemetry target can be changed if need be +// b. to avoid the depedency loop between telemetr <> Analytics +// c. to make the code testable +export const setupTelemetryEventListeners = (tracker: UserEventTracker) => { + const handleOnClick = generateOnClickHandler(tracker); + const handleOnChange = generateOnChangeHandler(tracker); + document.addEventListener('click', handleOnClick); + document.addEventListener('change', handleOnChange); + return () => { + document.removeEventListener('click', handleOnClick); + document.removeEventListener('change', handleOnChange); + }; +}; + +// a hook that sets up telemetry event listeners on component mount +export const useSetupTelemetryEventListeners = ( + tracker: UserEventTracker, + skip: boolean +) => { + React.useEffect(() => { + if (!skip) { + const cleaner = setupTelemetryEventListeners(tracker); + return cleaner; + } + return () => null; + }, [skip, tracker]); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Analytics/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/Analytics/index.ts index 9e63492066a..b051eeb28aa 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Analytics/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/Analytics/index.ts @@ -11,6 +11,7 @@ export { programmaticallyTraceError } from './core/programmaticallyTraceError'; // REACT UTILITIES export { Analytics } from './components/Analytics'; +export { InitializeTelemetry } from './core/telemetry/components/InitializeTelemetry'; export { useGetAnalyticsAttributes } from './hooks/useGetAnalyticsAttributes'; // CUSTOM EVENTS diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDB/EditConnection.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDB/EditConnection.tsx index f8ed85f6222..ccfad03d859 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDB/EditConnection.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDB/EditConnection.tsx @@ -82,37 +82,42 @@ export const EditConnection = () => { if (!schema) return <>Could not find schema; return ( - -
- - - + +
+ +
+
+ +
+
+ +
+ + {!!Object(formState.errors)?.keys?.length && ( +
+ + Error submitting form, see error messages above + +
+ )} +
+ +
); }; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDB/hooks/useAvailableDrivers.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDB/hooks/useAvailableDrivers.ts index cfa53781f2e..f20acca507b 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDB/hooks/useAvailableDrivers.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDB/hooks/useAvailableDrivers.ts @@ -1,14 +1,31 @@ -import { DataSource } from '../../DataSource'; -import { useHttpClient } from '../../Network'; +import React from 'react'; import { useQuery } from 'react-query'; +import { DataSource, DriverInfo } from '../../DataSource'; +import { useHttpClient } from '../../Network'; -export const useAvailableDrivers = () => { +// default options should return pretty much `list_source_kinds` response +export const useAvailableDrivers = ({ + onFirstSuccess, +}: { + onFirstSuccess?: (drivers: DriverInfo[]) => void; +} = {}) => { const httpClient = useHttpClient(); + + const firstSuccess = React.useRef(true); + return useQuery({ queryKey: ['get_available_drivers'], queryFn: async () => { - const drivers = await DataSource(httpClient).driver.getAllSourceKinds(); - return drivers.filter(driver => driver.release !== 'disabled'); + const unfilteredDrivers = await DataSource( + httpClient + ).driver.getAllSourceKinds(); + return unfilteredDrivers.filter(driver => driver.release !== 'disabled'); + }, + onSuccess: drivers => { + if (firstSuccess.current === true) { + onFirstSuccess?.(drivers); + firstSuccess.current = false; + } }, }); }; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDatabase.route.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDatabase.route.tsx new file mode 100644 index 00000000000..6ea3ce70420 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDatabase.route.tsx @@ -0,0 +1,12 @@ +import { ConnectDatabaseV2 } from './ConnectDatabase'; +import { useEnvironmentState } from './hooks/useEnvironmentState'; + +/** + * + * This is a wrapper component intended to be used directly as a route + * + */ +export const ConnectDatabaseRouteWrapper = () => { + const env = useEnvironmentState(); + return ; +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDatabase.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDatabase.stories.tsx index 476a9d7a9a0..405bdd0dcdc 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDatabase.stories.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDatabase.stories.tsx @@ -1,37 +1,327 @@ -import { hasuraToast } from '../../new-components/Toasts'; -import { useArgs } from '@storybook/client-api'; import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { ConnectDatabase } from './ConnectDatabase'; +import globals from '../../Globals'; +import { ReactQueryDecorator } from '../../storybook/decorators/react-query'; +import { isCloudConsole } from '../../utils'; +import { ConnectDatabaseV2 } from './ConnectDatabase'; +import { useEnvironmentState } from './hooks'; +import { handlers } from './mocks/handlers.mock'; export default { - component: ConnectDatabase, - argTypes: { - onEnableEnterpriseTrial: { action: 'Enable Enterprise Clicked' }, - onContactSales: { action: 'Contact Sales Clicked' }, + component: ConnectDatabaseV2, + decorators: [ReactQueryDecorator()], + parameters: { + msw: handlers({ dcAgentsAdded: true }), }, -} as ComponentMeta; +} as ComponentMeta; -export const Primary: ComponentStory = args => { - const [, updateArgs] = useArgs(); +const Template: ComponentStory = args => { + return ; +}; + +Template.args = { + eeLicenseInfo: 'eligible', + consoleType: 'pro-lite', +}; + +/** + * + * This Story attempts to get oss/cloud/license info from environment. + * + * DC Agents are mocked as available + * + * TODO: Add mocks for licensing api calls. + * + * The new Storybook Console Mode drop down can be used to interact with this version + * + */ + +export const FromEnvironment: ComponentStory = () => { + const env = useEnvironmentState(); + const cloud = isCloudConsole(globals); return ( - { - hasuraToast({ - message: - 'Missing EE Trial Forms here. Setting ee trial prop to active as a temporary measure.', - title: 'Sign Up Not Implemented', - toastOptions: { - duration: 3000, - }, - }); - updateArgs({ ...args, eeState: 'active' }); - }} - /> +
+
+ This component attempts to read Console Type, and EE License Info from + the environment +
+
isCloud: {cloud.toString()}
+
Console Type: {globals.consoleType}
+
Tenant Id: {globals.hasuraCloudTenantId}
+ +
); }; +FromEnvironment.storyName = '💠 Using Environment (DC Agents Available)'; -Primary.args = { - eeState: 'inactive', - initialDb: 'snowflake', +/** + * + * This Story attempts to get oss/cloud/license info from environment. + * + * DC Agents are mocked as NOT available + * + * + * The new Storybook Console Mode drop down can be used to interact with this version + * + */ + +export const FromEnvironment2 = FromEnvironment.bind({}); +FromEnvironment2.storyName = '💠 Using Environment (DC Agents NOT Available)'; +FromEnvironment2.parameters = { + msw: handlers({ dcAgentsAdded: false }), }; +/** + * + * Playground + * + * Mock DC Agents are NOT added in this version + * + */ +export const Playground = Template.bind({}); +Playground.storyName = '💠 Playground (DC Agents NOT Available)'; +Playground.parameters = { + msw: handlers({ dcAgentsAdded: false }), +}; +Playground.args = Template.args; + +/** + * + * Playground 2 + * + * + * Mock DC Agents are added in this version + * + * + */ + +export const Playground2 = Template.bind({}); +Playground2.storyName = '💠 Playground (DC Agents Available)'; +Playground2.args = Template.args; + +/** + * TODO: + * + * Re-write old tests to work with refactored component.... + * + * + */ + +// const defaultArgs: ConnectDatabaseProps = { +// environmentState: { +// eeLicenseInfo: 'active', +// isCloud: false, +// isOss: false, +// isPro: false, +// }, +// }; + +// export const No_Enterprise_Drivers = Template.bind({}); +// No_Enterprise_Drivers.storyName = 'No Enterprise Drivers (OSS)'; +// No_Enterprise_Drivers.args = { +// ...defaultArgs, +// }; + +// No_Enterprise_Drivers.play = async ({ canvasElement }) => { +// const c = within(canvasElement); +// await waitFor(() => c.findByTestId('fancy-radio-postgres')); +// await expect( +// c.queryByTestId('fancy-radio-snowflake') +// ).not.toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-athena')).not.toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-sqlite')).toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-mysqlgdc')).toBeInTheDocument(); +// }; + +// export const License_Inactive = Template.bind({}); +// License_Inactive.args = { +// ...defaultArgs, +// showEnterpriseDrivers: true, +// licenseState: 'forbidden', +// }; + +// License_Inactive.play = async ({ canvasElement }) => { +// const c = within(canvasElement); +// await waitFor(() => c.findByTestId('fancy-radio-postgres')); + +// await expect(c.queryByTestId('fancy-radio-snowflake')).toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-athena')).toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-sqlite')).toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-mysqlgdc')).toBeInTheDocument(); + +// await userEvent.click(await c.findByTestId('fancy-label-snowflake')); + +// await expect( +// await c.findByTestId('license-inactive-card') +// ).toBeInTheDocument(); + +// await expect( +// c.queryByTestId('connect-existing-button') +// ).not.toBeInTheDocument(); +// }; + +// export const License_Active_GDC_N = Template.bind({}); +// License_Active_GDC_N.storyName = 'License Active: DC Agents Not Added'; +// License_Active_GDC_N.args = { +// ...defaultArgs, +// showEnterpriseDrivers: true, +// licenseState: 'active', +// initialDriverName: 'snowflake', +// }; + +// License_Active_GDC_N.play = async ({ canvasElement }) => { +// const c = within(canvasElement); +// await waitFor(() => c.findByTestId('fancy-radio-postgres')); + +// await expect(c.queryByTestId('fancy-radio-snowflake')).toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-athena')).toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-sqlite')).toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-mysqlgdc')).toBeInTheDocument(); + +// await userEvent.click(await c.findByTestId('fancy-label-snowflake')); + +// await expect( +// await c.findByTestId('setup-data-connector-card') +// ).toBeInTheDocument(); + +// await expect( +// c.queryByTestId('connect-existing-button') +// ).not.toBeInTheDocument(); +// }; + +// export const License_Active_GDC_Y = Template.bind({}); +// License_Active_GDC_Y.parameters = { +// msw: handlers({ dcAgentsAdded: true }), +// }; +// License_Active_GDC_Y.storyName = 'License Active: DC Agents Added'; +// License_Active_GDC_Y.args = { +// ...defaultArgs, +// showEnterpriseDrivers: true, +// licenseState: 'active', +// initialDriverName: 'snowflake', +// }; + +// License_Active_GDC_Y.play = async ({ canvasElement }) => { +// const c = within(canvasElement); +// await waitFor(() => c.findByTestId('fancy-radio-postgres')); + +// await expect(c.queryByTestId('fancy-radio-snowflake')).toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-athena')).toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-sqlite')).toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-mysqlgdc')).toBeInTheDocument(); + +// await userEvent.click(await c.findByTestId('fancy-label-snowflake')); + +// await expect( +// c.queryByTestId('setup-data-connector-card') +// ).not.toBeInTheDocument(); +// await expect(c.queryByTestId('connect-existing-button')).toBeInTheDocument(); +// }; + +// export const License_Expired = Template.bind({}); +// License_Expired.args = { +// ...defaultArgs, +// showEnterpriseDrivers: true, +// licenseState: 'expired', +// initialDriverName: 'snowflake', +// }; + +// License_Expired.play = async ({ canvasElement }) => { +// const c = within(canvasElement); +// await waitFor(() => c.findByTestId('fancy-radio-postgres')); + +// await expect(c.queryByTestId('fancy-radio-snowflake')).toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-athena')).toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-sqlite')).toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-mysqlgdc')).toBeInTheDocument(); +// await userEvent.click(await c.findByTestId('fancy-label-snowflake')); +// await expect( +// await c.findByTestId('license-expired-card') +// ).toBeInTheDocument(); +// await expect( +// await c.queryByTestId('connect-existing-button') +// ).not.toBeInTheDocument(); +// }; + +// export const License_Deactivated = Template.bind({}); +// License_Deactivated.args = { +// ...defaultArgs, +// showEnterpriseDrivers: true, +// licenseState: 'deactivated', +// initialDriverName: 'snowflake', +// }; + +// License_Deactivated.play = License_Expired.play; + +// export const Cloud_GDC_Y = Template.bind({}); +// Cloud_GDC_Y.storyName = 'Cloud: DC Agents Available'; +// Cloud_GDC_Y.parameters = { +// msw: handlers({ dcAgentsAdded: true }), +// }; +// Cloud_GDC_Y.args = { +// ...defaultArgs, +// dataConnectorHostType: 'cloud', +// showEnterpriseDrivers: true, +// initialDriverName: 'snowflake', +// }; + +// Cloud_GDC_Y.play = async ({ canvasElement }) => { +// const c = within(canvasElement); +// await waitFor(() => c.findByTestId('fancy-radio-postgres')); + +// await expect(c.queryByTestId('fancy-radio-snowflake')).toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-athena')).toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-sqlite')).toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-mysqlgdc')).toBeInTheDocument(); +// await userEvent.click(await c.findByTestId('fancy-label-snowflake')); +// await expect( +// await c.queryByTestId('cloud-driver-not-available') +// ).not.toBeInTheDocument(); +// await expect( +// await c.queryByTestId('connect-existing-button') +// ).toBeInTheDocument(); +// }; + +// export const Cloud_GDC_N = Template.bind({}); +// Cloud_GDC_N.storyName = 'Cloud: DC Agents Not Available'; +// Cloud_GDC_N.parameters = { +// msw: handlers({ dcAgentsAdded: false }), +// }; +// Cloud_GDC_N.args = { +// ...defaultArgs, +// dataConnectorHostType: 'cloud', +// showEnterpriseDrivers: true, +// initialDriverName: 'snowflake', +// }; + +// Cloud_GDC_N.play = async ({ canvasElement }) => { +// const c = within(canvasElement); +// await waitFor(() => c.findByTestId('fancy-radio-postgres')); + +// await expect(c.queryByTestId('fancy-radio-snowflake')).toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-athena')).toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-sqlite')).toBeInTheDocument(); +// await expect(c.queryByTestId('fancy-radio-mysqlgdc')).toBeInTheDocument(); +// await userEvent.click(await c.findByTestId('fancy-label-snowflake')); +// await expect( +// await c.findByTestId('cloud-driver-not-available') +// ).toBeInTheDocument(); +// await expect( +// await c.queryByTestId('connect-existing-button') +// ).not.toBeInTheDocument(); +// }; + +// export const Neon_Connect = Template.bind({}); +// Neon_Connect.args = { +// ...defaultArgs, +// allowNeonConnect: true, +// }; + +// Neon_Connect.play = async ({ canvasElement }) => { +// const c = within(canvasElement); +// await waitFor(() => c.findByTestId('fancy-radio-postgres')); + +// await userEvent.click(await c.findByTestId('fancy-label-postgres')); +// await expect(await c.findByTestId('neon-connect')).toBeInTheDocument(); +// await expect( +// await c.findByTestId('connect-existing-button') +// ).toBeInTheDocument(); +// }; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDatabase.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDatabase.tsx index 9006b5f1450..44bf6773bf2 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDatabase.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDatabase.tsx @@ -1,23 +1,74 @@ -import { - SelectDatabase, - SelectDatabaseProps, -} from './components/SelectDatabase/SelectDatabase'; +import React from 'react'; +import { DriverInfo } from '../DataSource'; +import { EELiteAccess } from '../EETrial'; +import { ConnectDatabaseWrapper, FancyRadioCards } from './components'; +import { ConnectDbBody } from './ConnectDbBody'; +import { DEFAULT_DRIVER } from './constants'; +import { useDatabaseConnectDrivers } from './hooks/useConnectDatabaseDrivers'; +import { DbConnectConsoleType } from './types'; + +export type ConnectDatabaseProps = { + /** + * + * Can be used to set initial selected database. Will default to Postgres if not set. + * + */ + initialDriverName?: string; + + /** + * + * Used to drive the rendering of body content after the radio cards + * + */ + consoleType: DbConnectConsoleType; + /** + * + * Possible license statuses that are relevant to ProLite + * + */ + eeLicenseInfo: EELiteAccess['access']; +}; + +export const ConnectDatabaseV2 = (props: ConnectDatabaseProps) => { + const { initialDriverName, eeLicenseInfo, consoleType } = props; + + const [selectedDriver, setSelectedDriver] = + React.useState(DEFAULT_DRIVER); + + const { cardData, allDrivers, availableDrivers } = useDatabaseConnectDrivers({ + showEnterpriseDrivers: consoleType !== 'oss', + onFirstSuccess: () => + setSelectedDriver( + currentDriver => + allDrivers.find( + d => + d.name === initialDriverName && + (d.enterprise === false || consoleType !== 'oss') + ) || currentDriver + ), + }); + + const isDriverAvailable = (availableDrivers ?? []).some( + d => d.name === selectedDriver.name + ); -export const ConnectDatabase = (props: SelectDatabaseProps) => { return ( -
-
-
-
Connect Your First Database
-
- Connect your first database to access your database objects in your - GraphQL API. -
-
-
-
- -
-
+ + { + setSelectedDriver( + prev => allDrivers?.find(d => d.name === val) || prev + ); + }} + /> + + ); }; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/ConnectDbBody.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/ConnectDbBody.tsx new file mode 100644 index 00000000000..7733f2a0fa7 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/ConnectDbBody.tsx @@ -0,0 +1,44 @@ +import { DriverInfo } from '../../DataSource'; +import { EELiteAccess } from '../../EETrial'; +import { DbConnectConsoleType } from '../types'; +import { Cloud, Oss, Pro, ProLite } from './parts'; + +type ConnectDbBodyProps = { + consoleType: DbConnectConsoleType; + selectedDriver: DriverInfo; + isDriverAvailable: boolean; + eeLicenseInfo: EELiteAccess['access']; +}; +export const ConnectDbBody = ({ + consoleType, + selectedDriver, + isDriverAvailable, + eeLicenseInfo, +}: ConnectDbBodyProps) => { + switch (consoleType) { + case 'oss': + return ; + case 'pro-lite': + return ( + + ); + case 'pro': + return ( + + ); + case 'cloud': + return ( + + ); + } +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/index.ts new file mode 100644 index 00000000000..1fde70718e6 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/index.ts @@ -0,0 +1 @@ +export { ConnectDbBody } from './ConnectDbBody'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/Cloud.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/Cloud.tsx new file mode 100644 index 00000000000..b24a4a6c689 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/Cloud.tsx @@ -0,0 +1,52 @@ +import { NeonConnect } from '../../../../components/Services/Data/DataSources/CreateDataSource/Neon'; +import { IndicatorCard } from '../../../../new-components/IndicatorCard'; +import { useAppDispatch } from '../../../../storeHooks'; +import { DriverInfo } from '../../../DataSource'; +import { useMetadata } from '../../../hasura-metadata-api'; +import { ConnectButton } from '../../components/ConnectButton'; +import { DEFAULT_DRIVER } from '../../constants'; + +export const Cloud = ({ + selectedDriver, + isDriverAvailable, +}: { + selectedDriver: DriverInfo; + isDriverAvailable: boolean; +}) => { + const { data: sourceNames } = useMetadata(m => + m?.metadata.sources.map(s => s.name) + ); + + const dispatch = useAppDispatch(); + + const selectedDriverName = selectedDriver?.name ?? DEFAULT_DRIVER.name; + + return ( + <> + {selectedDriver?.name === 'postgres' && ( +
+ +
+ )} + + {!isDriverAvailable ? ( +
+ + The response fromlist_source_kindsdid not return your + selected driver. Please verify if the data connector agent is + reachable from your Hasura instance. + +
+ ) : ( + + )} + + ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/Oss.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/Oss.tsx new file mode 100644 index 00000000000..f2565dca9a6 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/Oss.tsx @@ -0,0 +1,6 @@ +import { DriverInfo } from '../../../DataSource'; +import { ConnectButton } from '../../components/ConnectButton'; + +export const Oss = ({ selectedDriver }: { selectedDriver: DriverInfo }) => ( + +); diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/Pro.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/Pro.tsx new file mode 100644 index 00000000000..e8c050d51c2 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/Pro.tsx @@ -0,0 +1,28 @@ +import { DriverInfo } from '../../../DataSource'; +import { SetupConnector } from '../../components'; +import { ConnectButton } from '../../components/ConnectButton'; +import { usePushRoute } from '../../hooks'; + +export const Pro = ({ + selectedDriver, + isDriverAvailable, +}: { + selectedDriver: DriverInfo; + isDriverAvailable: boolean; +}) => { + const pushRoute = usePushRoute(); + return isDriverAvailable ? ( + + ) : ( +
+ { + pushRoute( + `/data/v2/manage/database/add?driver=${selectedDriver?.name}` + ); + }} + /> +
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/ProLite.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/ProLite.tsx new file mode 100644 index 00000000000..7bba59a9fc4 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/ProLite.tsx @@ -0,0 +1,85 @@ +import { IndicatorCard } from '../../../../new-components/IndicatorCard'; +import { DriverInfo } from '../../../DataSource'; +import { EELiteAccess, EETrialCard } from '../../../EETrial'; +import { SetupConnector } from '../../components'; +import { ConnectButton } from '../../components/ConnectButton'; +import { eeCardContentMap } from '../../constants'; +import { usePushRoute } from '../../hooks'; +import { indefiniteArticle } from '../../utils'; + +export const ProLite = ({ + selectedDriver, + isDriverAvailable, + eeLicenseInfo, +}: { + selectedDriver: DriverInfo; + isDriverAvailable: boolean; + eeLicenseInfo: EELiteAccess['access']; +}) => { + const pushRoute = usePushRoute(); + const dbWithArticle = `${indefiniteArticle(selectedDriver.displayName)} ${ + selectedDriver.displayName + }`; + + return ( + <> + {selectedDriver?.enterprise && + (() => { + switch (eeLicenseInfo) { + case 'active': + return !isDriverAvailable ? ( +
+ { + pushRoute( + `/data/v2/manage/database/add?driver=${selectedDriver?.name}` + ); + }} + /> +
+ ) : null; + case 'forbidden': + /** + * + * The only way "forbidden" happens here is if the licensing API is not reachable. + * + */ + return ( +
+ + Unable to determine your Enterprise License status. + +
+ ); + + /** + * Being verbose for state clarity + */ + case 'loading': + case 'deactivated': + case 'expired': + case 'eligible': + return ( +
+ +
+ ); + } + })()} + + {(!selectedDriver?.enterprise || + (eeLicenseInfo === 'active' && isDriverAvailable)) && ( + + )} + + ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/index.ts new file mode 100644 index 00000000000..22095b79d5e --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/ConnectDbBody/parts/index.ts @@ -0,0 +1,4 @@ +export { Cloud } from './Cloud'; +export { Oss } from './Oss'; +export { Pro } from './Pro'; +export { ProLite } from './ProLite'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/SelectDatabase.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/SelectDatabase.stories.tsx deleted file mode 100644 index 3cdf9dbecfb..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/SelectDatabase.stories.tsx +++ /dev/null @@ -1,23 +0,0 @@ -// Button.stories.ts|tsx - -import React from 'react'; -import { ReactQueryDecorator } from '../../storybook/decorators/react-query'; -import { ComponentMeta, ComponentStory } from '@storybook/react'; -import { SelectDatabase } from '.'; - -export default { - /* 👇 The title prop is optional. - * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading - * to learn how to generate automatic titles - */ - component: SelectDatabase, - decorators: [ReactQueryDecorator()], -} as ComponentMeta; - -export const Primary: ComponentStory = () => ( -
- Note: This container has a max width set. When rendering this component keep - width in mind to avoid it growing too large. - -
-); diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/SelectDatabase.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/SelectDatabase.tsx deleted file mode 100644 index 8a795b7de2b..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/SelectDatabase.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { NeonBanner } from '../../components/Services/Data/DataSources/CreateDataSource/Neon/components/Neon/NeonBanner'; -import { DatabaseKind } from './types'; -import { Button } from '../../new-components/Button'; -import React from 'react'; -import DbConnectSVG from '../../graphics/database-connect.svg'; -import { FancyRadioCards } from './components/FancyRadioCards'; -import { databases } from './databases'; - -const enterpriseDbs: DatabaseKind[] = ['snowflake', 'athena']; - -export const SelectDatabase: React.VFC = () => { - const [selectedDb, setSelectedDb] = React.useState('snowflake'); - return ( -
- Database Connection Diagram - { - console.log('selected value', val); - setSelectedDb(val); - }} - /> - {selectedDb === 'postgres' && ( -
- - window.alert('todo: implement Neon integration') - } - status={{ status: 'default' }} - buttonText="Create a Neon Database" - /> -
- )} - {enterpriseDbs.includes(selectedDb) && ( -
-
-
-
- Looking to connect to{' '} - {selectedDb === 'snowflake' ? 'a Snowflake' : 'an Athena'}{' '} - database? -
-
- Deploy data connectors to add data sources such as Snowflake, - Amazon Athena, and more to your GraphQL API. -
-
-
- -
-
-
- )} - -
- ); -}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectBigQueryWidget/ConnectBigQueryWidget.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectBigQueryWidget/ConnectBigQueryWidget.tsx index 2bf8bf076bb..f4720718abb 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectBigQueryWidget/ConnectBigQueryWidget.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectBigQueryWidget/ConnectBigQueryWidget.tsx @@ -1,14 +1,15 @@ +import { useEffect, useState } from 'react'; import { InputField, useConsoleForm } from '../../../../new-components/Form'; import { Button } from '../../../../new-components/Button'; -import { useEffect } from 'react'; import { GraphQLCustomization } from '../GraphQLCustomization/GraphQLCustomization'; import { Configuration } from './parts/Configuration'; import { getDefaultValues, BigQueryConnectionSchema, schema } from './schema'; -import { useManageDatabaseConnection } from '../../hooks/useManageDatabaseConnection'; import { hasuraToast } from '../../../../new-components/Toasts'; import { useMetadata } from '../../../hasura-metadata-api'; -import { generatePostgresRequestPayload } from './utils/generateRequests'; +import { useManageDatabaseConnection } from '../../hooks/useManageDatabaseConnection'; +import { generateBigQueryRequestPayload } from './utils/generateRequests'; import { Collapsible } from '../../../../new-components/Collapsible'; +import { Tabs } from '../../../../new-components/Tabs'; interface ConnectBigQueryWidgetProps { dataSourceName?: string; @@ -23,6 +24,8 @@ export const ConnectBigQueryWidget = (props: ConnectBigQueryWidgetProps) => { m.metadata.sources.find(source => source.name === dataSourceName) ); + const [tab, setTab] = useState('connectionDetails'); + const { createConnection, editConnection, isLoading } = useManageDatabaseConnection({ onSuccess: () => { @@ -43,7 +46,7 @@ export const ConnectBigQueryWidget = (props: ConnectBigQueryWidgetProps) => { }); const handleSubmit = (formValues: BigQueryConnectionSchema) => { - const payload = generatePostgresRequestPayload({ + const payload = generateBigQueryRequestPayload({ driver: 'bigquery', values: formValues, }); @@ -80,37 +83,52 @@ export const ConnectBigQueryWidget = (props: ConnectBigQueryWidgetProps) => {
{isEditMode ? 'Edit BigQuery Connection' : 'Connect BigQuery Database'}
-
- - -
- - GraphQL Customization + setTab(value)} + items={[ + { + value: 'connectionDetails', + label: 'Connection Details', + content: ( +
+ + + + +
+ + GraphQL Customization +
+ } + > + + +
+ +
+ +
+
- } - > - - -
- -
- -
- + ), + }, + ]} + />
); }; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectBigQueryWidget/utils/generateRequests.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectBigQueryWidget/utils/generateRequests.ts index 7a4c1305ac7..91c6b88bb41 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectBigQueryWidget/utils/generateRequests.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectBigQueryWidget/utils/generateRequests.ts @@ -3,7 +3,7 @@ import { generateGraphQLCustomizationInfo } from '../../GraphQLCustomization/uti import { BigQueryConnectionSchema } from '../schema'; import { cleanEmpty } from '../../ConnectPostgresWidget/utils/helpers'; -export const generatePostgresRequestPayload = ({ +export const generateBigQueryRequestPayload = ({ driver, values, }: { diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectButton.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectButton.tsx new file mode 100644 index 00000000000..e03a4663531 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectButton.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Button } from '../../../new-components/Button'; +import { usePushRoute } from '../hooks'; + +export const ConnectButton = ({ driverName }: { driverName: string }) => { + const pushRoute = usePushRoute(); + + return ( + + ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectDatabaseWrapper.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectDatabaseWrapper.tsx new file mode 100644 index 00000000000..d1aaaf95074 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectDatabaseWrapper.tsx @@ -0,0 +1,41 @@ +import { useMetadata } from '../../hasura-metadata-api'; +import React from 'react'; +import DbConnectSVG from '../graphics/database-connect.svg'; +export const ConnectDatabaseWrapper: React.FC = ({ children }) => { + const { data: metadataSources } = useMetadata(m => m.metadata.sources); + + return ( +
+
+
+
+ {metadataSources?.length + ? 'Connect Database' + : 'Connect Your First Database'} +
+ {metadataSources?.length ? ( +
+ Connect a database to access your database objects in your GraphQL + API. +
+ ) : ( +
+ Connect your first database to access your database objects in + your GraphQL API. +
+ )} +
+
+
+
+ Database Connection Diagram + {children} +
+
+
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectGDCSourceWidget/ConnectGDCSourceWidget.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectGDCSourceWidget/ConnectGDCSourceWidget.tsx index 6ec9b4590f0..88905bd5b50 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectGDCSourceWidget/ConnectGDCSourceWidget.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectGDCSourceWidget/ConnectGDCSourceWidget.tsx @@ -18,6 +18,7 @@ import { generateGDCRequestPayload } from './utils/generateRequest'; import { hasuraToast } from '../../../../new-components/Toasts'; import { useManageDatabaseConnection } from '../../hooks/useManageDatabaseConnection'; import { capitaliseFirstLetter } from '../../../../components/Common/ConfigureTransformation/utils'; +import { Collapsible } from '../../../../new-components/Collapsible'; interface ConnectGDCSourceWidgetProps { driver: string; @@ -159,14 +160,21 @@ export const ConnectGDCSourceWidget = (props: ConnectGDCSourceWidgetProps) => { schemaObject={data?.configSchemas.configSchema} references={data?.configSchemas.otherSchemas} /> + +
+ + GraphQL Customization +
+ } + > + + +
), }, - { - value: 'customization', - label: 'GraphQL Customization', - content: , - }, ]} />
diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectMssqlWidget/ConnectMssqlWidget.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectMssqlWidget/ConnectMssqlWidget.tsx index e6d03edfe63..6bb3b331672 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectMssqlWidget/ConnectMssqlWidget.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectMssqlWidget/ConnectMssqlWidget.tsx @@ -1,6 +1,6 @@ import { InputField, useConsoleForm } from '../../../../new-components/Form'; import { Button } from '../../../../new-components/Button'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { GraphQLCustomization } from '../GraphQLCustomization/GraphQLCustomization'; import { getDefaultValues, MssqlConnectionSchema, schema } from './schema'; import { ReadReplicas } from './parts/ReadReplicas'; @@ -9,9 +9,10 @@ import { hasuraToast } from '../../../../new-components/Toasts'; import { useMetadata } from '../../../hasura-metadata-api'; import { generateMssqlRequestPayload } from './utils/generateRequests'; import { ConnectionString } from './parts/ConnectionString'; -import { areReadReplicasEnabled } from '../ConnectPostgresWidget/utils/helpers'; import { Collapsible } from '../../../../new-components/Collapsible'; import { PoolSettings } from './parts/PoolSettings'; +import { LimitedFeatureWrapper } from '../LimitedFeatureWrapper/LimitedFeatureWrapper'; +import { Tabs } from '../../../../new-components/Tabs'; interface ConnectMssqlWidgetProps { dataSourceName?: string; @@ -21,6 +22,7 @@ export const ConnectMssqlWidget = (props: ConnectMssqlWidgetProps) => { const { dataSourceName } = props; const isEditMode = !!dataSourceName; + const [tab, setTab] = useState('connectionDetails'); const { data: metadataSource } = useMetadata(m => m.metadata.sources.find(source => source.name === dataSourceName) @@ -82,59 +84,82 @@ export const ConnectMssqlWidget = (props: ConnectMssqlWidgetProps) => {
{isEditMode ? 'Edit MSSQL Connection' : 'Connect MSSQL Database'}
-
- - -
- Advanced Settings
- } - > - - -
+ setTab(value)} + items={[ + { + value: 'connectionDetails', + label: 'Connection Details', + content: ( +
+ + + - {areReadReplicasEnabled() && ( -
- Read Replicas
- } - > - - -
- )} +
+ + Advanced Settings +
+ } + > + + +
-
- - GraphQL Customization +
+ + GraphQL Customization +
+ } + > + +
+
+ +
+ + + Read Replicas +
+ } + > + + + +
+ +
+ +
+
- } - > - - -
- -
- -
- + ), + }, + ]} + />
); }; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectPostgresWidget/ConnectPostgresWidget.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectPostgresWidget/ConnectPostgresWidget.tsx index 8816333fd7f..ad46f9b38a3 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectPostgresWidget/ConnectPostgresWidget.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectPostgresWidget/ConnectPostgresWidget.tsx @@ -14,8 +14,12 @@ import { UsePreparedStatements } from './parts/UsePreparedStatements'; import { SslSettings } from './parts/SslSettings'; import { Collapsible } from '../../../../new-components/Collapsible'; import { ExtensionSchema } from './parts/ExtensionSchema'; -import { areReadReplicasEnabled, areSSLSettingsEnabled } from './utils/helpers'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; +import { LimitedFeatureWrapper } from '../LimitedFeatureWrapper/LimitedFeatureWrapper'; +import { DynamicDBRouting } from './parts/DynamicDBRouting'; +import { Tabs } from '../../../../new-components/Tabs'; +import { isProConsole } from '../../../../utils'; +import globals from '../../../../Globals'; interface ConnectPostgresWidgetProps { dataSourceName?: string; @@ -27,6 +31,7 @@ interface ConnectPostgresWidgetProps { export const ConnectPostgresWidget = (props: ConnectPostgresWidgetProps) => { const { dataSourceName, overrideDriver, overrideDisplayName } = props; + const [tab, setTab] = useState('connectionDetails'); const isEditMode = !!dataSourceName; @@ -88,102 +93,150 @@ export const ConnectPostgresWidget = (props: ConnectPostgresWidgetProps) => { const hiddenOptions = overrideDriver === 'cockroach' ? ['connectionParams'] : []; + const dynamicDBRoutingTab = + dataSourceName && isEditMode && isProConsole(globals) + ? [ + { + value: 'dynamicDBRouting', + label: 'Dynamic DB Routing', + content: ( +
+ +
+ ), + }, + ] + : []; + return ( -
+ <>
{isEditMode ? `Edit ${overrideDisplayName ?? 'Postgres'} Connection` : `Connect ${overrideDisplayName ?? 'Postgres'} Database`}
-
- + setTab(value)} + items={[ + { + value: 'connectionDetails', + label: 'Connection Details', + content: ( +
+ + -
- -
- -
- Advanced Settings
- } - > - - - - - {areSSLSettingsEnabled() && ( - - SSL Certificates Settings - - (Certificates will be loaded from{' '} - - environment variables - - ) - +
+
- } - > - -
- )} - -
- {areReadReplicasEnabled() && ( -
- Read Replicas
- } - > - - -
- )} +
+ + Advanced Settings +
+ } + > + + + + + +
+ + SSL Certificates Settings + + (Certificates will be loaded from{' '} + + environment variables + + ) + +
+ } + > + + +
+ + +
-
- - GraphQL Customization +
+ + GraphQL Customization +
+ } + > + +
+
+ +
+ + + Read Replicas +
+ } + > + + + +
+ +
+ +
+
- } - > - - -
- -
- -
- -
+ ), + }, + ...dynamicDBRoutingTab, + ]} + /> + ); }; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectPostgresWidget/parts/DynamicDBRouting/DynamicDBRoutingForm.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectPostgresWidget/parts/DynamicDBRouting/DynamicDBRoutingForm.tsx index 73437fefa06..d9602269198 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectPostgresWidget/parts/DynamicDBRouting/DynamicDBRoutingForm.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectPostgresWidget/parts/DynamicDBRouting/DynamicDBRoutingForm.tsx @@ -136,7 +136,7 @@ export const DynamicDBRoutingForm = (props: DynamicDBRoutingFormProps) => { className={`flex items-center rounded bg-gray-200 border border-gray-300 py-sm px-sm mb-md`} > -
+
Dynamic Routing Precedence

{' '} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectPostgresWidget/parts/ReadReplicas.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectPostgresWidget/parts/ReadReplicas.tsx index b23efe07107..315cf7fb226 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectPostgresWidget/parts/ReadReplicas.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/ConnectPostgresWidget/parts/ReadReplicas.tsx @@ -29,7 +29,7 @@ export const ReadReplicas = ({ >({ name, }); - const { watch, setValue } = + const { watch, setValue, trigger } = useFormContext>(); const [mode, setMode] = useState<'idle' | 'add' | 'edit'>('idle'); @@ -141,9 +141,14 @@ export const ReadReplicas = ({

- ); -}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/DatabaseLogo.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/DatabaseLogo.tsx deleted file mode 100644 index 0ef3f164455..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/DatabaseLogo.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; - -export const DatabaseLogo: React.FC<{ title: string; image: string }> = ({ - title, - image, -}) => { - return ( -
- {`${title} -
{title}
-
- ); -}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/EETrialActive.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/EETrialActive.tsx deleted file mode 100644 index 3dafe2b9622..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/EETrialActive.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import { CopyableInputField, InformationCard } from '.'; -import { dbDisplayNames } from '../databases'; -import { indefiniteArticle } from '../utils'; -import { DatabaseKind } from '../../../types'; -import { Button } from '../../../../../new-components/Button'; -import { InputField, SimpleForm } from '../../../../../new-components/Form'; -import { hasuraToast } from '../../../../../new-components/Toasts'; -import React from 'react'; -import { FaExternalLinkAlt } from 'react-icons/fa'; -import { GrDocker } from 'react-icons/gr'; -import { z } from 'zod'; - -export const EETrialActive: React.VFC<{ selectedDb: DatabaseKind }> = ({ - selectedDb, -}) => { - const dbWithArticle = `${indefiniteArticle(selectedDb)} ${ - dbDisplayNames[selectedDb] - }`; - return ( - -
-
-
-
- {dbDisplayNames[selectedDb]} Connector Required -
-
- {`The Hasura GraphQL Data Connector Service is required to connect to ${dbWithArticle} database.`} -
-
-
- -
-
-
-
- -
Docker Initialization
-
- { - console.log(values); - }} - > - - -
- -
-
-
-
-
- ); -}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/EETrialExpired.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/EETrialExpired.tsx deleted file mode 100644 index 9b3b16d8ee7..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/EETrialExpired.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Button } from '../../../../../new-components/Button'; -import React from 'react'; -import { FiAlertTriangle } from 'react-icons/fi'; -import { DatabaseKind } from '../../../types'; -import { InformationCard } from './InformationCard'; - -export const EETrialExpired: React.VFC<{ - onContactSales: () => void; - selectedDb: DatabaseKind; -}> = ({ onContactSales, selectedDb }) => { - return ( - -
-
-
- Enterprise Trial Expired -
-
- With an Enterprise Edition license you can add data sources such as - Snowflake, Amazon Athena, and more to your GraphQL API. -
-
-
- -
-
-
- ); -}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/EETrialInactive.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/EETrialInactive.tsx deleted file mode 100644 index a56c208d6db..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/EETrialInactive.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Button } from '../../../../../new-components/Button'; -import React from 'react'; -import { DatabaseKind } from '../../../types'; -import { dbDisplayNames } from '../databases'; -import { indefiniteArticle } from '../utils'; -import { InformationCard } from './InformationCard'; - -export const EETrialInactive: React.VFC<{ - onEnableEnterpriseTrial: () => void; - selectedDb: DatabaseKind; -}> = ({ onEnableEnterpriseTrial, selectedDb }) => { - const dbWithArticle = `${indefiniteArticle(selectedDb)} ${ - dbDisplayNames[selectedDb] - }`; - return ( - -
-
-
- {`Looking to connect to ${dbWithArticle} database?`} -
-
- Deploy data connectors to add data sources such as Snowflake, Amazon - Athena, and more to your GraphQL API. -
-
-
- -
-
-
- ); -}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/FancyRadioCards.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/FancyRadioCards.tsx deleted file mode 100644 index 35e6a365753..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/FancyRadioCards.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as RadioGroup from '@radix-ui/react-radio-group'; -import clsx from 'clsx'; -import React from 'react'; -import { DatabaseKind } from '../../../types'; - -const twRadioStyles = { - root: `grid grid-cols-4 gap-3`, - itemContainer: { - default: `flex items-center border bg-white shadow-sm rounded border-gray-300 cursor-pointer relative flex-[0_0_160px] h-[88px]`, - active: `ring-2 ring-blue-300 border-blue-400`, - disabled: ` cursor-not-allowed bg-gray-200`, - }, - radioButton: `bg-white w-[20px] h-[20px] rounded-full shadow-eq shadow-blue-900 hover:bg-blue-100 flex-[2] absolute top-0 left-0 m-3`, - indicator: `flex items-center justify-center w-full h-full relative after:content[''] after:block after:w-[10px] after:h-[10px] after:rounded-[50%] after:bg-blue-600`, - label: `text-base whitespace-nowrap cursor-pointer flex-[1] h-full w-full flex justify-center items-center`, -}; - -export const FancyRadioCards: React.VFC<{ - value: string; - items: { - value: string; - content: React.ReactNode | string; - }[]; - onChange: (value: DatabaseKind) => void; -}> = ({ value, items, onChange }) => { - return ( - - {items.map((item, i) => { - return ( -
- - - - -
- ); - })} -
- ); -}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/InformationCard.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/InformationCard.tsx deleted file mode 100644 index 80d607f0a1e..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/InformationCard.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import clsx from 'clsx'; -import React from 'react'; - -const twStyles = { - container: `border border-gray-300 mt-3 shadow-md rounded bg-white p-6`, - blueBorder: `border-l-4 border-l-[#297393]`, -}; - -export const InformationCard: React.FC<{ - blueLeftBorder?: boolean; - className?: string; - innerContainerClassName?: string; -}> = ({ children, blueLeftBorder, className, innerContainerClassName }) => { - return ( -
- {children} -
- ); -}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/index.ts deleted file mode 100644 index ea21fbe2840..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/components/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export { CopyableInputField } from './CopyableInputField'; -export { DatabaseLogo } from './DatabaseLogo'; -export { EETrialActive } from './EETrialActive'; -export { EETrialExpired } from './EETrialExpired'; -export { EETrialInactive } from './EETrialInactive'; -export { FancyRadioCards } from './FancyRadioCards'; -export { InformationCard } from './InformationCard'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/databases.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/databases.tsx deleted file mode 100644 index b5101d5b2c8..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/databases.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React from 'react'; -import { DatabaseLogo } from './components'; -import postgresLogo from './graphics/db-logos/postgres.svg'; -import googleLogo from './graphics/db-logos/google.svg'; -import microsoftLogo from './graphics/db-logos/microsoft.svg'; -import citusLogo from './graphics/db-logos/citus.svg'; -import cockroachLogo from './graphics/db-logos/cockroach.svg'; -import amazonLogo from './graphics/db-logos/amazon.svg'; -import snowflakeLogo from './graphics/db-logos/snowflake.svg'; -import { DatabaseKind } from '../../types'; - -export const dbDisplayNames: Record = { - postgres: 'PostgresSQL', - citus: 'Citus', - cockroach: 'CockroachDB', - alloydb: 'AlloyDB', - mssql: 'MSSQL', - bigquery: 'BigQuery', - snowflake: 'Snowflake', - athena: 'Amazon Athena', -}; - -export const databases: { value: DatabaseKind; content: React.ReactNode }[] = [ - { - value: 'postgres', - content: ( - - ), - }, - { - value: 'citus', - content: , - }, - { - value: 'cockroach', - content: ( - - ), - }, - { - value: 'alloydb', - content: , - }, - { - value: 'mssql', - content: ( - - ), - }, - { - value: 'bigquery', - content: ( - - ), - }, - { - value: 'snowflake', - content: ( - - ), - }, - { - value: 'athena', - content: , - }, -]; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/amazon.svg b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/amazon.svg deleted file mode 100644 index 17bf6086029..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/amazon.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/citus.svg b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/citus.svg deleted file mode 100644 index 392d31bf734..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/citus.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/cockroach.svg b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/cockroach.svg deleted file mode 100644 index 4e1b95eda68..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/cockroach.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/google.svg b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/google.svg deleted file mode 100644 index 5ac44180f5d..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/google.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/microsoft.svg b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/microsoft.svg deleted file mode 100644 index 49806988d65..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/microsoft.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/postgres.svg b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/postgres.svg deleted file mode 100644 index cc822350a3f..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/postgres.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/snowflake.svg b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/snowflake.svg deleted file mode 100644 index e0b98208118..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/snowflake.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/index.ts deleted file mode 100644 index a4292a5b3f1..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { SelectDatabase } from './SelectDatabase'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/styles.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/styles.ts deleted file mode 100644 index d258446cef5..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/styles.ts +++ /dev/null @@ -1 +0,0 @@ -export const twLayoutWidth = `w-[672px]`; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/SetupConnector.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/SetupConnector.tsx new file mode 100644 index 00000000000..e7d69630634 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/SetupConnector.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { GrConnect } from 'react-icons/gr'; +import { Button } from '../../../../new-components/Button'; +import { IndicatorCard } from '../../../../new-components/IndicatorCard'; +import { DriverInfo } from '../../../DataSource'; +import { DockerConfigDialog } from './parts/DockerConfigDialog'; + +export const SetupConnector: React.VFC<{ + selectedDriver: DriverInfo; + onSetupSuccess: () => void; +}> = ({ selectedDriver, onSetupSuccess }) => { + const [showSetup, setShowSetup] = React.useState(false); + return ( + <> + +
+
+
+
+ Data Connector Required +
+
+ {`The Hasura Data Connector Service is required for ${selectedDriver.displayName} databases.`} +
+
+
+ +
+
+
+
+ {showSetup && ( + setShowSetup(false)} + onSetupSuccess={onSetupSuccess} + /> + )} + + ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/hooks/useAgentForm.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/hooks/useAgentForm.tsx new file mode 100644 index 00000000000..bba7e2a2a29 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/hooks/useAgentForm.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { MdOutlineTipsAndUpdates } from 'react-icons/md'; +import { z } from 'zod'; +import { DropDown } from '../../../../../new-components/AdvancedDropDown'; +import { InputField, useConsoleForm } from '../../../../../new-components/Form'; +import { Nullable } from '../../../../../types'; + +const schema = z.object({ + port: z.coerce.number().min(1).max(65535), + containerName: z.string().min(1), + path: z.string().min(1), + protocol: z.union([z.literal('http'), z.literal('https')]), +}); + +export type AgentFormValues = z.infer; + +export const useAgentForm = () => { + const { + Form, + methods: { watch, setValue }, + } = useConsoleForm({ + schema, + options: { + mode: 'onBlur', + defaultValues: { + port: 8081, + containerName: 'hasura-graphql-data-connector', + path: 'host.docker.internal', + protocol: 'http', + }, + }, + }); + + const { port, containerName, path, protocol } = watch(); + + const agentPath = `http://${path}:${port}`; + + const [container, setContainer] = React.useState>(); + + const AgentForm = () => ( +
{}}> +
setContainer(r)}> +
+ Changing these values + will dynamically alter the install command. +
+
+ {protocol}://} + container={container} + side="right" + align="end" + > + { + setValue('protocol', value as 'http' | 'https'); + }} + > + http + https + + + } + label="Network Path" + tooltip="This is the network path that Hasura will use to communicate with the Data Connector Service." + inputTransform={v => v.replace(' ', '')} + inputClassName="rounded-none border-r-0 before:content-['Hello'] before:text-sky-300 before:text-4xl after:content-['Goodbye'] after:text-amber-300 after:text-4xl" + placeholder="127.0.0.1" + description="Protocol      Host Name / IP Address" + /> + + v.replace(/[\D]/, '')} + placeholder="Port" + description="Port" + prependLabel=":" + /> +
+
+
+ ); + + return { + AgentForm, + watchedValues: { + port, + containerName, + path, + protocol, + }, + agentPath, + }; +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/hooks/useCommandForm.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/hooks/useCommandForm.tsx new file mode 100644 index 00000000000..7dd68b0041f --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/hooks/useCommandForm.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import { z } from 'zod'; +import { + CopyableInputField, + useConsoleForm, +} from '../../../../../new-components/Form'; +import { buildAgentPath, buildDockerCommand } from '../utils'; +import { AgentFormValues } from './useAgentForm'; + +const schema = z.object({ command: z.string(), agentPath: z.string() }); + +export const useDockerCommandForm = ({ + containerName, + port, + path, + protocol, +}: AgentFormValues) => { + const { Form, methods } = useConsoleForm({ + schema, + options: { + defaultValues: { + command: buildDockerCommand(containerName, port), + agentPath: buildAgentPath('docker.host.internal', 8081, 'http'), + }, + }, + }); + + React.useEffect(() => { + methods.setValue('command', buildDockerCommand(containerName, port)); + }, [containerName, port]); + + React.useEffect(() => { + methods.setValue('agentPath', buildAgentPath(path, port, protocol)); + }, [path, port, protocol]); + + const DockerCommandForm = () => ( +
{}}> + + + ); + + return { + DockerCommandForm, + }; +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/hooks/useSuperConnectorAgents.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/hooks/useSuperConnectorAgents.ts new file mode 100644 index 00000000000..7931d616788 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/hooks/useSuperConnectorAgents.ts @@ -0,0 +1,59 @@ +import { useAddAgent } from '../../../../ManageAgents/hooks'; +import { AddAgentResponse } from '../../../../ManageAgents/hooks/useAddAgent'; + +export type KnownSuperConnectorDrivers = + | 'snowflake' + | 'athena' + | 'mysqlgdc' + | string; + +export const agentPaths: Record = { + snowflake: '/api/v1/snowflake', + athena: '/api/v1/athena', + mysqlgdc: '/api/v1/mysql', +}; + +function ensure( + argument: T | undefined | null, + message = 'This value was promised to be there.' +): T { + if (argument === undefined || argument === null) { + throw new TypeError(message); + } + + return argument; +} + +export const useAddSuperConnectorAgents = () => { + const { addMultipleAgents, ...rest } = useAddAgent(); + + const addAgents = async ( + superConnectorPath: string, + selectedAgent: KnownSuperConnectorDrivers + ) => { + const args = Object.entries(agentPaths).map( + ([driverKind, agentPath]) => ({ + name: driverKind, + url: superConnectorPath + agentPath, + }) + ); + + const responses = await addMultipleAgents(args); + + const selectedAgentResponse = ensure( + responses.find(r => r.name === selectedAgent) + ); + + return { + // while we are going to attempt to add all known super connector agents at the same time, + // our success criteria is only that the selected agents was added or was already added. + success: + selectedAgentResponse.status === 'added' || + selectedAgentResponse.status === 'already-added', + responses: responses, + makeToast: selectedAgentResponse.makeToast, + }; + }; + + return { addAgents, ...rest }; +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/parts/DockerConfigDialog.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/parts/DockerConfigDialog.stories.tsx new file mode 100644 index 00000000000..fd5a85013ba --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/parts/DockerConfigDialog.stories.tsx @@ -0,0 +1,21 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { ReactQueryDecorator } from '../../../../../storybook/decorators/react-query'; +import { DockerConfigDialog } from './DockerConfigDialog'; + +export default { + component: DockerConfigDialog, + decorators: [ReactQueryDecorator()], +} as ComponentMeta; + +export const Primary: ComponentStory = args => { + return ( +
+ +
+ ); +}; + +Primary.args = { + onCancel: () => {}, + onSetupSuccess: () => {}, +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/parts/DockerConfigDialog.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/parts/DockerConfigDialog.tsx new file mode 100644 index 00000000000..84a2c6c95c4 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/parts/DockerConfigDialog.tsx @@ -0,0 +1,68 @@ +import { GrDocker } from 'react-icons/gr'; +import { Dialog } from '../../../../../new-components/Dialog'; +import { DriverInfo } from '../../../../DataSource'; +import { useAgentForm } from '../hooks/useAgentForm'; +import { useDockerCommandForm } from '../hooks/useCommandForm'; +import { useAddSuperConnectorAgents } from '../hooks/useSuperConnectorAgents'; + +export const DockerConfigDialog = ({ + onCancel, + onSetupSuccess, + selectedDriver, +}: { + onCancel: () => void; + onSetupSuccess: () => void; + selectedDriver: DriverInfo; +}) => { + const { AgentForm, watchedValues, agentPath } = useAgentForm(); + + const { DockerCommandForm } = useDockerCommandForm(watchedValues); + + const { addAgents, isLoading } = useAddSuperConnectorAgents(); + + return ( + { + onCancel(); + }, + onSubmit: async () => { + const { success, makeToast } = await addAgents( + agentPath, + selectedDriver?.name + ); + + makeToast(); + + if (success) { + onSetupSuccess(); + } + }, + isLoading, + }} + > +
+
+
+ +
Docker Setup
+
+
+ Run the command below to install the Hasura Data Connector Service. +
+ + {DockerCommandForm()} + {AgentForm()} +
+
+
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/utils.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/utils.ts new file mode 100644 index 00000000000..2cc33f2d1f6 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SetupConnector/utils.ts @@ -0,0 +1,7 @@ +export const buildDockerCommand = (containerName: string, port: number) => + `docker run -d --name ${containerName} -p 127.0.0.1:${port.toString()}:8081 hasura/graphql-data-connector`; +export const buildAgentPath = ( + path: string, + port: number, + protocol: 'http' | 'https' +) => `${protocol}://${path}:${port}`; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/index.ts new file mode 100644 index 00000000000..e55c8305a34 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/index.ts @@ -0,0 +1,4 @@ +export { ConnectDatabaseWrapper } from './ConnectDatabaseWrapper'; +export { DatabaseLogo } from './DatabaseLogo'; +export { SetupConnector } from './SetupConnector/SetupConnector'; +export { FancyRadioCards } from './FancyRadioCards'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/constants.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/constants.ts new file mode 100644 index 00000000000..cf066eec7da --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/constants.ts @@ -0,0 +1,39 @@ +import { DriverInfo } from '../DataSource'; +import { EELiteAccess } from '../EETrial'; + +export const DEFAULT_DRIVER: DriverInfo = { + name: 'postgres', + displayName: 'Postgres', + release: 'GA', + native: true, + enterprise: false, +}; +export const eeCardContentMap = ( + dbName: string +): Record< + Extract< + EELiteAccess['access'], + 'eligible' | 'expired' | 'deactivated' | 'loading' + >, + { cardTitle: string; cardText: string } +> => ({ + eligible: { + cardTitle: `Looking to connect to ${dbName} database?`, + cardText: + 'Deploy data connectors to add data sources such as Snowflake, Amazon Athena, and more to your GraphQL API.', + }, + expired: { + cardTitle: 'Enterprise License Expired', + cardText: + 'With an Enterprise Edition license you can add data sources such as Snowflake, Amazon Athena, and more to your GraphQL API.', + }, + deactivated: { + cardTitle: 'Enterprise License Deactivated', + cardText: + 'With an Enterprise Edition license you can add data sources such as Snowflake, Amazon Athena, and more to your GraphQL API.', + }, + loading: { + cardTitle: '', + cardText: '', + }, +}); diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/databases.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/databases.tsx deleted file mode 100644 index ec75dc4d960..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/databases.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import PlaceholderLogo from '../../graphics/db-logos/placeholder.svg'; -import PostgresLogo from '../../graphics/db-logos/postgres.svg'; -import { DatabaseLogo } from './components/DatabaseLogo'; -import { DatabaseKind } from './types'; - -export const databases: { value: DatabaseKind; content: React.ReactNode }[] = [ - { - value: 'postgres', - content: , - }, - { - value: 'citus', - content: , - }, - { - value: 'cockroach', - content: , - }, - { - value: 'alloydb', - content: , - }, - { - value: 'mssql', - content: , - }, - { - value: 'bigquery', - content: , - }, - { - value: 'snowflake', - content: , - }, - { - value: 'athena', - content: , - }, -]; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/database-connect.svg b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/database-connect.svg similarity index 100% rename from frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/database-connect.svg rename to frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/database-connect.svg diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/amazon.webp b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/amazon.webp new file mode 100644 index 0000000000000000000000000000000000000000..334a81b26159407c8d3606046d926ee42cfc4b5e GIT binary patch literal 7250 zcmV-Y9IfM0Nk&FW8~^}UMM6+kP&il$0000G0002g004gg06|PpNEH?U00FQ@Yny31 z*0-=vPHdmx#_f)cj_tOs@CJ?C8{2N%h;7?w#J11r9PCw9-xzCEeN}6plYM{PF(Lw@ zWUF->WXFo+xRs*F6jpyZPtp8LvDN0RY$1!nA3~V4NHc zxR{``%*aUUI(GSA$pR5Y#@QX;4{BFhhGaq5WJ$=X_FHlR4H7%@Nh{mW=zv0zCXIvC zq(LQ$Srfu5)^8GkYj}U6IXJK&&%=$S^w|Ac+&;~v`>wr>+LXL|M(097`-IH_KC z$=or{g(8A;#?enldnbTn(KJ1jLg;p^wy9bU)I6eUfLEUZ$2i}EQlTFW&S#^>FHuC~ zjAVi_3B&C%$M4}h)azd*(@Z1Qj|*rsQZWYnC}_zNB;432M!iwi^0 zqll_%qLd9K?$QczY+-b>3m*{pIqm^V<#-xKCB4as$@3GF<|+c{UhP+^1E*0UxJUjC zrkxx-d=^|)+gcP_4|{-X()R69{j4HxY z-}UGI+I{r=EeWxwY2?ef74#N3$B+~9n@QulFq$R=8LlwtMi%6~Ml^~*<(*_hMTVTp zA!798)Eu*)R0ewF{W zR;d{>@*G=OHfvlz*|OHz8qd0d$ifsSWe6W}+T8B>B=ua!wk!+$av#18P;M`Ic=!~! zCLPwp-r(xnz7cZm9NEjD!iEjV=ow6w1!U*D+Yo>t5dZ=@0(iOK+Kbp9957AW5&!%k z2>^WZ47g4JdnYn9c`Z9IhftZ}JiS&fyoa7vel_Vvh3}{>BJe^^up}_W|3N`WJIH3h z23V4X{cyDq1}7BlQazNl9f6X*ZYBfLg_nGxiO_y5P-_TYPDBjAwIRTH_^sT5T+&Kn zxBw!dY=0}nA!%TPc}k$$T3|h;nQbVYuA4A*%49#Md?6oszxKy2j3awlH&sTHU>*lI z1S7T`&N;AkOdIsgWE7fLp>k|QW@1>WpsJ}@Qx+-+OspvjwyDqs-Yx`|EI+6skO$^> z)=Un}Az7QHic9t!hKqF{F1#g!68Lv(LO$0sUNRMhihLo_^;T01|5Gx{iVN?MtSwVn zv*8i#%TFWU$-;1+drM}F;{kmubtPNrHE>mPmXg%0D-P5}LC7wWw8O}Wqb_~1!2Vmt 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 gzTK|1KN02W!uBu2RfM>rYxlx!-`hVf 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 literal 0 HcmV?d00001 diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/cockroach.webp b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/cockroach.webp new file mode 100644 index 0000000000000000000000000000000000000000..9cea88eb11f95b91527b507cabeb657a18f7a2c9 GIT binary patch literal 6892 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 literal 0 HcmV?d00001 diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/placeholder.svg b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/default.svg similarity index 100% rename from frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/graphics/db-logos/placeholder.svg rename to frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/default.svg diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/google.webp b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/google.webp new file mode 100644 index 0000000000000000000000000000000000000000..690ff0adc9182a00640f5fc7c5849d154ba32770 GIT binary patch literal 3364 zcmV+<4cqckNk&E-4FCXFMM6+kP&il$0000G0001g004gg06|PpNE!$L00FRtZL?`R z)?YPZCp&H1wr$(CZQHi(v=y{%yE{fRc2ZUU#oF0x%(>>iJLh>KVgm3#o<&4jDG^Oc zwMI216=_8zB|%nADy@Mg0BAegGCQAo`Rxxq(WlQ758is&sXHy*yA1#&tvFQxZI<8T z+V=)cV4dR!zI)Yf%O(J-)ujrwUE`7gquH#xTeI%YX8-MX{wi$%)oM}!7CHX?$t1%( zf^|l?1<6V89JLT2T0Nq`%C`(B>E^-A;pQZV-ncxVQZ15zRUVu`x(5pmcXHgFD*#C< zM1lEk97me7ly2ncYv%#9DkMRV970;Kv|;2Q2LnkeK?F;`NLsMeZe*XuAyNSe*l!fW zS$Yo+-viKwOXzSTX~6~zV_e-1+F%KDzeYM6)X5j;Kx(LjrN5>J8#bJuEr!%U39ApJ zv7wDWFOSqP32Tg?vB8alRzzx$gcXL+*zm^RmPBd@#R5OjSb@f`=YckWqWfDkR-&R=fF#H+2{>^oEl)7EMJfmMbvm;;$7}~t6so{ZW9V6Ed_ho3#Rk)v zH#m4zh$blnqS$;iJxg%v8qjjwMa%W|(W`a_kfaDm!nTtcIq1B(6p96h(DN|JiCki6GZbt z(en#>1{xoN6sDMcIHLfPVL`sX1)%wmu#GV?1c%KIEl352vVNP=U^D$pBpKQVy;hUEVTzO z1tbp=uFXUm-|G#{py)P$#*znlN;bu|!Q94IIzj6a-~k#-@0_tWB#&V35sXYU-T_EG zijx>oKF;7{fMja_2|ZT?kAT*Zu=aR5%Wv#EI|O+JkJ56{Ic8O8iv*oMpt1bMzD-bM zR-9b`jc>Grv_x?#o#i+7YeGWqgd^$s=sXVE0>vUD8^PGO2?+%pRM4RLp)I+d#sZ@@ zgRx%|5{lTD9{FgztObI_|6$|`cQza5br}0LA)%0cNV-R^;HU*5%_Uq)XJ!wQ6aO0d z?=+GYEiv|MLP9Y+j-5ar$jvw9^=_07)@R(S)v#(@A5$ zCM1+p6L8cFMvmYgy&-^NOJmMp910b*JfYolG-KZ;B$QVLN0E_%oUslRNVt@q!+9-K zP;bJVKa%~LkWgk7+)B^kJPQdTc#@XE`Be`H>TiSX*uNJN%B|@A13iQBX^05AeUT%A z2OuG%pu-9CBcbdGhcmK#*b$Pj^uP4_oZt3@=4e(>dc_>S(Cc#!oEMs~?leY@TcOA- zf(A^ugH~T~@>)m<+cF2aQ@+MbIGB-x+t%#Cs4qBac}SHgR-VkLFSvWn-gIX8XW?oQ zEc8zXI`=N%?**!6!M_XGE6?b~s%FvA8N#_&&CZO-FntYZl_)lv$@(L>bIscIx_|jL;@5m zOkiZ7vrh-Kk;aoY=<+h18G{p+fdGPzU(j>V`O-W>>a(HLL>AU4NxBtMM!-03@tEg^`8Ecx{g+G<5IXUn7lKgX33kPN1*T^3X{ReEyt6 z4{Fdu&v{`WNoQW;%Yf7%VLwJDhOyx-m^rvR6wL}ce@@RtGTgjj&4byUA9jQ^hlB$e zk*%U5xEB&;DQN!|t#vW>YYUkb6q`(Av?jqR>qF5}#r3q-#CQc1b@cp{)_NE}>|PHP ztBs+z7S6v{greSr{es>)IDK~{)GdP3NpB6DJQ;$F0$f6R^*MPyBnt$%fV8SJ@?1cW zSAY{{(5lTi?Px%dTY&9{(Y?CdIe05TkY5x_yhd77QyBU3qDX>56WX5m7im#FVdSV| z+dxsY0!!XEm9(gqFmlR0O8|=EB>`(cHjboQRD+w79Q(+cfC@^f0<3cRFC^(^9u4hg zPLe-gx-uY&(y9Qxwz=cuF|5SVAKkW9FF+J!7F7TpSK9ZSd*1tb@Q9I(I{4>z?ml~; z6*~Y>6_j06QQNl1>~lBl?A_a-R#niLl9CDr_b1w0 z|NmGA|NgXooPS6B6#Z4FH{kt|Vz~-`+fQqoX%snOeW(e9y(1zbKp}+2?^C@pTN3{z zaT7i9G%A$sTBV0=NIA*#@9#=vh1ctzk}6N(2iDWLF}9?ImT_LCPOJ$p2%e1Ig+*yJ z1X8&cBr~=rV;>$BZKZ)dwu36DG)3nFNq1*(fB^pA=o3n_Eh@`eVj%pRpAD{A9>!jr z8$>(z5e;~}jO&J@4V0%p+wZXD+VqtpiwfnIXbNJH3^mKYI&OY6uzMY5N|CA5|8S`Z zxAu6J{7(U|tU)YEhF?C=nX_%@BX^i$)xpY5U*EnrH6K6O^i))!Jpsv&C}HsT1X+?` z5nij5+-Fa)Ur{l(+hi_+o`MG~qp3+?B}OAmW|f`?PaTxIT{=sV1xnz6d{rTjmK>wl zGjMnnzz*-5{;I880w&D8Wed*3vs=1k-Kc))I6_`yHjcXApoeu z`>Xg(XL;%IKQYLM3jkyEC6zjTwj{Et{`bf)BONYukGdBCAOuhNBh2QAli+j=)BYWN zfy^%=@fX6lPXt;HLBWlrWS28-)BL)Lx{61sYoGFB)Eypsu~`l>E(~BZbC~nUe-ILP zD^!9k7uj}v-Z27u@W)z_z%*AnN0f~4f0(!tB7ziMpfAwTOQ6BT>*{;ARvV9L7-hew zLcwM#owX2c_RR88^SE3dlqFP=t!>{%b8Wn5lW3lk%o&Twr0{G{+=mfXLm?6Z*Kwp`yCGub&r_%W_xq9IZegQ8SIsoZw*6MMX*9u)179#5 z6vUj118HhU)$s&#Dp;g+BV6x{)o-imzUKg?#e9m6>_$eKh<=;N{l)XV+h`ddaQNmW zPHHW=c%h4E`Jvs6I$(9CZv zcJ}~1XK5*P%q*f7;>yKayg>RxrCt5HKgY!uCn>5k*|Flx!?qhQT7rN6AFvY{BL_=# zM0h-tGICIUF(K^hFZ ztwqHf4;mPl0R6#4ay^nF13Qjdgf~{q?kXNg(7|C%X=N~a0S-6``PW8l%H8)rY8r(U z4xjfE=D)*mSTO^T@DfFu1qky9}7`K-r4u9(6DH z_&?sMBTfonDVEqEw+3K+z9r_LGZqOFGZk};8?^XSc`{-gzbpNehUmJ6jIzkPPpai( z9G*B$PNkp)rZRE*eCQ>Tn*xsmCg$3txhE8F>mCQ|cr5Q6`eZs~%K+!Rqj@hfeDDJB uZQ$kzdD(`kxbI;t`)d1$Ce?tYTe6l^)DLwi7cAz`o;b^9>kohc0002PiYvze literal 0 HcmV?d00001 diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/index.ts new file mode 100644 index 00000000000..8112fcf813d --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/index.ts @@ -0,0 +1,27 @@ +import postgresLogo from './postgres.webp'; +import googleLogo from './google.webp'; +import microsoftLogo from './microsoft.webp'; +import citusLogo from './citus.webp'; +import cockroachLogo from './cockroach.webp'; +import amazonLogo from './amazon.webp'; +import snowflakeLogo from './snowflake.webp'; +import defaultDbLogo from './default.svg'; +import mysqlLogo from './mysql.webp'; +import sqliteLogo from './sqlite.webp'; + +const dbLogos: Record = { + pg: postgresLogo, + postgres: postgresLogo, + alloy: googleLogo, + citus: citusLogo, + cockroach: cockroachLogo, + mssql: microsoftLogo, + bigquery: googleLogo, + snowflake: snowflakeLogo, + athena: amazonLogo, + default: defaultDbLogo, + mysqlgdc: mysqlLogo, + sqlite: sqliteLogo, +}; + +export default dbLogos; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/microsoft.webp b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/microsoft.webp new file mode 100644 index 0000000000000000000000000000000000000000..a5e9475d8b2680d5b933288c6d325e2a82d691c0 GIT binary patch literal 890 zcmV-=1BLujNk&F;0{{S5MM6+kP&il$0000G0001g004gg06|PpNEZSC01cocNw#X+ zsowjX%lH|@Y_W-R(ZK#p?Je(~DsTMwVxp%7$${PjcUhiMo?FuL)D@bYPvB4mcmN1J|+ zxYZz+(j)H8Zq1BC8=|CkJp?%2E;>sQx*=ONhL)&Jt$-RDM9|D1!q%_d|Nj3MQ2pvz zp)9bX_=i|goC{Q9IwJ#06hz1bxiuum5ZU!xEy;2S$ZAK>5;DVLU1idklnB|vZ1>jJ zlWx^UF!{{;_h8t(aAi}h2;HJRoV)(&4MwN(vB{EGcMYiU$+ed!VENeOX2-3Q)i+;& z&9NI?3#g)#k5&NY*bQ9=l8^}~Bi9O6l9350WAm$T8;39$WK^rcA^ffR;#}8BJ~kPp zy#DAQYVXS2#mL7dyKO)F`iTc0XWv-IX4#s*a_xgez4Xk(Taa~Z*6EmMpSx>GJ13%m zTVGxYPak8NMZ^|{Uv&F9T@!(l(l;|K>TX(V4$<{kk2^I~g}|u@z+JD(ihw9C84@uC z{*Ib6CTfvEK|mNlfWnAh!f*Y$TfcVy`~P1+^{Z$7+66C4lqk9CTXrf@GL^BRA0xx) zqyWu;Mlo6#GU*7cR4^$MxNY8ES9VQ^lFqK~!(#Ww;>(&8;dDZ~eE+Z`IfUFzu7cUW zv+2u3RAoK}09H^qAT$9008k77odGI<0Du5KZ8n)mBqJiBCYC68fDMUbZr+#YONf_! zFYvGNH=HwMyD79DyRc0CU@`zYR)EEHBy;^JM(leR4&!aqS;?_!mep1Ve9f=03FkDF zK&$GfASE_Av9nDfjMF+U`7lxd0RGjr|K_`8szU$&Z3pYUKzjyKQfuSSE-}Bj`Dn>6 z`J&p70vK95XPwOJh6}ok+0y_RKm=p|j>M=TLHb1clyCM%ecA?4{A@e442CI_?Oe^1 z{*C!i2&>)tKp{>=#fL1VgQUhP;n*6{nbX=EC8~%Z9C!o$3oGeaaJt{XNaZnE>1T#i znO)%ZTT49y%7j-bzaIXrc4J1gGq@~(-;Of=RUnDI40nbtttkc@n-I28ww<=l&p~6c Qx&fDbZYBX^RT=;Q0Ioo+U;qFB literal 0 HcmV?d00001 diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/mysql.webp b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/mysql.webp new file mode 100644 index 0000000000000000000000000000000000000000..5466243bbdd64e4ae0cf9d819ab815489d265255 GIT binary patch literal 6128 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 Cmg=Se literal 0 HcmV?d00001 diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/postgres.webp b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/postgres.webp new file mode 100644 index 0000000000000000000000000000000000000000..4e1519aebe37efe934d1a7a46ef0bf8a08b623b8 GIT binary patch literal 5498 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 GfB*n))g{dU literal 0 HcmV?d00001 diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/sqlite.webp b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/sqlite.webp new file mode 100644 index 0000000000000000000000000000000000000000..5990ec8c1eb710f6e5dda8da02992f1f0fdf4a14 GIT binary patch literal 2618 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 void; + showEnterpriseDrivers?: boolean; +}; + +// a GDC driver is only "available" once an agent is added for it +// these are drivers are a special case bc we may want to display them in the UI before their agent's are added in certain cases +const SuperConnectorDrivers: readonly DriverInfo[] = [ + { + name: 'mysqlgdc', + displayName: 'MySql', + native: false, + release: 'Alpha', + enterprise: true, + }, + { + name: 'snowflake', + displayName: 'Snowflake', + native: false, + release: 'Beta', + enterprise: true, + }, + { + name: 'athena', + displayName: 'Amazon Athena', + native: false, + release: 'Beta', + enterprise: true, + }, +] as const; + +// this is a wrapper around useAvailableDrivers +export const useDatabaseConnectDrivers = ({ + showEnterpriseDrivers = true, + onFirstSuccess, +}: useDatabaseConnectDriversProps = {}) => { + const { data: availableDrivers } = useAvailableDrivers({ + onFirstSuccess, + }); + + const allDrivers = sortBy( + uniqBy( + [...(availableDrivers ?? []), ...SuperConnectorDrivers], + d => d.name + ), + d => d.displayName + ); + + const cardData = allDrivers + .filter(d => d.enterprise !== true || showEnterpriseDrivers) + .map(d => ({ + value: d.name, + content: ( + + ), + })); + + return { cardData, allDrivers, availableDrivers }; +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useEnvironmentState.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useEnvironmentState.ts new file mode 100644 index 00000000000..795eb2a1051 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useEnvironmentState.ts @@ -0,0 +1,31 @@ +import globals from '../../../Globals'; + +import { isCloudConsole } from '../../../utils'; +import { isEECloud } from '../../../utils/cloudConsole'; +import { useEELiteAccess } from '../../EETrial'; +import { DbConnectConsoleType } from '../types'; + +const determineConsoleType = (): DbConnectConsoleType => { + const hasuraCloud = isCloudConsole(globals); + const eeCloud = isEECloud(globals); + + if (globals.consoleType === 'pro-lite') { + return 'pro-lite'; + } else if (globals.consoleType === 'oss') { + return 'oss'; + } else if (hasuraCloud || eeCloud) { + return 'cloud'; + } else { + return 'pro'; + } +}; + +export const useEnvironmentState = () => { + // isPro is pro + cloud (both self-hosted && hasura cloud) + const { access: eeLicenseInfo } = useEELiteAccess(globals); + + return { + eeLicenseInfo, + consoleType: determineConsoleType(), + }; +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/index.ts index 85b284ecf7c..ad04e5d3430 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/index.ts @@ -1,2 +1,3 @@ -export { SelectDatabase } from './components/SelectDatabase'; +export { ConnectUIContainer } from './components/ConnectUIContainer'; +export { ConnectDatabaseV2 } from './ConnectDatabase'; export { ListConnectedDatabases } from './components/ListConnectedDatabases'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/mocks/data.mock.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/mocks/data.mock.ts new file mode 100644 index 00000000000..c36e6390e2b --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/mocks/data.mock.ts @@ -0,0 +1,304 @@ +import { Metadata } from '../../hasura-metadata-types'; + +export const mockMetadata: Metadata = { + resource_version: 1, + metadata: { + version: 3, + sources: [ + { + name: 'sqlite_test', + kind: 'sqlite', + tables: [], + configuration: { + template: null, + timeout: null, + value: { + db: './chinook.db', + explicit_main_schema: false, + include_sqlite_meta_tables: false, + tables: ['Album', 'Artist', 'Genre', 'Track'], + }, + }, + }, + { + name: 'chinook', + kind: 'postgres', + tables: [], + configuration: { + connection_info: { + database_url: + 'postgres://postgres:test@host.docker.internal:6001/chinook', + isolation_level: 'repeatable-read', + pool_settings: { + connection_lifetime: 100, + idle_timeout: 200, + pool_timeout: 300, + retries: 400, + total_max_connections: 500, + }, + use_prepared_statements: true, + }, + extensions_schema: 'test_public', + read_replicas: [ + { + database_url: + 'postgres://postgres:test@host.docker.internal:6001/chinook', + isolation_level: 'read-committed', + use_prepared_statements: false, + }, + ], + }, + customization: { + root_fields: { + namespace: 'namespace_', + prefix: 'prefix_', + suffix: '_suffix', + }, + type_names: { + prefix: 'prefix_', + suffix: '_suffix', + }, + }, + }, + { + name: 'mssql1', + kind: 'mssql', + tables: [], + configuration: { + connection_info: { + connection_string: + 'DRIVER={ODBC Driver 17 for SQL Server};SERVER=host.docker.internal;DATABASE=bikes;Uid=SA;Pwd=reallyStrongPwd123', + pool_settings: { + total_max_connections: 50, + idle_timeout: 180, + }, + }, + }, + customization: { + root_fields: { + namespace: 'some_field_name', + prefix: 'some_field_name_prefix', + suffix: 'some_field_name_suffix', + }, + type_names: { + prefix: 'some_type_name_prefix', + suffix: 'some_type_name_suffix', + }, + }, + }, + { + name: 'bigquery_test', + kind: 'bigquery', + tables: [], + configuration: { + datasets: ['sample_dataset', 'sample_dataset_2'], + global_select_limit: '1.0', + project_id: 'hasura-test', + service_account: { + client_email: 'service-account@someemail.com', + private_key: + '-----BEGIN PRIVATE KEY-----\nsecretkey\n-----END PRIVATE KEY-----\n', + project_id: 'hasura-test', + }, + }, + }, + ], + }, +}; + +export const mockCapabilitiesResponse = { + capabilities: { + comparisons: { subquery: { supports_relations: true } }, + data_schema: { supports_foreign_keys: true, supports_primary_keys: true }, + explain: {}, + queries: {}, + raw: {}, + relationships: {}, + scalar_types: { + DateTime: { + comparison_operators: { _in_year: 'int' }, + graphql_type: 'String', + }, + bool: { + comparison_operators: { + _and: 'bool', + _nand: 'bool', + _or: 'bool', + _xor: 'bool', + }, + graphql_type: 'Boolean', + }, + decimal: { + aggregate_functions: { max: 'decimal', min: 'decimal', sum: 'decimal' }, + comparison_operators: { _modulus_is_zero: 'decimal' }, + graphql_type: 'Float', + update_column_operators: { + dec: { argument_type: 'decimal' }, + inc: { argument_type: 'decimal' }, + }, + }, + number: { + aggregate_functions: { max: 'number', min: 'number', sum: 'number' }, + comparison_operators: { _modulus_is_zero: 'number' }, + graphql_type: 'Float', + update_column_operators: { + dec: { argument_type: 'number' }, + inc: { argument_type: 'number' }, + }, + }, + string: { + aggregate_functions: { max: 'string', min: 'string' }, + comparison_operators: { _glob: 'string', _like: 'string' }, + graphql_type: 'String', + }, + }, + }, + config_schema_response: { + config_schema: { + nullable: false, + properties: { + DEBUG: { + additionalProperties: true, + description: 'For debugging.', + nullable: true, + type: 'object', + }, + db: { description: 'The SQLite database file to use.', type: 'string' }, + explicit_main_schema: { + default: false, + description: "Prefix all tables with the 'main' schema", + nullable: true, + type: 'boolean', + }, + include_sqlite_meta_tables: { + description: + 'By default index tables, etc are not included, set this to true to include them.', + nullable: true, + type: 'boolean', + }, + tables: { + description: + 'List of tables to make available in the schema and for querying', + items: { $ref: '#/other_schemas/TableName' }, + nullable: true, + type: 'array', + }, + }, + required: ['db'], + type: 'object', + }, + other_schemas: { TableName: { nullable: false, type: 'string' } }, + }, + display_name: 'Hasura SQLite', + options: { uri: 'http://host.docker.internal:8100' }, +}; + +export const mockSourceKinds = { + agentsAdded: { + sources: [ + { + available: true, + builtin: true, + display_name: 'pg', + kind: 'pg', + }, + { + available: true, + builtin: true, + display_name: 'citus', + kind: 'citus', + }, + { + available: true, + builtin: true, + display_name: 'cockroach', + kind: 'cockroach', + }, + { + available: true, + builtin: true, + display_name: 'mssql', + kind: 'mssql', + }, + { + available: true, + builtin: true, + display_name: 'bigquery', + kind: 'bigquery', + }, + { + available: true, + builtin: false, + display_name: 'Hasura SQLite', + kind: 'sqlite', + }, + { + available: true, + builtin: false, + display_name: 'Amazon Athena', + kind: 'athena', + release_name: 'Beta', + }, + { + available: true, + builtin: false, + display_name: 'Snowflake', + kind: 'snowflake', + release_name: 'Beta', + }, + { + available: true, + builtin: false, + display_name: 'MySQL', + kind: 'mysqlgdc', + release_name: 'Alpha', + }, + ], + }, + agentsNotAdded: { + sources: [ + { + available: true, + builtin: true, + display_name: 'pg', + kind: 'pg', + }, + { + available: true, + builtin: true, + display_name: 'citus', + kind: 'citus', + }, + { + available: true, + builtin: true, + display_name: 'cockroach', + kind: 'cockroach', + }, + { + available: true, + builtin: true, + display_name: 'mssql', + kind: 'mssql', + }, + { + available: true, + builtin: true, + display_name: 'bigquery', + kind: 'bigquery', + }, + { + available: true, + builtin: true, + display_name: 'MySQL', + kind: 'mysqlgdc', + }, + { + available: true, + builtin: false, + display_name: 'Hasura SQLite', + kind: 'sqlite', + }, + ], + }, +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/mocks/handlers.mock.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/mocks/handlers.mock.ts index bf7fbadf044..8eefc2e3295 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/mocks/handlers.mock.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/mocks/handlers.mock.ts @@ -1,203 +1,24 @@ -import { Metadata } from '../../hasura-metadata-types'; import { rest } from 'msw'; +import { + mockCapabilitiesResponse, + mockMetadata, + mockSourceKinds, +} from './data.mock'; -const mockMetadata: Metadata = { - resource_version: 1, - metadata: { - version: 3, - sources: [ - { - name: 'sqlite_test', - kind: 'sqlite', - tables: [], - configuration: { - template: null, - timeout: null, - value: { - db: './chinook.db', - explicit_main_schema: false, - include_sqlite_meta_tables: false, - tables: ['Album', 'Artist', 'Genre', 'Track'], - }, - }, - }, - { - name: 'chinook', - kind: 'postgres', - tables: [], - configuration: { - connection_info: { - database_url: - 'postgres://postgres:test@host.docker.internal:6001/chinook', - isolation_level: 'repeatable-read', - pool_settings: { - connection_lifetime: 100, - idle_timeout: 200, - pool_timeout: 300, - retries: 400, - total_max_connections: 500, - }, - use_prepared_statements: true, - }, - extensions_schema: 'test_public', - read_replicas: [ - { - database_url: - 'postgres://postgres:test@host.docker.internal:6001/chinook', - isolation_level: 'read-committed', - use_prepared_statements: false, - }, - ], - }, - customization: { - root_fields: { - namespace: 'namespace_', - prefix: 'prefix_', - suffix: '_suffix', - }, - type_names: { - prefix: 'prefix_', - suffix: '_suffix', - }, - }, - }, - { - name: 'mssql1', - kind: 'mssql', - tables: [], - configuration: { - connection_info: { - connection_string: - 'DRIVER={ODBC Driver 17 for SQL Server};SERVER=host.docker.internal;DATABASE=bikes;Uid=SA;Pwd=reallyStrongPwd123', - pool_settings: { - total_max_connections: 50, - idle_timeout: 180, - }, - }, - }, - customization: { - root_fields: { - namespace: 'some_field_name', - prefix: 'some_field_name_prefix', - suffix: 'some_field_name_suffix', - }, - type_names: { - prefix: 'some_type_name_prefix', - suffix: 'some_type_name_suffix', - }, - }, - }, - { - name: 'bigquery_test', - kind: 'bigquery', - tables: [], - configuration: { - datasets: ['sample_dataset', 'sample_dataset_2'], - global_select_limit: '1.0', - project_id: 'hasura-test', - service_account: { - client_email: 'service-account@someemail.com', - private_key: - '-----BEGIN PRIVATE KEY-----\nsecretkey\n-----END PRIVATE KEY-----\n', - project_id: 'hasura-test', - }, - }, - }, - ], - }, -}; - -export const mockCapabilitiesResponse = { - capabilities: { - comparisons: { subquery: { supports_relations: true } }, - data_schema: { supports_foreign_keys: true, supports_primary_keys: true }, - explain: {}, - queries: {}, - raw: {}, - relationships: {}, - scalar_types: { - DateTime: { - comparison_operators: { _in_year: 'int' }, - graphql_type: 'String', - }, - bool: { - comparison_operators: { - _and: 'bool', - _nand: 'bool', - _or: 'bool', - _xor: 'bool', - }, - graphql_type: 'Boolean', - }, - decimal: { - aggregate_functions: { max: 'decimal', min: 'decimal', sum: 'decimal' }, - comparison_operators: { _modulus_is_zero: 'decimal' }, - graphql_type: 'Float', - update_column_operators: { - dec: { argument_type: 'decimal' }, - inc: { argument_type: 'decimal' }, - }, - }, - number: { - aggregate_functions: { max: 'number', min: 'number', sum: 'number' }, - comparison_operators: { _modulus_is_zero: 'number' }, - graphql_type: 'Float', - update_column_operators: { - dec: { argument_type: 'number' }, - inc: { argument_type: 'number' }, - }, - }, - string: { - aggregate_functions: { max: 'string', min: 'string' }, - comparison_operators: { _glob: 'string', _like: 'string' }, - graphql_type: 'String', - }, - }, - }, - config_schema_response: { - config_schema: { - nullable: false, - properties: { - DEBUG: { - additionalProperties: true, - description: 'For debugging.', - nullable: true, - type: 'object', - }, - db: { description: 'The SQLite database file to use.', type: 'string' }, - explicit_main_schema: { - default: false, - description: "Prefix all tables with the 'main' schema", - nullable: true, - type: 'boolean', - }, - include_sqlite_meta_tables: { - description: - 'By default index tables, etc are not included, set this to true to include them.', - nullable: true, - type: 'boolean', - }, - tables: { - description: - 'List of tables to make available in the schema and for querying', - items: { $ref: '#/other_schemas/TableName' }, - nullable: true, - type: 'array', - }, - }, - required: ['db'], - type: 'object', - }, - other_schemas: { TableName: { nullable: false, type: 'string' } }, - }, - display_name: 'Hasura SQLite', - options: { uri: 'http://host.docker.internal:8100' }, -}; - -export const handlers = () => [ +export const handlers = ({ + dcAgentsAdded, +}: { dcAgentsAdded?: boolean } = {}) => [ rest.post('http://localhost:8080/v1/metadata', (req, res, ctx) => { const requestBody = req.body as Record; - + if (requestBody.type === 'list_source_kinds') { + return res( + ctx.json( + dcAgentsAdded + ? mockSourceKinds.agentsAdded + : mockSourceKinds.agentsNotAdded + ) + ); + } if (requestBody.type === 'export_metadata') return res(ctx.json(mockMetadata)); @@ -230,6 +51,7 @@ export const handlers = () => [ }), rest.get(`http://localhost:8080/v1alpha1/config`, (req, res, ctx) => { return res( + ctx.delay(3000), ctx.json({ version: 'dev-fb2bab3-test-app', is_function_permissions_inferred: true, diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/prototypes/ConnectDatabaseSidebar.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/prototypes/ConnectDatabaseSidebar.stories.tsx new file mode 100644 index 00000000000..f81433fc309 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/prototypes/ConnectDatabaseSidebar.stories.tsx @@ -0,0 +1,25 @@ +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { ReactQueryDecorator } from '../../../storybook/decorators/react-query'; +import { ConnectDatabaseSidebar } from './ConnectDatabaseSidebar'; +import { handlers } from '../mocks/handlers.mock'; + +export default { + component: ConnectDatabaseSidebar, + decorators: [ReactQueryDecorator()], + parameters: { + msw: handlers({ dcAgentsAdded: false }), + }, +} as ComponentMeta; + +export const Primary: ComponentStory = args => ( +
+ +
+); + +Primary.args = { + //licenseState: 'none', + //hostType: 'self-hosted', + //allowNeonConnect: false, + //showEnterpriseDrivers: true, +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/prototypes/ConnectDatabaseSidebar.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/prototypes/ConnectDatabaseSidebar.tsx new file mode 100644 index 00000000000..8834628b9ae --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/prototypes/ConnectDatabaseSidebar.tsx @@ -0,0 +1,172 @@ +import * as ScrollArea from '@radix-ui/react-scroll-area'; +import clsx from 'clsx'; +import React from 'react'; +import { z } from 'zod'; +import { Button } from '../../../new-components/Button'; +import { Collapsible } from '../../../new-components/Collapsible'; +import { InputField, useConsoleForm } from '../../../new-components/Form'; +import { DriverInfo } from '../../DataSource'; +import { useAddAgent } from '../../ManageAgents/hooks'; +import { + agentPaths, + KnownSuperConnectorDrivers, +} from '../components/SetupConnector/hooks/useSuperConnectorAgents'; +import { ConnectDatabaseProps } from '../ConnectDatabase'; +import dbLogos from '../graphics/db-logos'; +import { useDatabaseConnectDrivers } from '../hooks'; + +export const ConnectDatabaseSidebar = (props: ConnectDatabaseProps) => { + const { consoleType } = props; + const showEnterpriseDrivers = consoleType !== 'oss'; + const { allDrivers, availableDrivers } = useDatabaseConnectDrivers({ + showEnterpriseDrivers: showEnterpriseDrivers, + }); + + const { Form, methods } = useConsoleForm({ + schema: z.object({ search: z.string() }), + }); + const searchQuery = methods.watch('search'); + + const nativeDrivers = React.useMemo( + () => + allDrivers.filter( + d => + d.native && + (!searchQuery?.trim() || + d.displayName.toLowerCase().includes(searchQuery.toLowerCase())) + ), + [searchQuery, allDrivers] + ); + const dataConnectors = React.useMemo( + () => + allDrivers.filter( + d => + !d.native && + (!d.enterprise || showEnterpriseDrivers) && + (!searchQuery?.trim() || + d.displayName.toLowerCase().includes(searchQuery.toLowerCase())) + ), + [searchQuery, allDrivers] + ); + return ( +
+
{}}> + + + + +
+ ); +}; + +const ScrollContainer: React.FC<{ className?: string }> = ({ + children, + className, +}) => ( + + + {children} + + + + + + + + + +); + +type ListContentProps = { + items: DriverInfo[]; + title: string; + availableDrivers: DriverInfo[]; + contentClassName?: string; +} & ConnectDatabaseProps; + +const ListContent = ({ + items, + title, + availableDrivers, + contentClassName, + consoleType, +}: ListContentProps) => { + const { addAgent } = useAddAgent(); + return ( + +
{title}
+
+ {items.length} +
+
+ } + > + + {items.map(d => ( +
+ {`${d.displayName} +
+ {d.displayName} + {consoleType !== 'cloud' && + !availableDrivers.some(driver => driver.name === d.name) && ( + + )} +
+
+ ))} +
+ + ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/styles.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/styles.ts deleted file mode 100644 index d258446cef5..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/styles.ts +++ /dev/null @@ -1 +0,0 @@ -export const twLayoutWidth = `w-[672px]`; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/types.d.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/types.d.ts index ac3018258b3..8d141f24134 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/types.d.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/types.d.ts @@ -7,6 +7,14 @@ export type DatabaseConnection = { }; }; +/** + * + * This type is a list of the types we care about for db connect. + * + * This is not a 1-1 correspondence with window.__env/globals + * + */ +export type DbConnectConsoleType = 'oss' | 'pro' | 'pro-lite' | 'cloud'; export type DatabaseKind = | 'postgres' | 'mssql' diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/utils.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/utils.ts similarity index 54% rename from frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/utils.ts rename to frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/utils.ts index 6dc99d08663..ee18c9be8aa 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/components/SelectDatabase/utils.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/utils.ts @@ -3,3 +3,11 @@ export const indefiniteArticle = (word: string): string => { const vowels = ['a', 'e', 'i', 'o', 'u']; return vowels.includes(word.charAt(0)) ? 'an' : 'a'; }; + +export const getDriverNameFromUrlParams = (): string | undefined => { + const urlParams = new URLSearchParams(window.location.search); + + const driver = urlParams.get('driver'); + + return driver ?? undefined; +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ControlPlane/client.ts b/frontend/libs/console/legacy-ce/src/lib/features/ControlPlane/client.ts index cf567fbe665..068164e8db3 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ControlPlane/client.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/ControlPlane/client.ts @@ -8,7 +8,7 @@ import { GraphQLError } from 'graphql/error'; export const createControlPlaneClient = ( endpoint: string = endpoints.luxDataGraphql, - headers = { + headers: Record = { 'content-type': 'application/json', 'hasura-client-name': 'hasura-console', } diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ControlPlane/generatedGraphQLTypes.ts b/frontend/libs/console/legacy-ce/src/lib/features/ControlPlane/generatedGraphQLTypes.ts index 5c7dbb0a602..938590f8c1e 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ControlPlane/generatedGraphQLTypes.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/ControlPlane/generatedGraphQLTypes.ts @@ -60496,6 +60496,27 @@ export type TriggerOneClickDeploymentMutation = { } | null; }; +export type RegisterEeTrialMutationVariables = Exact<{ + metadataDbId: Scalars['uuid']; + first: Scalars['String']; + last: Scalars['String']; + email: Scalars['String']; + jobFunction: Scalars['String']; + organization: Scalars['String']; + phone: Scalars['String']; +}>; + +export type RegisterEeTrialMutation = { + __typename?: 'mutation_root'; + registerEETrial?: { + __typename?: 'RegisterEETrialResponse'; + status?: string | null; + type?: string | null; + expiry_at?: any | null; + grace_at?: any | null; + } | null; +}; + export type FetchAllSurveysDataQueryVariables = Exact<{ currentTime: Scalars['timestamptz']; }>; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/common/getAllSourceKinds/getAllSourceKinds.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/common/getAllSourceKinds/getAllSourceKinds.ts index 3a2272bd270..d98bea31f90 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/common/getAllSourceKinds/getAllSourceKinds.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/common/getAllSourceKinds/getAllSourceKinds.ts @@ -6,6 +6,7 @@ type SourceKindsResponse = { kind: string; display_name: string; release_name?: string; + available: boolean; }[]; }; export const getAllSourceKinds = async ({ httpClient }: NetworkArgs) => { diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/index.ts index a7dbcaa7eb4..623392cfa14 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/index.ts @@ -12,7 +12,7 @@ import { mssql } from './mssql'; import { postgres } from './postgres'; import { alloy, AlloyDbTable } from './alloydb'; import type { - DriverInfoResponse, + DriverInfo, GetDefaultQueryRootProps, GetFKRelationshipProps, GetSupportedOperatorsProps, @@ -78,7 +78,7 @@ export type Database = { getVersion?: ( props: GetVersionProps ) => Promise; - getDriverInfo: () => Promise; + getDriverInfo: () => Promise; getDatabaseConfiguration: ( httpClient: AxiosInstance, driver?: string @@ -169,12 +169,19 @@ const getDriverMethods = (driver: SupportedDrivers) => { export const DataSource = (httpClient: AxiosInstance) => ({ driver: { - getAllSourceKinds: async () => { + getAllSourceKinds: async (): Promise => { const serverSupportedDrivers = await getAllSourceKinds({ httpClient }); - + const knownEnterpriseDrivers = ['athena', 'snowflake', 'mysqlgdc']; const allSupportedDrivers = serverSupportedDrivers // NOTE: AlloyDB is added here and not returned by the server because it's not a new data source (it's Postgres) - .concat([{ builtin: true, kind: 'alloy', display_name: 'AlloyDB' }]) + .concat([ + { + builtin: true, + kind: 'alloy', + display_name: 'AlloyDB', + available: true, + }, + ]) .sort((a, b) => (a.kind > b.kind ? 1 : -1)); const allDrivers = allSupportedDrivers.map(async driver => { @@ -188,13 +195,17 @@ export const DataSource = (httpClient: AxiosInstance) => ({ displayName: driverInfo.displayName, release: driverInfo.release, native: driver.builtin, + available: true, + enterprise: false, }; return { name: driver.kind, displayName: driver.display_name, - release: driver.release_name ?? 'GA', + release: (driver.release_name as ReleaseType) ?? 'GA', native: driver.builtin, + available: driver.available, + enterprise: knownEnterpriseDrivers.includes(driver.kind), }; }); return Promise.all(allDrivers); diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/index.ts index ee8d419e8bd..3e13e5ec1c3 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/postgres/index.ts @@ -33,6 +33,7 @@ export const postgres: Database = { name: 'postgres', displayName: 'Postgres', release: 'GA', + native: true, }), getDatabaseConfiguration, getDriverCapabilities: async () => Promise.resolve(postgresCapabilities), diff --git a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/types.ts b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/types.ts index 96d2be39a9c..c81df0b6312 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/DataSource/types.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/DataSource/types.ts @@ -109,10 +109,13 @@ export type GetTablesListAsTreeProps = { export type ReleaseType = 'GA' | 'Beta' | 'Alpha' | 'disabled'; -export type DriverInfoResponse = { +export type DriverInfo = { name: SupportedDrivers; displayName: string; release: ReleaseType; + native?: boolean; + available?: boolean; + enterprise?: boolean; }; export type GetTableRowsProps = { diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/eetrial-loading.svg b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/eetrial-loading.svg new file mode 100644 index 00000000000..1f8ac6592f6 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/eetrial-loading.svg @@ -0,0 +1,69 @@ + + + + + Hasura is preparing your free EE trial + + + + + + + + + + + + + + + + + + + + + + + + Sending your key... + + Generating your key... + + + + + + + + diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/icon-opentelemetry.svg b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/icon-opentelemetry.svg new file mode 100644 index 00000000000..b44fd409b8b --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/icon-opentelemetry.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/icon-query-caching.png b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/icon-query-caching.png new file mode 100644 index 0000000000000000000000000000000000000000..2c9008fb86831745cb57d91960fdaf97f70331f9 GIT binary patch literal 689 zcmV;i0#5yjP)Fzv9W(oXAGDPm1vz+A+tlpvVT%w@&ZxJ$M?a?<@2y~rVAE2JK)p7{ji$6 z3FS-YU_RCk(_s@HS`sFM0!p^EMzjQ=gmSjk?o%utJ^=OWS9xWf24kw#fI<8-X3r z-Mk&{=mGWrtZhP8$_}lUu>?NA^A=`3qHnUu$VZcNWWgYs`Bq_3!a5^ zm_b6Kd}Qg+FC?UxOlU4+(KP2vX)YjxqK`3Iz#>I@q!YdkXBFj_vnOkWc_bXxB~T9q z!c>wB&+lcM9-Xnx3Xx@`s1=Nvx@w~44|{hjD^EfOWUC^5Lcm=^LcYyZxl!Nd5Ee{T zZNA3jsJT9sXvc6*R%qXkBI7|r2_OVM|7BDB^c!h9*`&CSbUy8E{C$eKqTRseo3I%# z;DkmtNw|nLFR*5)PGm&)Y*K#2Kr*tKsoo|{&9$mTtzoc%O&Uf+Yc^5LaXDY`YdrVYRFgzYkU+&==eGLe=tw(0L#ALq?m{t^8R X_=ey?=QE(R00000NkvXXu0mjf{k}J` literal 0 HcmV?d00001 diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-airbus.svg b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-airbus.svg new file mode 100644 index 00000000000..15298c74838 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-airbus.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-atlassian.svg b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-atlassian.svg new file mode 100644 index 00000000000..fdb0f41320a --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-atlassian.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-bbva.svg b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-bbva.svg new file mode 100644 index 00000000000..6d967b9999e --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-bbva.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-netlify.svg b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-netlify.svg new file mode 100644 index 00000000000..fd477f1169f --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-netlify.svg @@ -0,0 +1,4 @@ + + + + diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-pipe.svg b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-pipe.svg new file mode 100644 index 00000000000..863444b9565 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-pipe.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-prometheus.svg b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-prometheus.svg new file mode 100644 index 00000000000..67856ddc5d1 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-prometheus.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-university-virginia.svg b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-university-virginia.svg new file mode 100644 index 00000000000..17ef2d1c8d1 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/logo-university-virginia.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/ConsentCheckbox.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/ConsentCheckbox.tsx new file mode 100644 index 00000000000..2f97806b7e6 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/ConsentCheckbox.tsx @@ -0,0 +1,72 @@ +import React, { useEffect } from 'react'; +import { useFormContext } from 'react-hook-form'; +import { + Checkbox, + ErrorComponentTemplate, +} from '../../../../../new-components/Form'; +import { CheckedState } from '@radix-ui/react-checkbox'; +import { FaExclamationCircle } from 'react-icons/fa'; + +type Props = { + fieldName: string; +}; + +export const ConsentCheckbox = (props: Props) => { + const { fieldName } = props; + const { watch, setValue, formState } = useFormContext(); + const field = watch(fieldName); + const [errorMessage, setErrorMessage] = React.useState(''); + + useEffect(() => { + if (field) { + setErrorMessage(''); + } else if (formState?.errors?.[fieldName]) { + setErrorMessage(formState?.errors?.[fieldName]?.message); + } else { + setErrorMessage(''); + } + }, [formState?.errors?.[fieldName], field]); + + const onCheckedChange = (value: CheckedState) => { + setValue(fieldName, value); + }; + return ( + <> + + By signing up for Hasura Enterprise Edition, you acknowledge that you + agree to our{' '} +
+ Terms of Service + {' '} + and{' '} + + Privacy Policy + + + {errorMessage ? ( + + + {errorMessage} + + } + ariaLabel={errorMessage ?? ''} + role="alert" + /> + ) : null} + + ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/Form.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/Form.stories.tsx new file mode 100644 index 00000000000..096f27731af --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/Form.stories.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { ReactQueryDecorator } from '../../../../../storybook/decorators/react-query'; +import { Dialog } from '../../../../../new-components/Dialog'; +import { + registerEETrialErrorMutation, + registerEETrialLicenseActiveMutation, + registerEETrialLicenseAlreadyAppliedMutation, +} from '../../../mocks/registration.mock'; +import { Form } from './Form'; + +export default { + title: 'features / EETrial / Activate EE Form / Form 🧬️', + component: Form, + decorators: [ReactQueryDecorator()], +} as ComponentMeta; + +export const Default: ComponentStory = () => ( + {}} hasBackdrop> +
{}} /> +
+); +Default.storyName = '💠 Default'; +Default.parameters = { + msw: [registerEETrialLicenseActiveMutation], +}; + +export const GraphqlError: ComponentStory = () => ( + {}} hasBackdrop> + <> +
+ Tip: Fill and submit the form to see error states. +
+ {}} /> + +
+); +GraphqlError.storyName = '💠 GraphqlError'; +GraphqlError.parameters = { + msw: [registerEETrialErrorMutation], +}; + +export const LicenseAlreadyApplied: ComponentStory = () => ( + {}} hasBackdrop> + <> +
+ Tip: Fill and submit the form to see error states. +
+ {}} /> + +
+); +LicenseAlreadyApplied.storyName = '💠 License Already Applied'; +LicenseAlreadyApplied.parameters = { + msw: [registerEETrialLicenseAlreadyAppliedMutation], +}; + +export const ActivateExistingLicense: ComponentStory = () => ( + {}} hasBackdrop> + {}} formState="activate" /> + +); +Default.storyName = '💠 Activate Existing License'; +Default.parameters = { + msw: [registerEETrialLicenseActiveMutation], +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/Form.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/Form.tsx new file mode 100644 index 00000000000..fb1cdec3efe --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/Form.tsx @@ -0,0 +1,308 @@ +import React from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { InputField } from '../../../../../new-components/Form'; +import { Button } from '../../../../../new-components/Button'; +import { Analytics } from '../../../../Analytics'; +import { ConsentCheckbox } from './ConsentCheckbox'; +import { + ActivateEEFormSchema, + RegisterEEFormSchema, + registrationSchema, + activationSchema, +} from './schema'; +import { useRegisterEETrial } from './useRegisterEETrial'; +import { useActivateEETrial } from './useActivateEETrial'; +import { EE_TRIAL_DOCS_URL } from '../../../constants'; + +type FormState = 'register' | 'activate'; +type Props = { + onSuccess?: VoidFunction; + formState?: FormState; +}; + +export const Form: React.VFC = props => { + const { onSuccess } = props; + const [state, setState] = React.useState( + props.formState || 'register' + ); + + const onActivation = () => { + if (onSuccess) { + onSuccess(); + } + }; + + return ( +
+
+
+ {state === 'register' && ( + + )} + {state === 'activate' && ( + + )} +
+
+
+ ); +}; + +export const ActivationForm: React.FC = (props: Props) => { + const { onSuccess } = props; + + const { activateEETrial, isLoading, errorMessage } = + useActivateEETrial(onSuccess); + + const onSubmit = (data: ActivateEEFormSchema) => { + activateEETrial(data); + }; + + const methods = useForm({ + resolver: zodResolver(activationSchema), + }); + + const handleSubmitClick = () => { + methods.handleSubmit(onSubmit)(); + }; + + return ( + + +

+ Activate your free Hasura Enterprise trial license +

+
+ Unlock extra observability, security, and performance features for + your Hasura instance. +
+ + + + + + + {errorMessage ? ( +
+ {errorMessage} +
+ ) : null} +
+ + + +
+ +
+ ); +}; + +export const RegistrationForm: React.FC = (props: Props) => { + const { onSuccess } = props; + + const { registerEETrial, isLoading, errorMessage } = + useRegisterEETrial(onSuccess); + + const onSubmit = (data: RegisterEEFormSchema) => { + registerEETrial(data); + }; + + const methods = useForm({ + resolver: zodResolver(registrationSchema), + defaultValues: { consent: false }, + }); + + const handleSubmitClick = () => { + methods.handleSubmit(onSubmit)(); + }; + + return ( + +
+

+ Activate your free Hasura Enterprise trial license +

+
+ Unlock extra observability, security, and performance features for + your Hasura instance.  + + + Read more + + . + +
+
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + {errorMessage ? ( +
+ {errorMessage} +
+ ) : null} +
+ + + +
+
+
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/index.ts new file mode 100644 index 00000000000..1aa6463bca7 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/index.ts @@ -0,0 +1,2 @@ +export type { ActivateEEFormSchema } from './schema'; +export { Form } from './Form'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/schema.ts b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/schema.ts new file mode 100644 index 00000000000..989a53df563 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/schema.ts @@ -0,0 +1,46 @@ +import { z } from 'zod'; + +const internationalPhoneNumberRegex = + /\+(9[976]\d|8[987530]\d|6[987]\d|5[90]\d|42\d|3[875]\d|2[98654321]\d|9[8543210]|8[6421]|6[6543210]|5[87654321]|4[987654310]|3[9643210]|2[70]|7|1)\W*\d\W*\d\W*\d\W*\d\W*\d\W*\d\W*\d\W*\d\W*(\d{1,2})$/; + +export const registrationSchema = z.object({ + firstName: z.string().min(1, { message: 'Please add your first name' }), + lastName: z.string().min(1, { message: 'Please add your last name' }), + email: z + .string() + .min(1, { message: 'Please add your work email' }) + .email({ message: 'Please add a valid work email' }), + organization: z.string().min(1, { message: 'Please add your organization' }), + jobFunction: z.string(), + phoneNumber: z + .string() + .regex(internationalPhoneNumberRegex, { + message: 'Please add valid phone number', + }) + .optional() + .or(z.literal('')), + password: z.string().min(8).nonempty({ + message: + 'Password must include at least 8 characters and at most 64 characters with at least: 1 upper case letter, 1 lower case letter, 1 number and 1 special character.', + }), + consent: z + .boolean() + .refine( + value => value === true, + 'Please agree our Terms of Service and Privacy Policy' + ), +}); + +export const activationSchema = z.object({ + email: z + .string() + .min(1, { message: 'Please add your work email' }) + .email({ message: 'Please add a valid work email' }), + password: z.string().min(8).nonempty({ + message: + 'Password must include at least 8 characters and at most 64 characters with at least: 1 upper case letter, 1 lower case letter, 1 number and 1 special character.', + }), +}); + +export type RegisterEEFormSchema = z.infer; +export type ActivateEEFormSchema = z.infer; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/useActivateEETrial.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/useActivateEETrial.tsx new file mode 100644 index 00000000000..21fb5fa2021 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/useActivateEETrial.tsx @@ -0,0 +1,73 @@ +import React from 'react'; +import globals from '../../../../../Globals'; +import { ACTIVATE_EE_TRIALS_MUTATION } from '../../../constants'; +import { EETrialRegistrationResponse } from '../../../types'; +import { GraphQLError } from 'graphql'; +import { useMutation } from 'react-query'; +import { ActivateEEFormSchema } from './schema'; +import { eeTrialsControlPlaneClient } from '../../../utils'; +import { useClientCredentialsPost } from './useClientCredentialsPost'; + +export type ActivateEeTrialResponseWithError = { + data?: EETrialRegistrationResponse; + errors?: GraphQLError[]; +}; + +export type ActivateEeTrialMutationVariables = { + email: string; + password: string; +}; + +const activateEETrialMutationFn = (formData: ActivateEEFormSchema) => { + return eeTrialsControlPlaneClient.query< + ActivateEeTrialResponseWithError, + ActivateEeTrialMutationVariables + >(ACTIVATE_EE_TRIALS_MUTATION, { + email: formData.email, + password: formData.password, + }); +}; + +export const useActivateEETrial = (onSuccess?: VoidFunction) => { + const [errorMessage, setErrorMessage] = React.useState(''); + + const { post: postClientCredentials } = useClientCredentialsPost( + onSuccess, + msg => setErrorMessage('Error: ' + msg) + ); + + const { mutate, isLoading } = useMutation(activateEETrialMutationFn, { + onSuccess: data => { + if (data.data?.registerEETrial?.client_id) { + setErrorMessage(''); + postClientCredentials({ + clientId: data.data?.registerEETrial?.client_id, + clientSecret: data.data?.registerEETrial?.client_secret, + adminSecret: globals.adminSecret || '', + }); + } else if (data.errors && data.errors.length > 0) { + // As graphql does not return error codes, react-query will always consider a + // successful request, we have to parse the data to check for errors + setErrorMessage('Error: ' + data.errors[0].message); + } else { + setErrorMessage( + 'Something went wrong while activating your Enterprise Trial' + ); + } + }, + // there might still be network errors, etc. which could be caught here + onError: (error: Error) => { + setErrorMessage('Error: ' + error.message); + }, + }); + + const activateEETrial = (formData: ActivateEEFormSchema) => { + mutate(formData); + }; + + return { + activateEETrial, + isLoading, + errorMessage, + }; +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/useClientCredentialsPost.ts b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/useClientCredentialsPost.ts new file mode 100644 index 00000000000..8051b8a47de --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/useClientCredentialsPost.ts @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { useMutation } from 'react-query'; +import { useAppSelector } from '../../../../../storeHooks'; +import { Api } from '../../../../../hooks/apiUtils'; +import Endpoints from '../../../../../Endpoints'; + +type MutationArgs = { + clientId: string; + clientSecret: string; + adminSecret: string; +}; + +const postClientCreds = ( + clientId: string, + clientSecret: string, + adminSecretHeader: Record +) => { + // doing this to remove content-type from the data headers + return Api.post({ + headers: adminSecretHeader, + url: Endpoints.license, + body: { + client_id: clientId, + client_secret: clientSecret, + }, + }); +}; + +export const useClientCredentialsPost = ( + onSuccess?: VoidFunction, + onError?: (msg: string) => void +) => { + const headers = useAppSelector(state => state.tables.dataHeaders); + const { mutate, isLoading } = useMutation( + (args: MutationArgs) => + postClientCreds(args.clientId, args.clientSecret, headers), + { + onSuccess: () => { + if (onSuccess) { + onSuccess(); + } + }, + onError: (e: any) => { + if (onError && e?.message) { + onError(e.message); + } + }, + } + ); + + return { + post: mutate, + isLoading, + }; +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/useRegisterEETrial.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/useRegisterEETrial.tsx new file mode 100644 index 00000000000..059d4e938b7 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/Form/useRegisterEETrial.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { REGISTER_EE_TRIALS_MUTATION } from '../../../constants'; +import globals from '../../../../../Globals'; +import { EETrialRegistrationResponse } from '../../../types'; +import { GraphQLError } from 'graphql'; +import { useMutation } from 'react-query'; +import { RegisterEEFormSchema } from './schema'; +import { eeTrialsControlPlaneClient } from '../../../utils'; +import { useClientCredentialsPost } from './useClientCredentialsPost'; + +export type RegisterEeTrialResponseWithError = { + data?: EETrialRegistrationResponse; + errors?: GraphQLError[]; +}; + +export type RegisterEeTrialMutationVariables = { + first: string; + last: string; + email: string; + jobFunction: string; + organization: string; + phone: string; + password: string; +}; + +const registerEETrialMutationFn = (formData: RegisterEEFormSchema) => { + return eeTrialsControlPlaneClient.query< + RegisterEeTrialResponseWithError, + RegisterEeTrialMutationVariables + >(REGISTER_EE_TRIALS_MUTATION, { + first: formData.firstName, + last: formData.lastName, + email: formData.email, + jobFunction: formData.jobFunction, + organization: formData.organization, + phone: formData.phoneNumber as string, + // password: formData.password + password: formData.password, + }); +}; + +export const useRegisterEETrial = (onSuccess?: VoidFunction) => { + const [errorMessage, setErrorMessage] = React.useState(''); + + const { post: postClientCredentials } = useClientCredentialsPost( + onSuccess, + msg => setErrorMessage('Error: ' + msg) + ); + + const { mutate, isLoading } = useMutation(registerEETrialMutationFn, { + onSuccess: data => { + if (data.data?.registerEETrial?.client_id) { + setErrorMessage(''); + postClientCredentials({ + clientId: data.data?.registerEETrial?.client_id, + clientSecret: data.data?.registerEETrial?.client_secret, + adminSecret: globals.adminSecret || '', + }); + } else if (data.errors && data.errors.length > 0) { + // As graphql does not return error codes, react-query will always consider a + // successful request, we have to parse the data to check for errors + setErrorMessage('Error: ' + data.errors[0].message); + } else { + setErrorMessage( + 'Something went wrong while activating your Enterprise Trial' + ); + } + }, + // there might still be network errors, etc. which could be caught here + onError: (error: Error) => { + setErrorMessage('Error: ' + error.message); + }, + }); + + const registerEETrial = (formData: RegisterEEFormSchema) => { + mutate(formData); + }; + + return { + registerEETrial, + isLoading, + errorMessage, + }; +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/FormWrapper.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/FormWrapper.stories.tsx new file mode 100644 index 00000000000..a63ab7f7199 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/FormWrapper.stories.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query'; +import { registerEETrialLicenseActiveMutation } from '../../mocks/registration.mock'; + +import { FormWrapper } from './FormWrapper'; + +export default { + title: 'features / EETrial / Activate EE Form / Form Wrapper 🧬️', + component: FormWrapper, + decorators: [ReactQueryDecorator()], +} as ComponentMeta; + +export const Default: ComponentStory = () => ( + {}} showBenefitsView /> +); +Default.storyName = '💠 Default'; +Default.parameters = { + msw: [registerEETrialLicenseActiveMutation], +}; + +export const LicenseExpired: ComponentStory = () => ( + {}} showBenefitsView /> +); +LicenseExpired.storyName = '💠 License Expired'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/FormWrapper.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/FormWrapper.tsx new file mode 100644 index 00000000000..dad066c7e58 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/FormWrapper.tsx @@ -0,0 +1,76 @@ +import React, { useState } from 'react'; +import { Dialog } from '../../../../new-components/Dialog'; +import { BenefitsView } from '../BenefitsView'; +import { Form } from './Form'; +import { SuccessScreen } from './SuccessScreen/SuccessScreen'; +import { reactQueryClient } from '../../../../lib/reactQuery'; +import { EE_LICENSE_INFO_QUERY_NAME } from '../../constants'; + +type Props = { + /** + * Show `View Benefits` button on the success screen. + */ + showBenefitsView?: boolean; + /** + * Callback for the action to be performed on close of the form + */ + onFormClose?: VoidFunction; +}; + +export function FormWrapper(props: Props) { + const { onFormClose } = props; + return ( + + + + ); +} + +function FormStateMachine(props: Props) { + const { onFormClose, showBenefitsView = false } = props; + + const [formState, setFormState] = useState< + 'default' | 'successScreen' | 'benefitsScreen' + >('default'); + + if (formState === 'default') { + return ( +
{ + setFormState('successScreen'); + // on success, invalidate the license status stored in react query cache, + // overriding the stale time + reactQueryClient.invalidateQueries(EE_LICENSE_INFO_QUERY_NAME); + }} + /> + ); + } + + if (formState === 'successScreen') { + return ( + { + setFormState('benefitsScreen'); + }} + /> + ); + } + + if (formState === 'benefitsScreen') { + return ( + // TODO: remove hardcoded values + + ); + } + + return null; +} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/SuccessScreen/SuccessScreen.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/SuccessScreen/SuccessScreen.stories.tsx new file mode 100644 index 00000000000..8e93beb6128 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/SuccessScreen/SuccessScreen.stories.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import { SuccessScreen } from './SuccessScreen'; +import { Dialog } from '../../../../../new-components/Dialog'; + +export default { + title: 'features / EETrial / Activate EE Form / Success Screen 🧬️', + component: SuccessScreen, +} as ComponentMeta; + +export const Demo: ComponentStory = () => { + return ( + {}} hasBackdrop> + + + ); +}; +Demo.storyName = '💠 Demo'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/SuccessScreen/SuccessScreen.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/SuccessScreen/SuccessScreen.tsx new file mode 100644 index 00000000000..3e25ae6b17f --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/SuccessScreen/SuccessScreen.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { Button } from '../../../../../new-components/Button'; +import { FaArrowRight, FaCheck } from 'react-icons/fa'; +import { Analytics } from '../../../../Analytics'; + +type Props = { + /** + * Show `View Benefits` button in the bottom right + */ + showBenefitsButton?: boolean; + /** + * Callback for the action to be performed on `View Benefits` button click + */ + onViewBenefitsClick?: VoidFunction; + /** + * Callback for the action to be performed on `Close and Continue` button click + */ + onCloseClick?: VoidFunction; +}; + +export const SuccessScreen = (props: Props) => { + const { showBenefitsButton, onViewBenefitsClick, onCloseClick } = props; + return ( + <> +
+
+ +
+

+ Your 30-day trial of Hasura Enterprise has been activated +

+

+ What's next? +
+ Please restart your Hasura service in order to start using your new + Hasura Enterprise features. +

+

+ In Docker, you can restart your container using: +

+

+ docker restart [container-name] +

+

+ Read our{' '} + + Hasura Enterprise Edition documentation + {' '} + to learn how to get the most out of the features of your trial. +

+
+
+ {showBenefitsButton ? ( + + ) : null} + + + +
+ + ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/index.ts new file mode 100644 index 00000000000..88bf9f152e5 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ActivateEEForm/index.ts @@ -0,0 +1 @@ +export { FormWrapper as ActivateEEForm } from './FormWrapper'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ApiSecurityTab/ApiSecuritySvg.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ApiSecurityTab/ApiSecuritySvg.tsx new file mode 100644 index 00000000000..aa9544d276b --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ApiSecurityTab/ApiSecuritySvg.tsx @@ -0,0 +1,580 @@ +import React from 'react'; + +type Props = { + className?: string; +}; + +export function ApiSecuritySvg(props: Props) { + const { className } = props; + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ApiSecurityTab/ApiSecurityTab.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ApiSecurityTab/ApiSecurityTab.stories.tsx new file mode 100644 index 00000000000..549711d1df8 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ApiSecurityTab/ApiSecurityTab.stories.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query'; +import { + registerEETrialLicenseActiveMutation, + registerEETrialLicenseDeactivatedMutation, + registerEETrialLicenseExpiredMutation, +} from '../../mocks/registration.mock'; +import { ApiSecurityTabEELiteWrapper } from './ApiSecurityTab'; +import { SecurityTabs } from '../../../../components/Services/ApiExplorer/Security/SecurityTabs'; +import { eeLicenseInfo } from '../../mocks/http'; +import { useQueryClient } from 'react-query'; +import { EE_LICENSE_INFO_QUERY_NAME } from '../../constants'; + +export default { + title: 'features / EETrial / API Security Tab 🧬️', + component: ApiSecurityTabEELiteWrapper, + decorators: [ + // This is done so as we have set some cache time on the EE_LICENSE_INFO_QUERY_NAME query. + // So we need to refetch the cache data, so it doesn't persist across different stories. And + // it makes sure that our component actually does the network call, letting msw mocks return the + // desired response. + Story => { + const queryClient = useQueryClient(); + queryClient.refetchQueries(EE_LICENSE_INFO_QUERY_NAME); + return ; + }, + ReactQueryDecorator(), + ], +} as ComponentMeta; + +export const Default: ComponentStory< + typeof ApiSecurityTabEELiteWrapper +> = () => { + return ( + + + + ); +}; +Default.storyName = '💠 Default'; +Default.parameters = { + msw: [registerEETrialLicenseActiveMutation, eeLicenseInfo.none], + consoleType: 'pro-lite', +}; + +export const LicenseActive: ComponentStory< + typeof ApiSecurityTabEELiteWrapper +> = () => { + return ( + + + + ); +}; +LicenseActive.storyName = '💠 License Active'; +LicenseActive.parameters = { + msw: [registerEETrialLicenseActiveMutation, eeLicenseInfo.active], + consoleType: 'pro-lite', +}; + +export const LicenseExpired: ComponentStory< + typeof ApiSecurityTabEELiteWrapper +> = () => { + return ( + + + + ); +}; +LicenseExpired.storyName = '💠 License Expired'; +LicenseExpired.parameters = { + msw: [registerEETrialLicenseExpiredMutation, eeLicenseInfo.expired], + consoleType: 'pro-lite', +}; + +export const LicenseDeactivated: ComponentStory< + typeof ApiSecurityTabEELiteWrapper +> = () => { + return ( + + + + ); +}; +LicenseDeactivated.storyName = '💠 License Deactivated'; +LicenseDeactivated.parameters = { + msw: [registerEETrialLicenseDeactivatedMutation, eeLicenseInfo.deactivated], + consoleType: 'pro-lite', +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ApiSecurityTab/ApiSecurityTab.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ApiSecurityTab/ApiSecurityTab.tsx new file mode 100644 index 00000000000..411ac720121 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ApiSecurityTab/ApiSecurityTab.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import globals from '../../../../Globals'; +import { ApiSecuritySvg } from './ApiSecuritySvg'; +import { useEELiteAccess } from '../../hooks/useEELiteAccess'; +import { EETrialCard } from '../EETrialCard/EETrialCard'; + +type Props = { + children?: React.ReactElement; +}; + +// This tab shows an example component of how the EE registration button and hooks for fetching +// license info can be used to build a promotional component for EE, behind which the actual +// feature can live. +// +// This component has a check for pro-lite and license status. If the license is not active we show the component +// specific EE promotion UI. And use the Enable Enterprise button wrapper to start the registration flow. +export function ApiSecurityTabEELiteWrapper(props: Props) { + const { children } = props; + const { access } = useEELiteAccess(globals); + + if ( + globals.consoleType === 'cloud' || + globals.consoleType === 'pro' || + access === 'active' + ) { + return children ?? null; + } + + if (access === 'forbidden') { + return null; + } + + return ( +
+
+
+ API Security +
+
+ + Enable advanced security options to help secure your GraphQL API for + production. + + + (Know More) + +
+ + + Add additional security features to your API such as depth / node + limits, rate limiting (RPM), batch requests limits, timeouts, and + schema introspection. + + } + buttonLabel="Enable Enterprise" + eeAccess={access} + horizontal + /> +
+
+ ); +} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/BenefitsView.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/BenefitsView.stories.tsx new file mode 100644 index 00000000000..92bbcc7747d --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/BenefitsView.stories.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; + +import { BenefitsView } from './BenefitsView'; +import { Dialog } from '../../../../new-components/Dialog'; + +export default { + title: 'features/EETrial/ BenefitsView 🧬️', + parameters: { + Benefits: { + source: { type: 'code' }, + }, + }, + component: BenefitsView, +} as ComponentMeta; + +export const NoEnterpriseLicense: ComponentStory< + typeof BenefitsView +> = args => ( + {}} hasBackdrop> + + +); + +export const ActiveEnterpriceLicense: ComponentStory< + typeof BenefitsView +> = args => ( + {}} hasBackdrop> + + +); + +export const ExpiredEnterpriseLicenseWithGrace: ComponentStory< + typeof BenefitsView +> = args => ( + {}} hasBackdrop> + + +); + +export const ExpiredEnterpriseLicenseWithoutGrace: ComponentStory< + typeof BenefitsView +> = args => ( + {}} hasBackdrop> + + +); + +export const ExpiredEnterpriseLicenseAfterGrace: ComponentStory< + typeof BenefitsView +> = args => ( + {}} hasBackdrop> + + +); + +export const DeactivatedEnterpriseLicenseAfterGrace: ComponentStory< + typeof BenefitsView +> = args => ( + {}} hasBackdrop> + + +); diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/BenefitsView.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/BenefitsView.tsx new file mode 100644 index 00000000000..7854cc6d767 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/BenefitsView.tsx @@ -0,0 +1,183 @@ +import React from 'react'; +import { Badge } from '../../../../new-components/Badge'; +import { Button } from '../../../../new-components/Button'; +import { + FaClock, + FaCopy, + FaDatabase, + FaExternalLinkAlt, + FaShieldAlt, + // FaUsers, + FaTimesCircle, +} from 'react-icons/fa'; +import PrometheusLogo from '../../assets/logo-prometheus.svg'; +import IconOpenTelemetry from '../../assets/icon-opentelemetry.svg'; +import IconQueryCaching from '../../assets/icon-query-caching.png'; +import { ListItem } from './ListItem'; +import { ListHeader } from './ListHeader'; +import { EELicenseInfo } from '../../types'; +import { getDaysFromNow } from '../../utils'; +import { EE_TRIAL_CONTACT_US_URL } from '../../constants'; +import { Analytics } from '../../../Analytics'; + +type EETrialBenefitsProps = { + licenseInfo: EELicenseInfo; +}; + +export const BenefitsView = (props: EETrialBenefitsProps) => { + const { + licenseInfo: { status }, + } = props; + const badgeBgClassName = + status === 'active' ? 'bg-secondary-200' : 'bg-red-200'; + const badgeTextClassName = + status === 'active' ? 'text-secondary-600' : 'text-red-600'; + + let expirationMessage = ''; + let expiryBannerBadge: React.ReactNode = null; + let ctaButtonText = 'Enable Enterprise'; + let ctaButtonIcon: React.ReactElement | undefined; + + const benefitsHeaderText = 'Benefits of Hasura Enterprise Edition'; + switch (props.licenseInfo.status) { + case 'active': { + const expiryDaysFromNow = Math.abs( + getDaysFromNow(props.licenseInfo.expiry_at) + ); + const expiryDaysText = + expiryDaysFromNow === 1 + ? `${expiryDaysFromNow} day` + : `${expiryDaysFromNow} days`; + expiryBannerBadge = ; + expirationMessage = `Your Enterprise license is expiring in ${expiryDaysText}.`; + ctaButtonText = 'Renew License'; + ctaButtonIcon = ; + break; + } + case 'deactivated': { + expirationMessage = `Your Enterprise license has been deactivated. Please get in touch.`; + expiryBannerBadge = ( + + ); + ctaButtonText = 'Get In Touch'; + ctaButtonIcon = ; + break; + } + case 'expired': { + expirationMessage = `Your Enterprise license has expired`; + expiryBannerBadge = ( + + ); + ctaButtonText = 'Renew License'; + ctaButtonIcon = ; + break; + } + case 'none': + default: + expirationMessage = ''; + ctaButtonText = 'Get License'; + ctaButtonIcon = ; + expiryBannerBadge = null; + break; + } + + return ( + <> +
+
+ {expirationMessage && ( + + {expiryBannerBadge} +
{expirationMessage}
+
+ )} +

+ {benefitsHeaderText} +

+
+ + + + + + } + url="https://hasura.io/docs/latest/databases/database-config/read-replicas/" + /> + {/* } + url="https://hasura.io/docs/latest/databases/connect-db/dynamic-db-connection/" + /> +*/} + + } + url="https://hasura.io/docs/latest/databases/overview/" + /> + + {/* } + url="https://hasura.io/docs/latest/hasura-cloud/sso/" + /> +*/}{' '} + } + url="https://hasura.io/docs/latest/security/allow-list/#role-based-allow-list" + /> + } + url="https://hasura.io/docs/latest/queries/response-caching/#rate-limiting" + /> + } + url="https://hasura.io/docs/latest/security/disable-graphql-introspection/" + /> +
+ + + ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/ListHeader.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/ListHeader.tsx new file mode 100644 index 00000000000..9035bd1060a --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/ListHeader.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +type Props = { + label: string; +}; + +export function ListHeader(props: Props) { + const { label } = props; + return ( +
+ {label} +
+ ); +} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/ListItem.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/ListItem.tsx new file mode 100644 index 00000000000..2f1ce1a0b7f --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/ListItem.tsx @@ -0,0 +1,27 @@ +import React, { ReactElement } from 'react'; +import { Analytics } from '../../../Analytics'; + +type Props = { + label: string; + icon: string | ReactElement; + url: string; + id: string; +}; + +export function ListItem(props: Props) { + const { label, icon, url, id } = props; + return ( + +
+ {typeof icon === 'string' ? ( + {label} + ) : ( +
{icon}
+ )} + +

{label}

+
+
+
+ ); +} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/WithEEBenefits.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/WithEEBenefits.stories.tsx new file mode 100644 index 00000000000..d5c36859378 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/WithEEBenefits.stories.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query'; +import { Button } from '../../../../new-components/Button'; +import { eeLicenseInfo } from '../../mocks/http'; +import { WithEEBenefits } from './WithEEBenefits'; +import { useQueryClient } from 'react-query'; +import { EE_LICENSE_INFO_QUERY_NAME } from '../../constants'; + +export default { + title: 'features/EETrial/ BenefitsView 🧬️', + parameters: { + Benefits: { + source: { type: 'code' }, + }, + }, + component: WithEEBenefits, + decorators: [ + // This is done so as we have set some cache time on the EE_LICENSE_INFO_QUERY_NAME query. + // So we need to refetch the cache data, so it doesn't persist across different stories. And + // it makes sure that our component actually does the network call, letting msw mocks return the + // desired response. + Story => { + const queryClient = useQueryClient(); + queryClient.refetchQueries(EE_LICENSE_INFO_QUERY_NAME); + return ; + }, + ReactQueryDecorator(), + ], +} as ComponentMeta; + +export const ButtonWithEEBenefits: ComponentStory< + typeof WithEEBenefits +> = args => ( +
+ + + +
+); + +ButtonWithEEBenefits.parameters = { + msw: [eeLicenseInfo.active], +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/WithEEBenefits.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/WithEEBenefits.tsx new file mode 100644 index 00000000000..a34654f1704 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/WithEEBenefits.tsx @@ -0,0 +1,56 @@ +import * as React from 'react'; +import { Dialog } from '../../../../new-components/Dialog'; +import { BenefitsView } from './BenefitsView'; +import { useEELicenseInfo } from '../../hooks/useEELicenseInfo'; +import { EELicenseInfo } from '../../types'; +import globals from '../../../../Globals'; + +export const WithEEBenefits: React.FC<{ + children: React.ReactNode; + id: string; + 'data-testid'?: string; +}> = props => { + const { children, id } = props; + const [show, setShow] = React.useState(false); + + const { + data: licenseData, + error, + isLoading, + } = useEELicenseInfo({ + enabled: globals.consoleType === 'pro-lite', + }); + + let licenseInfo: EELicenseInfo; + if (isLoading || error || !licenseData) { + licenseInfo = { + status: 'none', + type: 'trial', + expiry_at: new Date(), + }; + } else { + licenseInfo = licenseData; + } + + const toggleEEBenefits = () => { + setShow(s => !s); + }; + + return ( + <> + {show && ( + + + + )} +
+ {children} +
+ + ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/index.ts new file mode 100644 index 00000000000..ff952508a41 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/BenefitsView/index.ts @@ -0,0 +1 @@ +export { BenefitsView } from './BenefitsView'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EETrialCard/EETrialCard.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EETrialCard/EETrialCard.stories.tsx new file mode 100644 index 00000000000..8626c9683a4 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EETrialCard/EETrialCard.stories.tsx @@ -0,0 +1,187 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { QueryClient, QueryClientProvider } from 'react-query'; + +import { EETrialCard } from './EETrialCard'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + cacheTime: 0, + }, + }, +}); + +export default { + title: 'features/EETrial/ EETrialCard 🧬', + parameters: { + docs: { + description: { + component: `A card displaying advantages of pro console.
+Default CSS display is \`block\`, provided without padding and margin (displayed here with \`padding: 1rem;\`)`, + }, + source: { type: 'code' }, + }, + }, + decorators: [ + Story => { + window.localStorage.getItem = () => { + return JSON.stringify([ + { + enabled: true, + dismissed: false, + enableDate: '2022-11-23T16:50:45.080Z', + id: 'f996c937-7935-4f2f-8120-b06eab0e39b9', + }, + ]); + }; + return ( + +
+ {Story()} +
+
+ ); + }, + ], + component: EETrialCard, +} as ComponentMeta; + +export const ApiPlayground: ComponentStory = args => ( + +); +ApiPlayground.storyName = '⚙️ API'; +ApiPlayground.args = { + cardTitle: 'title', + cardText: 'text', + buttonLabel: 'buttonLabel', +}; + +export const Basic: ComponentStory = () => ( + +); +Basic.storyName = '🧰 Basic'; + +export const TrialExpired: ComponentStory = () => ( + +); +TrialExpired.storyName = '🧰 Trial Expired'; + +export const TrialDeactivated: ComponentStory = () => ( + +); +TrialDeactivated.storyName = '🧰 Trial Deactivated'; + +export const VariantHorizontal: ComponentStory = () => ( + <> + + + +); +VariantHorizontal.storyName = '🎭 Variant - Horizontal'; +VariantHorizontal.parameters = { + docs: { + source: { state: 'open' }, + }, +}; + +export const VariantDisabled: ComponentStory = () => ( + +); +VariantDisabled.storyName = '🎭 Variant - Disabled'; +VariantDisabled.parameters = { + docs: { + source: { state: 'open' }, + }, +}; + +export const ForPrometheus: ComponentStory = () => ( + <> + + Get production-ready today with a 30-day free trial{' '} + of Hasura EE, no credit card required. + + } + buttonLabel="Get Started with EE" + horizontal + className="w-full" + /> + + Get production-ready today with a 30-day free trial{' '} + of Hasura EE, no credit card required. + + } + buttonLabel="Get Started with EE" + horizontal + /> + +); +ForPrometheus.storyName = '💠 Demo for Prometheus'; +ForPrometheus.parameters = { + docs: { + source: { state: 'open' }, + }, +}; + +export const ForAllowList: ComponentStory = () => ( + <> + + + +); +ForAllowList.storyName = '💠 Demo for Allow List'; +ForAllowList.parameters = { + docs: { + source: { state: 'open' }, + }, +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EETrialCard/EETrialCard.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EETrialCard/EETrialCard.tsx new file mode 100644 index 00000000000..671d38417f2 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EETrialCard/EETrialCard.tsx @@ -0,0 +1,154 @@ +import clsx from 'clsx'; +import React from 'react'; +import Skeleton from 'react-loading-skeleton'; +import { useQueryClient } from 'react-query'; +import { Button, ButtonProps } from '../../../../new-components/Button'; +import { + EE_LICENSE_INFO_QUERY_NAME, + EE_TRIAL_CONTACT_US_URL, +} from '../../constants'; +import { EELiteAccessStatus } from '../../types'; +import { EnableEEButtonWrapper } from '../EnableEnterpriseButton/EnableEEButton'; +import { ErrorMessage } from '../ErrorMessage/ErrorMessage'; +import { LoadingMessage } from '../LoadingMessage/LoadingMessage'; +import { Analytics } from '../../../Analytics'; + +interface EETrialCardProps extends React.ComponentProps<'div'> { + /** + * The card title. + */ + cardTitle?: React.ReactNode; + /** + * The card text + */ + cardText?: React.ReactNode; + /** + * The card button label + */ + buttonLabel?: string; + /** + * The card button type + */ + buttonType?: ButtonProps['mode']; + /** + * The card orientation + */ + horizontal?: boolean; + /** + * EE lite access status + */ + eeAccess?: EELiteAccessStatus; + + id: string; +} + +export const EETrialCard = ({ + cardTitle = '', + cardText = '', + horizontal = false, + buttonLabel = 'Enable Enterprise', + buttonType = 'primary', + eeAccess = 'active', + className, + id, +}: EETrialCardProps) => { + const queryClient = useQueryClient(); + const cardClassName = clsx( + 'flex bg-white border-2 shadow-sm p-5 rounded', + !horizontal && 'flex-col', + className + ); + const isButtonFull = !horizontal; + + const handleFormClose = React.useCallback(() => { + queryClient.invalidateQueries(EE_LICENSE_INFO_QUERY_NAME); + }, [queryClient]); + + const enableButtonDisabled = + eeAccess === 'expired' || + eeAccess === 'deactivated' || + eeAccess === 'forbidden'; + + return ( +
+
+
+ {eeAccess === 'loading' ? ( + + ) : ( +
{cardTitle}
+ )} + {eeAccess === 'loading' ? ( + + ) : ( +
{cardText}
+ )} +
+
+ + {eeAccess === 'loading' ? ( + + ) : ( + + + + )} + +
+
+ {eeAccess === 'loading' ? ( + + ) : null} + {eeAccess === 'deactivated' && ( + + Your EE trial has been deactivated. Please{' '} + + contact us + {' '} + for more info. + + } + /> + )} + {eeAccess === 'expired' && ( + + Your EE trial has expired. Please{' '} + + contact us + {' '} + for more info. + + } + /> + )} +
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ETAutoCleanupWrapper/ETAutoCleanupWrapper.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ETAutoCleanupWrapper/ETAutoCleanupWrapper.stories.tsx new file mode 100644 index 00000000000..d3863cd5548 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ETAutoCleanupWrapper/ETAutoCleanupWrapper.stories.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query'; +import { AutoCleanupForm } from '../../../../components/Services/Events/EventTriggers/Common/AutoCleanupForm'; +import { + registerEETrialLicenseActiveMutation, + registerEETrialLicenseDeactivatedMutation, + registerEETrialLicenseExpiredMutation, +} from '../../mocks/registration.mock'; +import { ETAutoCleanupWrapper } from './ETAutoCleanupWrapper'; +import { eeLicenseInfo } from '../../mocks/http'; +import { EE_LICENSE_INFO_QUERY_NAME } from '../../constants'; +import { useQueryClient } from 'react-query'; + +export default { + title: 'features / EETrial / Event Trigger Auto Cleanup Card 🧬️', + component: ETAutoCleanupWrapper, + decorators: [ + // This is done so as we have set some cache time on the EE_LICENSE_INFO_QUERY_NAME query. + // So we need to refetch the cache data, so it doesn't persist across different stories. And + // it makes sure that our component actually does the network call, letting msw mocks return the + // desired response. + Story => { + const queryClient = useQueryClient(); + queryClient.refetchQueries(EE_LICENSE_INFO_QUERY_NAME); + return ; + }, + ReactQueryDecorator(), + ], +} as ComponentMeta; + +export const Default: ComponentStory = () => { + return ( + + {}} /> + + ); +}; +Default.storyName = '💠 Default'; +Default.parameters = { + msw: [registerEETrialLicenseActiveMutation, eeLicenseInfo.none], + consoleType: 'pro-lite', +}; + +export const LicenseActive: ComponentStory< + typeof ETAutoCleanupWrapper +> = () => { + return ( + + {}} /> + + ); +}; +LicenseActive.storyName = '💠 License Active'; +LicenseActive.parameters = { + msw: [registerEETrialLicenseActiveMutation, eeLicenseInfo.active], + consoleType: 'pro-lite', +}; + +export const LicenseExpired: ComponentStory< + typeof ETAutoCleanupWrapper +> = () => { + return ( + + {}} /> + + ); +}; +LicenseExpired.storyName = '💠 License Expired'; +LicenseExpired.parameters = { + msw: [registerEETrialLicenseExpiredMutation, eeLicenseInfo.expired], + consoleType: 'pro-lite', +}; + +export const LicenseDeactivated: ComponentStory< + typeof ETAutoCleanupWrapper +> = () => { + return ( + + {}} /> + + ); +}; +LicenseDeactivated.storyName = '💠 License Deactivated'; +LicenseDeactivated.parameters = { + msw: [registerEETrialLicenseDeactivatedMutation, eeLicenseInfo.deactivated], + consoleType: 'pro-lite', +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ETAutoCleanupWrapper/ETAutoCleanupWrapper.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ETAutoCleanupWrapper/ETAutoCleanupWrapper.tsx new file mode 100644 index 00000000000..859ddcf65eb --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ETAutoCleanupWrapper/ETAutoCleanupWrapper.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import globals from '../../../../Globals'; +import { useEELiteAccess } from '../../hooks/useEELiteAccess'; +import { EETrialCard } from '../EETrialCard/EETrialCard'; + +type Props = { + children?: React.ReactElement; +}; + +export function ETAutoCleanupWrapper(props: Props) { + const { children } = props; + const { access } = useEELiteAccess(globals); + + if ( + globals.consoleType === 'cloud' || + globals.consoleType === 'pro' || + access === 'active' + ) { + return children ?? null; + } + + if (access === 'forbidden') { + return null; + } + + return ( +
+ + Reduce database log bloat by setting granular event-log cleanup + rules on a global and per-event basis. + + } + buttonLabel="Enable Enterprise" + buttonType="default" + eeAccess={access} + horizontal + /> +
+ ); +} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ETAutoCleanupWrapper/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ETAutoCleanupWrapper/index.ts new file mode 100644 index 00000000000..5e2c29b6452 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ETAutoCleanupWrapper/index.ts @@ -0,0 +1 @@ +export { ETAutoCleanupWrapper } from './ETAutoCleanupWrapper'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EnableEnterpriseButton/EnableEEButton.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EnableEnterpriseButton/EnableEEButton.stories.tsx new file mode 100644 index 00000000000..0f9f4961312 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EnableEnterpriseButton/EnableEEButton.stories.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query'; +import { registerEETrialLicenseActiveMutation } from '../../mocks/registration.mock'; +import { EnableEEButtonWrapper } from './EnableEEButton'; +import { Button } from '../../../../new-components/Button'; + +export default { + title: 'features / EETrial / EnableEEButtonWrapper 🧬️', + component: EnableEEButtonWrapper, + parameters: { + msw: [registerEETrialLicenseActiveMutation], + }, + decorators: [ReactQueryDecorator()], +} as ComponentMeta; + +export const Demo: ComponentStory = () => ( + + + +); +Demo.storyName = '💠 Demo'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EnableEnterpriseButton/EnableEEButton.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EnableEnterpriseButton/EnableEEButton.tsx new file mode 100644 index 00000000000..aa3bba84663 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EnableEnterpriseButton/EnableEEButton.tsx @@ -0,0 +1,58 @@ +import React, { useState } from 'react'; +import { ActivateEEForm } from '../ActivateEEForm'; + +type Props = { + children?: React.ReactNode; + /** + * Show `View Benefits` button on the success screen. + */ + showBenefitsView?: boolean; + /** + * Trigger when the form is closed + */ + onFormClose?: VoidFunction; + /** + * Disabled state so form will not appear if button is disabled + */ + disabled?: boolean; +}; + +/** + * Component which contains the button wrapper, which start the registration flow. + * This button should only show up if the user is not registered for EE trial. + */ +export function EnableEEButtonWrapper(props: Props) { + const { + children, + showBenefitsView = false, + onFormClose = () => {}, + disabled, + } = props; + const [showForm, setShowForm] = useState(false); + + return ( + <> +
{ + if (disabled !== true) { + setShowForm(true); + } + }} + > + {children} +
+ {showForm ? ( +
+ { + setShowForm(false); + onFormClose(); + }} + showBenefitsView={showBenefitsView} + /> +
+ ) : null} + + ); +} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EnableEnterpriseButton/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EnableEnterpriseButton/index.ts new file mode 100644 index 00000000000..f96efe557a3 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/EnableEnterpriseButton/index.ts @@ -0,0 +1 @@ +export { EnableEEButtonWrapper } from './EnableEEButton'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ErrorMessage/ErrorMessage.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ErrorMessage/ErrorMessage.stories.tsx new file mode 100644 index 00000000000..32c104f9152 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ErrorMessage/ErrorMessage.stories.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { ErrorMessage } from './ErrorMessage'; + +export default { + title: 'features / EETrial / EE Error Message 🧬️', + component: ErrorMessage, +} as ComponentMeta; + +export const Default: ComponentStory = () => { + return Some error occured} />; +}; +Default.storyName = '💠 Default'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ErrorMessage/ErrorMessage.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ErrorMessage/ErrorMessage.tsx new file mode 100644 index 00000000000..72b3a4a8524 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/ErrorMessage/ErrorMessage.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { FaExclamationCircle } from 'react-icons/fa'; + +import { ErrorComponentTemplate } from '../../../../new-components/Form'; + +type Props = { + message: React.ReactElement; +}; + +export function ErrorMessage(props: Props) { + const { message } = props; + return ( + + + {message} + + } + role="alert" + /> + ); +} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/LoadingMessage/LoadingMessage.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/LoadingMessage/LoadingMessage.stories.tsx new file mode 100644 index 00000000000..e25f3dbd9f5 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/LoadingMessage/LoadingMessage.stories.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { LoadingMessage } from './LoadingMessage'; + +export default { + title: 'features / EETrial / EE Loading Message 🧬️', + component: LoadingMessage, +} as ComponentMeta; + +export const Default: ComponentStory = () => { + return ; +}; +Default.storyName = '💠 Default'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/LoadingMessage/LoadingMessage.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/LoadingMessage/LoadingMessage.tsx new file mode 100644 index 00000000000..822f09a61e0 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/LoadingMessage/LoadingMessage.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { CgSpinner } from 'react-icons/cg'; + +type Props = { + message: string; + showIcon?: boolean; +}; + +export function LoadingMessage(props: Props) { + const { message, showIcon = true } = props; + return ( +
+ {showIcon ? : null} + {message} +
+ ); +} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleAdminSecrets/MultipleAdminSecretsPage.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleAdminSecrets/MultipleAdminSecretsPage.stories.tsx new file mode 100644 index 00000000000..5f522209f3c --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleAdminSecrets/MultipleAdminSecretsPage.stories.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query'; +import { registerEETrialLicenseActiveMutation } from '../../mocks/registration.mock'; +import { MultipleAdminSecretsPage } from './MultipleAdminSecretsPage'; +import { eeLicenseInfo } from '../../mocks/http'; +import { useQueryClient } from 'react-query'; +import { EE_LICENSE_INFO_QUERY_NAME } from '../../constants'; + +export default { + title: 'features / EETrial / Multiple Admin Secrets Page 🧬️', + component: MultipleAdminSecretsPage, + decorators: [ + // This is done so as we have set some cache time on the EE_LICENSE_INFO_QUERY_NAME query. + // So we need to refetch the cache data, so it doesn't persist across different stories. And + // it makes sure that our component actually does the network call, letting msw mocks return the + // desired response. + Story => { + const queryClient = useQueryClient(); + queryClient.refetchQueries(EE_LICENSE_INFO_QUERY_NAME); + return ; + }, + ReactQueryDecorator(), + ], +} as ComponentMeta; + +export const Default: ComponentStory = () => { + return ; +}; +Default.storyName = '💠 Default'; +Default.parameters = { + msw: [registerEETrialLicenseActiveMutation, eeLicenseInfo.none], + consoleType: 'pro-lite', +}; + +export const LicenseActive: ComponentStory< + typeof MultipleAdminSecretsPage +> = () => { + return ; +}; +LicenseActive.storyName = '💠 License Active'; +LicenseActive.parameters = { + msw: [registerEETrialLicenseActiveMutation, eeLicenseInfo.active], + consoleType: 'pro-lite', +}; + +export const LicenseExpired: ComponentStory< + typeof MultipleAdminSecretsPage +> = () => { + return ; +}; +LicenseExpired.storyName = '💠 License Expired'; +LicenseExpired.parameters = { + msw: [eeLicenseInfo.expired], + consoleType: 'pro-lite', +}; + +export const LicenseDeactivated: ComponentStory< + typeof MultipleAdminSecretsPage +> = () => { + return ; +}; +LicenseDeactivated.storyName = '💠 License Deactivated'; +LicenseDeactivated.parameters = { + msw: [eeLicenseInfo.deactivated], + consoleType: 'pro-lite', +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleAdminSecrets/MultipleAdminSecretsPage.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleAdminSecrets/MultipleAdminSecretsPage.tsx new file mode 100644 index 00000000000..15f595836f1 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleAdminSecrets/MultipleAdminSecretsPage.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { MultipleAdminSecretsSvg } from './MultipleAdminSecretsSvg'; +import { EETrialCard } from '../EETrialCard/EETrialCard'; +import { useEELiteAccess } from '../../hooks/useEELiteAccess'; +import globals from '../../../../Globals'; + +export const MultipleAdminSecretsPage = () => { + const { access } = useEELiteAccess(globals); + const isFeatureForbidden = access === 'forbidden'; + + const isFeatureActive = access === 'active'; + + if (isFeatureForbidden) return null; + + return ( +
+
+
+ Multiple Admin Secrets +
+
+ + Enable access to your Hasura instance using multiple + x-hasura-admin-secrets. + + + (Know More) + +
+ + {isFeatureActive ? ( +

+ Setup Multiple Admin Secrets +
+ + Read more + {' '} + on setting up multiple admin secrets for your Hasura instance. +
+ Multiple admin secrets may be enabled by setting the environment + variable:HASURA_GRAPHQL_ADMIN_SECRETS +

+ ) : ( + + Implement security mechanisms like rotating secrets and have + different lifecycles for individual admin secrets. + + } + buttonLabel="Enable Enterprise" + eeAccess={access} + horizontal + /> + )} +
+
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleAdminSecrets/MultipleAdminSecretsSvg.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleAdminSecrets/MultipleAdminSecretsSvg.tsx new file mode 100644 index 00000000000..6c89d9312f8 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleAdminSecrets/MultipleAdminSecretsSvg.tsx @@ -0,0 +1,697 @@ +import React from 'react'; + +type Props = { + className?: string; +}; + +export function MultipleAdminSecretsSvg(props: Props) { + const { className } = props; + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleAdminSecrets/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleAdminSecrets/index.ts new file mode 100644 index 00000000000..1a0045101a3 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleAdminSecrets/index.ts @@ -0,0 +1 @@ +export * from './MultipleAdminSecretsPage'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleJWTSecrets/MultipleJWTSecretsPage.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleJWTSecrets/MultipleJWTSecretsPage.stories.tsx new file mode 100644 index 00000000000..d0336f7d6bc --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleJWTSecrets/MultipleJWTSecretsPage.stories.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query'; +import { + registerEETrialLicenseActiveMutation, + registerEETrialLicenseDeactivatedMutation, + registerEETrialLicenseExpiredMutation, +} from '../../mocks/registration.mock'; +import { MultipleJWTSecretsPage } from './MultipleJWTSecretsPage'; +import { eeLicenseInfo } from '../../mocks/http'; +import { useQueryClient } from 'react-query'; +import { EE_LICENSE_INFO_QUERY_NAME } from '../../constants'; + +export default { + title: 'features / EETrial / Multiple JWT Secrets Page 🧬️', + component: MultipleJWTSecretsPage, + decorators: [ + // This is done so as we have set some cache time on the EE_LICENSE_INFO_QUERY_NAME query. + // So we need to refetch the cache data, so it doesn't persist across different stories. And + // it makes sure that our component actually does the network call, letting msw mocks return the + // desired response. + Story => { + const queryClient = useQueryClient(); + queryClient.refetchQueries(EE_LICENSE_INFO_QUERY_NAME); + return ; + }, + ReactQueryDecorator(), + ], +} as ComponentMeta; + +export const Default: ComponentStory = () => { + return ; +}; +Default.storyName = '💠 Default'; +Default.parameters = { + msw: [registerEETrialLicenseActiveMutation, eeLicenseInfo.none], + consoleType: 'pro-lite', +}; + +export const LicenseActive: ComponentStory< + typeof MultipleJWTSecretsPage +> = () => { + return ; +}; +LicenseActive.storyName = '💠 License Active'; +LicenseActive.parameters = { + msw: [registerEETrialLicenseActiveMutation, eeLicenseInfo.active], + consoleType: 'pro-lite', +}; + +export const LicenseExpired: ComponentStory< + typeof MultipleJWTSecretsPage +> = () => { + return ; +}; +LicenseExpired.storyName = '💠 License Expired'; +LicenseExpired.parameters = { + msw: [registerEETrialLicenseExpiredMutation, eeLicenseInfo.expired], + consoleType: 'pro-lite', +}; + +export const LicenseDeactivated: ComponentStory< + typeof MultipleJWTSecretsPage +> = () => { + return ; +}; +LicenseDeactivated.storyName = '💠 License Deactivated'; +LicenseDeactivated.parameters = { + msw: [registerEETrialLicenseDeactivatedMutation, eeLicenseInfo.deactivated], + consoleType: 'pro-lite', +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleJWTSecrets/MultipleJWTSecretsPage.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleJWTSecrets/MultipleJWTSecretsPage.tsx new file mode 100644 index 00000000000..7571269f740 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleJWTSecrets/MultipleJWTSecretsPage.tsx @@ -0,0 +1,70 @@ +import React from 'react'; +import { MultipleJWTSecretsSvg } from './MultipleJWTSecretsSvg'; +import { EETrialCard } from '../EETrialCard/EETrialCard'; +import { useEELiteAccess } from '../../hooks/useEELiteAccess'; +import globals from '../../../../Globals'; + +export const MultipleJWTSecretsPage = () => { + const { access } = useEELiteAccess(globals); + const isFeatureForbidden = access === 'forbidden'; + + const isFeatureActive = access === 'active'; + + if (isFeatureForbidden) return null; + + return ( +
+
+
+ Multiple JWT Secrets +
+
+ + Enable access to your Hasura instance using multiple JSON web token + secrets + + + (Know More) + +
+ + {isFeatureActive ? ( +

+ Setup Multiple Admin Secrets +
+ + Read more + {' '} + on setting up multiple JWT secrets for your Hasura instance. +
+ Multiple admin secrets may be enabled by setting the environment + variable: HASURA_GRAPHQL_JWT_SECRETS +

+ ) : ( + + Get production-ready today with a 30-day free trial of Hasura + EE, no credit card required. + + } + buttonLabel="Enable Enterprise" + eeAccess={access} + horizontal + /> + )} +
+
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleJWTSecrets/MultipleJWTSecretsSvg.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleJWTSecrets/MultipleJWTSecretsSvg.tsx new file mode 100644 index 00000000000..f2cf477b334 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleJWTSecrets/MultipleJWTSecretsSvg.tsx @@ -0,0 +1,938 @@ +import React from 'react'; + +type Props = { + className?: string; +}; + +export function MultipleJWTSecretsSvg(props: Props) { + const { className } = props; + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleJWTSecrets/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleJWTSecrets/index.ts new file mode 100644 index 00000000000..d5c82381b59 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/MultipleJWTSecrets/index.ts @@ -0,0 +1 @@ +export * from './MultipleJWTSecretsPage'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/NavbarButton.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/NavbarButton.stories.tsx new file mode 100644 index 00000000000..fbb9b843a71 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/NavbarButton.stories.tsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { ReactQueryDecorator } from '../../../storybook/decorators/react-query'; +import { eeLicenseInfo } from '../mocks/http'; + +import { NavbarButton as EnterpriseButton } from './NavbarButton'; +import { useQueryClient } from 'react-query'; +import { EE_LICENSE_INFO_QUERY_NAME } from '../constants'; + +export default { + title: 'features/EETrial/NavbarButton', + component: EnterpriseButton, + decorators: [ + // This is done so as we have set some cache time on the EE_LICENSE_INFO_QUERY_NAME query. + // So we need to refetch the cache data, so it doesn't persist across different stories. And + // it makes sure that our component actually does the network call, letting msw mocks return the + // desired response. + Story => { + const queryClient = useQueryClient(); + queryClient.refetchQueries(EE_LICENSE_INFO_QUERY_NAME); + return ; + }, + ReactQueryDecorator(), + ], +} as ComponentMeta; + +export const Loading: ComponentStory = args => ( +
+ +
+); +Loading.parameters = { + consoleType: 'pro-lite', +}; + +export const NoEnterpriseLicense: ComponentStory< + typeof EnterpriseButton +> = args => ( +
+ +
+); +NoEnterpriseLicense.parameters = { + msw: [eeLicenseInfo.none], + consoleType: 'pro-lite', +}; + +export const ActiveEnterpriceLicense: ComponentStory< + typeof EnterpriseButton +> = args => ( +
+ +
+); +ActiveEnterpriceLicense.parameters = { + msw: [eeLicenseInfo.active], + consoleType: 'pro-lite', +}; + +export const ExpiredEnterpriseLicenseWithGrace: ComponentStory< + typeof EnterpriseButton +> = args => ( +
+ +
+); +ExpiredEnterpriseLicenseWithGrace.parameters = { + msw: [eeLicenseInfo.expired], + consoleType: 'pro-lite', +}; + +export const ExpiredEnterpriseLicenseWithoutGrace: ComponentStory< + typeof EnterpriseButton +> = args => ( +
+ +
+); +ExpiredEnterpriseLicenseWithGrace.parameters = { + msw: [eeLicenseInfo.expiredWithoutGrace], + consoleType: 'pro-lite', +}; + +export const ExpiredEnterpriseLicenseAfterGrace: ComponentStory< + typeof EnterpriseButton +> = args => ( +
+ +
+); +ExpiredEnterpriseLicenseAfterGrace.parameters = { + msw: [eeLicenseInfo.expiredAfterGrace], + consoleType: 'pro-lite', +}; + +export const DeactivatedEnterpriseLicense: ComponentStory< + typeof EnterpriseButton +> = args => ( +
+ +
+); +DeactivatedEnterpriseLicense.parameters = { + msw: [eeLicenseInfo.deactivated], + consoleType: 'pro-lite', +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/NavbarButton.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/NavbarButton.tsx new file mode 100644 index 00000000000..e6dbe6ce0bc --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/NavbarButton.tsx @@ -0,0 +1,175 @@ +import * as React from 'react'; +import { Button } from '../../../new-components/Button'; +import globals from '../../../Globals'; +import { FaStar, FaTimesCircle } from 'react-icons/fa'; +import { useEELiteAccess } from '../hooks/useEELiteAccess'; +import { EELiteAccess } from '../types'; +import { WithEEBenefits } from './BenefitsView/WithEEBenefits'; +import { getDaysFromNow } from '../utils'; +import { EnableEEButtonWrapper } from './EnableEnterpriseButton'; +import { Analytics } from '../../Analytics'; + +export const NavbarButton: React.VFC<{ + className?: string; + globals: typeof globals; +}> = props => { + const eeLite = useEELiteAccess(globals); + const { access } = eeLite; + + if (access === 'forbidden') { + return null; + } + + return ( +
+ +
+ ); +}; + +type ButtonProps = { + accessInfo: EELiteAccess; +}; + +export const EnterpriseButton: React.VFC = props => { + const { accessInfo } = props; + + switch (accessInfo.access) { + case 'active': { + switch (accessInfo.kind) { + case 'grace': { + return ( + + + + ); + } + case 'default': + default: { + const daysFromNow = Math.abs(getDaysFromNow(accessInfo.expires_at)); + const daysFromNowDisplayText = + daysFromNow === 1 ? `${daysFromNow} day` : `${daysFromNow} days`; + return ( + + + + ); + } + } + } + case 'expired': { + return ( + + + + ); + } + case 'deactivated': { + return ( + + + + ); + } + + case 'eligible': { + return ( + + + + ); + } + case 'forbidden': + case 'loading': + default: { + return null; + } + } +}; + +type EEButtonProps = + | { + kind: 'active'; + text: string; + } + | { + kind: 'inactive'; + primaryText: string; + secondaryText: string; + } + | { + kind: 'loading'; + text: string; + }; +export const EEButton: React.FC = props => { + const { kind } = props; + switch (kind) { + case 'active': { + const { text } = props; + return ( + + + + ); + } + case 'inactive': { + const { primaryText, secondaryText } = props; + return ( + + + + ); + } + case 'loading': { + const { text } = props; + return ( + + + + ); + } + default: + return null; + } +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/SingleSignOn/SingleSignOnPage.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/SingleSignOn/SingleSignOnPage.stories.tsx new file mode 100644 index 00000000000..32adc5c58cc --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/SingleSignOn/SingleSignOnPage.stories.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query'; +import { registerEETrialLicenseActiveMutation } from '../../mocks/registration.mock'; +import { SingleSignOnPage } from './SingleSignOnPage'; +import { eeLicenseInfo } from '../../mocks/http'; +import { useQueryClient } from 'react-query'; +import { EE_LICENSE_INFO_QUERY_NAME } from '../../constants'; + +export default { + title: 'features / EETrial / Single Sign On (SSO) Page 🧬️', + component: SingleSignOnPage, + decorators: [ + // This is done so as we have set some cache time on the EE_LICENSE_INFO_QUERY_NAME query. + // So we need to refetch the cache data, so it doesn't persist across different stories. And + // it makes sure that our component actually does the network call, letting msw mocks return the + // desired response. + Story => { + const queryClient = useQueryClient(); + queryClient.refetchQueries(EE_LICENSE_INFO_QUERY_NAME); + return ; + }, + ReactQueryDecorator(), + ], +} as ComponentMeta; + +export const Default: ComponentStory = () => { + return ; +}; +Default.storyName = '💠 Default'; +Default.parameters = { + msw: [registerEETrialLicenseActiveMutation, eeLicenseInfo.none], + consoleType: 'pro-lite', +}; + +export const LicenseActive: ComponentStory = () => { + return ; +}; +LicenseActive.storyName = '💠 License Active'; +LicenseActive.parameters = { + msw: [registerEETrialLicenseActiveMutation, eeLicenseInfo.active], + consoleType: 'pro-lite', +}; + +export const LicenseExpired: ComponentStory = () => { + return ; +}; +LicenseExpired.storyName = '💠 License Expired'; +LicenseExpired.parameters = { + msw: [eeLicenseInfo.expired], + consoleType: 'pro-lite', +}; + +export const LicenseDeactivated: ComponentStory< + typeof SingleSignOnPage +> = () => { + return ; +}; +LicenseDeactivated.storyName = '💠 License Deactivated'; +LicenseDeactivated.parameters = { + msw: [eeLicenseInfo.deactivated], + consoleType: 'pro-lite', +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/SingleSignOn/SingleSignOnPage.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/SingleSignOn/SingleSignOnPage.tsx new file mode 100644 index 00000000000..d7551dbed00 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/SingleSignOn/SingleSignOnPage.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { SingleSignOnSvg } from './SingleSignOnSvg'; +import { useEELiteAccess } from '../../hooks/useEELiteAccess'; +import { EETrialCard } from '../EETrialCard/EETrialCard'; +import globals from '../../../../Globals'; + +export const SingleSignOnPage = () => { + const { access } = useEELiteAccess(globals); + + return ( +
+
+
+ Single Sign On (SSO) +
+
+ + Enable secure organization access to manage your Hasura instance by + integrating with single sign-on (SSO) + + + (Know More) + +
+ + {access === 'active' || + globals.consoleType === 'cloud' || + globals.consoleType === 'pro' ? ( +

+ Setup Single Sign-On (SSO) +
+ + Read more + {' '} + on setting up multiple single sign-on (SSO) for your Hasura instance + and your organization. +

+ ) : ( + + Get production-ready today with a 30-day free trial of Hasura + EE, no credit card required. + + } + buttonLabel="Enable Enterprise" + eeAccess={access} + horizontal + /> + )} +
+
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/SingleSignOn/SingleSignOnSvg.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/SingleSignOn/SingleSignOnSvg.tsx new file mode 100644 index 00000000000..f8e7552b489 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/SingleSignOn/SingleSignOnSvg.tsx @@ -0,0 +1,407 @@ +import React from 'react'; + +type Props = { + className?: string; +}; + +export function SingleSignOnSvg(props: Props) { + const { className } = props; + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/SingleSignOn/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/SingleSignOn/index.ts new file mode 100644 index 00000000000..5aaf92226d2 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/SingleSignOn/index.ts @@ -0,0 +1 @@ +export * from './SingleSignOnPage'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/WithEELiteAccess.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/WithEELiteAccess.tsx new file mode 100644 index 00000000000..e8ab752f601 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/WithEELiteAccess.tsx @@ -0,0 +1,18 @@ +import * as React from 'react'; +import { useEELiteAccess } from '../hooks/useEELiteAccess'; +import { EELiteAccess } from '../types'; +import Globals from '../../../Globals'; + +type Props = { + children: (result: EELiteAccess) => React.ReactNode; + globals: typeof Globals; +}; +/* + This component uses the render-prop pattern to allow using + the logic from `useEELiteAcces` hook in React class copmonents +*/ +export const WithEELiteAccess = (props: Props) => { + const { children, globals } = props; + const access = useEELiteAccess(globals); + return children(access); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/WithLicenseInfo.tsx b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/WithLicenseInfo.tsx new file mode 100644 index 00000000000..ebb6356eb46 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/components/WithLicenseInfo.tsx @@ -0,0 +1,16 @@ +import * as React from 'react'; +import { UseQueryResult } from 'react-query'; +import { useEELicenseInfo } from '../hooks/useEELicenseInfo'; +import { EELicenseInfo } from '../types'; + +type Props = { + children: (result: UseQueryResult) => React.ReactNode; +}; +/* + This component uses the render-prop pattern to allow using + the logic from `useEELicenseInfo` hook in React class copmonents +*/ +export const WithLicenseInfo = (props: Props) => { + const licenseInfoResult = useEELicenseInfo(); + return props.children(licenseInfoResult); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/constants.ts b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/constants.ts new file mode 100644 index 00000000000..98ace90f825 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/constants.ts @@ -0,0 +1,54 @@ +import { parse as gql } from 'graphql'; +export const eeApiHeaders = {}; +export const EE_LICENSE_INFO_QUERY_NAME = 'EE_LICENSE_INFO_QUERY_NAME'; +export const LICENSE_REFRESH_INTERVAL = 3600000; +export const EE_TRIAL_DOCS_URL = + 'https://hasura.io/docs/latest/enterprise/try-hasura-enterprise-edition'; + +/** + * GraphQl mutation to register the user for EE trial + */ +export const REGISTER_EE_TRIALS_MUTATION = gql(` + mutation registerEETrial( + $first: String! + $last: String! + $email: String! + $jobFunction: String! + $organization: String! + $phone: String! + $password: String! + ) { + registerEETrial( + first: $first, + last: $last, + email: $email, + jobFunction: $jobFunction, + organization: $organization, + phone: $phone + password: $password + ){ + client_id + client_secret + } + } +`); + +/** + * GraphQl mutation to activate an existing EE trial license + */ +export const ACTIVATE_EE_TRIALS_MUTATION = gql(` + mutation registerEETrial( + $email: String! + $password: String! + ) { + registerEETrial( + email: $email + password: $password + ){ + client_id + client_secret + } + } +`); + +export const EE_TRIAL_CONTACT_US_URL = 'https://hasura.io/contact-us-eetrial'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/hooks/useEELicenseInfo.ts b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/hooks/useEELicenseInfo.ts new file mode 100644 index 00000000000..f44b2ff82ff --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/hooks/useEELicenseInfo.ts @@ -0,0 +1,21 @@ +import { useQuery } from 'react-query'; +import { useAppSelector } from '../../../storeHooks'; +import { fetchEELicenseInfo } from '../utils'; +import { + EE_LICENSE_INFO_QUERY_NAME, + LICENSE_REFRESH_INTERVAL, +} from '../constants'; + +export const useEELicenseInfo = (opts?: { enabled: boolean }) => { + const headers = useAppSelector(state => state.tables.dataHeaders); + return useQuery({ + queryKey: EE_LICENSE_INFO_QUERY_NAME, + queryFn: () => { + return fetchEELicenseInfo(headers); + }, + refetchOnMount: false, + refetchOnWindowFocus: true, + staleTime: LICENSE_REFRESH_INTERVAL, + enabled: opts?.enabled !== false, + }); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/hooks/useEELiteAccess.ts b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/hooks/useEELiteAccess.ts new file mode 100644 index 00000000000..80a41d2e656 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/hooks/useEELiteAccess.ts @@ -0,0 +1,24 @@ +import globals from '../../../Globals'; +import { useEELicenseInfo } from './useEELicenseInfo'; +import { EELiteAccess } from '../types'; +import { transformEntitlementToAccess } from '../utils'; + +export const useEELiteAccess = (g: typeof globals): EELiteAccess => { + const { data, error, isLoading } = useEELicenseInfo({ + enabled: g.consoleType === 'pro-lite', + }); + + if (isLoading) { + return { + access: 'loading', + }; + } + + if (error || !data) { + return { + access: 'forbidden', + }; + } + + return transformEntitlementToAccess(data); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/index.ts new file mode 100644 index 00000000000..c0c4a5215b7 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/index.ts @@ -0,0 +1,20 @@ +export { fetchEELicenseInfo, prefetchEELicenseInfo } from './utils'; + +export { + EE_LICENSE_INFO_QUERY_NAME, + EE_TRIAL_CONTACT_US_URL, +} from './constants'; + +export { useEELicenseInfo } from './hooks/useEELicenseInfo'; +export { useEELiteAccess } from './hooks/useEELiteAccess'; +export { WithEELiteAccess } from './components/WithEELiteAccess'; +export type { EELicenseInfo, EELiteAccess, EELiteAccessStatus } from './types'; + +export { NavbarButton } from './components/NavbarButton'; +export { EETrialCard } from './components/EETrialCard/EETrialCard'; +export { EnableEEButtonWrapper } from './components/EnableEnterpriseButton'; +export { ApiSecurityTabEELiteWrapper } from './components/ApiSecurityTab/ApiSecurityTab'; +export { MultipleAdminSecretsPage } from './components/MultipleAdminSecrets'; +export { MultipleJWTSecretsPage } from './components/MultipleJWTSecrets'; +export { SingleSignOnPage } from './components/SingleSignOn/SingleSignOnPage'; +export { ETAutoCleanupWrapper } from './components/ETAutoCleanupWrapper'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/mocks/http.ts b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/mocks/http.ts new file mode 100644 index 00000000000..e3e7ac77e1c --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/mocks/http.ts @@ -0,0 +1,84 @@ +import { rest } from 'msw'; +import Endpoints from '../../../Endpoints'; + +export const eeLicenseInfo = { + active: rest.get(Endpoints.entitlement, async (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + status: 'active', + type: 'trial', + expiry_at: new Date(new Date().getTime() + 100000000), + grace_at: new Date(), + }) + ); + }), + expired: rest.get(Endpoints.entitlement, async (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + status: 'expired', + type: 'trial', + expiry_at: new Date(new Date().getTime() - 100000000), + grace_at: new Date(), + }) + ); + }), + expiredWithoutGrace: rest.get( + Endpoints.entitlement, + async (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + status: 'expired', + type: 'trial', + expiry_at: new Date(new Date().getTime() - 100000000), + }) + ); + } + ), + expiredAfterGrace: rest.get(Endpoints.entitlement, async (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + status: 'expired', + type: 'trial', + expiry_at: new Date(new Date().getTime() - 1000000000), + grace_at: new Date(new Date().getTime() - 2000000000), + }) + ); + }), + deactivated: rest.get(Endpoints.entitlement, async (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + status: 'deactivated', + type: 'trial', + expiry_at: new Date(), + grace_at: new Date(), + }) + ); + }), + none: rest.get(Endpoints.entitlement, async (req, res, ctx) => { + return res( + ctx.status(200), + ctx.json({ + status: 'none', + type: 'trial', + expiry_at: new Date(), + grace_at: new Date(), + }) + ); + }), + noneOnce: rest.get(Endpoints.entitlement, async (req, res, ctx) => { + return res.once( + ctx.status(200), + ctx.json({ + status: 'none', + type: 'trial', + expiry_at: new Date(), + grace_at: new Date(), + }) + ); + }), +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/mocks/registration.mock.ts b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/mocks/registration.mock.ts new file mode 100644 index 00000000000..a4afe8b8736 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/mocks/registration.mock.ts @@ -0,0 +1,73 @@ +import { graphql } from 'msw'; +import { eeTrialsLuxDataEndpoint } from '../utils'; +import { GraphQLError } from 'graphql'; +import { EETrialRegistrationResponse } from '../types'; + +const controlPlaneApi = graphql.link(eeTrialsLuxDataEndpoint); + +export const registerEETrialLicenseActiveMutation = + controlPlaneApi.mutation( + 'registerEETrial', + (req, res, ctx) => { + return res( + ctx.status(200), + ctx.data({ + registerEETrial: { + client_id: 'id', + client_secret: 'secret', + }, + }) + ); + } + ); + +export const registerEETrialErrorMutation = controlPlaneApi.mutation< + GraphQLError[] +>('registerEETrial', (req, res, ctx) => { + return res( + ctx.status(200), + ctx.errors([ + { + extensions: { + code: 'legacyError', + }, + message: "couldn't find registerEETrial in mutation_root", + }, + ]) + ); +}); + +export const registerEETrialLicenseAlreadyAppliedMutation = + controlPlaneApi.mutation( + 'registerEETrial', + (req, res, ctx) => { + return res( + ctx.status(200), + ctx.errors([ + { + extensions: { + code: 'legacyError', + id: '8fae8d3f-e411-4476-b28d-12cfbd715c21', + }, + message: 'license already applied', + }, + ]) + ); + } + ); + +export const activateEETrialMutatationSuccess = + controlPlaneApi.mutation( + 'registerEETrial', + (req, res, ctx) => { + return res( + ctx.status(200), + ctx.data({ + registerEETrial: { + client_id: 'id', + client_secret: 'secret', + }, + }) + ); + } + ); diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/types.ts b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/types.ts new file mode 100644 index 00000000000..0e287af259e --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/types.ts @@ -0,0 +1,60 @@ +export type EELicenseType = 'trial' | 'paid'; + +export type EELicenseRegisterMutationResponse = { + registerEETrial: { + status: 'active' | 'expired' | 'deactivated'; + type: EELicenseType; + expiry_at: string; + grace_at?: string; + }; +}; + +export type EELiteAccess = + | { + access: 'forbidden'; + } + | { + access: 'loading'; + } + | { + access: 'active'; + license: EELicenseInfo; + expires_at: Date; + kind: 'default' | 'grace'; + } + | { + access: 'expired'; + license: EELicenseInfo; + } + | { + access: 'deactivated'; + license: EELicenseInfo; + } + | { + access: 'eligible'; + }; + +export type EELicenseInfo = + | { + status: 'active' | 'expired' | 'deactivated'; + type: EELicenseType; + expiry_at: Date; + grace_at?: Date; + } + | { + status: 'none'; + type: EELicenseType; + expiry_at?: Date; + grace_at?: Date; + }; + +export type EELicenseStatus = EELicenseInfo['status']; + +export type EELiteAccessStatus = EELiteAccess['access']; + +export type EETrialRegistrationResponse = { + registerEETrial: { + client_id: string; + client_secret: string; + }; +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/utils.test.ts b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/utils.test.ts new file mode 100644 index 00000000000..3692cd637c5 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/utils.test.ts @@ -0,0 +1,77 @@ +import { transformEntitlementToAccess } from './utils'; +import { EELicenseInfo } from './types'; + +describe('transformEntitlementToAccess', () => { + it('for status none, has access eligible', () => { + expect( + transformEntitlementToAccess({ + status: 'none', + type: 'trial', + }) + ).toEqual({ + access: 'eligible', + }); + }); + it('for status active, has access active with kind default', () => { + const license: EELicenseInfo = { + status: 'active', + type: 'trial', + expiry_at: new Date(new Date().getTime() + 200000000), + }; + expect(transformEntitlementToAccess(license)).toEqual({ + access: 'active', + expires_at: license['expiry_at'], + license, + kind: 'default', + }); + }); + it('for status expired before grace, has access active with kind grace', () => { + const license: EELicenseInfo = { + status: 'expired', + type: 'trial', + expiry_at: new Date(new Date().getTime() - 200000000), + grace_at: new Date(new Date().getTime() + 200000000), + }; + expect(transformEntitlementToAccess(license)).toEqual({ + access: 'active', + expires_at: license['grace_at'], + license, + kind: 'grace', + }); + }); + it('for status expired without grace, has access expired', () => { + const license: EELicenseInfo = { + status: 'expired', + type: 'trial', + expiry_at: new Date(new Date().getTime() - 200000000), + }; + expect(transformEntitlementToAccess(license)).toEqual({ + access: 'expired', + license, + }); + }); + it('for status expired after grace, has access expired', () => { + const license: EELicenseInfo = { + status: 'expired', + type: 'trial', + expiry_at: new Date(new Date().getTime() - 200000000), + grace_at: new Date(new Date().getTime() - 100000000), + }; + expect(transformEntitlementToAccess(license)).toEqual({ + access: 'expired', + license, + }); + }); + it('for status deactivated, has access deactivated', () => { + const license: EELicenseInfo = { + status: 'deactivated', + type: 'trial', + expiry_at: new Date(new Date().getTime() + 200000000), + grace_at: new Date(new Date().getTime() + 300000000), + }; + expect(transformEntitlementToAccess(license)).toEqual({ + access: 'deactivated', + license, + }); + }); +}); diff --git a/frontend/libs/console/legacy-ce/src/lib/features/EETrial/utils.ts b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/utils.ts new file mode 100644 index 00000000000..6b934f48a55 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/utils.ts @@ -0,0 +1,120 @@ +import { Api } from '../../hooks/apiUtils'; +import moment from 'moment'; +import { EELicenseInfo, EELiteAccess } from './types'; +import { reactQueryClient } from '../../lib/reactQuery'; +import { + EE_LICENSE_INFO_QUERY_NAME, + LICENSE_REFRESH_INTERVAL, +} from './constants'; +import Endpoints from '../../Endpoints'; +import { createControlPlaneClient } from '../ControlPlane'; +import endpoints from '../../Endpoints'; + +export const fetchEELicenseInfo = (headers: Record) => { + return Api.get( + { + headers, + url: Endpoints.entitlement, + }, + (resp: any) => { + const licenseInfo: EELicenseInfo = { + status: resp.status, + type: resp.type, + expiry_at: resp.expiry_at ? new Date(resp.expiry_at) : undefined, + grace_at: resp.grace_at ? new Date(resp.grace_at) : undefined, + }; + return licenseInfo; + } + ); +}; + +export const prefetchEELicenseInfo = (headers: Record) => { + reactQueryClient.prefetchQuery({ + queryKey: EE_LICENSE_INFO_QUERY_NAME, + queryFn: () => { + return fetchEELicenseInfo(headers); + }, + staleTime: LICENSE_REFRESH_INTERVAL, + }); +}; + +export const getExpiryDetails = ( + expiry_at: Date, + grace_at?: Date +): { + status: 'grace' | 'expired'; + expiresAt: moment.Moment; +} => { + const expiry = grace_at ? moment(grace_at) : moment(expiry_at); + const status = grace_at + ? grace_at.getTime() > new Date().getTime() + ? 'grace' + : 'expired' + : 'expired'; + + return { + status, + expiresAt: expiry, + }; +}; + +export const getDaysFromNow = (refDate: Date) => { + const momentRef = moment(refDate); + const momentNow = moment(new Date()); + return momentNow.diff(momentRef, 'days'); +}; + +export const eeTrialsLuxDataEndpoint = endpoints.registerEETrial; + +export const eeTrialsControlPlaneClient = createControlPlaneClient( + eeTrialsLuxDataEndpoint, + { + 'x-hasura-role': 'public', + } +); + +export const transformEntitlementToAccess = ( + data: EELicenseInfo +): EELiteAccess => { + switch (data.status) { + case 'active': { + return { + access: 'active', + license: data, + expires_at: new Date(data.expiry_at), + kind: 'default', + }; + } + case 'expired': { + const { status, expiresAt } = getExpiryDetails( + data.expiry_at, + data.grace_at + ); + if (status === 'grace') { + return { + access: 'active', + license: data, + expires_at: expiresAt.toDate(), + kind: 'grace', + }; + } else { + return { + access: 'expired', + license: data, + }; + } + } + case 'deactivated': { + return { + access: 'deactivated', + license: data, + }; + } + case 'none': + default: { + return { + access: 'eligible', + }; + } + } +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/FeatureFlags/availableFeatureFlags.ts b/frontend/libs/console/legacy-ce/src/lib/features/FeatureFlags/availableFeatureFlags.ts index 6cecf8dc9ea..1c6d37baeeb 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/FeatureFlags/availableFeatureFlags.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/FeatureFlags/availableFeatureFlags.ts @@ -1,13 +1,28 @@ import { FeatureFlagDefinition } from './types'; +import { isProConsole } from '../../utils/proConsole'; +import globals from '../../Globals'; const relationshipTabTablesId = 'f6c57c31-abd3-46d9-aae9-b97435793273'; const importActionFromOpenApiId = '12e5aaf4-c794-4b8f-b762-5fda0bff946a'; const enabledNewUIForBigQuery = 'e2d790ba-96fb-11ed-a8fc-0242ac120002'; +const connectDBRedesign = '532492b6-adca-11ed-afa1-0242ac120002'; + +const importActionFromOpenApi: FeatureFlagDefinition = { + id: importActionFromOpenApiId, + title: 'Import Action from OpenAPI', + description: + 'Try out the very experimental feature to generate one action from an OpenAPI endpoint', + section: 'data', + status: 'experimental', + defaultValue: false, + discussionUrl: '', +}; export const availableFeatureFlagIds = { relationshipTabTablesId, importActionFromOpenApiId, enabledNewUIForBigQuery, + connectDBRedesign, }; export const availableFeatureFlags: FeatureFlagDefinition[] = [ @@ -30,4 +45,15 @@ export const availableFeatureFlags: FeatureFlagDefinition[] = [ defaultValue: true, discussionUrl: '', }, + { + id: connectDBRedesign, + title: 'Enable the revamped UI for Connect database experience', + description: 'Try out the new UI experience for connecting a database.', + section: 'data', + status: 'alpha', + defaultValue: true, + discussionUrl: '', + }, + // eslint-disable-next-line no-underscore-dangle + ...(isProConsole(globals) ? [importActionFromOpenApi] : []), ]; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ManageAgents/_test_/useAddAgent.spec.ts b/frontend/libs/console/legacy-ce/src/lib/features/ManageAgents/_test_/useAddAgent.spec.ts deleted file mode 100644 index 9b1ea115962..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/features/ManageAgents/_test_/useAddAgent.spec.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks'; -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; -import { wrapper } from '../../../hooks/__tests__/common/decorator'; -import { useAddAgent } from '../hooks'; - -const server = setupServer( - rest.post('http://localhost/v1/metadata', (req, res, ctx) => { - if ((req.body as Record).args.name === 'wrong_payload') - return res(ctx.status(400), ctx.json({ message: 'Bad request' })); - return res(ctx.status(200), ctx.json({ message: 'success' })); - }) -); - -describe('useAddAgent tests: ', () => { - beforeAll(() => { - server.listen(); - jest.spyOn(console, 'error').mockImplementation(() => null); - }); - afterAll(() => { - server.close(); - jest.spyOn(console, 'error').mockRestore(); - }); - - it('calls the custom success callback after adding a DC agent', async () => { - const { result, waitFor } = renderHook(() => useAddAgent(), { wrapper }); - - const { addAgent } = result.current; - - const mockCallback = jest.fn(() => { - console.log('success'); - }); - - addAgent({ - name: 'test_dc_agent', - url: 'http://localhost:8001', - onSuccess: () => { - mockCallback(); - }, - }); - - await waitFor(() => result.current.isSuccess); - - await waitFor(() => { - expect(mockCallback).toHaveBeenCalledTimes(1); - }); - }); - - it('calls the custom error callback after failing to add a DC agent', async () => { - const { result, waitFor } = renderHook(() => useAddAgent(), { wrapper }); - - const { addAgent } = result.current; - - const mockCallback = jest.fn(() => { - console.log('error'); - }); - - addAgent({ - name: 'wrong_payload', - url: '', - onError: () => { - mockCallback(); - }, - }); - - await waitFor(() => result.current.isError); - - await waitFor(() => { - expect(mockCallback).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ManageAgents/components/AddAgentForm.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ManageAgents/components/AddAgentForm.tsx index 9747be73a4e..beaf133fd47 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ManageAgents/components/AddAgentForm.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/ManageAgents/components/AddAgentForm.tsx @@ -1,6 +1,5 @@ import { Button } from '../../../new-components/Button'; import { InputField, SimpleForm } from '../../../new-components/Form'; -import React from 'react'; import { z } from 'zod'; import { useAddAgent } from '../hooks/useAddAgent'; @@ -22,7 +21,11 @@ export const AddAgentForm = (props: CreateAgentFormProps) => { const handleSubmit = (values: FormValues) => { addAgent({ ...values, - onSuccess: props.onSuccess, + }).then(response => { + response.makeToast(); + if (response.status === 'added') { + props?.onSuccess?.(); + } }); }; @@ -30,46 +33,44 @@ export const AddAgentForm = (props: CreateAgentFormProps) => { - <> -
-

- Connect a Data Connector Agent -

-
+
+

+ Connect a Data Connector Agent +

+
- + - -
- - -
+ +
+ +
- +
); }; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ManageAgents/components/ManageAgentsTable.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ManageAgents/components/ManageAgentsTable.tsx index 45918c25752..4ecca257ea8 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/ManageAgents/components/ManageAgentsTable.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/ManageAgents/components/ManageAgentsTable.tsx @@ -42,7 +42,7 @@ export const ManageAgentsTable = () => { {agent.url} -
+
); } diff --git a/frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetryFeature.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetryFeature.stories.tsx new file mode 100644 index 00000000000..1b6cfa23102 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetryFeature.stories.tsx @@ -0,0 +1,148 @@ +import React from 'react'; +import { ComponentStory, ComponentMeta } from '@storybook/react'; +import { rest, DelayMode } from 'msw'; +import { QueryClient, QueryClientProvider } from 'react-query'; +import { ReactQueryDevtools } from 'react-query/devtools'; + +import { OpenTelemetryFeature } from './OpenTelemetryFeature'; +import { eeLicenseInfo } from '../EETrial/mocks/http'; +import { registerEETrialLicenseActiveMutation } from '../EETrial/mocks/registration.mock'; +import { HasuraMetadataV3 } from '../../metadata/types'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + cacheTime: 0, + }, + }, +}); +const baseUrl = 'http://localhost:8080'; +// eslint-disable-next-line no-underscore-dangle +window.__env = { + // eslint-disable-next-line no-underscore-dangle + ...window.__env, + dataApiUrl: baseUrl, +}; + +const mockMetadataHandler = ( + openTelemetryEnabled: boolean, + delay: number | DelayMode, + status = 200 +) => { + return rest.post(`${baseUrl}/v1/metadata`, (req, res, ctx) => { + let result: HasuraMetadataV3 = { + version: 3, + sources: [], + inherited_roles: [], + }; + if (openTelemetryEnabled) { + result = { + ...result, + opentelemetry: { + status: 'enabled', + exporter_otlp: { + headers: [], + protocol: 'http/protobuf', + resource_attributes: [], + otlp_traces_endpoint: '', + }, + data_types: [], + batch_span_processor: { + max_export_batch_size: 0, + }, + }, + }; + } else { + result = { + ...result, + opentelemetry: { + status: 'disabled', + exporter_otlp: { + headers: [], + protocol: 'http/protobuf', + resource_attributes: [], + otlp_traces_endpoint: '', + }, + data_types: [], + batch_span_processor: { + max_export_batch_size: 0, + }, + }, + }; + } + return res( + ctx.status(status), + ctx.delay(delay), + ctx.json({ metadata: result }) + ); + }); +}; + +export default { + title: 'Features/OpenTelemetry/Feature', + component: OpenTelemetryFeature, + parameters: { + docs: { disable: true }, + }, + decorators: [ + (Story: React.FC) => ( + + + + + ), + ], +} as ComponentMeta; + +export const DisabledWithoutLicense: ComponentStory< + typeof OpenTelemetryFeature +> = () => { + return OpenTelemetryFeature() ||
; +}; +DisabledWithoutLicense.storyName = '💠 Demo Feature Disabled without license'; +DisabledWithoutLicense.parameters = { + msw: [ + mockMetadataHandler(false, 1), + eeLicenseInfo.noneOnce, + registerEETrialLicenseActiveMutation, + eeLicenseInfo.active, + ], + consoleType: 'pro-lite', +}; + +export const Loading: ComponentStory = () => { + return OpenTelemetryFeature() ||
; +}; +Loading.storyName = '💠 Demo Feature Loading'; +Loading.parameters = { + msw: [mockMetadataHandler(true, 'infinite'), eeLicenseInfo.active], + consoleType: 'pro-lite', +}; + +export const Enabled: ComponentStory = () => { + return OpenTelemetryFeature() ||
; +}; +Enabled.storyName = '💠 Demo Feature Enabled'; +Enabled.parameters = { + msw: [mockMetadataHandler(true, 1), eeLicenseInfo.active], + consoleType: 'pro-lite', +}; + +export const Disabled: ComponentStory = () => { + return OpenTelemetryFeature() ||
; +}; +Disabled.storyName = '💠 Demo Feature Disabled'; +Disabled.parameters = { + msw: [mockMetadataHandler(false, 1), eeLicenseInfo.active], + consoleType: 'pro-lite', +}; + +export const Error: ComponentStory = () => { + return OpenTelemetryFeature() ||
; +}; +Error.storyName = '💠 Demo Feature Error'; +Error.parameters = { + msw: [mockMetadataHandler(false, 1, 500), eeLicenseInfo.active], + consoleType: 'pro-lite', +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetryFeature.tsx b/frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetryFeature.tsx index 997089845bf..f1248196152 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetryFeature.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetryFeature.tsx @@ -1,5 +1,6 @@ import * as React from 'react'; -import { isProLiteConsole } from '../../utils'; +import globals from '../../Globals'; +import { useEELiteAccess } from '../../features/EETrial'; import { OpenTelemetryProvider } from './OpenTelemetryProvider/OpenTelemetryProvider'; export function OpenTelemetryFeature() { @@ -9,7 +10,8 @@ export function OpenTelemetryFeature() { // But the feature itself should not be aware of when it's rendered or not. // eslint-disable-next-line no-underscore-dangle - if (!isProLiteConsole(window.__env)) return null; + const { access } = useEELiteAccess(globals); + if (access === 'forbidden') return null; return ; } diff --git a/frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetryProvider/OpenTelemetryProvider.tsx b/frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetryProvider/OpenTelemetryProvider.tsx index c1b07f5e2c1..1de62ec0785 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetryProvider/OpenTelemetryProvider.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/OpenTelemetry/OpenTelemetryProvider/OpenTelemetryProvider.tsx @@ -4,12 +4,15 @@ import { OpenTelemetry } from '../OpenTelemetry/OpenTelemetry'; import { useOpenTelemetry } from './hooks/useOpenTelemetry'; import { useSetOpenTelemetry } from './hooks/useSetOpenTelemetry'; +import { useEELiteAccess } from '../../EETrial'; +import globals from '../../../Globals'; /** * Allow isolating OpenTelemetry (the UI core of the feature) from every ap logic like * notifications, metadata loading, etc. */ export function OpenTelemetryProvider() { + const { access } = useEELiteAccess(globals); const { isLoadingMetadata, metadataFormValues, isFirstTimeSetup } = useOpenTelemetry(); @@ -18,9 +21,11 @@ export function OpenTelemetryProvider() { return ( ); } diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Prometheus/PrometheusSettings.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Prometheus/PrometheusSettings.stories.tsx index 857038dd0d8..30041ea4e20 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Prometheus/PrometheusSettings.stories.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/Prometheus/PrometheusSettings.stories.tsx @@ -2,8 +2,11 @@ import React from 'react'; import { ComponentStory, ComponentMeta } from '@storybook/react'; import { rest, DelayMode } from 'msw'; import { QueryClient, QueryClientProvider } from 'react-query'; +import { ReactQueryDevtools } from 'react-query/devtools'; import { PrometheusSettings } from '.'; +import { eeLicenseInfo } from '../EETrial/mocks/http'; +import { registerEETrialLicenseActiveMutation } from '../EETrial/mocks/registration.mock'; const queryClient = new QueryClient({ defaultOptions: { @@ -21,7 +24,7 @@ window.__env = { dataApiUrl: baseUrl, }; -const mockHandler = ( +const mockConfigHandler = ( prometheusEnabled: boolean, delay: number | DelayMode, status = 200 @@ -66,17 +69,33 @@ export default { (Story: React.FC) => ( + ), ], } as ComponentMeta; +export const DisabledWithoutLicense: ComponentStory< + typeof PrometheusSettings +> = () => ; +DisabledWithoutLicense.storyName = '💠 Demo Page Disabled without license'; +DisabledWithoutLicense.parameters = { + msw: [ + mockConfigHandler(false, 1), + eeLicenseInfo.noneOnce, + registerEETrialLicenseActiveMutation, + eeLicenseInfo.active, + ], + consoleType: 'pro-lite', +}; + export const Loading: ComponentStory = () => ( ); Loading.storyName = '💠 Demo Page Loading'; Loading.parameters = { - msw: [mockHandler(true, 'infinite')], + msw: [mockConfigHandler(true, 'infinite'), eeLicenseInfo.active], + consoleType: 'pro-lite', }; export const Enabled: ComponentStory = () => ( @@ -84,7 +103,8 @@ export const Enabled: ComponentStory = () => ( ); Enabled.storyName = '💠 Demo Page Enabled'; Enabled.parameters = { - msw: [mockHandler(true, 1)], + msw: [mockConfigHandler(true, 1), eeLicenseInfo.active], + consoleType: 'pro-lite', }; export const Disabled: ComponentStory = () => ( @@ -92,7 +112,8 @@ export const Disabled: ComponentStory = () => ( ); Disabled.storyName = '💠 Demo Page Disabled'; Disabled.parameters = { - msw: [mockHandler(false, 1)], + msw: [mockConfigHandler(false, 1), eeLicenseInfo.active], + consoleType: 'pro-lite', }; export const Error: ComponentStory = () => ( @@ -100,5 +121,6 @@ export const Error: ComponentStory = () => ( ); Error.storyName = '💠 Demo Page Error'; Error.parameters = { - msw: [mockHandler(false, 1, 500)], + msw: [mockConfigHandler(false, 1, 500), eeLicenseInfo.active], + consoleType: 'pro-lite', }; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Prometheus/PrometheusSettingsForm.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Prometheus/PrometheusSettingsForm.stories.tsx index 260ae160da1..d527211803d 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Prometheus/PrometheusSettingsForm.stories.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/Prometheus/PrometheusSettingsForm.stories.tsx @@ -2,6 +2,19 @@ import React from 'react'; import { ComponentStory, ComponentMeta } from '@storybook/react'; import { PrometheusSettingsForm } from './PrometheusSettingsForm'; +import { ReactQueryDecorator } from '../../storybook/decorators/react-query'; +import { EELiteAccess } from '../EETrial'; + +const eeLiteAccessInfoMockActive: EELiteAccess = { + access: 'active', + kind: 'default', + license: {} as any, + expires_at: new Date(new Date().getTime() + 10000000), +}; + +const eeLiteAccessInfoMockEligible: EELiteAccess = { + access: 'eligible', +}; export default { title: 'Features/Settings/Prometheus/Form', @@ -11,6 +24,7 @@ export default { source: { type: 'code', state: 'open' }, }, }, + decorators: [ReactQueryDecorator()], } as ComponentMeta; export const Loading: ComponentStory = args => ( @@ -19,6 +33,16 @@ export const Loading: ComponentStory = args => ( Loading.storyName = '💠 Demo Form Loading'; Loading.args = { loading: true, + eeLiteAccess: eeLiteAccessInfoMockActive, +}; + +export const DisabledWithoutLicense: ComponentStory< + typeof PrometheusSettingsForm +> = args => ; +DisabledWithoutLicense.storyName = '💠 Demo Form Disabled without license'; +DisabledWithoutLicense.args = { + enabled: false, + eeLiteAccess: eeLiteAccessInfoMockEligible, }; export const Disabled: ComponentStory = args => ( @@ -27,6 +51,7 @@ export const Disabled: ComponentStory = args => ( Disabled.storyName = '💠 Demo Form Disabled'; Disabled.args = { enabled: false, + eeLiteAccess: eeLiteAccessInfoMockActive, }; export const Error: ComponentStory = args => ( @@ -35,6 +60,7 @@ export const Error: ComponentStory = args => ( Error.storyName = '💠 Demo Form Error'; Error.args = { errorMode: true, + eeLiteAccess: eeLiteAccessInfoMockActive, }; const urlRegExp = @@ -48,6 +74,7 @@ Enabled.storyName = '💠 Demo Form Enabled'; Enabled.args = { enabled: true, prometheusUrl, + eeLiteAccess: eeLiteAccessInfoMockActive, prometheusConfig: `global: scrape_interval: 60s scrape_configs: diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Prometheus/PrometheusSettingsForm.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Prometheus/PrometheusSettingsForm.tsx index ee8bced4b20..d478cd2ac84 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Prometheus/PrometheusSettingsForm.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/Prometheus/PrometheusSettingsForm.tsx @@ -18,14 +18,15 @@ import { FaTimesCircle, } from 'react-icons/fa'; import { PrometheusAnimation } from './PrometheusAnimation'; +import { EETrialCard, EELiteAccess } from '../EETrial'; type PrometheusFormProps = { /** - * Flag indicating wheter the form is loading + * Flag indicating whether the form is loading */ loading?: boolean; /** - * Flag indicating wheter the form is enabled + * Flag indicating whether the form is enabled */ enabled?: boolean; /** @@ -37,9 +38,13 @@ type PrometheusFormProps = { */ prometheusConfig?: string; /** - * Flag indicating wheter the form should display error mode + * Flag indicating whether the form should display error mode */ errorMode: boolean; + /** + * Flag indicating whether a EETrial license is activated + */ + eeLiteAccess: EELiteAccess; }; const PrometheusFormIntro = () => ( @@ -49,7 +54,7 @@ const PrometheusFormIntro = () => (

); -type PrometheidFormFieldsProps = { +type PrometheusFormFieldsProps = { loading?: boolean; prometheusUrl?: string; prometheusConfig?: string; @@ -59,7 +64,7 @@ const PrometheusFormFields = ({ loading, prometheusUrl, prometheusConfig, -}: PrometheidFormFieldsProps) => ( +}: PrometheusFormFieldsProps) => ( {}} @@ -186,19 +191,22 @@ export const PrometheusSettingsForm: React.VFC = ({ prometheusUrl = '', prometheusConfig = '', errorMode = false, + eeLiteAccess, }) => { let PrometheusBadge = () => <>; let PrometheusSettings = () => <>; + const withoutLicense = eeLiteAccess.access !== 'active'; + if (loading) { PrometheusBadge = () => ; PrometheusSettings = () => ( <> - + ); - } else if (errorMode) { + } else if (errorMode && !withoutLicense) { PrometheusBadge = () => ( @@ -211,7 +219,7 @@ export const PrometheusSettingsForm: React.VFC = ({ ); - } else if (enabled) { + } else if (enabled && !withoutLicense) { PrometheusBadge = () => ( @@ -237,7 +245,25 @@ export const PrometheusSettingsForm: React.VFC = ({ PrometheusSettings = () => ( <> - + {eeLiteAccess.access !== 'active' ? ( + + Collect, store and query for time-series metrics for your API to + provide you with actionable insights and alerting capabilities + so you can optimize performance and troubleshoot issues in + real-time. + + } + buttonLabel="Enable Enterprise" + horizontal + eeAccess={eeLiteAccess.access} + /> + ) : ( + + )} ); } diff --git a/frontend/libs/console/legacy-ce/src/lib/features/Prometheus/index.tsx b/frontend/libs/console/legacy-ce/src/lib/features/Prometheus/index.tsx index 33523eb0699..baf82f994ab 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/Prometheus/index.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/features/Prometheus/index.tsx @@ -3,6 +3,8 @@ import React from 'react'; import { useServerConfig } from '../../hooks'; import endpoints from '../../Endpoints'; import { PrometheusSettingsForm } from './PrometheusSettingsForm'; +import globals from '../../Globals'; +import { useEELiteAccess } from '../../features/EETrial'; export const extractPrometheusUrl = (prometheusUrl: string) => { const urlRegExp = @@ -15,13 +17,20 @@ export const extractPrometheusUrl = (prometheusUrl: string) => { }; export const PrometheusSettings: React.VFC> = () => { - const { data: configData, isLoading, isError } = useServerConfig(); + const eeLiteAccess = useEELiteAccess(globals); + const { data: configData, isLoading } = useServerConfig(); + + // eslint-disable-next-line no-underscore-dangle + if (eeLiteAccess.access === 'forbidden') { + return null; + } + const prometheusUrlExtract = extractPrometheusUrl(endpoints.prometheusUrl); return ( ); }; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/QueryResponseCaching.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/QueryResponseCaching.stories.tsx new file mode 100644 index 00000000000..0edca712288 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/QueryResponseCaching.stories.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; + +import { QueryResponseCaching } from './QueryResponseCaching'; +import { + registerEETrialLicenseActiveMutation, + registerEETrialLicenseAlreadyAppliedMutation, +} from '../EETrial/mocks/registration.mock'; +import { eeLicenseInfo } from '../EETrial/mocks/http'; +import { useQueryClient } from 'react-query'; +import { ReactQueryDecorator } from '../../storybook/decorators/react-query'; +import { EE_LICENSE_INFO_QUERY_NAME } from '../EETrial'; + +export default { + title: 'Features/Settings/Query Response Caching', + component: QueryResponseCaching, + parameters: { + docs: { + source: { type: 'code', state: 'open' }, + }, + }, + decorators: [ + // This is done so as we have set some cache time on the EE_LICENSE_INFO_QUERY_NAME query. + // So we need to refetch the cache data, so it doesn't persist across different stories. And + // it makes sure that our component actually does the network call, letting msw mocks return the + // desired response. + Story => { + const queryClient = useQueryClient(); + queryClient.refetchQueries(EE_LICENSE_INFO_QUERY_NAME); + return ; + }, + ReactQueryDecorator(), + ], +} as ComponentMeta; + +export const UnregisteredUser: ComponentStory< + typeof QueryResponseCaching +> = () => { + return ; +}; +UnregisteredUser.storyName = '💠 Unregistered User'; +UnregisteredUser.parameters = { + msw: [registerEETrialLicenseActiveMutation, eeLicenseInfo.none], + consoleType: 'pro-lite', +}; + +export const LicenseActive: ComponentStory< + typeof QueryResponseCaching +> = () => { + return ; +}; +LicenseActive.storyName = '💠 License Active'; +LicenseActive.parameters = { + msw: [registerEETrialLicenseAlreadyAppliedMutation, eeLicenseInfo.active], + consoleType: 'pro-lite', +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/QueryResponseCaching.tsx b/frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/QueryResponseCaching.tsx new file mode 100644 index 00000000000..f4b0757110c --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/QueryResponseCaching.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { EETrialCard, useEELiteAccess } from '../EETrial'; +// import { StatusBadge } from './StatusBadge'; +import { LearnMoreLink } from '../../new-components/LearnMoreLink'; +import { QueryResponseCachingSvg } from './QueryResponseCachingSvg'; +import globals from '../../Globals'; +// import { StatusText } from './StatusText'; + +const Header = () => ( + <> +
+

Query Response Caching

+
+

+ Improve API performance by caching frequently executed GraphQL queries. + +

+ + +); + +const Body = () => { + return ( +
+ {/**/} +
+ Setup Query Caching +
{' '} +

+ + Read more + {' '} + on setting up caching for your Hasura GraphQL instance. +

+

+ Redis may be enabled by setting the environment variable:{' '} + HASURA_GRAPHQL_REDIS_URL +

+

+ GraphQL operations using the cache directive will be served + using your cache. +

+
+ ); +}; + +export const QueryResponseCaching: React.VFC> = () => { + const eeLite = useEELiteAccess(globals); + + const isFeatureForbidden = eeLite.access === 'forbidden'; + + const isFeatureActive = eeLite.access === 'active'; + + if (isFeatureForbidden) return null; + + return ( +
+
+
+ {isFeatureActive ? ( + + ) : ( + + By storing and quickly returning results for repeated queries, + query caching can improve the performance of your application. + This saves time and resources from your origin sources. + + } + buttonLabel="Enable Enterprise" + eeAccess={eeLite.access} + horizontal + /> + )} +
+
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/QueryResponseCachingSvg.tsx b/frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/QueryResponseCachingSvg.tsx new file mode 100644 index 00000000000..762cbe8519b --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/QueryResponseCachingSvg.tsx @@ -0,0 +1,418 @@ +import React from 'react'; + +type Props = { + className?: string; +}; + +export function QueryResponseCachingSvg(props: Props) { + const { className } = props; + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/StatusBadge.tsx b/frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/StatusBadge.tsx new file mode 100644 index 00000000000..71f57c580d0 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/StatusBadge.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { FaCheckCircle, FaTimesCircle } from 'react-icons/fa'; +import { Badge } from '../../new-components/Badge'; + +type Props = { + status: 'enabled' | 'disabled'; +}; + +export function StatusBadge(props: Props) { + const { status } = props; + + if (status === 'enabled') + return ( + + + Enabled + + ); + + return ( + + + Disabled + + ); +} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/StatusText.tsx b/frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/StatusText.tsx new file mode 100644 index 00000000000..d7f53a86c35 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/StatusText.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { FaCheckCircle, FaRegCircle } from 'react-icons/fa'; + +type Props = { + status: 'enabled' | 'disabled'; +}; + +export function StatusText(props: Props) { + const { status } = props; + + return ( +
+
Current Status
+ {status === 'enabled' ? ( +
+ + Cache Enabled +
+ ) : ( +
+ + Cache Disabled +
+ )} +
+ ); +} diff --git a/frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/index.tsx b/frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/index.tsx new file mode 100644 index 00000000000..fc77a27dd0c --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/QueryResponseCaching/index.tsx @@ -0,0 +1 @@ +export { QueryResponseCaching } from './QueryResponseCaching'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-types/source/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-types/source/index.ts index 28040bcb8c3..ac9ca369523 100644 --- a/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-types/source/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/features/hasura-metadata-types/source/index.ts @@ -1,7 +1,7 @@ export * from './source'; export * from './table'; export * from './relationships'; -export { +export type { PostgresConfiguration, MssqlConfiguration, BigQueryConfiguration, diff --git a/frontend/libs/console/legacy-ce/src/lib/graphics/HasuraEngineFlow.tsx b/frontend/libs/console/legacy-ce/src/lib/graphics/HasuraEngineFlow.tsx deleted file mode 100644 index 49df9d27124..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/graphics/HasuraEngineFlow.tsx +++ /dev/null @@ -1,903 +0,0 @@ -/* eslint react/no-unknown-property: 0 */ -import React from 'react'; - -type Props = { - className?: string; -}; - -export function HasuraEngineFlow(props: Props) { - const { className } = props; - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/frontend/libs/console/legacy-ce/src/lib/graphics/database-connect.svg b/frontend/libs/console/legacy-ce/src/lib/graphics/database-connect.svg deleted file mode 100644 index 3270ed8bb8d..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/graphics/database-connect.svg +++ /dev/null @@ -1,106 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/alloydb.svg b/frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/alloydb.svg deleted file mode 100644 index c07a2fa6b0f..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/alloydb.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/bigquery.svg b/frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/bigquery.svg deleted file mode 100644 index 3fff65b35cb..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/bigquery.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/citus.svg b/frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/citus.svg deleted file mode 100644 index 3d1e8c3eb7e..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/citus.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/placeholder.svg b/frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/placeholder.svg deleted file mode 100644 index 1ddb9bc3324..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/placeholder.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/postgres.svg b/frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/postgres.svg deleted file mode 100644 index 49bb4122210..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/postgres.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/sql-server.svg b/frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/sql-server.svg deleted file mode 100644 index 5334aa7ca68..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/graphics/db-logos/sql-server.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/libs/console/legacy-ce/src/lib/graphics/index.ts b/frontend/libs/console/legacy-ce/src/lib/graphics/index.ts deleted file mode 100644 index 96a916f654d..00000000000 --- a/frontend/libs/console/legacy-ce/src/lib/graphics/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { HasuraEngineFlow } from './HasuraEngineFlow'; diff --git a/frontend/libs/console/legacy-ce/src/lib/hooks/apiUtils.ts b/frontend/libs/console/legacy-ce/src/lib/hooks/apiUtils.ts index d331c2ace47..5125b77b7e5 100644 --- a/frontend/libs/console/legacy-ce/src/lib/hooks/apiUtils.ts +++ b/frontend/libs/console/legacy-ce/src/lib/hooks/apiUtils.ts @@ -8,7 +8,7 @@ interface IApiArgs { headers: Record; url: string; method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; - body?: Record; + body?: Record | string; credentials?: 'include' | 'omit' | 'same-origin'; } @@ -30,7 +30,7 @@ async function fetchApi( const response = await fetch(url, { headers, method, - body: JSON.stringify(body), + body: typeof body !== 'string' ? JSON.stringify(body) : body, credentials, }); const contentType = response.headers.get('Content-Type'); diff --git a/frontend/libs/console/legacy-ce/src/lib/new-components/AdvancedDropDown/components/Root.tsx b/frontend/libs/console/legacy-ce/src/lib/new-components/AdvancedDropDown/components/Root.tsx index 5c68440775f..a9c38b872d2 100644 --- a/frontend/libs/console/legacy-ce/src/lib/new-components/AdvancedDropDown/components/Root.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/new-components/AdvancedDropDown/components/Root.tsx @@ -1,5 +1,6 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import React from 'react'; +import { Nullable } from '../../../types'; import * as StyleWrappers from './style-wrappers'; @@ -9,6 +10,7 @@ type RootProps = { align?: DropdownMenu.DropdownMenuContentProps['align']; side?: DropdownMenu.DropdownMenuContentProps['side']; arrow?: boolean; + container?: Nullable; }; export const Root: React.FC = ({ @@ -18,10 +20,11 @@ export const Root: React.FC = ({ align, side, arrow = true, + container, }) => ( {trigger} - + { /** * The button label when in loading state */ - loadingText?: string; + loadingText?: React.ReactNode; /** * The button icon */ @@ -44,13 +44,13 @@ export interface ButtonProps extends React.ComponentProps<'button'> { full?: boolean; } -const buttonSizing: Record = { +export const buttonSizing: Record = { lg: 'px-md py-sm', md: 'h-btn px-sm', sm: 'h-btnsm px-sm ', }; -const buttonModesStyles: Record = { +export const buttonModesStyles: Record = { default: 'text-gray-600 bg-gray-50 from-transparent to-white border-gray-300 hover:border-gray-400 disabled:border-gray-300 focus-visible:from-bg-gray-50 focus-visible:to-bg-gray-50', destructive: @@ -59,7 +59,7 @@ const buttonModesStyles: Record = { 'text-gray-600 from-primary to-primary-light border-primary-dark hover:border-primary-darker focus-visible:from-primary focus-visible:to-primary disabled:border-primary-dark', }; -const sharedButtonStyle = +export const sharedButtonStyle = 'items-center max-w-full justify-center inline-flex items-center text-sm font-sans font-semibold bg-gradient-to-t border rounded shadow-sm focus-visible:outline-none focus-visible:bg-gradient-to-t focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-yellow-400 disabled:opacity-60'; const fullWidth = 'w-full'; diff --git a/frontend/libs/console/legacy-ce/src/lib/new-components/Collapsible/Collapsible.tsx b/frontend/libs/console/legacy-ce/src/lib/new-components/Collapsible/Collapsible.tsx index e82f9772f03..19cd0e54237 100644 --- a/frontend/libs/console/legacy-ce/src/lib/new-components/Collapsible/Collapsible.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/new-components/Collapsible/Collapsible.tsx @@ -1,7 +1,7 @@ import React from 'react'; import * as RadixCollapsible from '@radix-ui/react-collapsible'; import clsx from 'clsx'; -import { FaChevronRight } from 'react-icons/fa'; +import { BsChevronRight } from 'react-icons/bs'; export type CollapsibleProps = { /** @@ -16,6 +16,22 @@ export type CollapsibleProps = { * The collapse content children */ children: React.ReactNode; + /** + * Disables content styles (border, padding, margin) + */ + disableContentStyles?: boolean; + /** + * Collapsible animation duration + */ + animationSpeed?: 'default' | 'fast'; + /** + * Allows styling of the RadixCollapsible.Trigger element. e.g. add a background color that includes the chevron + children + */ + triggerClassName?: string; + /** + * Disabled wrapping trigger children in a span + */ + doNotWrapChildren?: boolean; } & Pick; export const Collapsible: React.VFC = ({ @@ -23,9 +39,15 @@ export const Collapsible: React.VFC = ({ children, disabled = false, defaultOpen = false, + disableContentStyles = false, + animationSpeed = 'default', + triggerClassName, + doNotWrapChildren = false, }) => { const [open, setOpen] = React.useState(false); + const Chevron = BsChevronRight; + React.useEffect(() => { if (defaultOpen) { setOpen(true); @@ -39,28 +61,37 @@ export const Collapsible: React.VFC = ({ disabled={disabled} > - - {triggerChildren} + {doNotWrapChildren ? triggerChildren : {triggerChildren}} -
+
{children}
diff --git a/frontend/libs/console/legacy-ce/src/lib/new-components/Form/CopyableInputField.stories.tsx b/frontend/libs/console/legacy-ce/src/lib/new-components/Form/CopyableInputField.stories.tsx new file mode 100644 index 00000000000..bf3bcc7ed58 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/new-components/Form/CopyableInputField.stories.tsx @@ -0,0 +1,187 @@ +import { action } from '@storybook/addon-actions'; +import { ComponentMeta, ComponentStory } from '@storybook/react'; +import { within, userEvent } from '@storybook/testing-library'; +import React from 'react'; +import { FaAirFreshener, FaBabyCarriage } from 'react-icons/fa'; +import { z } from 'zod'; +import { CopyableInputField as InputField, SimpleForm } from '.'; + +type StoryType = ComponentStory; + +export default { + title: 'components/Forms 📁/CopyableInputField 🧬', + component: InputField, + //argTypes: { onCopy: { action: 'copied' } }, + parameters: { + docs: { + description: { + component: `A component wrapping that allows copying of input text`, + }, + source: { type: 'code' }, + }, + }, +} as ComponentMeta; + +export const Basic: StoryType = args => { + const validationSchema = z.object({}); + const [clipboardText, setClipboardText] = React.useState(''); + const [show, setShow] = React.useState(false); + + return ( +
+ + { + navigator.clipboard.readText().then(t => { + setClipboardText(t); + setShow(true); + }); + }} + {...args} + name="demo" + /> + +
Current Clipboard Text:
+
(Click the copy button!)
+ {show &&
{clipboardText}
} +
+ ); +}; + +Basic.args = { + name: 'input', + label: 'An input field with copy button', + placeholder: 'Type something and then use the button to copy it!', +}; + +Basic.storyName = 'Basic Usage'; + +export const MoreExamples: StoryType = () => { + const [clipboardText, setClipboardText] = React.useState(''); + const [show, setShow] = React.useState(false); + const validationSchema = z.object({ + enabled: z.string().optional(), + disabled: z.string().optional(), + }); + + return ( +
+ + { + navigator.clipboard.readText().then(t => { + setClipboardText(t); + setShow(true); + }); + }} + prependLabel={'A Label, Prepended'} + description="A showcase of this feature playing nice with existing input features" + icon={} + labelIcon={} + learnMoreLink={'https://www.google.com/search?q=learn+more'} + tooltip={'A very useful tip!'} + name="enabled" + label="ALL THE OPTIONS!" + /> +
+ Note: The following props are not + available on this component as they create UI conflicts/issues: +
appendLabel, clearButton, iconPosition, size
+
+ { + action('onCopy'); + navigator.clipboard.readText().then(t => { + setClipboardText(t); + setShow(true); + }); + }} + name="disabled" + disabled + label="Set field to disabled to present non-editable text that a user can copy" + /> +
Current Clipboard Text:
+
(Try copying some text!)
+ {show && ( +
{clipboardText}
+ )} +
+
+ ); +}; + +export const Testing: StoryType = args => { + const validationSchema = z.object({}); + const [clipboardText, setClipboardText] = React.useState(''); + const [show, setShow] = React.useState(false); + + return ( +
+ + { + navigator.clipboard.readText().then(t => { + setClipboardText(t); + setShow(true); + }); + }} + {...args} + name="demo" + /> + +
Current Clipboard Text:
+
(Click the copy button!)
+ {show && ( +
+ {clipboardText} +
+ )} +
+ ); +}; + +Testing.args = { + name: 'input', + label: 'An input field with copy button', + placeholder: 'Type something and then use the button to copy it!', +}; + +Testing.storyName = 'Tests'; +Testing.play = async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const testValue = 'You can copy this text to the clipboard!'; + + const input = await canvas.findByTestId('demo'); + + await userEvent.type(input, testValue); + + await userEvent.click(await canvas.findByTestId('copy-button')); + + // const clipboardText = await navigator.clipboard.readText(); + // await waitFor(async () => { + // expect(await canvas.findByTestId('clipboard-contents')).toHaveTextContent( + // testValue + // ); + // }); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/new-components/Form/CopyableInputField.tsx b/frontend/libs/console/legacy-ce/src/lib/new-components/Form/CopyableInputField.tsx new file mode 100644 index 00000000000..1893027a81b --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/new-components/Form/CopyableInputField.tsx @@ -0,0 +1,107 @@ +import { ExtendInputFieldProps, InputField } from '.'; +import clsx from 'clsx'; +import React from 'react'; +import { useFormContext } from 'react-hook-form'; +import { FaRegCopy } from 'react-icons/fa'; + +const twStyles = { + alignWithInput: `bottom-[22px]`, + inputHeight: `h-[36px]`, + buttonWidth: `w-[42px]`, + copyConfirm: { + base: `select-none transition-opacity duration-300 font-bold bg-slate-100/50 rounded backdrop-blur-sm absolute left-0 w-full flex items-center justify-center`, + invisible: `opacity-0 pointer-events-none`, + visible: `opacity-100 pointer-events-auto`, + }, + copyButton: `text-gray-600 bg-gray-50 px-sm focus-visible:bg-blue-100 h-[calc(2.5rem-2px)] rounded-r outline-none active:opacity-50 border-none bg-transparent absolute right-[1px] shadow-none bg-none`, + input: `pr-[42px]`, +}; + +type ExtendedType = ExtendInputFieldProps<{ + onCopy?: (currentValue: string) => void; +}>; + +/** + * Both `appendLabel` and `clearButton` are typed as `never` to prevent usage. + * Due to an issue with `Omit` not working correctly with Unions, we are unable to simply `Omit` these properties from the type. + * More information can be found here: https://github.com/microsoft/TypeScript/issues/31501#issuecomment-1079728677 + * + * `appendLabel` and `clearButton` cannot be used as they all occupy the same UI space + * + * `iconPosition` may also not be used. The default position is `start` so not allowing this prop keeps any icons rendered at the start. + * If positioned at the end, then it would occupy the same UI space as the copy button. + * + * `size` is also prohibited as it breaks the absolute position of the copy button + */ +type CopyableInputFieldProps = ExtendedType & { + appendLabel?: never; + clearButton?: never; + iconPosition?: never; + size?: never; +}; + +export const CopyableInputField = (props: CopyableInputFieldProps) => { + const { watch } = useFormContext(); + const fieldValue = watch(props.name); + + // state to control visibility of copy confirmation + const [showCopiedConfirmation, setShowCopiedConfirmation] = + React.useState(false); + + const copyTimer = React.useRef(); + + const handleCopyButton = () => { + // clear timer if already going... + if (copyTimer.current) { + clearTimeout(copyTimer.current); + } + + // copy text to clipboard + navigator.clipboard.writeText(fieldValue); + + props.onCopy?.(fieldValue); + + // show confirmation + setShowCopiedConfirmation(true); + + // hide after 1.5s + copyTimer.current = setTimeout(() => { + setShowCopiedConfirmation(false); + }, 1500); + }; + + return ( +
+ + + +
+ Copied! +
+
+ ); +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/new-components/Form/FieldWrapper.tsx b/frontend/libs/console/legacy-ce/src/lib/new-components/Form/FieldWrapper.tsx index ef0f5aee4c4..e3e3c364b23 100644 --- a/frontend/libs/console/legacy-ce/src/lib/new-components/Form/FieldWrapper.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/new-components/Form/FieldWrapper.tsx @@ -36,7 +36,7 @@ export type FieldWrapperPassThroughProps = { */ dataTest?: string; /** - * Flag indicating wheteher the field is loading + * Flag indicating whether the field is loading */ loading?: boolean; /** diff --git a/frontend/libs/console/legacy-ce/src/lib/new-components/Form/Input.tsx b/frontend/libs/console/legacy-ce/src/lib/new-components/Form/Input.tsx index fc431b7a901..671287fa704 100644 --- a/frontend/libs/console/legacy-ce/src/lib/new-components/Form/Input.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/new-components/Form/Input.tsx @@ -63,11 +63,11 @@ export type InputProps = FieldWrapperPassThroughProps & { /** * The input field prepend label */ - prependLabel?: string; + prependLabel?: string | React.ReactNode; /** * The input field append label */ - appendLabel?: string; + appendLabel?: string | React.ReactNode; /** * Renders a button to clear the input onClick */ diff --git a/frontend/libs/console/legacy-ce/src/lib/new-components/Form/InputField.tsx b/frontend/libs/console/legacy-ce/src/lib/new-components/Form/InputField.tsx index d60185758a2..faeaf10242a 100644 --- a/frontend/libs/console/legacy-ce/src/lib/new-components/Form/InputField.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/new-components/Form/InputField.tsx @@ -9,12 +9,22 @@ import { import { z, ZodType, ZodTypeDef } from 'zod'; import { FieldWrapper, FieldWrapperPassThroughProps } from './FieldWrapper'; import { Input } from './Input'; +import { filterDataAttributes } from './utils/filterDataAttributes'; type TFormValues = Record; export type Schema = ZodType; -export type InputFieldProps> = +// for convenience +type InputFieldDefaultType = z.infer; + +// wrappers that want to extend the props in a simple way can use this type +export type ExtendInputFieldProps< + T, + X extends InputFieldDefaultType = InputFieldDefaultType +> = T & InputFieldProps; + +export type InputFieldProps = FieldWrapperPassThroughProps & { /** * The input field name @@ -51,11 +61,11 @@ export type InputFieldProps> = /** * The input field prepend label */ - prependLabel?: string; + prependLabel?: string | React.ReactNode; /** * The input field append label */ - appendLabel?: string; + appendLabel?: string | React.ReactNode; /** * A callback for transforming the input onChange for things like sanitizing input */ @@ -97,6 +107,10 @@ export const InputField = >({ fieldProps = {}, ...wrapperProps }: InputFieldProps) => { + const dataAttributes = filterDataAttributes( + wrapperProps as Record + ); + const { register, formState: { errors }, @@ -162,7 +176,7 @@ export const InputField = >({ onChange={onInputChangeEvent} onClearButtonClick={onClearButtonClick} inputProps={regReturn} - fieldProps={fieldProps} + fieldProps={{ ...fieldProps, ...dataAttributes }} rightButton={wrapperProps?.rightButton} /> diff --git a/frontend/libs/console/legacy-ce/src/lib/new-components/Form/index.ts b/frontend/libs/console/legacy-ce/src/lib/new-components/Form/index.ts index 2e89e24f89f..d5db573a262 100644 --- a/frontend/libs/console/legacy-ce/src/lib/new-components/Form/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/new-components/Form/index.ts @@ -3,9 +3,9 @@ export * from './CheckboxesField'; export * from './CodeEditorField'; export * from './SimpleForm'; export { InputField } from './InputField'; +export type { InputFieldProps, ExtendInputFieldProps } from './InputField'; export { Input } from './Input'; export type { InputProps } from './Input'; -export type { InputFieldProps } from './InputField'; export * from './GraphQLSanitizedInputField'; export * from './Radio'; export * from './Select'; @@ -14,3 +14,4 @@ export * from './AdvancedSelectField'; export * from './Textarea'; export * from './FieldWrapper'; export * from './hooks/useConsoleForm'; +export * from './CopyableInputField'; diff --git a/frontend/libs/console/legacy-ce/src/lib/new-components/Form/utils/filterDataAttributes.ts b/frontend/libs/console/legacy-ce/src/lib/new-components/Form/utils/filterDataAttributes.ts new file mode 100644 index 00000000000..b52602d8ceb --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/new-components/Form/utils/filterDataAttributes.ts @@ -0,0 +1,11 @@ +export function filterDataAttributes( + obj: Record +): Record { + const result: Record = {}; + for (const key in obj) { + if (key.startsWith('data-')) { + result[key] = obj[key]; + } + } + return result; +} diff --git a/frontend/libs/console/legacy-ce/src/lib/new-components/IndicatorCard/IndicatorCard.tsx b/frontend/libs/console/legacy-ce/src/lib/new-components/IndicatorCard/IndicatorCard.tsx index a8cf45805b6..f5babb9d437 100644 --- a/frontend/libs/console/legacy-ce/src/lib/new-components/IndicatorCard/IndicatorCard.tsx +++ b/frontend/libs/console/legacy-ce/src/lib/new-components/IndicatorCard/IndicatorCard.tsx @@ -9,6 +9,9 @@ export type IndicatorCardProps = { headline?: string; children?: React.ReactNode; showIcon?: boolean; + className?: string; + contentFullWidth?: boolean; + customIcon?: React.VFC<{ className: string }>; }; const cardColors: Record = { @@ -37,14 +40,18 @@ export const IndicatorCard = ({ headline, showIcon, children, + className, + contentFullWidth = false, + customIcon, }: IndicatorCardProps) => { - const Icon = IconPerStatus[status]; + const Icon = customIcon ?? IconPerStatus[status]; return (
{showIcon ? ( @@ -57,9 +64,9 @@ export const IndicatorCard = ({
) : null} -
+
{headline ?

{headline}

: null} -

{children}

+
{children}
); diff --git a/frontend/libs/console/legacy-ce/src/lib/shared/utils/sdlUtils.js b/frontend/libs/console/legacy-ce/src/lib/shared/utils/sdlUtils.js index 246bb5893e7..e3e2ec436af 100644 --- a/frontend/libs/console/legacy-ce/src/lib/shared/utils/sdlUtils.js +++ b/frontend/libs/console/legacy-ce/src/lib/shared/utils/sdlUtils.js @@ -389,7 +389,7 @@ export const toggleCacheDirective = operationString => { operationAst = sdlParse(operationString); } catch (e) { console.error(e); - return; + throw e; } const shouldAddCacheDirective = !operationAst.definitions.some(def => { diff --git a/frontend/libs/console/legacy-ce/src/lib/telemetry/index.ts b/frontend/libs/console/legacy-ce/src/lib/telemetry/index.ts index fd77d15eb6b..8e1d14a164d 100644 --- a/frontend/libs/console/legacy-ce/src/lib/telemetry/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/telemetry/index.ts @@ -28,10 +28,25 @@ export type SetFeatureFlagEvent = { }; }; +export type HTMLUserEvent = + | { + type: 'CLICK_EVENT'; + data: { + id: string; + }; + } + | { + type: 'INPUT_CHANGE_EVENT'; + data: { + id: string; + }; + }; + export type TelemetryEvent = | RunTimeErrorEvent | ConnectDBEvent - | SetFeatureFlagEvent; + | SetFeatureFlagEvent + | HTMLUserEvent; export type TelemetryPayload = { server_version: string; @@ -99,3 +114,34 @@ export const trackRuntimeError = (error: Error) => { data: { message: error.message, stack: error.stack }, }); }; + +// This function accepts the event identifier and kind, constructs the telemetry payload and sends it +export const telemetryUserEventsTracker = ( + id: string, + kind: 'click' | 'change' +) => { + const isNotProduction = process.env.NODE_ENV !== 'production'; + // Keeping this log in debug mode so that it's easier to see the tracking items + if (isNotProduction) { + console.log('Tracking: ', id, kind); + } + switch (kind) { + case 'change': { + sendTelemetryEvent({ + type: 'INPUT_CHANGE_EVENT', + data: { + id, + }, + }); + break; + } + case 'click': { + sendTelemetryEvent({ + type: 'CLICK_EVENT', + data: { + id, + }, + }); + } + } +}; diff --git a/frontend/libs/console/legacy-ce/src/lib/utils/__tests__/proConsole.spec.ts b/frontend/libs/console/legacy-ce/src/lib/utils/__tests__/proConsole.spec.ts index 817ef6cbd75..142edf00f9a 100644 --- a/frontend/libs/console/legacy-ce/src/lib/utils/__tests__/proConsole.spec.ts +++ b/frontend/libs/console/legacy-ce/src/lib/utils/__tests__/proConsole.spec.ts @@ -43,7 +43,7 @@ describe('isProConsole', () => { consoleMode: 'server', consoleType: 'pro-lite', }; - expect(isProConsole(env)).toBe(true); + expect(isProConsole(env)).toBe(false); }); }); @@ -93,7 +93,7 @@ describe('isProConsole', () => { consoleMode: 'cli', consoleType: 'pro-lite', }; - expect(isProConsole(env)).toBe(true); + expect(isProConsole(env)).toBe(false); }); }); diff --git a/frontend/libs/console/legacy-ce/src/lib/utils/cloudConsole.ts b/frontend/libs/console/legacy-ce/src/lib/utils/cloudConsole.ts index a413a3b424c..3852d288b57 100644 --- a/frontend/libs/console/legacy-ce/src/lib/utils/cloudConsole.ts +++ b/frontend/libs/console/legacy-ce/src/lib/utils/cloudConsole.ts @@ -10,6 +10,10 @@ export function isCloudConsole(g: typeof globals) { return !!g.hasuraCloudTenantId && g.consoleType === 'cloud'; } +export function isEECloud(g: typeof globals) { + return !g.hasuraCloudTenantId && g.consoleType === 'cloud'; +} + // This function returns true if the current user has access to a lux feature export function hasLuxFeatureAccess(g: typeof globals, feature: LuxFeature) { return (globals.allowedLuxFeatures || []).includes(feature); diff --git a/frontend/libs/console/legacy-ce/src/lib/utils/getDataRoute.ts b/frontend/libs/console/legacy-ce/src/lib/utils/getDataRoute.ts index 1d1085ae220..18dafd7de4c 100644 --- a/frontend/libs/console/legacy-ce/src/lib/utils/getDataRoute.ts +++ b/frontend/libs/console/legacy-ce/src/lib/utils/getDataRoute.ts @@ -1,6 +1,10 @@ import { Table } from '../features/hasura-metadata-types'; export const getRoute = () => ({ + connectDatabase: (driver?: string) => + driver + ? `/data/v2/manage/database/add?driver=${driver}` + : 'data/v2/manage/connect', database: (dataSourceName: string) => encodeURI(`/data/v2/manage/database?database=${dataSourceName}`), table: (dataSourceName: string, table: Table, operation?: string) => { diff --git a/frontend/libs/console/legacy-ce/src/lib/utils/index.ts b/frontend/libs/console/legacy-ce/src/lib/utils/index.ts index 1bd9e67844b..8ef18499c77 100644 --- a/frontend/libs/console/legacy-ce/src/lib/utils/index.ts +++ b/frontend/libs/console/legacy-ce/src/lib/utils/index.ts @@ -21,7 +21,6 @@ export { } from './permissions'; export { isProConsole, - isProLiteConsole, isMonitoringTabSupportedEnvironment, isEnvironmentSupportMultiTenantConnectionPooling, isImportFromOpenAPIEnabled, diff --git a/frontend/libs/console/legacy-ce/src/lib/utils/proConsole.ts b/frontend/libs/console/legacy-ce/src/lib/utils/proConsole.ts index f12381c4291..04b02c7d924 100644 --- a/frontend/libs/console/legacy-ce/src/lib/utils/proConsole.ts +++ b/frontend/libs/console/legacy-ce/src/lib/utils/proConsole.ts @@ -7,33 +7,19 @@ export type ProConsoleEnv = { }; export const isProConsole = (env: ProConsoleEnv) => { - if ( - env.consoleMode === 'server' && - (env.consoleType === 'cloud' || - env.consoleType === 'pro' || - env.consoleType === 'pro-lite') - ) { + if (env.consoleType === 'cloud' || env.consoleType === 'pro') { return true; } - if (env.consoleMode === 'cli') { - if ( - env.consoleType === 'cloud' || - env.consoleType === 'pro' || - env.consoleType === 'pro-lite' - ) - return true; - - // to support old CLI logic, when consoleType is not provided by the CLI - if (env.pro === true) return true; - } + if (env.consoleMode === 'cli' && env.pro === true) return true; return false; }; -export const isProLiteConsole = (env: ProConsoleEnv) => { - return env.consoleType === 'pro-lite'; -}; +// Commented this function so that it's not used +// export const isProLiteConsole = (env: ProConsoleEnv) => { +// return env.consoleType === 'pro-lite'; +// }; export const isMonitoringTabSupportedEnvironment = (env: ProConsoleEnv) => { // pro-lite and OSS environments won't have access to metrics server diff --git a/frontend/libs/console/legacy-ce/src/lib/utils/routeUtils.ts b/frontend/libs/console/legacy-ce/src/lib/utils/routeUtils.ts new file mode 100644 index 00000000000..ebedbc243c9 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/utils/routeUtils.ts @@ -0,0 +1,3 @@ +export const getQueryResponseCachingRoute = () => { + return '/settings/query-response-caching'; +}; diff --git a/frontend/libs/console/legacy-ee/src/lib/components/Main/Main.js b/frontend/libs/console/legacy-ee/src/lib/components/Main/Main.js index 690f9f126f1..0a40480ad15 100644 --- a/frontend/libs/console/legacy-ee/src/lib/components/Main/Main.js +++ b/frontend/libs/console/legacy-ee/src/lib/components/Main/Main.js @@ -27,7 +27,15 @@ import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; import globals from '../../Globals'; import 'react-toggle/style.css'; -import { Spinner } from '@hasura/console-legacy-ce'; +import { + Spinner, + EntepriseNavbarButton, + WithEELiteAccess, + InitializeTelemetry, + telemetryUserEventsTracker, + Analytics, +} from '@hasura/console-legacy-ce'; + import { Badge, NotificationSection, @@ -43,6 +51,7 @@ import { isCloudConsole, ControlPlane, } from '@hasura/console-legacy-ce'; + import { versionGT, FT_JWT_ANALYZER } from '../../helpers/versionUtils'; import { loadServerVersion, @@ -292,9 +301,8 @@ class Main extends React.Component { */ isEnterpriseProject() { return ( - (this.props?.project?.is_enterprise_user && - this.props?.project?.plan_name !== 'cloud_free') || - globals.consoleType === 'pro-lite' + this.props?.project?.is_enterprise_user && + this.props?.project?.plan_name !== 'cloud_free' ); } @@ -523,25 +531,53 @@ class Main extends React.Component { const renderMetadataIcon = () => (
- - {getMetadataIcon()} - Settings - + + + {getMetadataIcon()} + Settings + +
); - const getLogoSrc = () => { - if (this.isEnterpriseProject()) { - return EELogo; - } + const getLogo = () => { + return ( + + {({ access }) => { + const getLogoSrc = () => { + if (this.isEnterpriseProject()) { + return EELogo; + } + if (access === 'active') { + return EELogo; + } + return logo; + }; + return HasuraLogo; + }} + + ); + }; - return logo; + const renderTelemetrySetup = () => { + return ( + + {({ access }) => { + return ( + + ); + }} + + ); }; return ( @@ -553,7 +589,7 @@ class Main extends React.Component { - + {getLogo()} {getAdminSecretSection()} + + {renderTelemetrySetup()} {renderProjectInfo()} {renderMetadataIcon()}
diff --git a/frontend/libs/console/legacy-ee/src/lib/routes.js b/frontend/libs/console/legacy-ee/src/lib/routes.js index 5042dec353f..629da7521bf 100644 --- a/frontend/libs/console/legacy-ee/src/lib/routes.js +++ b/frontend/libs/console/legacy-ee/src/lib/routes.js @@ -12,7 +12,9 @@ import { isMetadataStatusPage, prefetchSurveysData, prefetchOnboardingData, + prefetchEELicenseInfo, PageNotFound, + dataHeaders, } from '@hasura/console-legacy-ce'; import { dataRouterUtils, @@ -44,8 +46,13 @@ import { isMonitoringTabSupportedEnvironment, AllowListDetail, PrometheusSettings, + QueryResponseCaching, OpenTelemetryFeature, + MultipleAdminSecretsPage, + MultipleJWTSecretsPage, + SingleSignOnPage, } from '@hasura/console-legacy-ce'; + import AccessDeniedComponent from './components/AccessDenied/AccessDenied'; import { restrictedPathsMetadata } from './utils/redirectUtils'; import generatedCallbackConnector from './components/OAuthCallback/OAuthCallback'; @@ -251,6 +258,11 @@ const routes = store => { prefetchSurveysData(); prefetchOnboardingData(); } + + if (globals.consoleType === 'pro-lite') { + prefetchEELicenseInfo(dataHeaders(store.getState)); + } + const onEnterHooks = [validateAccessToRoute]; const { shouldLoadOpts, shouldLoadServer } = shouldLoadAsyncGlobals(store); if (shouldLoadOpts || shouldLoadServer) { @@ -370,6 +382,19 @@ const routes = store => { + + + + diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 73aa3f1bcd8..c1b5d9bf025 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,11 +17,13 @@ "@hasura/dc-api-types": "^0.26.0", "@hookform/resolvers": "2.8.10", "@netsells/storybook-mockdate": "^0.3.2", + "@radix-ui/colors": "^0.1.8", "@radix-ui/react-checkbox": "1.0.1", "@radix-ui/react-collapsible": "^1.0.0", "@radix-ui/react-dialog": "^1.0.0", "@radix-ui/react-dropdown-menu": "^1.0.0", "@radix-ui/react-radio-group": "^1.1.1", + "@radix-ui/react-scroll-area": "^1.0.2", "@radix-ui/react-switch": "^1.0.0", "@radix-ui/react-tabs": "^1.0.0", "@radix-ui/react-tooltip": "^1.0.0", @@ -39,6 +41,7 @@ "apollo-link": "1.2.14", "apollo-link-http": "^1.5.16", "apollo-link-ws": "1.0.20", + "await-to-js": "^3.0.0", "axios": "0.27.2", "babel-plugin-transform-runtime": "^6.23.0", "brace": "0.11.1", @@ -101,7 +104,7 @@ "react-helmet": "5.2.1", "react-hook-form": "7.15.4", "react-hot-toast": "2.4.0", - "react-icons": "^4.3.1", + "react-icons": "^4.7.1", "react-json-view": "^1.21.3", "react-loading-skeleton": "^3.1.0", "react-lottie": "^1.2.3", @@ -128,7 +131,7 @@ "styled-components": "5.0.1", "styled-system": "5.1.5", "subscriptions-transport-ws": "0.9.16", - "tailwindcss-radix": "^2.5.0", + "tailwindcss-radix": "^2.7.0", "ts-essentials": "7.0.3", "tslib": "^2.3.0", "unplugin": "^1.0.1", @@ -11123,6 +11126,19 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@radix-ui/colors": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-0.1.8.tgz", + "integrity": "sha512-jwRMXYwC0hUo0mv6wGpuw254Pd9p/R6Td5xsRpOmaWkUHlooNWqVcadgyzlRumMq3xfOTXwJReU0Jv+EIy4Jbw==" + }, + "node_modules/@radix-ui/number": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.0.tgz", + "integrity": "sha512-Ofwh/1HX69ZfJRiRBMTy7rgjAzHmwe4kW9C9Y99HTRUcYLUuVT0KESFj15rPjRgKJs20GPq8Bm5aEDJ8DuA3vA==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, "node_modules/@radix-ui/primitive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", @@ -11735,6 +11751,40 @@ "react": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.3.tgz", + "integrity": "sha512-sBX9j8Q+0/jReNObEAveKIGXJtk3xUoSIx4cMKygGtO128QJyVDn01XNOFsyvihKDCTcu7SINzQ2jPAZEhIQtw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.0", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-direction": "1.0.0", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-scroll-area/node_modules/@radix-ui/react-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz", + "integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", @@ -26877,6 +26927,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -73764,6 +73822,19 @@ "integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==", "dev": true }, + "@radix-ui/colors": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-0.1.8.tgz", + "integrity": "sha512-jwRMXYwC0hUo0mv6wGpuw254Pd9p/R6Td5xsRpOmaWkUHlooNWqVcadgyzlRumMq3xfOTXwJReU0Jv+EIy4Jbw==" + }, + "@radix-ui/number": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.0.0.tgz", + "integrity": "sha512-Ofwh/1HX69ZfJRiRBMTy7rgjAzHmwe4kW9C9Y99HTRUcYLUuVT0KESFj15rPjRgKJs20GPq8Bm5aEDJ8DuA3vA==", + "requires": { + "@babel/runtime": "^7.13.10" + } + }, "@radix-ui/primitive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", @@ -74238,6 +74309,34 @@ } } }, + "@radix-ui/react-scroll-area": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.0.3.tgz", + "integrity": "sha512-sBX9j8Q+0/jReNObEAveKIGXJtk3xUoSIx4cMKygGtO128QJyVDn01XNOFsyvihKDCTcu7SINzQ2jPAZEhIQtw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/number": "1.0.0", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-direction": "1.0.0", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "dependencies": { + "@radix-ui/react-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz", + "integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + } + } + } + }, "@radix-ui/react-slot": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", @@ -85990,6 +86089,11 @@ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==" }, + "await-to-js": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/await-to-js/-/await-to-js-3.0.0.tgz", + "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==" + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index a4c34202505..51f76bd0575 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -55,11 +55,13 @@ "@hasura/dc-api-types": "^0.26.0", "@hookform/resolvers": "2.8.10", "@netsells/storybook-mockdate": "^0.3.2", + "@radix-ui/colors": "^0.1.8", "@radix-ui/react-checkbox": "1.0.1", "@radix-ui/react-collapsible": "^1.0.0", "@radix-ui/react-dialog": "^1.0.0", "@radix-ui/react-dropdown-menu": "^1.0.0", "@radix-ui/react-radio-group": "^1.1.1", + "@radix-ui/react-scroll-area": "^1.0.2", "@radix-ui/react-switch": "^1.0.0", "@radix-ui/react-tabs": "^1.0.0", "@radix-ui/react-tooltip": "^1.0.0", @@ -77,6 +79,7 @@ "apollo-link": "1.2.14", "apollo-link-http": "^1.5.16", "apollo-link-ws": "1.0.20", + "await-to-js": "^3.0.0", "axios": "0.27.2", "babel-plugin-transform-runtime": "^6.23.0", "brace": "0.11.1", @@ -139,7 +142,7 @@ "react-helmet": "5.2.1", "react-hook-form": "7.15.4", "react-hot-toast": "2.4.0", - "react-icons": "^4.3.1", + "react-icons": "^4.7.1", "react-json-view": "^1.21.3", "react-loading-skeleton": "^3.1.0", "react-lottie": "^1.2.3", @@ -166,7 +169,7 @@ "styled-components": "5.0.1", "styled-system": "5.1.5", "subscriptions-transport-ws": "0.9.16", - "tailwindcss-radix": "^2.5.0", + "tailwindcss-radix": "^2.7.0", "ts-essentials": "7.0.3", "tslib": "^2.3.0", "unplugin": "^1.0.1", diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index a8c4a8a94d9..092c904a6f5 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,5 +1,6 @@ const colors = require('tailwindcss/colors'); const plugin = require('tailwindcss/plugin'); +const { blackA, mauve, violet } = require('@radix-ui/colors'); function dataStateVariant(state, { addVariant, e }) { addVariant(`data-state-${state}`, ({ modifySelectors, separator }) => { @@ -202,6 +203,8 @@ module.exports = { animation: { collapsibleContentOpen: 'collapsibleContentOpen 300ms ease-out', collapsibleContentClose: 'collapsibleContentClose 300ms ease-out', + collapsibleContentOpenFast: 'collapsibleContentOpen 200ms ease-out', + collapsibleContentCloseFast: 'collapsibleContentClose 200ms ease-out', slideUpAndFade: 'slideUpAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)', slideRightAndFade: 'slideRightAndFade 400ms cubic-bezier(0.16, 1, 0.3, 1)', diff --git a/server/graphql-engine.cabal b/server/graphql-engine.cabal index 88efc46c0ab..44fe2b8f214 100644 --- a/server/graphql-engine.cabal +++ b/server/graphql-engine.cabal @@ -692,6 +692,7 @@ library -- Exposed for benchmark: , Hasura.Cache.Bounded + , Hasura.CredentialCache , Hasura.Logging , Hasura.HTTP , Hasura.PingSources @@ -807,6 +808,7 @@ library , Hasura.RQL.Types.Common , Hasura.RQL.Types.ComputedField , Hasura.RQL.Types.CustomTypes + , Hasura.RQL.Types.EECredentials , Hasura.RQL.Types.Endpoint , Hasura.RQL.Types.Endpoint.Trie , Hasura.RQL.Types.EventTrigger diff --git a/server/lib/api-tests/src/Test/DataConnector/MetadataApiSpec.hs b/server/lib/api-tests/src/Test/DataConnector/MetadataApiSpec.hs index e8a8de6cc79..b31d10ab6a1 100644 --- a/server/lib/api-tests/src/Test/DataConnector/MetadataApiSpec.hs +++ b/server/lib/api-tests/src/Test/DataConnector/MetadataApiSpec.hs @@ -226,6 +226,7 @@ schemaInspectionTests = describe "Schema and Source Inspection" $ do <&> Lens.set (key "config_schema_response" . key "other_schemas") J.Null <&> Lens.set (key "config_schema_response" . key "config_schema") J.Null <&> Lens.set (key "capabilities" . _Object . Lens.at "datasets") Nothing + <&> Lens.set (key "capabilities" . _Object . Lens.at "licensing") Nothing <&> Lens.set (key "options" . key "uri") J.Null <&> Lens.set (_Object . Lens.at "display_name") Nothing <&> Lens.set (_Object . Lens.at "release_name") Nothing diff --git a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Capabilities.hs b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Capabilities.hs index af46ba7b608..0795733043a 100644 --- a/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Capabilities.hs +++ b/server/lib/dc-api/src/Hasura/Backends/DataConnector/API/V0/Capabilities.hs @@ -5,10 +5,13 @@ {-# HLINT ignore "Use onNothing" #-} +-------------------------------------------------------------------------------- + module Hasura.Backends.DataConnector.API.V0.Capabilities ( Capabilities (..), cDataSchema, cQueries, + cLicensing, cMutations, cSubscriptions, cScalarTypes, @@ -52,9 +55,12 @@ module Hasura.Backends.DataConnector.API.V0.Capabilities crConfigSchemaResponse, crDisplayName, crReleaseName, + Licensing (..), ) where +-------------------------------------------------------------------------------- + import Autodocodec import Autodocodec.OpenAPI () import Control.Applicative ((<|>)) @@ -75,6 +81,8 @@ import Language.GraphQL.Draft.Syntax qualified as GQL.Syntax import Servant.API.UVerb qualified as Servant import Prelude +-------------------------------------------------------------------------------- + -- | The 'Capabilities' describes the _capabilities_ of the -- service. Specifically, the service is capable of serving queries -- which involve relationships. @@ -89,14 +97,15 @@ data Capabilities = Capabilities _cMetrics :: Maybe MetricsCapabilities, _cExplain :: Maybe ExplainCapabilities, _cRaw :: Maybe RawCapabilities, - _cDatasets :: Maybe DatasetCapabilities + _cDatasets :: Maybe DatasetCapabilities, + _cLicensing :: Maybe Licensing } deriving stock (Eq, Show, Generic) deriving anyclass (NFData, Hashable) deriving (FromJSON, ToJSON, ToSchema) via Autodocodec Capabilities defaultCapabilities :: Capabilities -defaultCapabilities = Capabilities defaultDataSchemaCapabilities Nothing Nothing Nothing mempty Nothing Nothing Nothing Nothing Nothing Nothing +defaultCapabilities = Capabilities defaultDataSchemaCapabilities Nothing Nothing Nothing mempty Nothing Nothing Nothing Nothing Nothing Nothing Nothing instance HasCodec Capabilities where codec = @@ -113,6 +122,9 @@ instance HasCodec Capabilities where <*> optionalField "explain" "The agent's explain capabilities" .= _cExplain <*> optionalField "raw" "The agent's raw query capabilities" .= _cRaw <*> optionalField "datasets" "The agent's dataset capabilities" .= _cDatasets + <*> optionalField "licensing" "The agent's licensing requirements" .= _cLicensing + +-------------------------------------------------------------------------------- data DataSchemaCapabilities = DataSchemaCapabilities { _dscSupportsPrimaryKeys :: Bool, @@ -536,6 +548,15 @@ instance ToSchema CapabilitiesResponse where pure $ NamedSchema (Just "CapabilitiesResponse") schema +data Licensing = Licensing {} + deriving stock (Eq, Show, Generic) + deriving anyclass (NFData, Hashable) + deriving (FromJSON, ToJSON, ToSchema) via Autodocodec Licensing + +instance HasCodec Licensing where + codec = + object "Licensing" $ pure Licensing + $(makeLenses ''CapabilitiesResponse) $(makeLenses ''Capabilities) $(makeLenses ''QueryCapabilities) diff --git a/server/lib/dc-api/test/Main.hs b/server/lib/dc-api/test/Main.hs index d344ded9de2..10111dafd18 100644 --- a/server/lib/dc-api/test/Main.hs +++ b/server/lib/dc-api/test/Main.hs @@ -5,15 +5,16 @@ module Main (main) where import Command (AgentConfig (..), AgentOptions (..), Command (..), SandwichArguments (..), TestOptions (..), parseCommandLine) import Control.Exception (bracket) import Data.Aeson.Text (encodeToLazyText) +import Data.ByteString.Char8 qualified as Char8 import Data.Foldable (for_) import Data.Maybe (isJust) import Data.Text.Lazy.IO qualified as Text import Hasura.Backends.DataConnector.API (openApiSchema) import Hasura.Backends.DataConnector.API qualified as API import Servant.Client ((//)) -import System.Environment (withArgs) +import System.Environment qualified as Env import Test.AgentAPI (guardCapabilitiesResponse, guardSchemaResponse, mergeAgentConfig) -import Test.AgentClient (AgentIOClient (..), introduceAgentClient, mkAgentClientConfig, mkAgentIOClient) +import Test.AgentClient (AgentAuthKey (..), AgentIOClient (..), introduceAgentClient, mkAgentClientConfig, mkAgentIOClient) import Test.AgentDatasets (DatasetCloneInfo (..), chinookTemplate, createClone, deleteClone, testingEdgeCasesTemplate, usesDataset) import Test.AgentTestContext (AgentTestContext (..), introduceAgentTestContext) import Test.Data (EdgeCasesTestData, TestData, mkEdgeCasesTestData, mkTestData) @@ -79,17 +80,21 @@ getTestingEdgeCasesSchema API.Capabilities {..} agentConfig agentIOClient@(Agent else pure Nothing else pure Nothing +lookupAgentAuthKey :: IO (Maybe AgentAuthKey) +lookupAgentAuthKey = fmap (AgentAuthKey . Char8.pack) <$> Env.lookupEnv "HASURA_GRAPHQL_EE_LICENSE_KEY" + main :: IO () main = do command <- parseCommandLine + agentAuthKey <- lookupAgentAuthKey case command of - Test TestOptions {..} (SandwichArguments arguments) -> withArgs arguments $ do - agentIOClient@(AgentIOClient agentClient) <- mkAgentIOClient _toSensitiveOutputHandling (_aoAgentBaseUrl _toAgentOptions) + Test TestOptions {..} (SandwichArguments arguments) -> Env.withArgs arguments $ do + agentIOClient@(AgentIOClient agentClient) <- mkAgentIOClient _toSensitiveOutputHandling agentAuthKey (_aoAgentBaseUrl _toAgentOptions) agentCapabilities <- (agentClient // API._capabilities) >>= guardCapabilitiesResponse chinookSchema <- getChinookSchema (API._crCapabilities agentCapabilities) (_aoAgentConfig _toAgentOptions) agentIOClient testingEdgeCasesSchema <- getTestingEdgeCasesSchema (API._crCapabilities agentCapabilities) (_aoAgentConfig _toAgentOptions) agentIOClient - agentClientConfig <- mkAgentClientConfig _toSensitiveOutputHandling (_aoAgentBaseUrl _toAgentOptions) + agentClientConfig <- mkAgentClientConfig _toSensitiveOutputHandling agentAuthKey (_aoAgentBaseUrl _toAgentOptions) let testData = mkTestData chinookSchema _toTestConfig let edgeCasesTestData = mkEdgeCasesTestData _toTestConfig <$> testingEdgeCasesSchema let testContext = AgentTestContext testSourceName agentCapabilities (_aoAgentConfig _toAgentOptions) diff --git a/server/lib/dc-api/test/Test/AgentClient.hs b/server/lib/dc-api/test/Test/AgentClient.hs index 7dd2ea5c503..c29642d279b 100644 --- a/server/lib/dc-api/test/Test/AgentClient.hs +++ b/server/lib/dc-api/test/Test/AgentClient.hs @@ -11,6 +11,7 @@ module Test.AgentClient getAgentClientConfig, AgentClientT, runAgentClientT, + AgentAuthKey (..), ) where @@ -55,20 +56,29 @@ newtype AgentIOClient = AgentIOClient (forall m. MonadIO m => Client m (NamedRou configHeader :: HeaderName configHeader = CI.mk "X-Hasura-DataConnector-Config" -mkHttpClientManager :: MonadIO m => SensitiveOutputHandling -> m HttpClient.Manager -mkHttpClientManager sensitiveOutputHandling = - let settings = HttpClient.defaultManagerSettings {HttpClient.managerModifyRequest = pure . addHeaderRedaction sensitiveOutputHandling} +newtype AgentAuthKey = AgentAuthKey {getAgentAuthKey :: ByteString} + +eeLicenseKeyHeader :: HeaderName +eeLicenseKeyHeader = CI.mk "X-Hasura-License" + +mkHttpClientManager :: MonadIO m => SensitiveOutputHandling -> Maybe AgentAuthKey -> m HttpClient.Manager +mkHttpClientManager sensitiveOutputHandling agentAuthKey = + let modifyRequest = addHeaderRedaction sensitiveOutputHandling . maybe id addLicenseKeyHeader agentAuthKey + settings = HttpClient.defaultManagerSettings {HttpClient.managerModifyRequest = pure . modifyRequest} in liftIO $ HttpClient.newManager settings +addLicenseKeyHeader :: AgentAuthKey -> HttpClient.Request -> HttpClient.Request +addLicenseKeyHeader (AgentAuthKey eeKey) r = r {HttpClient.requestHeaders = (eeLicenseKeyHeader, eeKey) : HttpClient.requestHeaders r} + addHeaderRedaction :: SensitiveOutputHandling -> HttpClient.Request -> HttpClient.Request addHeaderRedaction sensitiveOutputHandling request = case sensitiveOutputHandling of AllowSensitiveOutput -> request - DisallowSensitiveOutput -> request {HttpClient.redactHeaders = HttpClient.redactHeaders request <> Set.singleton configHeader} + DisallowSensitiveOutput -> request {HttpClient.redactHeaders = HttpClient.redactHeaders request <> Set.fromList [configHeader, eeLicenseKeyHeader]} -mkAgentIOClient :: MonadIO m => SensitiveOutputHandling -> BaseUrl -> m AgentIOClient -mkAgentIOClient sensitiveOutputHandling agentBaseUrl = do - manager <- mkHttpClientManager sensitiveOutputHandling +mkAgentIOClient :: MonadIO m => SensitiveOutputHandling -> Maybe AgentAuthKey -> BaseUrl -> m AgentIOClient +mkAgentIOClient sensitiveOutputHandling agentAuthKey agentBaseUrl = do + manager <- mkHttpClientManager sensitiveOutputHandling agentAuthKey let clientEnv = mkClientEnv manager agentBaseUrl pure $ AgentIOClient $ hoistClient (Proxy @(NamedRoutes API.Routes)) (\m -> liftIO (runClientM m clientEnv >>= either throwIO pure)) API.apiClient @@ -80,9 +90,9 @@ data AgentClientConfig = AgentClientConfig _accSensitiveOutputHandling :: SensitiveOutputHandling } -mkAgentClientConfig :: MonadIO m => SensitiveOutputHandling -> BaseUrl -> m AgentClientConfig -mkAgentClientConfig sensitiveOutputHandling agentBaseUrl = do - manager <- mkHttpClientManager sensitiveOutputHandling +mkAgentClientConfig :: MonadIO m => SensitiveOutputHandling -> Maybe AgentAuthKey -> BaseUrl -> m AgentClientConfig +mkAgentClientConfig sensitiveOutputHandling agentAuthKey agentBaseUrl = do + manager <- mkHttpClientManager sensitiveOutputHandling agentAuthKey pure $ AgentClientConfig agentBaseUrl manager sensitiveOutputHandling ------------------------------------------------------------------------------- diff --git a/server/lib/test-harness/src/Harness/Backend/DataConnector/Mock/Server.hs b/server/lib/test-harness/src/Harness/Backend/DataConnector/Mock/Server.hs index 319fbcebc30..65b69caea0f 100644 --- a/server/lib/test-harness/src/Harness/Backend/DataConnector/Mock/Server.hs +++ b/server/lib/test-harness/src/Harness/Backend/DataConnector/Mock/Server.hs @@ -73,7 +73,8 @@ capabilities = API._cMetrics = Just API.MetricsCapabilities {}, API._cExplain = Just API.ExplainCapabilities {}, API._cRaw = Just API.RawCapabilities {}, - API._cDatasets = Just API.DatasetCapabilities {} + API._cDatasets = Just API.DatasetCapabilities {}, + API._cLicensing = Nothing }, _crConfigSchemaResponse = API.ConfigSchemaResponse diff --git a/server/lib/test-harness/src/Harness/GraphqlEngine.hs b/server/lib/test-harness/src/Harness/GraphqlEngine.hs index fe731b583fc..1ba243800e5 100644 --- a/server/lib/test-harness/src/Harness/GraphqlEngine.hs +++ b/server/lib/test-harness/src/Harness/GraphqlEngine.hs @@ -68,6 +68,7 @@ import Harness.WebSockets (responseListener) import Hasura.App qualified as App import Hasura.Logging (Hasura) import Hasura.Prelude +import Hasura.Server.App (CEConsoleType (OSSConsole)) import Hasura.Server.Init (PostgresConnInfo (..), ServeOptions (..), unsafePort) import Hasura.Server.Metrics (ServerMetricsSpec, createServerMetrics) import Hasura.Server.Prometheus (makeDummyPrometheusMetrics) @@ -412,6 +413,7 @@ runApp serveOptions = do appCtx initTime Nothing + OSSConsole ekgStore -- | Used only for 'runApp' above. diff --git a/server/src-exec/Main.hs b/server/src-exec/Main.hs index fe11856073a..0df4882c89f 100644 --- a/server/src-exec/Main.hs +++ b/server/src-exec/Main.hs @@ -27,6 +27,7 @@ import Hasura.GC qualified as GC import Hasura.Logging (Hasura, LogLevel (..), defaultEnabledEngineLogTypes) import Hasura.Prelude import Hasura.RQL.DDL.Schema +import Hasura.Server.App (CEConsoleType (OSSConsole)) import Hasura.Server.Init import Hasura.Server.Metrics (ServerMetricsSpec, createServerMetrics) import Hasura.Server.Migrate (downgradeCatalog) @@ -116,7 +117,7 @@ runApp env (HGEOptions rci metadataDbUrl hgeCmd) = do runAppM appEnv do appStateRef <- initialiseAppContext env serveOptions appInit lowerManagedT $ - runHGEServer (const $ pure ()) appStateRef initTime Nothing ekgStore + runHGEServer (const $ pure ()) appStateRef initTime Nothing OSSConsole ekgStore HCExport -> do metadataConnection <- initMetadataConnectionInfo env metadataDbUrl rci res <- runTxWithMinimalPool metadataConnection fetchMetadataFromCatalog diff --git a/server/src-lib/Hasura/App.hs b/server/src-lib/Hasura/App.hs index b44a487365f..db04850eb4a 100644 --- a/server/src-lib/Hasura/App.hs +++ b/server/src-lib/Hasura/App.hs @@ -127,6 +127,7 @@ import Hasura.RQL.DDL.Schema.Catalog import Hasura.RQL.Types.Allowlist import Hasura.RQL.Types.Backend import Hasura.RQL.Types.Common +import Hasura.RQL.Types.EECredentials import Hasura.RQL.Types.Eventing.Backend import Hasura.RQL.Types.Metadata import Hasura.RQL.Types.Network @@ -486,7 +487,8 @@ initialiseAppEnv env BasicConnectionInfo {..} serveOptions@ServeOptions {..} liv appEnvWebSocketConnectionInitTimeout = soWebSocketConnectionInitTimeout, appEnvGracefulShutdownTimeout = soGracefulShutdownTimeout, appEnvCheckFeatureFlag = CheckFeatureFlag $ checkFeatureFlag env, - appEnvSchemaPollInterval = soSchemaPollInterval + appEnvSchemaPollInterval = soSchemaPollInterval, + appEnvLicenseKeyCache = Nothing } ) @@ -737,8 +739,9 @@ instance MonadMetadataApiAuthorization AppM where throw400 AccessDenied accessDeniedErrMsg instance ConsoleRenderer AppM where - renderConsole path authMode enableTelemetry consoleAssetsDir consoleSentryDsn = - return $ mkConsoleHTML path authMode enableTelemetry consoleAssetsDir consoleSentryDsn + type ConsoleType AppM = CEConsoleType + renderConsole path authMode enableTelemetry consoleAssetsDir consoleSentryDsn consoleType = + return $ mkConsoleHTML path authMode enableTelemetry consoleAssetsDir consoleSentryDsn consoleType instance MonadVersionAPIWithExtraData AppM where -- we always default to CE as the `server_type` in this codebase @@ -816,6 +819,10 @@ instance MonadMetadataStorage AppM where clearActionData = runInSeparateTx . clearActionDataTx setProcessingActionLogsToPending = runInSeparateTx . setProcessingActionLogsToPendingTx +instance MonadEECredentialsStorage AppM where + getEEClientCredentials = runInSeparateTx getEEClientCredentialsTx + setEEClientCredentials a = runInSeparateTx $ setEEClientCredentialsTx a + -------------------------------------------------------------------------------- -- misc @@ -917,11 +924,12 @@ runHGEServer :: UTCTime -> -- | A hook which can be called to indicate when the server is started succesfully Maybe (IO ()) -> + ConsoleType m -> EKG.Store EKG.EmptyMetrics -> ManagedT m () -runHGEServer setupHook appStateRef initTime startupStatusHook ekgStore = do +runHGEServer setupHook appStateRef initTime startupStatusHook consoleType ekgStore = do AppEnv {..} <- lift askAppEnv - waiApplication <- mkHGEServer setupHook appStateRef ekgStore + waiApplication <- mkHGEServer setupHook appStateRef consoleType ekgStore let logger = _lsLogger appEnvLoggers -- `startupStatusHook`: add `Service started successfully` message to config_status @@ -1007,9 +1015,10 @@ mkHGEServer :: ) => (AppStateRef impl -> Spock.SpockT m ()) -> AppStateRef impl -> + ConsoleType m -> EKG.Store EKG.EmptyMetrics -> ManagedT m Application -mkHGEServer setupHook appStateRef ekgStore = do +mkHGEServer setupHook appStateRef consoleType ekgStore = do -- Comment this to enable expensive assertions from "GHC.AssertNF". These -- will log lines to STDOUT containing "not in normal form". In the future we -- could try to integrate this into our tests. For now this is a development @@ -1028,6 +1037,7 @@ mkHGEServer setupHook appStateRef ekgStore = do mkWaiApp setupHook appStateRef + consoleType ekgStore wsServerEnv @@ -1392,8 +1402,15 @@ setCatalogStateTx stateTy stateValue = --- helper functions --- -mkConsoleHTML :: Text -> AuthMode -> TelemetryStatus -> Maybe Text -> Maybe Text -> Either String Text -mkConsoleHTML path authMode enableTelemetry consoleAssetsDir consoleSentryDsn = +mkConsoleHTML :: + Text -> + AuthMode -> + TelemetryStatus -> + Maybe Text -> + Maybe Text -> + CEConsoleType -> + Either String Text +mkConsoleHTML path authMode enableTelemetry consoleAssetsDir consoleSentryDsn ceConsoleType = renderHtmlTemplate consoleTmplt $ -- variables required to render the template A.object @@ -1404,6 +1421,7 @@ mkConsoleHTML path authMode enableTelemetry consoleAssetsDir consoleSentryDsn = "consoleSentryDsn" A..= fromMaybe "" consoleSentryDsn, "assetsVersion" A..= consoleAssetsVersion, "serverVersion" A..= currentVersion, + "consoleType" A..= ceConsoleTypeIdentifier ceConsoleType, -- TODO(awjchen): This is a kludge that will be removed when the entitlement service is fully implemented. "consoleSentryDsn" A..= ("" :: Text) ] where @@ -1449,4 +1467,5 @@ mkMSSQLSourceResolver env _name (MSSQLConnConfiguration connInfo _) = runExceptT } (connString, mssqlPool) <- createMSSQLPool iConnString connOptions env let mssqlExecCtx = mkMSSQLExecCtx mssqlPool NeverResizePool - pure $ MSSQLSourceConfig connString mssqlExecCtx + numReadReplicas = 0 + pure $ MSSQLSourceConfig connString mssqlExecCtx numReadReplicas diff --git a/server/src-lib/Hasura/App/State.hs b/server/src-lib/Hasura/App/State.hs index 923a4e2de35..88485e09c92 100644 --- a/server/src-lib/Hasura/App/State.hs +++ b/server/src-lib/Hasura/App/State.hs @@ -27,7 +27,9 @@ import Control.Monad.Trans.Control (MonadBaseControl) import Data.Environment qualified as E import Data.HashSet qualified as Set import Database.PG.Query qualified as PG +import Hasura.Backends.DataConnector.Agent.Client (AgentLicenseKey) import Hasura.Base.Error +import Hasura.CredentialCache import Hasura.Eventing.Common (LockedEventsCtx) import Hasura.Eventing.EventTrigger import Hasura.GraphQL.Execute.Subscription.Options @@ -130,7 +132,8 @@ data AppEnv = AppEnv -- as this thread is initialised there before creating the `AppStateRef`. But eventually we need -- to do it for the Enterprise version. appEnvSchemaPollInterval :: OptionalInterval, - appEnvCheckFeatureFlag :: CheckFeatureFlag + appEnvCheckFeatureFlag :: CheckFeatureFlag, + appEnvLicenseKeyCache :: Maybe (CredentialCache AgentLicenseKey) } -- | Represents the Dynamic Hasura State, these field are mutable and can be changed diff --git a/server/src-lib/Hasura/Backends/BigQuery/Instances/Transport.hs b/server/src-lib/Hasura/Backends/BigQuery/Instances/Transport.hs index cb4108d5ab2..e8f7544976a 100644 --- a/server/src-lib/Hasura/Backends/BigQuery/Instances/Transport.hs +++ b/server/src-lib/Hasura/Backends/BigQuery/Instances/Transport.hs @@ -5,7 +5,9 @@ module Hasura.Backends.BigQuery.Instances.Transport () where import Control.Monad.Trans.Control import Data.Aeson qualified as J import Hasura.Backends.BigQuery.Instances.Execute () +import Hasura.Backends.DataConnector.Agent.Client (AgentLicenseKey) import Hasura.Base.Error +import Hasura.CredentialCache import Hasura.EncJSON import Hasura.GraphQL.Execute.Backend import Hasura.GraphQL.Logging @@ -49,13 +51,14 @@ runQuery :: RootFieldAlias -> UserInfo -> L.Logger L.Hasura -> + Maybe (CredentialCache AgentLicenseKey) -> SourceConfig 'BigQuery -> OnBaseMonad IdentityT (Maybe (AnyBackend ExecutionStats), EncJSON) -> Maybe Text -> ResolvedConnectionTemplate 'BigQuery -> -- | Also return the time spent in the PG query; for telemetry. m (DiffTime, EncJSON) -runQuery reqId query fieldName _userInfo logger _sourceConfig tx genSql _ = do +runQuery reqId query fieldName _userInfo logger _ _sourceConfig tx genSql _ = do -- log the generated SQL and the graphql query -- FIXME: fix logging by making logQueryLog expect something backend agnostic! logQueryLog logger $ mkQueryLog (QueryLogKindDatabase Nothing) query fieldName genSql reqId @@ -71,9 +74,10 @@ runQueryExplain :: MonadError QErr m, MonadTrace m ) => + Maybe (CredentialCache AgentLicenseKey) -> DBStepInfo 'BigQuery -> m EncJSON -runQueryExplain (DBStepInfo _ _ _ action _) = fmap arResult (run action) +runQueryExplain _ (DBStepInfo _ _ _ action _) = fmap arResult (run action) runMutation :: ( MonadError QErr m @@ -83,6 +87,7 @@ runMutation :: RootFieldAlias -> UserInfo -> L.Logger L.Hasura -> + Maybe (CredentialCache AgentLicenseKey) -> SourceConfig 'BigQuery -> OnBaseMonad IdentityT EncJSON -> Maybe Text -> @@ -90,7 +95,7 @@ runMutation :: -- | Also return 'Mutation' when the operation was a mutation, and the time -- spent in the PG query; for telemetry. m (DiffTime, EncJSON) -runMutation _reqId _query _fieldName _userInfo _logger _sourceConfig _tx _genSql _ = +runMutation _reqId _query _fieldName _userInfo _logger _ _sourceConfig _tx _genSql _ = -- do throw500 "BigQuery does not support mutations!" diff --git a/server/src-lib/Hasura/Backends/BigQuery/Instances/Types.hs b/server/src-lib/Hasura/Backends/BigQuery/Instances/Types.hs index 3487af56b7d..c8ea3b8ef96 100644 --- a/server/src-lib/Hasura/Backends/BigQuery/Instances/Types.hs +++ b/server/src-lib/Hasura/Backends/BigQuery/Instances/Types.hs @@ -118,3 +118,5 @@ instance Backend 'BigQuery where instance HasSourceConfiguration 'BigQuery where type SourceConfig 'BigQuery = BigQuery.BigQuerySourceConfig type SourceConnConfiguration 'BigQuery = BigQuery.BigQueryConnSourceConfig + sourceConfigNumReadReplicas = const 0 -- not supported + sourceConfigConnectonTemplateEnabled = const False -- not supported diff --git a/server/src-lib/Hasura/Backends/DataConnector/Adapter/Backend.hs b/server/src-lib/Hasura/Backends/DataConnector/Adapter/Backend.hs index 54837810294..6f1d75aa7a6 100644 --- a/server/src-lib/Hasura/Backends/DataConnector/Adapter/Backend.hs +++ b/server/src-lib/Hasura/Backends/DataConnector/Adapter/Backend.hs @@ -160,6 +160,8 @@ instance Backend 'DataConnector where instance HasSourceConfiguration 'DataConnector where type SourceConfig 'DataConnector = DC.SourceConfig type SourceConnConfiguration 'DataConnector = DC.ConnSourceConfig + sourceConfigNumReadReplicas = const 0 -- not supported + sourceConfigConnectonTemplateEnabled = const False -- not supported data CustomBooleanOperator a = CustomBooleanOperator { _cboName :: Text, diff --git a/server/src-lib/Hasura/Backends/DataConnector/Adapter/Metadata.hs b/server/src-lib/Hasura/Backends/DataConnector/Adapter/Metadata.hs index 98eb1b0c721..b3e8348423e 100644 --- a/server/src-lib/Hasura/Backends/DataConnector/Adapter/Metadata.hs +++ b/server/src-lib/Hasura/Backends/DataConnector/Adapter/Metadata.hs @@ -115,7 +115,7 @@ resolveBackendInfo' logger = proc (invalidationKeys, optionsMap) -> do getDataConnectorCapabilities options@DC.DataConnectorOptions {..} manager = runExceptT do capabilitiesU <- ignoreTraceT - . flip runAgentClientT (AgentClientContext logger _dcoUri manager Nothing) + . flip runAgentClientT (AgentClientContext logger _dcoUri manager Nothing Nothing) $ genericClient @API.Routes // API._capabilities let defaultAction = throw400 DataConnectorError "Unexpected data connector capabilities response - Unexpected Type" @@ -152,7 +152,7 @@ resolveSourceConfig' schemaResponseU <- ignoreTraceT - . flip runAgentClientT (AgentClientContext logger _dcoUri manager (DC.sourceTimeoutMicroseconds <$> timeout)) + . flip runAgentClientT (AgentClientContext logger _dcoUri manager (DC.sourceTimeoutMicroseconds <$> timeout) Nothing) $ (genericClient // API._schema) (toTxt sourceName) transformedConfig let defaultAction = throw400 DataConnectorError "Unexpected data connector schema response - Unexpected Type" diff --git a/server/src-lib/Hasura/Backends/DataConnector/Adapter/Transport.hs b/server/src-lib/Hasura/Backends/DataConnector/Adapter/Transport.hs index 8285ef432bc..8b62c2e7f5d 100644 --- a/server/src-lib/Hasura/Backends/DataConnector/Adapter/Transport.hs +++ b/server/src-lib/Hasura/Backends/DataConnector/Adapter/Transport.hs @@ -4,14 +4,17 @@ module Hasura.Backends.DataConnector.Adapter.Transport () where -------------------------------------------------------------------------------- +import Control.Concurrent.STM import Control.Exception.Safe (throwIO) import Control.Monad.Trans.Control import Data.Aeson qualified as J import Data.Text.Extended ((<>>)) +import Hasura.Backends.DataConnector.API.V0 import Hasura.Backends.DataConnector.Adapter.Execute (DataConnectorPreparedQuery (..), encodePreparedQueryToJsonText) import Hasura.Backends.DataConnector.Adapter.Types (SourceConfig (..)) -import Hasura.Backends.DataConnector.Agent.Client (AgentClientContext (..), AgentClientT, runAgentClientT) -import Hasura.Base.Error (QErr) +import Hasura.Backends.DataConnector.Agent.Client (AgentClientContext (..), AgentClientT, AgentLicenseKey (..), runAgentClientT) +import Hasura.Base.Error (QErr, throw401) +import Hasura.CredentialCache import Hasura.EncJSON (EncJSON) import Hasura.GraphQL.Execute.Backend (DBStepInfo (..), OnBaseMonad (..), arResult) import Hasura.GraphQL.Logging qualified as HGL @@ -50,19 +53,29 @@ runDBQuery' :: RootFieldAlias -> UserInfo -> Logger Hasura -> + Maybe (CredentialCache AgentLicenseKey) -> SourceConfig -> - OnBaseMonad AgentClientT (Maybe (AnyBackend HGL.ExecutionStats), a) -> + OnBaseMonad AgentClientT (Maybe (AnyBackend HGL.ExecutionStats), EncJSON) -> Maybe DataConnectorPreparedQuery -> ResolvedConnectionTemplate 'DataConnector -> - m (DiffTime, a) -runDBQuery' requestId query fieldName _userInfo logger SourceConfig {..} action queryRequest _ = do - void $ HGL.logQueryLog logger $ mkQueryLog query fieldName queryRequest requestId - withElapsedTime - . Tracing.newSpan ("Data Connector backend query for root field " <>> fieldName) - . flip runAgentClientT (AgentClientContext logger _scEndpoint _scManager _scTimeoutMicroseconds) - . fmap snd - . runOnBaseMonad - $ action + m (DiffTime, EncJSON) +runDBQuery' requestId query fieldName _userInfo logger licenseKeyCacheMaybe SourceConfig {..} action queryRequest _ = do + agentAuthKey <- + for licenseKeyCacheMaybe \licenseKeyCache -> do + (key, _requestKeyRefresh) <- liftIO $ atomically $ getCredential licenseKeyCache + -- TODO: If the license key has expired or is otherwise invalid, request a key refresh + pure key + + case (_cLicensing _scCapabilities, agentAuthKey) of + (Just _, Nothing) -> throw401 "EE License Key Required." + _ -> do + void $ HGL.logQueryLog logger $ mkQueryLog query fieldName queryRequest requestId + withElapsedTime + . Tracing.newSpan ("Data Connector backend query for root field " <>> fieldName) + . flip runAgentClientT (AgentClientContext logger _scEndpoint _scManager _scTimeoutMicroseconds agentAuthKey) + . runOnBaseMonad + . fmap snd + $ action mkQueryLog :: GQLReqUnparsed -> @@ -84,11 +97,21 @@ runDBQueryExplain' :: MonadError QErr m, Tracing.MonadTrace m ) => + Maybe (CredentialCache AgentLicenseKey) -> DBStepInfo 'DataConnector -> m EncJSON -runDBQueryExplain' (DBStepInfo _ SourceConfig {..} _ action _) = - flip runAgentClientT (AgentClientContext nullLogger _scEndpoint _scManager _scTimeoutMicroseconds) $ - fmap arResult (runOnBaseMonad action) +runDBQueryExplain' licenseKeyCacheMaybe (DBStepInfo _ SourceConfig {..} _ action _) = do + agentAuthKey <- + for licenseKeyCacheMaybe \licenseKeyCache -> do + (key, _requestKeyRefresh) <- liftIO $ atomically $ getCredential licenseKeyCache + -- TODO: If the license key has expired or is otherwise invalid, request a key refresh + pure key + case (_cLicensing _scCapabilities, agentAuthKey) of + (Just _, Nothing) -> throw401 "EE License Key Required." + _ -> + flip runAgentClientT (AgentClientContext nullLogger _scEndpoint _scManager _scTimeoutMicroseconds agentAuthKey) + . fmap arResult + $ runOnBaseMonad action runDBMutation' :: ( MonadIO m, @@ -102,15 +125,24 @@ runDBMutation' :: RootFieldAlias -> UserInfo -> Logger Hasura -> + Maybe (CredentialCache AgentLicenseKey) -> SourceConfig -> OnBaseMonad AgentClientT a -> Maybe DataConnectorPreparedQuery -> ResolvedConnectionTemplate 'DataConnector -> m (DiffTime, a) -runDBMutation' requestId query fieldName _userInfo logger SourceConfig {..} action queryRequest _ = do - void $ HGL.logQueryLog logger $ mkQueryLog query fieldName queryRequest requestId - withElapsedTime - . Tracing.newSpan ("Data Connector backend mutation for root field " <>> fieldName) - . flip runAgentClientT (AgentClientContext logger _scEndpoint _scManager _scTimeoutMicroseconds) - . runOnBaseMonad - $ action +runDBMutation' requestId query fieldName _userInfo logger licenseKeyCacheMaybe SourceConfig {..} action queryRequest _ = do + agentAuthKey <- + for licenseKeyCacheMaybe \licenseKeyCache -> do + (key, _requestKeyRefresh) <- liftIO $ atomically $ getCredential licenseKeyCache + -- TODO: If the license key has expired or is otherwise invalid, request a key refresh + pure key + case (_cLicensing _scCapabilities, agentAuthKey) of + (Just _, Nothing) -> throw401 "EE License Key Required." + _ -> do + void $ HGL.logQueryLog logger $ mkQueryLog query fieldName queryRequest requestId + withElapsedTime + . Tracing.newSpan ("Data Connector backend mutation for root field " <>> fieldName) + . flip runAgentClientT (AgentClientContext logger _scEndpoint _scManager _scTimeoutMicroseconds agentAuthKey) + . runOnBaseMonad + $ action diff --git a/server/src-lib/Hasura/Backends/DataConnector/Agent/Client.hs b/server/src-lib/Hasura/Backends/DataConnector/Agent/Client.hs index dc3df7bc679..c57b7ccb1e5 100644 --- a/server/src-lib/Hasura/Backends/DataConnector/Agent/Client.hs +++ b/server/src-lib/Hasura/Backends/DataConnector/Agent/Client.hs @@ -1,14 +1,18 @@ {-# LANGUAGE UndecidableInstances #-} module Hasura.Backends.DataConnector.Agent.Client - ( AgentClientContext (..), + ( AgentLicenseKey (..), + AgentClientContext (..), AgentClientT, runAgentClientT, ) where +-------------------------------------------------------------------------------- + import Control.Exception (try) -import Control.Lens ((&~), (.=)) +import Control.Lens ((%=), (&~), (.=)) +import Data.ByteString (ByteString) import Hasura.Backends.DataConnector.Logging (logAgentRequest, logClientError) import Hasura.Base.Error import Hasura.HTTP qualified @@ -21,11 +25,17 @@ import Servant.Client import Servant.Client.Core (Request, RunClient (..)) import Servant.Client.Internal.HttpClient (clientResponseToResponse, mkFailureResponse) +-------------------------------------------------------------------------------rs + +-- | Auth Key provided to the GDC Agent in 'Request' headers. +newtype AgentLicenseKey = AgentLicenseKey {unAgentLicenseKey :: ByteString} + data AgentClientContext = AgentClientContext { _accLogger :: Logger Hasura, _accBaseUrl :: BaseUrl, _accHttpManager :: HTTP.Manager, - _accResponseTimeout :: Maybe Int + _accResponseTimeout :: Maybe Int, + _accAgentLicenseKey :: Maybe AgentLicenseKey } newtype AgentClientT m a = AgentClientT (ReaderT AgentClientContext m a) @@ -49,7 +59,9 @@ runRequestAcceptStatus' acceptStatus req = do -- Set the response timeout explicitly if it is provided let transformableReq' = transformableReq &~ do - for _accResponseTimeout \x -> HTTP.timeout .= HTTP.responseTimeoutMicro x + for_ _accResponseTimeout \x -> HTTP.timeout .= HTTP.responseTimeoutMicro x + HTTP.headers + %= \headers -> maybe headers (\(AgentLicenseKey key) -> ("X-Hasura-License", key) : headers) _accAgentLicenseKey (tracedReq, responseOrException) <- traceHTTPRequest transformableReq' \tracedReq -> fmap (tracedReq,) . liftIO . try @HTTP.HttpException $ HTTP.httpLbs tracedReq _accHttpManager diff --git a/server/src-lib/Hasura/Backends/MSSQL/Connection.hs b/server/src-lib/Hasura/Backends/MSSQL/Connection.hs index d7249286a1b..17ffdbf45de 100644 --- a/server/src-lib/Hasura/Backends/MSSQL/Connection.hs +++ b/server/src-lib/Hasura/Backends/MSSQL/Connection.hs @@ -8,7 +8,7 @@ -- and conversion functions between MSSQL and graphql-engine. module Hasura.Backends.MSSQL.Connection ( MSSQLConnConfiguration (MSSQLConnConfiguration), - MSSQLSourceConfig (MSSQLSourceConfig, _mscExecCtx), + MSSQLSourceConfig (MSSQLSourceConfig, _mscExecCtx, _mscReadReplicas), MSSQLConnectionInfo (..), MSSQLPoolSettings (..), MSSQLExecCtx (..), @@ -268,7 +268,9 @@ mkMSSQLAnyQueryTx q = do data MSSQLSourceConfig = MSSQLSourceConfig { _mscConnectionString :: MSPool.ConnectionString, - _mscExecCtx :: MSSQLExecCtx + _mscExecCtx :: MSSQLExecCtx, + -- | Number of read replicas used by the execution context + _mscReadReplicas :: Int } deriving (Generic) @@ -276,7 +278,7 @@ instance Show MSSQLSourceConfig where show = show . _mscConnectionString instance Eq MSSQLSourceConfig where - MSSQLSourceConfig connStr1 _ == MSSQLSourceConfig connStr2 _ = + MSSQLSourceConfig connStr1 _ _ == MSSQLSourceConfig connStr2 _ _ = connStr1 == connStr2 instance ToJSON MSSQLSourceConfig where diff --git a/server/src-lib/Hasura/Backends/MSSQL/DDL/Source.hs b/server/src-lib/Hasura/Backends/MSSQL/DDL/Source.hs index 39c8b245b0a..9a41ad751d9 100644 --- a/server/src-lib/Hasura/Backends/MSSQL/DDL/Source.hs +++ b/server/src-lib/Hasura/Backends/MSSQL/DDL/Source.hs @@ -70,14 +70,14 @@ resolveDatabaseMetadata config = runExceptT do dbTablesMetadata <- mssqlRunReadOnly mssqlExecCtx $ loadDBMetadata pure $ DBObjectsIntrospection dbTablesMetadata mempty mempty where - MSSQLSourceConfig _connString mssqlExecCtx = config + MSSQLSourceConfig _connString mssqlExecCtx _numReadReplicas = config postDropSourceHook :: (MonadIO m, MonadBaseControl IO m) => MSSQLSourceConfig -> TableEventTriggers 'MSSQL -> m () -postDropSourceHook (MSSQLSourceConfig _ mssqlExecCtx) tableTriggersMap = do +postDropSourceHook (MSSQLSourceConfig _ mssqlExecCtx _) tableTriggersMap = do -- The SQL triggers for MSSQL source are created within the schema of the table, -- and is not associated with 'hdb_catalog' schema. Thus only deleting the -- 'hdb_catalog' schema is not sufficient, since it will still leave the SQL diff --git a/server/src-lib/Hasura/Backends/MSSQL/Instances/Transport.hs b/server/src-lib/Hasura/Backends/MSSQL/Instances/Transport.hs index 0fb746f36e3..8156530b7ba 100644 --- a/server/src-lib/Hasura/Backends/MSSQL/Instances/Transport.hs +++ b/server/src-lib/Hasura/Backends/MSSQL/Instances/Transport.hs @@ -16,12 +16,14 @@ import Data.Text.Encoding (encodeUtf8) import Data.Text.Extended import Database.MSSQL.Transaction (forJsonQueryE) import Database.ODBC.SQLServer qualified as ODBC +import Hasura.Backends.DataConnector.Agent.Client (AgentLicenseKey) import Hasura.Backends.MSSQL.Connection import Hasura.Backends.MSSQL.Execute.QueryTags (withQueryTags) import Hasura.Backends.MSSQL.Instances.Execute import Hasura.Backends.MSSQL.SQL.Error import Hasura.Backends.MSSQL.ToQuery import Hasura.Base.Error +import Hasura.CredentialCache import Hasura.EncJSON import Hasura.GraphQL.Execute.Backend import Hasura.GraphQL.Execute.Subscription.Plan @@ -66,13 +68,14 @@ runQuery :: RootFieldAlias -> UserInfo -> L.Logger L.Hasura -> + Maybe (CredentialCache AgentLicenseKey) -> SourceConfig 'MSSQL -> OnBaseMonad (ExceptT QErr) (Maybe (AnyBackend ExecutionStats), EncJSON) -> Maybe (PreparedQuery 'MSSQL) -> ResolvedConnectionTemplate 'MSSQL -> -- | Also return the time spent in the PG query; for telemetry. m (DiffTime, EncJSON) -runQuery reqId query fieldName _userInfo logger _sourceConfig tx genSql _ = do +runQuery reqId query fieldName _userInfo logger _ _sourceConfig tx genSql _ = do logQueryLog logger $ mkQueryLog query fieldName genSql reqId withElapsedTime $ newSpan ("MSSQL Query for root field " <>> fieldName) $ @@ -84,9 +87,10 @@ runQueryExplain :: MonadError QErr m, MonadTrace m ) => + Maybe (CredentialCache AgentLicenseKey) -> DBStepInfo 'MSSQL -> m EncJSON -runQueryExplain (DBStepInfo _ _ _ action _) = fmap arResult (run action) +runQueryExplain _ (DBStepInfo _ _ _ action _) = fmap arResult (run action) runMutation :: ( MonadIO m, @@ -100,6 +104,7 @@ runMutation :: RootFieldAlias -> UserInfo -> L.Logger L.Hasura -> + Maybe (CredentialCache AgentLicenseKey) -> SourceConfig 'MSSQL -> OnBaseMonad (ExceptT QErr) EncJSON -> Maybe (PreparedQuery 'MSSQL) -> @@ -107,7 +112,7 @@ runMutation :: -- | Also return 'Mutation' when the operation was a mutation, and the time -- spent in the PG query; for telemetry. m (DiffTime, EncJSON) -runMutation reqId query fieldName _userInfo logger _sourceConfig tx _genSql _ = do +runMutation reqId query fieldName _userInfo logger _ _sourceConfig tx _genSql _ = do logQueryLog logger $ mkQueryLog query fieldName Nothing reqId withElapsedTime $ newSpan ("MSSQL Mutation for root field " <>> fieldName) $ diff --git a/server/src-lib/Hasura/Backends/MSSQL/Instances/Types.hs b/server/src-lib/Hasura/Backends/MSSQL/Instances/Types.hs index 39f4fbc1b9b..f351a7b0635 100644 --- a/server/src-lib/Hasura/Backends/MSSQL/Instances/Types.hs +++ b/server/src-lib/Hasura/Backends/MSSQL/Instances/Types.hs @@ -124,3 +124,5 @@ instance Backend 'MSSQL where instance HasSourceConfiguration 'MSSQL where type SourceConfig 'MSSQL = MSSQL.MSSQLSourceConfig type SourceConnConfiguration 'MSSQL = MSSQL.MSSQLConnConfiguration + sourceConfigNumReadReplicas = MSSQL._mscReadReplicas + sourceConfigConnectonTemplateEnabled = const False -- not supported diff --git a/server/src-lib/Hasura/Backends/MySQL/Instances/Transport.hs b/server/src-lib/Hasura/Backends/MySQL/Instances/Transport.hs index 00f6d2c553c..74401c8ffe0 100644 --- a/server/src-lib/Hasura/Backends/MySQL/Instances/Transport.hs +++ b/server/src-lib/Hasura/Backends/MySQL/Instances/Transport.hs @@ -5,8 +5,10 @@ module Hasura.Backends.MySQL.Instances.Transport (runQuery) where import Control.Monad.Trans.Control import Data.Aeson qualified as J import Data.Text.Extended +import Hasura.Backends.DataConnector.Agent.Client (AgentLicenseKey) import Hasura.Backends.MySQL.Instances.Execute () import Hasura.Base.Error +import Hasura.CredentialCache import Hasura.EncJSON import Hasura.GraphQL.Execute.Backend import Hasura.GraphQL.Logging @@ -41,13 +43,14 @@ runQuery :: RootFieldAlias -> UserInfo -> L.Logger L.Hasura -> + Maybe (CredentialCache AgentLicenseKey) -> SourceConfig 'MySQL -> OnBaseMonad IdentityT (Maybe (AnyBackend ExecutionStats), EncJSON) -> Maybe (PreparedQuery 'MySQL) -> ResolvedConnectionTemplate 'MySQL -> -- | Also return the time spent in the PG query; for telemetry. m (DiffTime, EncJSON) -runQuery reqId query fieldName _userInfo logger _sourceConfig tx genSql _ = do +runQuery reqId query fieldName _userInfo logger _ _sourceConfig tx genSql _ = do logQueryLog logger $ mkQueryLog query fieldName genSql reqId withElapsedTime $ newSpan ("MySQL Query for root field " <>> fieldName) $ @@ -59,9 +62,10 @@ runQueryExplain :: MonadError QErr m, MonadTrace m ) => + Maybe (CredentialCache AgentLicenseKey) -> DBStepInfo 'MySQL -> m EncJSON -runQueryExplain (DBStepInfo _ _ _ action _) = fmap arResult (run action) +runQueryExplain _ (DBStepInfo _ _ _ action _) = fmap arResult (run action) run :: (MonadIO m, MonadBaseControl IO m, MonadError QErr m, MonadTrace m) => OnBaseMonad IdentityT a -> m a run = runIdentityT . runOnBaseMonad diff --git a/server/src-lib/Hasura/Backends/MySQL/Instances/Types.hs b/server/src-lib/Hasura/Backends/MySQL/Instances/Types.hs index 20766ea62b6..be8f6900dc6 100644 --- a/server/src-lib/Hasura/Backends/MySQL/Instances/Types.hs +++ b/server/src-lib/Hasura/Backends/MySQL/Instances/Types.hs @@ -156,3 +156,5 @@ instance Backend 'MySQL where instance HasSourceConfiguration 'MySQL where type SourceConfig 'MySQL = MySQL.SourceConfig type SourceConnConfiguration 'MySQL = MySQL.ConnSourceConfig + sourceConfigNumReadReplicas = const 0 -- not supported + sourceConfigConnectonTemplateEnabled = const False -- not supported diff --git a/server/src-lib/Hasura/Backends/Postgres/Execute/Types.hs b/server/src-lib/Hasura/Backends/Postgres/Execute/Types.hs index 8489a9b62cc..c2808865743 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Execute/Types.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Execute/Types.hs @@ -24,6 +24,8 @@ module Hasura.Backends.Postgres.Execute.Types applyConnectionTemplateResolverNonAdmin, pgResolveConnectionTemplate, resolvePostgresConnectionTemplate, + sourceConfigNumReadReplicas, + sourceConfigConnectonTemplateEnabled, ) where @@ -32,6 +34,7 @@ import Control.Monad.Trans.Control (MonadBaseControl (..)) import Data.Aeson.Extended qualified as J import Data.CaseInsensitive qualified as CI import Data.HashMap.Internal.Strict qualified as Map +import Data.List.NonEmpty qualified as List.NonEmpty import Database.PG.Query qualified as PG import Database.PG.Query.Connection qualified as PG import Hasura.Backends.Postgres.Connection.Settings (ConnectionTemplate (..), PostgresConnectionSetMemberName) @@ -195,8 +198,7 @@ connectionTemplateConfigResolver = \case -- | A hook to resolve connection template newtype ConnectionTemplateResolver = ConnectionTemplateResolver - { -- | Runs the connection template resolver. This will return Nothing if - -- there is no Connection template defined for the source. + { -- | Runs the connection template resolver. _runResolver :: forall m. (MonadError QErr m) => @@ -317,3 +319,14 @@ resolvePostgresConnectionTemplate (ConnectionTemplate _templateSrc connectionTem let serializedErr = Kriti.serialize err in throw400WithDetail TemplateResolutionFailed ("Connection template evaluation failed: " <> Kriti._message serializedErr) (J.toJSON $ serializedErr) Right val -> runAesonParser (J.parseJSON @PostgresResolvedConnectionTemplate) val + +sourceConfigNumReadReplicas :: PGSourceConfig -> Int +sourceConfigNumReadReplicas = + maybe 0 List.NonEmpty.length . _pscReadReplicaConnInfos + +sourceConfigConnectonTemplateEnabled :: PGSourceConfig -> Bool +sourceConfigConnectonTemplateEnabled pgSourceConfig = + case _pscConnectionTemplateConfig pgSourceConfig of + ConnTemplate_NotApplicable -> False + ConnTemplate_NotConfigured -> False + ConnTemplate_Resolver _ -> True diff --git a/server/src-lib/Hasura/Backends/Postgres/Instances/Transport.hs b/server/src-lib/Hasura/Backends/Postgres/Instances/Transport.hs index df3f0d2d330..93dbb32ef23 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Instances/Transport.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Instances/Transport.hs @@ -16,6 +16,7 @@ import Data.ByteString qualified as B import Data.HashMap.Strict.InsOrd qualified as OMap import Data.Text.Extended import Database.PG.Query qualified as PG +import Hasura.Backends.DataConnector.Agent.Client (AgentLicenseKey) import Hasura.Backends.Postgres.Connection.MonadTx import Hasura.Backends.Postgres.Execute.Subscription qualified as PGL import Hasura.Backends.Postgres.Execute.Types @@ -23,6 +24,7 @@ import Hasura.Backends.Postgres.Instances.Execute qualified as EQ import Hasura.Backends.Postgres.SQL.Value import Hasura.Backends.Postgres.Translate.Select (PostgresAnnotatedFieldJSON) import Hasura.Base.Error +import Hasura.CredentialCache import Hasura.EncJSON import Hasura.GraphQL.Execute.Backend import Hasura.GraphQL.Execute.Subscription.Plan @@ -69,13 +71,14 @@ runPGQuery :: RootFieldAlias -> UserInfo -> L.Logger L.Hasura -> + Maybe (CredentialCache AgentLicenseKey) -> SourceConfig ('Postgres pgKind) -> OnBaseMonad (PG.TxET QErr) (Maybe (AB.AnyBackend ExecutionStats), EncJSON) -> Maybe EQ.PreparedSql -> ResolvedConnectionTemplate ('Postgres pgKind) -> -- | Also return the time spent in the PG query; for telemetry. m (DiffTime, EncJSON) -runPGQuery reqId query fieldName _userInfo logger sourceConfig tx genSql resolvedConnectionTemplate = do +runPGQuery reqId query fieldName _userInfo logger _ sourceConfig tx genSql resolvedConnectionTemplate = do -- log the generated SQL and the graphql query logQueryLog logger $ mkQueryLog query fieldName genSql reqId (resolvedConnectionTemplate <$ resolvedConnectionTemplate) withElapsedTime $ @@ -95,12 +98,13 @@ runPGMutation :: RootFieldAlias -> UserInfo -> L.Logger L.Hasura -> + Maybe (CredentialCache AgentLicenseKey) -> SourceConfig ('Postgres pgKind) -> OnBaseMonad (PG.TxET QErr) EncJSON -> Maybe EQ.PreparedSql -> ResolvedConnectionTemplate ('Postgres pgKind) -> m (DiffTime, EncJSON) -runPGMutation reqId query fieldName userInfo logger sourceConfig tx _genSql resolvedConnectionTemplate = do +runPGMutation reqId query fieldName userInfo logger _ sourceConfig tx _genSql resolvedConnectionTemplate = do -- log the graphql query logQueryLog logger $ mkQueryLog query fieldName Nothing reqId (resolvedConnectionTemplate <$ resolvedConnectionTemplate) withElapsedTime $ @@ -141,9 +145,10 @@ runPGQueryExplain :: MonadError QErr m, MonadTrace m ) => + Maybe (CredentialCache AgentLicenseKey) -> DBStepInfo ('Postgres pgKind) -> m EncJSON -runPGQueryExplain (DBStepInfo _ sourceConfig _ action resolvedConnectionTemplate) = +runPGQueryExplain _ (DBStepInfo _ sourceConfig _ action resolvedConnectionTemplate) = runQueryTx (_pscExecCtx sourceConfig) (GraphQLQuery resolvedConnectionTemplate) $ fmap arResult (runOnBaseMonad action) diff --git a/server/src-lib/Hasura/Backends/Postgres/Instances/Types.hs b/server/src-lib/Hasura/Backends/Postgres/Instances/Types.hs index 0b034e0f45d..f9e553c1b04 100644 --- a/server/src-lib/Hasura/Backends/Postgres/Instances/Types.hs +++ b/server/src-lib/Hasura/Backends/Postgres/Instances/Types.hs @@ -168,3 +168,5 @@ instance where type SourceConfig ('Postgres pgKind) = Postgres.PGSourceConfig type SourceConnConfiguration ('Postgres pgKind) = Postgres.PostgresConnConfiguration + sourceConfigNumReadReplicas = Postgres.sourceConfigNumReadReplicas + sourceConfigConnectonTemplateEnabled = Postgres.sourceConfigConnectonTemplateEnabled diff --git a/server/src-lib/Hasura/CredentialCache.hs b/server/src-lib/Hasura/CredentialCache.hs new file mode 100644 index 00000000000..bdcbf3208f1 --- /dev/null +++ b/server/src-lib/Hasura/CredentialCache.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE NumericUnderscores #-} + +-- | Interface for a service for maintaining short-lived credentials, such as +-- access tokens or JWTs. +module Hasura.CredentialCache + ( CredentialCache (..), + ) +where + +import Control.Concurrent.STM +import Hasura.Prelude + +newtype CredentialCache cred = CredentialCache + { -- | Get the stored credential. Also returns an STM action for + -- requesting a refresh of the credential, which, in turn, returns an STM + -- action for waiting on the arrival of the fresh credential. + getCredential :: STM (cred, STM (STM ())) + } + deriving stock (Functor) diff --git a/server/src-lib/Hasura/GraphQL/Execute/RemoteJoin/Join.hs b/server/src-lib/Hasura/GraphQL/Execute/RemoteJoin/Join.hs index 357725c4c1a..0ed105d3011 100644 --- a/server/src-lib/Hasura/GraphQL/Execute/RemoteJoin/Join.hs +++ b/server/src-lib/Hasura/GraphQL/Execute/RemoteJoin/Join.hs @@ -16,7 +16,9 @@ import Data.HashSet qualified as HS import Data.IntMap.Strict qualified as IntMap import Data.Text qualified as T import Data.Tuple (swap) +import Hasura.Backends.DataConnector.Agent.Client (AgentLicenseKey) import Hasura.Base.Error +import Hasura.CredentialCache import Hasura.EncJSON import Hasura.GraphQL.Execute.Backend qualified as EB import Hasura.GraphQL.Execute.Instances () @@ -65,6 +67,7 @@ processRemoteJoins :: ) => RequestId -> L.Logger L.Hasura -> + Maybe (CredentialCache AgentLicenseKey) -> Env.Environment -> [HTTP.Header] -> UserInfo -> @@ -72,7 +75,7 @@ processRemoteJoins :: Maybe RemoteJoins -> GQLReqUnparsed -> m EncJSON -processRemoteJoins requestId logger env requestHeaders userInfo lhs maybeJoinTree gqlreq = +processRemoteJoins requestId logger agentLicenseKey env requestHeaders userInfo lhs maybeJoinTree gqlreq = forRemoteJoins maybeJoinTree lhs \joinTree -> do lhsParsed <- JO.eitherDecode (encJToLBS lhs) @@ -104,6 +107,7 @@ processRemoteJoins requestId logger env requestHeaders userInfo lhs maybeJoinTre _sjcRootFieldAlias userInfo logger + agentLicenseKey _sjcSourceConfig (fmap (statsToAnyBackend @b) (EB.dbsiAction _sjcStepInfo)) (EB.dbsiPreparedQuery _sjcStepInfo) diff --git a/server/src-lib/Hasura/GraphQL/Explain.hs b/server/src-lib/Hasura/GraphQL/Explain.hs index 356c8e64134..1fac19d8a30 100644 --- a/server/src-lib/Hasura/GraphQL/Explain.hs +++ b/server/src-lib/Hasura/GraphQL/Explain.hs @@ -11,7 +11,9 @@ import Data.Aeson qualified as J import Data.Aeson.TH qualified as J import Data.HashMap.Strict qualified as Map import Data.HashMap.Strict.InsOrd qualified as OMap +import Hasura.Backends.DataConnector.Agent.Client (AgentLicenseKey) import Hasura.Base.Error +import Hasura.CredentialCache import Hasura.EncJSON import Hasura.GraphQL.Context qualified as C import Hasura.GraphQL.Execute qualified as E @@ -53,18 +55,20 @@ $( J.deriveJSON -- NOTE: This function has a 'MonadTrace' constraint in master, but we don't need it -- here. We should evaluate if we need it here. explainQueryField :: + forall m. ( MonadError QErr m, MonadIO m, MonadBaseControl IO m, MonadTrace m ) => + Maybe (CredentialCache AgentLicenseKey) -> UserInfo -> [HTTP.Header] -> Maybe G.Name -> RootFieldAlias -> QueryRootField UnpreparedValue -> m EncJSON -explainQueryField userInfo reqHeaders operationName fieldName rootField = do +explainQueryField agentLicenseKey userInfo reqHeaders operationName fieldName rootField = do case rootField of RFRemote _ -> throw400 InvalidParams "only hasura queries can be explained" RFAction _ -> throw400 InvalidParams "query actions cannot be explained" @@ -78,7 +82,7 @@ explainQueryField userInfo reqHeaders operationName fieldName rootField = do unless (isNothing remoteJoins) $ throw400 InvalidParams "queries with remote relationships cannot be explained" mkDBQueryExplain fieldName userInfo sourceName sourceConfig newDB reqHeaders operationName - AB.dispatchAnyBackend @BackendTransport step runDBQueryExplain + AB.dispatchAnyBackend @BackendTransport step (runDBQueryExplain agentLicenseKey) explainGQLQuery :: forall m. @@ -90,10 +94,11 @@ explainGQLQuery :: MonadTrace m ) => SchemaCache -> + Maybe (CredentialCache AgentLicenseKey) -> [HTTP.Header] -> GQLExplain -> m EncJSON -explainGQLQuery sc reqHeaders (GQLExplain query userVarsRaw maybeIsRelay) = do +explainGQLQuery sc agentLicenseKey reqHeaders (GQLExplain query userVarsRaw maybeIsRelay) = do -- NOTE!: we will be executing what follows as though admin role. See e.g. notes in explainField: userInfo <- mkUserInfo @@ -109,7 +114,7 @@ explainGQLQuery sc reqHeaders (GQLExplain query userVarsRaw maybeIsRelay) = do E.parseGraphQLQuery graphQLContext varDefs (GH._grVariables query) directives inlinedSelSet -- TODO: validate directives here encJFromList - <$> for (OMap.toList unpreparedQueries) (uncurry (explainQueryField userInfo reqHeaders (_unOperationName <$> _grOperationName query))) + <$> for (OMap.toList unpreparedQueries) (uncurry (explainQueryField agentLicenseKey userInfo reqHeaders (_unOperationName <$> _grOperationName query))) G.TypedOperationDefinition G.OperationTypeMutation _ _ _ _ -> throw400 InvalidParams "only queries can be explained" G.TypedOperationDefinition G.OperationTypeSubscription _ varDefs directives inlinedSelSet -> do diff --git a/server/src-lib/Hasura/GraphQL/Transport/Backend.hs b/server/src-lib/Hasura/GraphQL/Transport/Backend.hs index 79c9c0a772d..cc9fcae128d 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/Backend.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/Backend.hs @@ -5,7 +5,9 @@ where import Control.Monad.Trans.Control import Data.ByteString qualified as B +import Hasura.Backends.DataConnector.Agent.Client (AgentLicenseKey) import Hasura.Base.Error +import Hasura.CredentialCache import Hasura.EncJSON import Hasura.GraphQL.Execute.Backend import Hasura.GraphQL.Execute.Subscription.Plan @@ -38,6 +40,7 @@ class BackendExecute b => BackendTransport (b :: BackendType) where RootFieldAlias -> UserInfo -> L.Logger L.Hasura -> + Maybe (CredentialCache AgentLicenseKey) -> SourceConfig b -> OnBaseMonad (ExecutionMonad b) (Maybe (AnyBackend ExecutionStats), EncJSON) -> Maybe (PreparedQuery b) -> @@ -56,6 +59,7 @@ class BackendExecute b => BackendTransport (b :: BackendType) where RootFieldAlias -> UserInfo -> L.Logger L.Hasura -> + Maybe (CredentialCache AgentLicenseKey) -> SourceConfig b -> OnBaseMonad (ExecutionMonad b) EncJSON -> Maybe (PreparedQuery b) -> @@ -86,5 +90,6 @@ class BackendExecute b => BackendTransport (b :: BackendType) where MonadBaseControl IO m, MonadTrace m ) => + Maybe (CredentialCache AgentLicenseKey) -> DBStepInfo b -> m EncJSON diff --git a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs index ff3d62597c8..65d7c3af68b 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/HTTP.hs @@ -39,8 +39,10 @@ import Data.Environment qualified as Env import Data.HashMap.Strict.InsOrd qualified as OMap import Data.Monoid (Any (..)) import Data.Text qualified as T +import Hasura.Backends.DataConnector.Agent.Client (AgentLicenseKey) import Hasura.Backends.Postgres.Instances.Transport (runPGMutationTransaction) import Hasura.Base.Error +import Hasura.CredentialCache import Hasura.EncJSON import Hasura.GraphQL.Execute qualified as E import Hasura.GraphQL.Execute.Action qualified as EA @@ -327,6 +329,7 @@ runGQ :: ReadOnlyMode -> PrometheusMetrics -> L.Logger L.Hasura -> + Maybe (CredentialCache AgentLicenseKey) -> RequestId -> UserInfo -> Wai.IpAddress -> @@ -334,7 +337,7 @@ runGQ :: E.GraphQLQueryType -> GQLReqUnparsed -> m (GQLQueryOperationSuccessLog, HttpResponse (Maybe GQResponse, EncJSON)) -runGQ env sqlGenCtx sc scVer enableAL readOnlyMode prometheusMetrics logger reqId userInfo ipAddress reqHeaders queryType reqUnparsed = do +runGQ env sqlGenCtx sc scVer enableAL readOnlyMode prometheusMetrics logger agentLicenseKey reqId userInfo ipAddress reqHeaders queryType reqUnparsed = do let gqlMetrics = pmGraphQLRequestMetrics prometheusMetrics (totalTime, (response, parameterizedQueryHash, gqlOpType)) <- withElapsedTime $ do @@ -484,9 +487,9 @@ runGQ env sqlGenCtx sc scVer enableAL readOnlyMode prometheusMetrics logger reqI AB.dispatchAnyBackend @BackendTransport exists \(EB.DBStepInfo _ sourceConfig genSql tx resolvedConnectionTemplate :: EB.DBStepInfo b) -> - runDBQuery @b reqId reqUnparsed fieldName userInfo logger sourceConfig (fmap (statsToAnyBackend @b) tx) genSql resolvedConnectionTemplate + runDBQuery @b reqId reqUnparsed fieldName userInfo logger agentLicenseKey sourceConfig (fmap (statsToAnyBackend @b) tx) genSql resolvedConnectionTemplate finalResponse <- - RJ.processRemoteJoins reqId logger env reqHeaders userInfo resp remoteJoins reqUnparsed + RJ.processRemoteJoins reqId logger agentLicenseKey env reqHeaders userInfo resp remoteJoins reqUnparsed pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Local finalResponse [] E.ExecStepRemote rsi resultCustomizer gqlReq remoteJoins -> do logQueryLog logger $ QueryLog reqUnparsed Nothing reqId QueryLogKindRemoteSchema @@ -496,7 +499,7 @@ runGQ env sqlGenCtx sc scVer enableAL readOnlyMode prometheusMetrics logger reqI (time, resp) <- doQErr $ do (time, (resp, _)) <- EA.runActionExecution userInfo aep finalResponse <- - RJ.processRemoteJoins reqId logger env reqHeaders userInfo resp remoteJoins reqUnparsed + RJ.processRemoteJoins reqId logger agentLicenseKey env reqHeaders userInfo resp remoteJoins reqUnparsed pure (time, finalResponse) pure $ AnnotatedResponsePart time Telem.Empty resp [] E.ExecStepRaw json -> do @@ -517,9 +520,9 @@ runGQ env sqlGenCtx sc scVer enableAL readOnlyMode prometheusMetrics logger reqI AB.dispatchAnyBackend @BackendTransport exists \(EB.DBStepInfo _ sourceConfig genSql tx resolvedConnectionTemplate :: EB.DBStepInfo b) -> - runDBMutation @b reqId reqUnparsed fieldName userInfo logger sourceConfig (fmap EB.arResult tx) genSql resolvedConnectionTemplate + runDBMutation @b reqId reqUnparsed fieldName userInfo logger agentLicenseKey sourceConfig (fmap EB.arResult tx) genSql resolvedConnectionTemplate finalResponse <- - RJ.processRemoteJoins reqId logger env reqHeaders userInfo resp remoteJoins reqUnparsed + RJ.processRemoteJoins reqId logger agentLicenseKey env reqHeaders userInfo resp remoteJoins reqUnparsed pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Local finalResponse responseHeaders E.ExecStepRemote rsi resultCustomizer gqlReq remoteJoins -> do logQueryLog logger $ QueryLog reqUnparsed Nothing reqId QueryLogKindRemoteSchema @@ -529,7 +532,7 @@ runGQ env sqlGenCtx sc scVer enableAL readOnlyMode prometheusMetrics logger reqI (time, (resp, hdrs)) <- doQErr $ do (time, (resp, hdrs)) <- EA.runActionExecution userInfo aep finalResponse <- - RJ.processRemoteJoins reqId logger env reqHeaders userInfo resp remoteJoins reqUnparsed + RJ.processRemoteJoins reqId logger agentLicenseKey env reqHeaders userInfo resp remoteJoins reqUnparsed pure (time, (finalResponse, hdrs)) pure $ AnnotatedResponsePart time Telem.Empty resp $ fromMaybe [] hdrs E.ExecStepRaw json -> do @@ -549,6 +552,7 @@ runGQ env sqlGenCtx sc scVer enableAL readOnlyMode prometheusMetrics logger reqI RJ.processRemoteJoins reqId logger + agentLicenseKey env reqHeaders userInfo @@ -758,6 +762,7 @@ runGQBatched :: ReadOnlyMode -> PrometheusMetrics -> L.Logger L.Hasura -> + Maybe (CredentialCache AgentLicenseKey) -> RequestId -> ResponseInternalErrorsConfig -> UserInfo -> @@ -767,10 +772,10 @@ runGQBatched :: -- | the batched request with unparsed GraphQL query GQLBatchedReqs (GQLReq GQLQueryText) -> m (HttpLogGraphQLInfo, HttpResponse EncJSON) -runGQBatched env sqlGenCtx sc scVer enableAL readOnlyMode prometheusMetrics logger reqId responseErrorsConfig userInfo ipAddress reqHdrs queryType query = +runGQBatched env sqlGenCtx sc scVer enableAL readOnlyMode prometheusMetrics logger agentLicenseKey reqId responseErrorsConfig userInfo ipAddress reqHdrs queryType query = case query of GQLSingleRequest req -> do - (gqlQueryOperationLog, httpResp) <- runGQ env sqlGenCtx sc scVer enableAL readOnlyMode prometheusMetrics logger reqId userInfo ipAddress reqHdrs queryType req + (gqlQueryOperationLog, httpResp) <- runGQ env sqlGenCtx sc scVer enableAL readOnlyMode prometheusMetrics logger agentLicenseKey reqId userInfo ipAddress reqHdrs queryType req let httpLoggingGQInfo = (CommonHttpLogMetadata L.RequestModeSingle (Just (GQLSingleRequest (GQLQueryOperationSuccess gqlQueryOperationLog))), (PQHSetSingleton (gqolParameterizedQueryHash gqlQueryOperationLog))) pure (httpLoggingGQInfo, snd <$> httpResp) GQLBatchedReqs reqs -> do @@ -783,7 +788,7 @@ runGQBatched env sqlGenCtx sc scVer enableAL readOnlyMode prometheusMetrics logg flip HttpResponse [] . encJFromList . map (either (encJFromJValue . encodeGQErr includeInternal) _hrBody) - responses <- for reqs \req -> fmap (req,) $ try $ (fmap . fmap . fmap) snd $ runGQ env sqlGenCtx sc scVer enableAL readOnlyMode prometheusMetrics logger reqId userInfo ipAddress reqHdrs queryType req + responses <- for reqs \req -> fmap (req,) $ try $ (fmap . fmap . fmap) snd $ runGQ env sqlGenCtx sc scVer enableAL readOnlyMode prometheusMetrics logger agentLicenseKey reqId userInfo ipAddress reqHdrs queryType req let requestsOperationLogs = map fst $ rights $ map snd responses batchOperationLogs = map diff --git a/server/src-lib/Hasura/GraphQL/Transport/WSServerApp.hs b/server/src-lib/Hasura/GraphQL/Transport/WSServerApp.hs index b45b4426780..2007de15b40 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/WSServerApp.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/WSServerApp.hs @@ -13,6 +13,8 @@ import Data.Aeson (object, toJSON, (.=)) import Data.ByteString.Char8 qualified as B (pack) import Data.Text (pack) import Hasura.App.State +import Hasura.Backends.DataConnector.Agent.Client (AgentLicenseKey) +import Hasura.CredentialCache import Hasura.GraphQL.Execute qualified as E import Hasura.GraphQL.Logging import Hasura.GraphQL.Transport.HTTP (MonadExecuteQuery) @@ -62,9 +64,10 @@ createWSServerApp :: HashSet (L.EngineLogType L.Hasura) -> WSServerEnv impl -> WSConnectionInitTimeout -> + Maybe (CredentialCache AgentLicenseKey) -> -- | aka generalized 'WS.ServerApp' WS.HasuraServerApp m -createWSServerApp enabledLogTypes serverEnv connInitTimeout = \ !ipAddress !pendingConn -> do +createWSServerApp enabledLogTypes serverEnv connInitTimeout licenseKeyCache = \ !ipAddress !pendingConn -> do let getMetricsConfig = scMetricsConfig <$> getSchemaCache (_wseAppStateRef serverEnv) WS.createServerApp getMetricsConfig connInitTimeout (_wseServer serverEnv) prometheusMetrics handlers ipAddress pendingConn where @@ -90,7 +93,7 @@ createWSServerApp enabledLogTypes serverEnv connInitTimeout = \ !ipAddress !pend onMessageHandler conn bs sp = mask_ $ - onMessage enabledLogTypes getAuthMode serverEnv conn bs (wsActions sp) + onMessage enabledLogTypes getAuthMode serverEnv conn bs (wsActions sp) licenseKeyCache onCloseHandler conn = mask_ do liftIO $ EKG.Gauge.dec $ smWebsocketConnections serverMetrics diff --git a/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs b/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs index d4195c57ee4..a6aa1619e63 100644 --- a/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs +++ b/server/src-lib/Hasura/GraphQL/Transport/WebSocket.hs @@ -42,8 +42,10 @@ import Data.Time.Clock qualified as TC import Data.Word (Word16) import GHC.AssertNF.CPP import Hasura.App.State +import Hasura.Backends.DataConnector.Agent.Client (AgentLicenseKey) import Hasura.Backends.Postgres.Instances.Transport (runPGMutationTransaction) import Hasura.Base.Error +import Hasura.CredentialCache import Hasura.EncJSON import Hasura.GraphQL.Execute qualified as E import Hasura.GraphQL.Execute.Action qualified as EA @@ -417,13 +419,14 @@ onStart :: ProvidesNetwork m ) => HashSet (L.EngineLogType L.Hasura) -> + Maybe (CredentialCache AgentLicenseKey) -> WSServerEnv impl -> WSConn -> ShouldCaptureQueryVariables -> StartMsg -> WS.WSActions WSConnData -> m () -onStart enabledLogTypes serverEnv wsConn shouldCaptureVariables (StartMsg opId q) onMessageActions = catchAndIgnore $ do +onStart enabledLogTypes agentLicenseKey serverEnv wsConn shouldCaptureVariables (StartMsg opId q) onMessageActions = catchAndIgnore $ do timerTot <- startTimer op <- liftIO $ STM.atomically $ STMMap.lookup opId opMap let opName = _grOperationName q @@ -532,12 +535,13 @@ onStart enabledLogTypes serverEnv wsConn shouldCaptureVariables (StartMsg opId q fieldName userInfo logger + agentLicenseKey sourceConfig (fmap (statsToAnyBackend @b) tx) genSql resolvedConnectionTemplate finalResponse <- - RJ.processRemoteJoins requestId logger env reqHdrs userInfo resp remoteJoins q + RJ.processRemoteJoins requestId logger agentLicenseKey env reqHdrs userInfo resp remoteJoins q pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Local finalResponse [] E.ExecStepRemote rsi resultCustomizer gqlReq remoteJoins -> do logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindRemoteSchema @@ -547,7 +551,7 @@ onStart enabledLogTypes serverEnv wsConn shouldCaptureVariables (StartMsg opId q (time, (resp, _)) <- doQErr $ do (time, (resp, hdrs)) <- EA.runActionExecution userInfo actionExecPlan finalResponse <- - RJ.processRemoteJoins requestId logger env reqHdrs userInfo resp remoteJoins q + RJ.processRemoteJoins requestId logger agentLicenseKey env reqHdrs userInfo resp remoteJoins q pure (time, (finalResponse, hdrs)) pure $ AnnotatedResponsePart time Telem.Empty resp [] E.ExecStepRaw json -> do @@ -609,19 +613,20 @@ onStart enabledLogTypes serverEnv wsConn shouldCaptureVariables (StartMsg opId q fieldName userInfo logger + agentLicenseKey sourceConfig (fmap EB.arResult tx) genSql resolvedConnectionTemplate finalResponse <- - RJ.processRemoteJoins requestId logger env reqHdrs userInfo resp remoteJoins q + RJ.processRemoteJoins requestId logger agentLicenseKey env reqHdrs userInfo resp remoteJoins q pure $ AnnotatedResponsePart telemTimeIO_DT Telem.Local finalResponse [] E.ExecStepAction actionExecPlan _ remoteJoins -> do logQueryLog logger $ QueryLog q Nothing requestId QueryLogKindAction (time, (resp, hdrs)) <- doQErr $ do (time, (resp, hdrs)) <- EA.runActionExecution userInfo actionExecPlan finalResponse <- - RJ.processRemoteJoins requestId logger env reqHdrs userInfo resp remoteJoins q + RJ.processRemoteJoins requestId logger agentLicenseKey env reqHdrs userInfo resp remoteJoins q pure (time, (finalResponse, hdrs)) pure $ AnnotatedResponsePart time Telem.Empty resp $ fromMaybe [] hdrs E.ExecStepRemote rsi resultCustomizer gqlReq remoteJoins -> do @@ -786,6 +791,7 @@ onStart enabledLogTypes serverEnv wsConn shouldCaptureVariables (StartMsg opId q RJ.processRemoteJoins requestId logger + agentLicenseKey env reqHdrs userInfo @@ -1026,8 +1032,9 @@ onMessage :: WSConn -> LBS.ByteString -> WS.WSActions WSConnData -> + Maybe (CredentialCache AgentLicenseKey) -> m () -onMessage enabledLogTypes authMode serverEnv wsConn msgRaw onMessageActions = +onMessage enabledLogTypes authMode serverEnv wsConn msgRaw onMessageActions agentLicenseKey = Tracing.newTrace (_wseTraceSamplingPolicy serverEnv) "websocket" do case J.eitherDecode msgRaw of Left e -> do @@ -1051,7 +1058,7 @@ onMessage enabledLogTypes authMode serverEnv wsConn msgRaw onMessageActions = if _mcAnalyzeQueryVariables (scMetricsConfig schemaCache) then CaptureQueryVariables else DoNotCaptureQueryVariables - onStart enabledLogTypes serverEnv wsConn shouldCaptureVariables startMsg onMessageActions + onStart enabledLogTypes agentLicenseKey serverEnv wsConn shouldCaptureVariables startMsg onMessageActions CMStop stopMsg -> onStop serverEnv wsConn stopMsg -- specfic to graphql-ws CMPing mPayload -> onPing wsConn mPayload diff --git a/server/src-lib/Hasura/Metadata/Class.hs b/server/src-lib/Hasura/Metadata/Class.hs index 3a83e9139e9..b2285a6a6fd 100644 --- a/server/src-lib/Hasura/Metadata/Class.hs +++ b/server/src-lib/Hasura/Metadata/Class.hs @@ -2,6 +2,7 @@ module Hasura.Metadata.Class ( SchemaSyncEventProcessResult (..), MonadMetadataStorage (..), + MonadEECredentialsStorage (..), createOneOffScheduledEvent, createCronEvents, dropFutureCronEvents, @@ -21,6 +22,7 @@ import Hasura.Base.Error import Hasura.Eventing.ScheduledTrigger.Types import Hasura.Prelude import Hasura.RQL.Types.Action +import Hasura.RQL.Types.EECredentials import Hasura.RQL.Types.EventTrigger import Hasura.RQL.Types.Eventing import Hasura.RQL.Types.Metadata @@ -230,3 +232,25 @@ fetchCatalogState = getCatalogState -- | Update the state from metadata storage catalog updateCatalogState :: MonadMetadataStorage m => CatalogStateType -> Value -> m (Either QErr ()) updateCatalogState = setCatalogState + +-- | Metadata database operations for EE credentials storage. +-- +-- This class is only necessary because we haven't written an implementation +-- for storing EE credentials in Cloud. +class (Monad m) => MonadEECredentialsStorage m where + getEEClientCredentials :: m (Either QErr (Maybe EEClientCredentials)) + setEEClientCredentials :: EEClientCredentials -> m (Either QErr ()) + +instance (MonadEECredentialsStorage m, MonadTrans t, Monad (t m)) => MonadEECredentialsStorage (TransT t m) where + getEEClientCredentials = lift getEEClientCredentials + setEEClientCredentials a = lift $ setEEClientCredentials a + +deriving via (TransT (ReaderT r) m) instance (MonadEECredentialsStorage m) => MonadEECredentialsStorage (ReaderT r m) + +deriving via (TransT (StateT s) m) instance (MonadEECredentialsStorage m) => MonadEECredentialsStorage (StateT s m) + +deriving via (TransT (ExceptT e) m) instance (MonadEECredentialsStorage m) => MonadEECredentialsStorage (ExceptT e m) + +deriving via (TransT MetadataT m) instance (MonadEECredentialsStorage m) => MonadEECredentialsStorage (MetadataT m) + +deriving via (TransT ManagedT m) instance (MonadEECredentialsStorage m) => MonadEECredentialsStorage (ManagedT m) diff --git a/server/src-lib/Hasura/RQL/DDL/DataConnector.hs b/server/src-lib/Hasura/RQL/DDL/DataConnector.hs index b85f40e298d..216194c3419 100644 --- a/server/src-lib/Hasura/RQL/DDL/DataConnector.hs +++ b/server/src-lib/Hasura/RQL/DDL/DataConnector.hs @@ -135,7 +135,7 @@ checkAgentAvailability url = do manager <- askHTTPManager logger <- asks getter res <- runExceptT $ do - capabilitiesU <- (ignoreTraceT . flip runAgentClientT (AgentClientContext logger url manager Nothing) $ genericClient @API.Routes // API._capabilities) + capabilitiesU <- (ignoreTraceT . flip runAgentClientT (AgentClientContext logger url manager Nothing Nothing) $ genericClient @API.Routes // API._capabilities) API.capabilitiesCase (Error.throw500 "Capabilities request failed unexpectedly") pure diff --git a/server/src-lib/Hasura/RQL/DDL/Schema/Source.hs b/server/src-lib/Hasura/RQL/DDL/Schema/Source.hs index 9ee3172e5a6..d844cd18036 100644 --- a/server/src-lib/Hasura/RQL/DDL/Schema/Source.hs +++ b/server/src-lib/Hasura/RQL/DDL/Schema/Source.hs @@ -485,7 +485,7 @@ querySourceSchema :: m API.SchemaResponse querySourceSchema logger manager timeout uri sourceName transformedConfig = Tracing.ignoreTraceT - . flip Agent.Client.runAgentClientT (Agent.Client.AgentClientContext logger uri manager (DC.Types.sourceTimeoutMicroseconds <$> timeout)) + . flip Agent.Client.runAgentClientT (Agent.Client.AgentClientContext logger uri manager (DC.Types.sourceTimeoutMicroseconds <$> timeout) Nothing) $ schemaGuard =<< (Servant.Client.genericClient // API._schema) (Text.E.toTxt sourceName) transformedConfig schemaGuard :: MonadError QErr m => Union API.SchemaResponses -> m API.SchemaResponse diff --git a/server/src-lib/Hasura/RQL/Types/CustomTypes.hs b/server/src-lib/Hasura/RQL/Types/CustomTypes.hs index 95bb4dac417..8925ce42107 100644 --- a/server/src-lib/Hasura/RQL/Types/CustomTypes.hs +++ b/server/src-lib/Hasura/RQL/Types/CustomTypes.hs @@ -82,7 +82,7 @@ import Text.Builder qualified as T -- Hasura.RQL.DDL.RemoteSchema.Permission.GraphQLType; it should perhaps be -- renamed, made internal to this module, or removed altogether? newtype GraphQLType = GraphQLType {unGraphQLType :: G.GType} - deriving (Show, Eq, Generic, NFData) + deriving (Show, Eq, Ord, Generic, NFData) instance HasCodec GraphQLType where codec = bimapCodec dec enc codec @@ -148,7 +148,7 @@ data InputObjectTypeDefinition = InputObjectTypeDefinition _iotdDescription :: Maybe G.Description, _iotdFields :: NonEmpty InputObjectFieldDefinition } - deriving (Show, Eq, Generic) + deriving (Show, Eq, Ord, Generic) instance NFData InputObjectTypeDefinition @@ -172,7 +172,7 @@ data InputObjectFieldDefinition = InputObjectFieldDefinition _iofdType :: GraphQLType -- TODO: support default values } - deriving (Show, Eq, Generic) + deriving (Show, Eq, Ord, Generic) instance NFData InputObjectFieldDefinition @@ -254,7 +254,7 @@ data ScalarTypeDefinition = ScalarTypeDefinition { _stdName :: G.Name, _stdDescription :: Maybe G.Description } - deriving (Show, Eq, Generic) + deriving (Show, Eq, Ord, Generic) instance NFData ScalarTypeDefinition @@ -276,7 +276,7 @@ data EnumTypeDefinition = EnumTypeDefinition _etdDescription :: Maybe G.Description, _etdValues :: NonEmpty EnumValueDefinition } - deriving (Show, Eq, Generic) + deriving (Show, Eq, Ord, Generic) instance NFData EnumTypeDefinition @@ -299,7 +299,7 @@ data EnumValueDefinition = EnumValueDefinition _evdDescription :: Maybe G.Description, _evdIsDeprecated :: Maybe Bool } - deriving (Show, Eq, Generic) + deriving (Show, Eq, Ord, Generic) instance NFData EnumValueDefinition @@ -379,17 +379,19 @@ data AnnotatedInputType = NOCTScalar AnnotatedScalarType | NOCTEnum EnumTypeDefinition | NOCTInputObject InputObjectTypeDefinition - deriving (Eq, Generic) + deriving (Eq, Ord, Generic) data AnnotatedScalarType = ASTCustom ScalarTypeDefinition | ASTReusedScalar G.Name (AnyBackend ScalarWrapper) - deriving (Eq, Generic) + deriving (Eq, Ord, Generic) newtype ScalarWrapper b = ScalarWrapper {unwrapScalar :: (ScalarType b)} deriving instance (Backend b) => Eq (ScalarWrapper b) +deriving instance (Backend b) => Ord (ScalarWrapper b) + data AnnotatedOutputType = AOTObject AnnotatedObjectType | AOTScalar AnnotatedScalarType diff --git a/server/src-lib/Hasura/RQL/Types/EECredentials.hs b/server/src-lib/Hasura/RQL/Types/EECredentials.hs new file mode 100644 index 00000000000..4aa3aa07f15 --- /dev/null +++ b/server/src-lib/Hasura/RQL/Types/EECredentials.hs @@ -0,0 +1,60 @@ +{-# LANGUAGE QuasiQuotes #-} + +module Hasura.RQL.Types.EECredentials + ( EEClientCredentials (..), + EEClientId (..), + getEEClientCredentialsTx, + setEEClientCredentialsTx, + ) +where + +import Data.Aeson (FromJSON, (.:)) +import Data.Aeson qualified as Aeson +import Database.PG.Query qualified as PG +import Hasura.Backends.Postgres.Execute.Types +import Hasura.Base.Error +import Hasura.Prelude + +data EEClientCredentials = EEClientCredentials + { eccClientId :: EEClientId, + eccClientSecret :: Text + } + +newtype EEClientId = EEClientId {_getEEClientId :: Text} + deriving newtype (FromJSON) + +instance FromJSON EEClientCredentials where + parseJSON = Aeson.withObject "EEClientCredentials" $ \o -> do + eccClientId <- o .: "client_id" + eccClientSecret <- o .: "client_secret" + pure EEClientCredentials {..} + +getEEClientCredentialsTx :: PG.TxE QErr (Maybe EEClientCredentials) +getEEClientCredentialsTx = + makeClientCredentials . PG.getRow + <$> PG.withQE + defaultTxErrorHandler + [PG.sql| + SELECT ee_client_id::text, ee_client_secret + FROM hdb_catalog.hdb_version + |] + () + False + where + makeClientCredentials :: (Maybe Text, Maybe Text) -> Maybe EEClientCredentials + makeClientCredentials (clientIdMaybe, clientSecretMaybe) = do + eccClientId <- EEClientId <$> clientIdMaybe + eccClientSecret <- clientSecretMaybe + pure EEClientCredentials {..} + +setEEClientCredentialsTx :: EEClientCredentials -> PG.TxE QErr () +setEEClientCredentialsTx EEClientCredentials {..} = + PG.unitQE + defaultTxErrorHandler + [PG.sql| + UPDATE hdb_catalog.hdb_version + SET ee_client_id = $1, + ee_client_secret = $2 + |] + (_getEEClientId eccClientId, eccClientSecret) + True diff --git a/server/src-lib/Hasura/RQL/Types/SourceConfiguration.hs b/server/src-lib/Hasura/RQL/Types/SourceConfiguration.hs index 952129bada2..b2ea1573117 100644 --- a/server/src-lib/Hasura/RQL/Types/SourceConfiguration.hs +++ b/server/src-lib/Hasura/RQL/Types/SourceConfiguration.hs @@ -34,3 +34,10 @@ class -- | Internal connection configuration for a database - connection string, -- connection pool etc type SourceConfig b :: Type + + -- | The number of read replicas specified in the source configuration + sourceConfigNumReadReplicas :: SourceConfig b -> Int + + -- | Whether the source configuration specifies the use of a connection + -- template + sourceConfigConnectonTemplateEnabled :: SourceConfig b -> Bool diff --git a/server/src-lib/Hasura/Server/App.hs b/server/src-lib/Hasura/Server/App.hs index e85431ca7ac..af41f467fe7 100644 --- a/server/src-lib/Hasura/Server/App.hs +++ b/server/src-lib/Hasura/Server/App.hs @@ -3,6 +3,7 @@ module Hasura.Server.App ( APIResp (JSONResp, RawResp), + CEConsoleType (..), ConsoleRenderer (..), MonadVersionAPIWithExtraData (..), Handler, @@ -12,6 +13,7 @@ module Hasura.Server.App MonadMetadataApiAuthorization (..), AppContext (..), boolToText, + ceConsoleTypeIdentifier, configApiGetHandler, isAdminSecretSet, mkGetHandler, @@ -39,6 +41,7 @@ import Data.ByteString.Lazy qualified as BL import Data.CaseInsensitive qualified as CI import Data.HashMap.Strict qualified as M import Data.HashSet qualified as S +import Data.Kind (Type) import Data.String (fromString) import Data.Text qualified as T import Data.Text.Conversions (convertText) @@ -48,8 +51,10 @@ import Data.Text.Lazy.Encoding qualified as TL import GHC.Stats.Extended qualified as RTS import Hasura.App.State import Hasura.Backends.DataConnector.API (openApiSchema) +import Hasura.Backends.DataConnector.Agent.Client (AgentLicenseKey) import Hasura.Backends.Postgres.Execute.Types import Hasura.Base.Error +import Hasura.CredentialCache import Hasura.EncJSON import Hasura.GraphQL.Execute qualified as E import Hasura.GraphQL.Execute.Subscription.State qualified as ES @@ -119,7 +124,8 @@ data HandlerCtx = HandlerCtx hcUser :: UserInfo, hcReqHeaders :: [HTTP.Header], hcRequestId :: RequestId, - hcSourceIpAddress :: Wai.IpAddress + hcSourceIpAddress :: Wai.IpAddress, + hcLicenseKeyCache :: Maybe (CredentialCache AgentLicenseKey) } newtype Handler m a = Handler (ReaderT HandlerCtx (ExceptT QErr m) a) @@ -333,7 +339,7 @@ mkSpockAction appStateRef qErrEncoder qErrModifier apiHandler = do pure ( userInfo, authHeaders, - HandlerCtx appContext schemaCache schemaCacheVer userInfo headers requestId ipAddress, + HandlerCtx appContext schemaCache schemaCacheVer userInfo headers requestId ipAddress appEnvLicenseKeyCache, shouldIncludeInternal (_uiRole userInfo) acResponseInternalErrorsConfig, extraUserInfo ) @@ -544,7 +550,7 @@ v1Alpha1GQHandler queryType query = do reqHeaders <- asks hcReqHeaders ipAddress <- asks hcSourceIpAddress requestId <- asks hcRequestId - GH.runGQBatched acEnvironment acSQLGenCtx schemaCache schemaCacheVer acEnableAllowlist appEnvEnableReadOnlyMode appEnvPrometheusMetrics (_lsLogger appEnvLoggers) requestId acResponseInternalErrorsConfig userInfo ipAddress reqHeaders queryType query + GH.runGQBatched acEnvironment acSQLGenCtx schemaCache schemaCacheVer acEnableAllowlist appEnvEnableReadOnlyMode appEnvPrometheusMetrics (_lsLogger appEnvLoggers) appEnvLicenseKeyCache requestId acResponseInternalErrorsConfig userInfo ipAddress reqHeaders queryType query v1GQHandler :: ( MonadIO m, @@ -602,7 +608,8 @@ gqlExplainHandler query = do onlyAdmin schemaCache <- asks hcSchemaCache reqHeaders <- asks hcReqHeaders - res <- GE.explainGQLQuery (lastBuiltSchemaCache schemaCache) reqHeaders query + licenseKeyCache <- asks hcLicenseKeyCache + res <- GE.explainGQLQuery (lastBuiltSchemaCache schemaCache) licenseKeyCache reqHeaders query return $ HttpResponse res [] v1Alpha1PGDumpHandler :: (MonadIO m, MonadError QErr m, MonadReader HandlerCtx m) => PGD.PGDumpReqBody -> m APIResp @@ -653,10 +660,29 @@ consoleAssetsHandler logger loggingSettings dir path = do headers = ("Content-Type", mimeType) : encHeader class (Monad m) => ConsoleRenderer m where - renderConsole :: Text -> AuthMode -> TelemetryStatus -> Maybe Text -> Maybe Text -> m (Either String Text) + type ConsoleType m :: Type + renderConsole :: + Text -> + AuthMode -> + TelemetryStatus -> + Maybe Text -> + Maybe Text -> + ConsoleType m -> + m (Either String Text) + +-- TODO(awjchen): This is a kludge that will be removed when the entitlement service is fully implemented. +data CEConsoleType + = OSSConsole + | ProLiteConsole + +ceConsoleTypeIdentifier :: CEConsoleType -> String +ceConsoleTypeIdentifier = \case + OSSConsole -> "oss" + ProLiteConsole -> "pro-lite" instance ConsoleRenderer m => ConsoleRenderer (Tracing.TraceT m) where - renderConsole a b c d e = lift $ renderConsole a b c d e + type ConsoleType (Tracing.TraceT m) = ConsoleType m + renderConsole a b c d e f = lift $ renderConsole a b c d e f -- Type class to get any extra [Pair] for the version API class (Monad m) => MonadVersionAPIWithExtraData m where @@ -741,17 +767,18 @@ mkWaiApp :: ) => (AppStateRef impl -> Spock.SpockT m ()) -> AppStateRef impl -> + ConsoleType m -> EKG.Store EKG.EmptyMetrics -> WS.WSServerEnv impl -> m HasuraApp -mkWaiApp setupHook appStateRef ekgStore wsServerEnv = do +mkWaiApp setupHook appStateRef consoleType ekgStore wsServerEnv = do appEnv@AppEnv {..} <- askAppEnv spockApp <- liftWithStateless $ \lowerIO -> Spock.spockAsApp $ Spock.spockT lowerIO $ - httpApp setupHook appStateRef appEnv ekgStore + httpApp setupHook appStateRef appEnv consoleType ekgStore - let wsServerApp = WS.createWSServerApp (_lsEnabledLogTypes appEnvLoggingSettings) wsServerEnv appEnvWebSocketConnectionInitTimeout + let wsServerApp = WS.createWSServerApp (_lsEnabledLogTypes appEnvLoggingSettings) wsServerEnv appEnvWebSocketConnectionInitTimeout appEnvLicenseKeyCache stopWSServer = WS.stopWSServerApp wsServerEnv waiApp <- liftWithStateless $ \lowerIO -> @@ -789,9 +816,10 @@ httpApp :: (AppStateRef impl -> Spock.SpockT m ()) -> AppStateRef impl -> AppEnv -> + ConsoleType m -> EKG.Store EKG.EmptyMetrics -> Spock.SpockT m () -httpApp setupHook appStateRef AppEnv {..} ekgStore = do +httpApp setupHook appStateRef AppEnv {..} consoleType ekgStore = do -- Additional spock action to run setupHook appStateRef @@ -873,7 +901,7 @@ httpApp setupHook appStateRef AppEnv {..} ekgStore = do Spock.PATCH -> pure EP.PATCH other -> throw400 BadRequest $ "Method " <> tshow other <> " not supported." _ -> throw400 BadRequest $ "Nonstandard method not allowed for REST endpoints" - fmap JSONResp <$> runCustomEndpoint acEnvironment acSQLGenCtx schemaCache schemaCacheVer acEnableAllowlist appEnvEnableReadOnlyMode appEnvPrometheusMetrics (_lsLogger appEnvLoggers) requestId userInfo reqHeaders ipAddress req endpoints + fmap JSONResp <$> runCustomEndpoint acEnvironment acSQLGenCtx schemaCache schemaCacheVer acEnableAllowlist appEnvEnableReadOnlyMode appEnvPrometheusMetrics (_lsLogger appEnvLoggers) appEnvLicenseKeyCache requestId userInfo reqHeaders ipAddress req endpoints -- See Issue #291 for discussion around restified feature Spock.hookRouteAll ("api" "rest" Spock.wildcard) $ \wildcard -> do @@ -1062,7 +1090,7 @@ httpApp setupHook appStateRef AppEnv {..} ekgStore = do AppContext {..} <- liftIO $ getAppContext appStateRef req <- Spock.request let headers = Wai.requestHeaders req - consoleHtml <- lift $ renderConsole path acAuthMode acEnableTelemetry appEnvConsoleAssetsDir appEnvConsoleSentryDsn + consoleHtml <- lift $ renderConsole path acAuthMode acEnableTelemetry appEnvConsoleAssetsDir appEnvConsoleSentryDsn consoleType either (raiseGenericApiError logger appEnvLoggingSettings headers . internalError . T.pack) Spock.html consoleHtml serveApiConsoleAssets = do diff --git a/server/src-lib/Hasura/Server/Auth.hs b/server/src-lib/Hasura/Server/Auth.hs index 2c4f8c0335e..ddc854e40d7 100644 --- a/server/src-lib/Hasura/Server/Auth.hs +++ b/server/src-lib/Hasura/Server/Auth.hs @@ -49,7 +49,7 @@ import Network.HTTP.Client qualified as HTTP import Network.HTTP.Types qualified as HTTP -- | Typeclass representing the @UserInfo@ authorization and resolving effect -class (Monad m) => UserAuthentication m where +class Monad m => UserAuthentication m where resolveUserInfo :: Logger Hasura -> HTTP.Manager -> diff --git a/server/src-lib/Hasura/Server/Rest.hs b/server/src-lib/Hasura/Server/Rest.hs index 012734fa699..ac3af375521 100644 --- a/server/src-lib/Hasura/Server/Rest.hs +++ b/server/src-lib/Hasura/Server/Rest.hs @@ -14,7 +14,9 @@ import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.Text.Extended import Data.These (These (..)) +import Hasura.Backends.DataConnector.Agent.Client (AgentLicenseKey) import Hasura.Base.Error +import Hasura.CredentialCache import Hasura.EncJSON import Hasura.GraphQL.Execute qualified as E import Hasura.GraphQL.Logging (MonadExecutionLog, MonadQueryLog) @@ -118,6 +120,7 @@ runCustomEndpoint :: ReadOnlyMode -> PrometheusMetrics -> L.Logger L.Hasura -> + Maybe (CredentialCache AgentLicenseKey) -> RequestId -> UserInfo -> [HTTP.Header] -> @@ -125,7 +128,7 @@ runCustomEndpoint :: RestRequest EndpointMethod -> EndpointTrie GQLQueryWithText -> m (HttpLogGraphQLInfo, HttpResponse EncJSON) -runCustomEndpoint env sqlGenCtx sc scVer enableAL readOnlyMode prometheusMetrics logger requestId userInfo reqHeaders ipAddress RestRequest {..} endpoints = do +runCustomEndpoint env sqlGenCtx sc scVer enableAL readOnlyMode prometheusMetrics logger agentLicenseKey requestId userInfo reqHeaders ipAddress RestRequest {..} endpoints = do -- First match the path to an endpoint. case matchPath reqMethod (T.split (== '/') reqPath) endpoints of MatchFound (queryx :: EndpointMetadata GQLQueryWithText) matches -> @@ -155,7 +158,7 @@ runCustomEndpoint env sqlGenCtx sc scVer enableAL readOnlyMode prometheusMetrics -- with the query string from the schema cache, and pass it -- through to the /v1/graphql endpoint. (httpLoggingMetadata, handlerResp) <- do - (gqlOperationLog, resp) <- GH.runGQ env sqlGenCtx sc scVer enableAL readOnlyMode prometheusMetrics logger requestId userInfo ipAddress reqHeaders E.QueryHasura (mkPassthroughRequest queryx resolvedVariables) + (gqlOperationLog, resp) <- GH.runGQ env sqlGenCtx sc scVer enableAL readOnlyMode prometheusMetrics logger agentLicenseKey requestId userInfo ipAddress reqHeaders E.QueryHasura (mkPassthroughRequest queryx resolvedVariables) let httpLoggingGQInfo = (CommonHttpLogMetadata RequestModeNonBatchable Nothing, (PQHSetSingleton (gqolParameterizedQueryHash gqlOperationLog))) return (httpLoggingGQInfo, fst <$> resp) case sequence handlerResp of diff --git a/server/src-lib/Hasura/Server/Types.hs b/server/src-lib/Hasura/Server/Types.hs index f2b301f8023..9ea123fc2bd 100644 --- a/server/src-lib/Hasura/Server/Types.hs +++ b/server/src-lib/Hasura/Server/Types.hs @@ -62,6 +62,8 @@ newtype MetadataDbId = MetadataDbId {getMetadataDbId :: Text} mdDbIdToDbUid :: MetadataDbId -> DbUid mdDbIdToDbUid = DbUid . getMetadataDbId +-- | A UUID for each running instance of graphql-engine, generated fresh each +-- time graphql-engine starts up newtype InstanceId = InstanceId {getInstanceId :: Text} deriving (Show, Eq, ToJSON, FromJSON, PG.FromCol, PG.ToPrepArg) diff --git a/server/src-rsr/catalog_version.txt b/server/src-rsr/catalog_version.txt index abac1ea7b75..21e72e8ac3d 100644 --- a/server/src-rsr/catalog_version.txt +++ b/server/src-rsr/catalog_version.txt @@ -1 +1 @@ -47 +48 diff --git a/server/src-rsr/console.html b/server/src-rsr/console.html index 194742b0ee6..96406c793fc 100644 --- a/server/src-rsr/console.html +++ b/server/src-rsr/console.html @@ -13,7 +13,7 @@ assetsVersion: "{{assetsVersion}}", cdnAssets: {{cdnAssets}}, serverVersion: "{{serverVersion}}", - consoleType: "oss", + consoleType: "{{consoleType}}", consoleSentryDsn: "{{consoleSentryDsn}}" }; window.__env.versionedAssetsPath = window.__env.assetsPath; diff --git a/server/src-rsr/initialise.sql b/server/src-rsr/initialise.sql index 56f9d1fa5e3..d0bf7969ed5 100644 --- a/server/src-rsr/initialise.sql +++ b/server/src-rsr/initialise.sql @@ -21,7 +21,9 @@ CREATE TABLE hdb_catalog.hdb_version ( version TEXT NOT NULL, upgraded_on TIMESTAMPTZ NOT NULL, cli_state JSONB NOT NULL DEFAULT '{}'::jsonb, - console_state JSONB NOT NULL DEFAULT '{}'::jsonb + console_state JSONB NOT NULL DEFAULT '{}'::jsonb, + ee_client_id TEXT, + ee_client_secret TEXT ); CREATE UNIQUE INDEX hdb_version_one_row diff --git a/server/src-rsr/migrations/47_to_48.sql b/server/src-rsr/migrations/47_to_48.sql new file mode 100644 index 00000000000..b1166d7becd --- /dev/null +++ b/server/src-rsr/migrations/47_to_48.sql @@ -0,0 +1,3 @@ +ALTER TABLE hdb_catalog.hdb_version + ADD COLUMN ee_client_id TEXT, + ADD COLUMN ee_client_secret TEXT; diff --git a/server/src-rsr/migrations/48_to_47.sql b/server/src-rsr/migrations/48_to_47.sql new file mode 100644 index 00000000000..1f8533ac2d3 --- /dev/null +++ b/server/src-rsr/migrations/48_to_47.sql @@ -0,0 +1,3 @@ +ALTER TABLE hdb_catalog.hdb_version + DROP COLUMN ee_client_id, + DROP COLUMN ee_client_secret; diff --git a/server/src-test/Hasura/Backends/DataConnector/API/V0/CapabilitiesSpec.hs b/server/src-test/Hasura/Backends/DataConnector/API/V0/CapabilitiesSpec.hs index 55df1aa741b..66604534011 100644 --- a/server/src-test/Hasura/Backends/DataConnector/API/V0/CapabilitiesSpec.hs +++ b/server/src-test/Hasura/Backends/DataConnector/API/V0/CapabilitiesSpec.hs @@ -158,6 +158,9 @@ genRawCapabilities = pure RawCapabilities {} genDatasetCapabilities :: MonadGen m => m DatasetCapabilities genDatasetCapabilities = pure DatasetCapabilities {} +genLicensing :: MonadGen m => m Licensing +genLicensing = pure Licensing {} + genCapabilities :: Gen Capabilities genCapabilities = Capabilities @@ -172,6 +175,7 @@ genCapabilities = <*> Gen.maybe genExplainCapabilities <*> Gen.maybe genRawCapabilities <*> Gen.maybe genDatasetCapabilities + <*> Gen.maybe genLicensing emptyConfigSchemaResponse :: ConfigSchemaResponse emptyConfigSchemaResponse = ConfigSchemaResponse mempty mempty