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 = (

{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 00000000000..334a81b2615 Binary files /dev/null and b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/amazon.webp differ diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/citus.webp b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/citus.webp new file mode 100644 index 00000000000..50927979765 Binary files /dev/null and b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/citus.webp differ 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 00000000000..9cea88eb11f Binary files /dev/null and b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/cockroach.webp differ 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 00000000000..690ff0adc91 Binary files /dev/null and b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/google.webp differ 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 00000000000..a5e9475d8b2 Binary files /dev/null and b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/microsoft.webp differ 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 00000000000..5466243bbdd Binary files /dev/null and b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/mysql.webp differ 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 00000000000..4e1519aebe3 Binary files /dev/null and b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/postgres.webp differ diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/snowflake.webp b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/snowflake.webp new file mode 100644 index 00000000000..cd71c38d7c1 Binary files /dev/null and b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/snowflake.webp differ 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 00000000000..5990ec8c1eb Binary files /dev/null and b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/graphics/db-logos/sqlite.webp differ diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/index.ts b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/index.ts new file mode 100644 index 00000000000..d71170d4769 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/index.ts @@ -0,0 +1,6 @@ +export { useDatabaseConnectDrivers } from './useConnectDatabaseDrivers'; +export { useEnvironmentState } from './useEnvironmentState'; +export { useDropSource } from './useDropSource'; +export { useManageDatabaseConnection } from './useManageDatabaseConnection'; +export { usePushRoute } from './usePushRoute'; +export { useReloadSource } from './useReloadSource'; diff --git a/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useConnectDatabaseDrivers.tsx b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useConnectDatabaseDrivers.tsx new file mode 100644 index 00000000000..1372ed8c159 --- /dev/null +++ b/frontend/libs/console/legacy-ce/src/lib/features/ConnectDBRedesign/hooks/useConnectDatabaseDrivers.tsx @@ -0,0 +1,68 @@ +import { sortBy, uniqBy } from 'lodash'; // eslint-disable-line @typescript-eslint/no-restricted-imports +import { useAvailableDrivers } from '../../ConnectDB/hooks'; +import { DriverInfo } from '../../DataSource'; +import { DatabaseLogo } from '../components'; +import dbLogos from '../graphics/db-logos'; + +type useDatabaseConnectDriversProps = { + onFirstSuccess?: (data: DriverInfo[]) => 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 00000000000..2c9008fb868 Binary files /dev/null and b/frontend/libs/console/legacy-ce/src/lib/features/EETrial/assets/icon-query-caching.png differ 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