mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-11 02:35:32 +03:00
[feature branch] EE Lite Trials
PR-URL: https://github.com/hasura/graphql-engine-mono/pull/8208 Co-authored-by: awjchen <13142944+awjchen@users.noreply.github.com> Co-authored-by: Vijay Prasanna <11921040+vijayprasanna13@users.noreply.github.com> Co-authored-by: Toan Nguyen <1615675+hgiasac@users.noreply.github.com> Co-authored-by: Abhijeet Khangarot <26903230+abhi40308@users.noreply.github.com> Co-authored-by: Solomon <24038+solomon-b@users.noreply.github.com> Co-authored-by: gneeri <10553562+gneeri@users.noreply.github.com> GitOrigin-RevId: 454ee0dea636da77e43810edb2f427137027956c
This commit is contained in:
parent
f2b76b7da7
commit
c6d65508b2
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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": {
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
7
dc-agents/dc-api-types/src/models/Licensing.ts
Normal file
7
dc-agents/dc-api-types/src/models/Licensing.ts
Normal file
@ -0,0 +1,7 @@
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
export type Licensing = {
|
||||
};
|
||||
|
10
dc-agents/package-lock.json
generated
10
dc-agents/package-lock.json
generated
@ -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",
|
||||
|
4
dc-agents/reference/package-lock.json
generated
4
dc-agents/reference/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
4
dc-agents/sqlite/package-lock.json
generated
4
dc-agents/sqlite/package-lock.json
generated
@ -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",
|
||||
|
@ -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",
|
||||
|
3
frontend/.vscode/settings.json
vendored
3
frontend/.vscode/settings.json
vendored
@ -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
|
||||
}
|
||||
|
@ -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/');
|
||||
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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<ConnectInjectedProps> = ({ dispatch, children }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const showLogo =
|
||||
globals.consoleType === 'pro' || globals.consoleType === 'pro-lite' ? (
|
||||
<img className="flex w-36 mx-auto" src={hasuraEELogo} alt="Hasura EE" />
|
||||
) : (
|
||||
<img src={hasuraLogo} alt="Hasura" />
|
||||
);
|
||||
const showLogo = isProConsole(globals) ? (
|
||||
<img className="flex w-36 mx-auto" src={hasuraEELogo} alt="Hasura EE" />
|
||||
) : (
|
||||
<img src={hasuraLogo} alt="Hasura" />
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-screen bg-gray-100">
|
||||
|
@ -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 = (
|
||||
<ul className="bootstrap-jail">
|
||||
@ -26,7 +26,11 @@ class Container extends React.Component {
|
||||
<Link className={styles.linkBorder} to={appPrefix + '/manage'}>
|
||||
Manage
|
||||
</Link>
|
||||
<LeftSidebar appPrefix={appPrefix} {...this.props} />
|
||||
<LeftSidebar
|
||||
appPrefix={appPrefix}
|
||||
{...this.props}
|
||||
allowOpenApiImport
|
||||
/>
|
||||
</li>
|
||||
<li
|
||||
role="presentation"
|
||||
|
@ -8,7 +8,6 @@ import { appPrefix, pageTitle } from '../constants';
|
||||
import globals from '../../../../Globals';
|
||||
import { Button } from '../../../../new-components/Button';
|
||||
import TopicDescription from '../../Common/Landing/TopicDescription';
|
||||
import { isImportFromOpenAPIEnabled } from '../../../../utils';
|
||||
import { FaEdit, FaFileImport } from 'react-icons/fa';
|
||||
import { Badge } from '../../../../new-components/Badge';
|
||||
|
||||
@ -65,29 +64,25 @@ class Landing extends React.Component {
|
||||
<div className={'flex'}>
|
||||
<h2 className="font-bold text-3xl pr-3">Actions</h2>
|
||||
{getAddBtn()}
|
||||
{isImportFromOpenAPIEnabled(window.__env) && (
|
||||
<Analytics
|
||||
name="action-tab-btn-import-action-from-openapi"
|
||||
passHtmlAttributesToChildren
|
||||
<Analytics
|
||||
name="action-tab-btn-import-action-from-openapi"
|
||||
passHtmlAttributesToChildren
|
||||
>
|
||||
<Button
|
||||
icon={<FaFileImport />}
|
||||
className="ml-2"
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
push(`${globals.urlPrefix}${appPrefix}/manage/add-oas`)
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
icon={<FaFileImport />}
|
||||
className="ml-2"
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
push(
|
||||
`${globals.urlPrefix}${appPrefix}/manage/add-oas`
|
||||
)
|
||||
);
|
||||
}}
|
||||
>
|
||||
Import from OpenAPI
|
||||
<Badge className="ml-2 font-xs" color="purple">
|
||||
New
|
||||
</Badge>
|
||||
</Button>
|
||||
</Analytics>
|
||||
)}
|
||||
Import from OpenAPI
|
||||
<Badge className="ml-2 font-xs" color="purple">
|
||||
New
|
||||
</Badge>
|
||||
</Button>
|
||||
</Analytics>
|
||||
</div>
|
||||
<hr className="mt-5 mb-5" />
|
||||
{getIntroSection()}
|
||||
|
@ -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 ? (
|
||||
<div
|
||||
className={`col-xs-4 text-center ${styles.padd_left_remove} ${styles.sidebarCreateTable}`}
|
||||
>
|
||||
|
@ -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<AllowListDetailProps> = props => {
|
||||
const { name, section } = props.params;
|
||||
|
||||
const { access: eeLiteAccess } = useEELiteAccess(globals);
|
||||
const {
|
||||
data: queryCollections,
|
||||
isLoading,
|
||||
@ -54,6 +56,11 @@ export const AllowListDetail: React.FC<AllowListDetailProps> = props => {
|
||||
pushUrl(queryCollections[0].name, section ?? 'operations');
|
||||
}
|
||||
|
||||
const isFeatureActive = isProConsole(globals) || eeLiteAccess === 'active';
|
||||
const isFeatureSupported =
|
||||
isProConsole(globals) || eeLiteAccess !== 'forbidden';
|
||||
const isEELiteContext = eeLiteAccess !== 'forbidden';
|
||||
|
||||
return (
|
||||
<Analytics name="AllowList" {...REDACT_EVERYTHING}>
|
||||
<div className="flex flex-auto overflow-y-hidden h-[calc(100vh-35.49px-54px)]">
|
||||
@ -90,7 +97,7 @@ export const AllowListDetail: React.FC<AllowListDetailProps> = props => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{isProConsole(window.__env) ? (
|
||||
{isFeatureSupported ? (
|
||||
<Tabs
|
||||
value={section}
|
||||
onValueChange={value => {
|
||||
@ -111,7 +118,22 @@ export const AllowListDetail: React.FC<AllowListDetailProps> = props => {
|
||||
label: 'Permissions',
|
||||
content: (
|
||||
<div className="p-4">
|
||||
<AllowListPermissions collectionName={name} />
|
||||
{isFeatureActive ? (
|
||||
<AllowListPermissions collectionName={name} />
|
||||
) : (
|
||||
isEELiteContext && (
|
||||
<div className="max-w-3xl">
|
||||
<EETrialCard
|
||||
id="allow-list-role-based-permission"
|
||||
cardTitle="Looking to add role based permissions to your Allow List?"
|
||||
cardText="Get production-ready today with a 30-day free trial of Hasura EE, no credit card required."
|
||||
buttonType="default"
|
||||
eeAccess={eeLiteAccess}
|
||||
horizontal
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
|
@ -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.Logo>GraphiQL</GraphiQL.Logo>
|
||||
<GraphiQL.Toolbar>
|
||||
{getGraphiqlButtons()}
|
||||
<WithEELiteAccess globals={globals}>
|
||||
{({ access: eeLiteAccess }) => {
|
||||
return getGraphiqlButtons(eeLiteAccess);
|
||||
}}
|
||||
</WithEELiteAccess>
|
||||
<AnalyzeButton
|
||||
operations={graphiqlContext && graphiqlContext.state.operations}
|
||||
analyzeFetcher={analyzeFetcherInstance}
|
||||
|
@ -3,6 +3,7 @@ import Helmet from 'react-helmet';
|
||||
import CommonTabLayout from '../../../Common/Layout/CommonTabLayout/CommonTabLayout';
|
||||
import { RightContainer } from '../../../Common/Layout/RightContainer';
|
||||
import styles from '../../../Common/Common.module.scss';
|
||||
import { ApiSecurityTabEELiteWrapper } from '../../../../features/EETrial';
|
||||
|
||||
const appPrefix = `/api`;
|
||||
|
||||
@ -32,21 +33,23 @@ export const SecurityTabs: React.FC<{ tabName: keyof typeof tabs }> = ({
|
||||
return (
|
||||
<RightContainer>
|
||||
<Helmet title={`${tabs[tabName].display_text} - Hasura`} />
|
||||
<div
|
||||
className={`${styles.view_stitch_schema_wrapper} ${styles.addWrapper}`}
|
||||
>
|
||||
<CommonTabLayout
|
||||
appPrefix={appPrefix}
|
||||
currentTab={tabName}
|
||||
heading={tabs[tabName].display_text}
|
||||
tabsInfo={tabs}
|
||||
breadCrumbs={breadCrumbs}
|
||||
baseUrl={`${appPrefix}/security`}
|
||||
showLoader={false}
|
||||
testPrefix="security-features-tabs"
|
||||
/>
|
||||
<div className={styles.add_pad_top}>{children}</div>
|
||||
</div>
|
||||
<ApiSecurityTabEELiteWrapper>
|
||||
<div
|
||||
className={`${styles.view_stitch_schema_wrapper} ${styles.addWrapper}`}
|
||||
>
|
||||
<CommonTabLayout
|
||||
appPrefix={appPrefix}
|
||||
currentTab={tabName}
|
||||
heading={tabs[tabName].display_text}
|
||||
tabsInfo={tabs}
|
||||
breadCrumbs={breadCrumbs}
|
||||
baseUrl={`${appPrefix}/security`}
|
||||
showLoader={false}
|
||||
testPrefix="security-features-tabs"
|
||||
/>
|
||||
<div className={styles.add_pad_top}>{children}</div>
|
||||
</div>
|
||||
</ApiSecurityTabEELiteWrapper>
|
||||
</RightContainer>
|
||||
);
|
||||
};
|
||||
|
@ -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<unknown, unknown>['location'];
|
||||
};
|
||||
|
||||
const TopNav: React.FC<TopNavProps> = ({ location }) => {
|
||||
const { access: eeLiteAccess } = useEELiteAccess(globals);
|
||||
|
||||
const sectionsData = [
|
||||
[
|
||||
{
|
||||
@ -32,7 +36,7 @@ const TopNav: React.FC<TopNavProps> = ({ location }) => {
|
||||
],
|
||||
];
|
||||
|
||||
if (canAccessSecuritySettings()) {
|
||||
if (isProConsole(globals) || eeLiteAccess !== 'forbidden') {
|
||||
sectionsData[1].push({
|
||||
key: 'security',
|
||||
link: '/api/security/api_limits',
|
||||
|
@ -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 = (
|
||||
|
||||
<Route path="v2">
|
||||
<Route path="manage">
|
||||
<Route path="connect" component={ConnectDatabaseRouteWrapper} />
|
||||
<Route path="database/add" component={ConnectUIContainer} />
|
||||
<Route path="database/edit" component={ConnectUIContainer} />
|
||||
<Route path="table" component={ManageTable}>
|
||||
<IndexRedirect to="modify" />
|
||||
<Route path=":operation" component={ManageTable} />
|
||||
|
@ -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<ConnectDatabaseProps> = 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<ConnectDatabaseProps> = props => {
|
||||
{getSupportedDrivers('connectDbForm.read_replicas.edit').includes(
|
||||
connectDBInputState.dbType
|
||||
) &&
|
||||
canAccessReadReplica(connectDBInputState.dbType) && (
|
||||
canAccessReadReplica && (
|
||||
<ReadReplicaForm
|
||||
readReplicaState={readReplicasState}
|
||||
readReplicaDispatch={readReplicaDispatch}
|
||||
@ -464,7 +473,7 @@ const ConnectDatabase: React.FC<ConnectDatabaseProps> = props => {
|
||||
{getSupportedDrivers('connectDbForm.read_replicas.create').includes(
|
||||
connectDBInputState.dbType
|
||||
) &&
|
||||
canAccessReadReplica(connectDBInputState.dbType) && (
|
||||
canAccessReadReplica && (
|
||||
<ReadReplicaForm
|
||||
readReplicaState={readReplicasState}
|
||||
readReplicaDispatch={readReplicaDispatch}
|
||||
|
@ -0,0 +1,49 @@
|
||||
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';
|
||||
|
||||
type NeonConnectProps = {
|
||||
allDatabases: string[];
|
||||
dispatch: Dispatch;
|
||||
connectDbUrl?: string;
|
||||
};
|
||||
// This component deals with Neon DB creation on connect DB page
|
||||
export function NeonConnect({
|
||||
dispatch,
|
||||
allDatabases,
|
||||
connectDbUrl = '/data/manage/connect',
|
||||
}: NeonConnectProps) {
|
||||
// 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(connectDbUrl));
|
||||
};
|
||||
|
||||
const neonIntegrationStatus = useNeonIntegration(
|
||||
getNeonDBName(allDatabases),
|
||||
pushToDataSource,
|
||||
pushToConnectDBPage,
|
||||
dispatch,
|
||||
'data-manage-create'
|
||||
);
|
||||
|
||||
const neonBannerProps = transformNeonIntegrationStatusToNeonBannerProps(
|
||||
neonIntegrationStatus
|
||||
);
|
||||
|
||||
return <NeonBanner {...neonBannerProps} />;
|
||||
}
|
@ -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 <NeonBanner {...neonBannerProps} />;
|
||||
}
|
||||
|
||||
export { NeonConnect } from './NeonConnect';
|
||||
export { useNeonIntegration } from './useNeonIntegration';
|
||||
|
@ -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<Props> = ({ dispatch, allDataSources }) => {
|
||||
<div className={styles.connect_db_content}>
|
||||
<div className={`${styles.container} mb-md`}>
|
||||
<div className="w-full mb-md">
|
||||
<Neon
|
||||
<NeonConnect
|
||||
allDatabases={allDataSources.map(d => d.name)}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
@ -52,3 +52,4 @@ const connector = connect(mapStateToProps, mapDispatchToPropsEmpty);
|
||||
type InjectedProps = ConnectedProps<typeof connector>;
|
||||
const ConnectedCreateDataSourcePage = connector(CreateDataSource);
|
||||
export default ConnectedCreateDataSourcePage;
|
||||
export { NeonConnect };
|
||||
|
@ -110,7 +110,7 @@ const DataSourceFormWrapper: React.FC<DataSourceFormWrapperProps> = props => {
|
||||
<Analytics name="EditDataSource" {...REDACT_EVERYTHING}>
|
||||
<form
|
||||
onSubmit={onSubmit}
|
||||
className={`${styles.connect_db_content} ${styles.connect_form_width}`}
|
||||
className={`${styles.connect_db_content} max-w-screen-md`}
|
||||
>
|
||||
<div className="max-w-xl">
|
||||
{!isReadReplica && (
|
||||
@ -154,12 +154,13 @@ const DataSourceFormWrapper: React.FC<DataSourceFormWrapperProps> = props => {
|
||||
|
||||
<ConnectDatabaseForm isEditState={isEditState} {...props} />
|
||||
{children}
|
||||
<div className={styles.add_button_layout}>
|
||||
<div>
|
||||
<Analytics
|
||||
name="data-tab-connect-db-button"
|
||||
passHtmlAttributesToChildren
|
||||
>
|
||||
<Button
|
||||
className="mt-sm"
|
||||
size="lg"
|
||||
mode="primary"
|
||||
type="submit"
|
||||
|
@ -17,9 +17,13 @@ import {
|
||||
makeConnectionStringFromConnectionParams,
|
||||
parseURI,
|
||||
} from './ManageDBUtils';
|
||||
import { useEELiteAccess } from '../../../../features/EETrial';
|
||||
|
||||
import styles from './DataSources.module.scss';
|
||||
import { LearnMoreLink } from '../../../../new-components/LearnMoreLink';
|
||||
import { EETrialCard } from '../../../../features/EETrial/components/EETrialCard/EETrialCard';
|
||||
import globals from '../../../../Globals';
|
||||
import { isProConsole } from '../../../../utils';
|
||||
|
||||
const checkIfFieldsAreEmpty = (
|
||||
currentReadReplicaConnectionType: string,
|
||||
@ -198,6 +202,7 @@ const ReadReplicaForm: React.FC<ReadReplicaProps> = ({
|
||||
readReplicaConnectionType,
|
||||
updateReadReplicaConnectionType,
|
||||
}) => {
|
||||
const { access: eeLiteAccess } = useEELiteAccess(globals);
|
||||
const [isReadReplicaButtonClicked, updateClickState] = useState(false);
|
||||
|
||||
const onClickAddReadReplica = () => {
|
||||
@ -229,52 +234,80 @@ const ReadReplicaForm: React.FC<ReadReplicaProps> = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<Analytics name="EditDataSource" {...REDACT_EVERYTHING}>
|
||||
<Collapse title="Read Replicas">
|
||||
<Collapse.Content>
|
||||
<div className={`${styles.flexColumn} my-1.5`}>
|
||||
<p>
|
||||
Hasura can load balance queries and subscriptions across read
|
||||
replicas while sending all mutations and metadata API calls to
|
||||
the master.
|
||||
<LearnMoreLink href="https://hasura.io/docs/latest/graphql/cloud/read-replicas.html" />
|
||||
</p>
|
||||
{readReplicaState.map((stateVar, index) => (
|
||||
<ReadReplicaListItem
|
||||
currentState={stateVar}
|
||||
onClickRemove={onClickRemoveReadReplica(stateVar.displayName)}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
{!isReadReplicaButtonClicked ? (
|
||||
<span className="py-1.5">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={onClickAddReadReplica}
|
||||
className={styles.add_button_styles}
|
||||
>
|
||||
Add Read Replica
|
||||
</Button>
|
||||
</span>
|
||||
) : (
|
||||
<Form
|
||||
connectDBState={connectDBState}
|
||||
connectDBStateDispatch={connectDBStateDispatch}
|
||||
onClickCancel={onClickCancelOnReadReplica}
|
||||
onClickSave={onClickSaveReadReplica}
|
||||
readReplicaConnectionType={readReplicaConnectionType}
|
||||
updateReadReplicaConnectionType={
|
||||
updateReadReplicaConnectionType
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Collapse.Content>
|
||||
</Collapse>
|
||||
</Analytics>
|
||||
<Analytics name="EditDataSource" {...REDACT_EVERYTHING}>
|
||||
<hr className={styles.line_width} />
|
||||
</Analytics>
|
||||
{
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
eeLiteAccess !== 'forbidden' || isProConsole(globals) ? (
|
||||
<Analytics name="EditDataSource" {...REDACT_EVERYTHING}>
|
||||
<Collapse
|
||||
title="Read Replicas"
|
||||
defaultOpen={eeLiteAccess !== 'active'}
|
||||
>
|
||||
<Collapse.Content>
|
||||
<div className={`${styles.flexColumn} my-1.5`}>
|
||||
<p>
|
||||
Hasura can load balance queries and subscriptions across
|
||||
read replicas while sending all mutations and metadata API
|
||||
calls to the master.
|
||||
<LearnMoreLink href="https://hasura.io/docs/latest/graphql/cloud/read-replicas.html" />
|
||||
</p>
|
||||
{readReplicaState.map((stateVar, index) => (
|
||||
<ReadReplicaListItem
|
||||
currentState={stateVar}
|
||||
onClickRemove={onClickRemoveReadReplica(
|
||||
stateVar.displayName
|
||||
)}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
{eeLiteAccess === 'eligible' ||
|
||||
eeLiteAccess === 'expired' ||
|
||||
eeLiteAccess === 'deactivated' ? (
|
||||
<EETrialCard
|
||||
cardTitle="Improve performance and handle increased traffic with read replicas"
|
||||
id="read-replicas-legacy"
|
||||
cardText={
|
||||
<span>
|
||||
Scale your database by offloading read queries to
|
||||
read-only replicas, allowing for better performance
|
||||
and availability for users.
|
||||
</span>
|
||||
}
|
||||
buttonLabel="Enable Enterprise"
|
||||
buttonType="default"
|
||||
eeAccess={eeLiteAccess}
|
||||
horizontal
|
||||
/>
|
||||
) : null}
|
||||
{(isProConsole(globals) || eeLiteAccess === 'active') &&
|
||||
(!isReadReplicaButtonClicked ? (
|
||||
<span className="py-1.5">
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={onClickAddReadReplica}
|
||||
className={styles.add_button_styles}
|
||||
>
|
||||
Add Read Replica
|
||||
</Button>
|
||||
</span>
|
||||
) : (
|
||||
<Form
|
||||
connectDBState={connectDBState}
|
||||
connectDBStateDispatch={connectDBStateDispatch}
|
||||
onClickCancel={onClickCancelOnReadReplica}
|
||||
onClickSave={onClickSaveReadReplica}
|
||||
readReplicaConnectionType={readReplicaConnectionType}
|
||||
updateReadReplicaConnectionType={
|
||||
updateReadReplicaConnectionType
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Collapse.Content>
|
||||
</Collapse>
|
||||
</Analytics>
|
||||
) : null
|
||||
}
|
||||
<Analytics name="EditDataSource" children={null} {...REDACT_EVERYTHING} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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<ManageDatabaseProps> = ({
|
||||
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<ManageDatabaseProps> = ({
|
||||
* 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<ManageDatabaseProps> = ({
|
||||
};
|
||||
|
||||
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<ManageDatabaseProps> = ({
|
||||
data-test="manage-database-section"
|
||||
>
|
||||
<BreadCrumb breadCrumbs={crumbs} />
|
||||
|
||||
<div className={styles.padd_top}>
|
||||
<div className={`${styles.display_flex} manage-db-header`}>
|
||||
<h2
|
||||
@ -436,157 +463,169 @@ const ManageDatabase: React.FC<ManageDatabaseProps> = ({
|
||||
<VPCBanner className="mt-md" onClose={dismissVPCBanner} />
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.manage_db_content}>
|
||||
<hr className="my-md" />
|
||||
|
||||
<div className="overflow-x-auto border border-gray-300 rounded">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<th className="px-sm py-xs max-w-xs text-left text-sm bg-gray-50 font-semibold text-gray-600 uppercase tracking-wider" />
|
||||
<th className="px-sm py-xs text-left text-sm bg-gray-50 font-semibold text-gray-600 uppercase tracking-wider">
|
||||
Database
|
||||
</th>
|
||||
<th className="px-sm py-xs text-left text-sm bg-gray-50 font-semibold text-gray-600 uppercase tracking-wider">
|
||||
Connection String
|
||||
</th>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{sourcesFromMetadata.length ? (
|
||||
sourcesFromMetadata.map(source => {
|
||||
if (nativeDrivers.includes(source.kind)) {
|
||||
const data = dataSources.find(
|
||||
s => s.name === source.name
|
||||
);
|
||||
if (!data) return null;
|
||||
<hr className="mt-sm" />
|
||||
|
||||
return (
|
||||
<DatabaseListItem
|
||||
key={data.name}
|
||||
dataSource={data}
|
||||
inconsistentObjects={inconsistentObjects}
|
||||
pushRoute={pushRoute}
|
||||
onEdit={onEdit}
|
||||
onReload={onReload}
|
||||
onRemove={onRemove}
|
||||
dispatch={dispatch}
|
||||
dataHeaders={dataHeaders}
|
||||
dbLatencyData={
|
||||
isCloudConsole(globals)
|
||||
? getSourceInfoFromLatencyData(
|
||||
data.name,
|
||||
latencyCheckData
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<GDCDatabaseListItem
|
||||
dataSource={{
|
||||
name: source.name,
|
||||
kind: source.kind,
|
||||
}}
|
||||
inconsistentObjects={inconsistentObjects}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<td colSpan={3} className="text-center px-sm py-xs">
|
||||
You don't have any data sources connected, please
|
||||
connect one to continue.
|
||||
</td>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
{isConnectDBRedesignEnabled ? (
|
||||
<div className="mt-sm">
|
||||
<ListConnectedDatabases />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className={styles.manage_db_content}>
|
||||
<div className="overflow-x-auto border border-gray-300 rounded">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<th className="px-sm py-xs max-w-xs text-left text-sm bg-gray-50 font-semibold text-gray-600 uppercase tracking-wider" />
|
||||
<th className="px-sm py-xs text-left text-sm bg-gray-50 font-semibold text-gray-600 uppercase tracking-wider">
|
||||
Database
|
||||
</th>
|
||||
<th className="px-sm py-xs text-left text-sm bg-gray-50 font-semibold text-gray-600 uppercase tracking-wider">
|
||||
Connection String
|
||||
</th>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{sourcesFromMetadata.length ? (
|
||||
sourcesFromMetadata.map(source => {
|
||||
if (nativeDrivers.includes(source.kind)) {
|
||||
const data = dataSources.find(
|
||||
s => s.name === source.name
|
||||
);
|
||||
if (!data) return null;
|
||||
|
||||
{showCheckLatencyButton ? (
|
||||
<Button
|
||||
size="md"
|
||||
className="mt-xs mr-xs"
|
||||
icon={<FaHourglassHalf />}
|
||||
onClick={checkDatabaseLatency}
|
||||
isLoading={queryResponse.isLoading}
|
||||
loadingText="Measuring Latencies..."
|
||||
>
|
||||
Check Database Latency
|
||||
</Button>
|
||||
) : null}
|
||||
{showAccelerateProjectSection ? (
|
||||
<div className="mt-xs">
|
||||
<IndicatorCard
|
||||
status="negative"
|
||||
headline="Accelerate your Hasura Project"
|
||||
>
|
||||
<div className="flex items-center flex-row">
|
||||
<span>
|
||||
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{' '}
|
||||
<b>lightning fast</b>, 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.
|
||||
<LearnMoreLink href={KNOW_MORE_PROJECT_REGION_UPDATE} />
|
||||
</span>
|
||||
<div className="flex items-center flex-row ml-xs">
|
||||
<Button
|
||||
className="mr-xs"
|
||||
onClick={checkDatabaseLatency}
|
||||
isLoading={queryResponse.isLoading}
|
||||
loadingText="Measuring Latencies..."
|
||||
icon={<FaRedoAlt />}
|
||||
>
|
||||
Re-check Database Latency
|
||||
</Button>
|
||||
<Button
|
||||
className="mr-xs"
|
||||
onClick={openUpdateProjectRegionPage}
|
||||
icon={<FaExternalLinkAlt />}
|
||||
>
|
||||
Update Project Region
|
||||
</Button>
|
||||
</div>
|
||||
return (
|
||||
<DatabaseListItem
|
||||
key={data.name}
|
||||
dataSource={data}
|
||||
inconsistentObjects={inconsistentObjects}
|
||||
pushRoute={pushRoute}
|
||||
onEdit={onEdit}
|
||||
onReload={onReload}
|
||||
onRemove={onRemove}
|
||||
dispatch={dispatch}
|
||||
dataHeaders={dataHeaders}
|
||||
dbLatencyData={
|
||||
isCloudConsole(globals)
|
||||
? getSourceInfoFromLatencyData(
|
||||
data.name,
|
||||
latencyCheckData
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<GDCDatabaseListItem
|
||||
dataSource={{
|
||||
name: source.name,
|
||||
kind: source.kind,
|
||||
}}
|
||||
inconsistentObjects={inconsistentObjects}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<td colSpan={3} className="text-center px-sm py-xs">
|
||||
You don't have any data sources connected, please
|
||||
connect one to continue.
|
||||
</td>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</IndicatorCard>
|
||||
</div>
|
||||
) : null}
|
||||
{showErrorIndicator ? (
|
||||
<div className="mt-xs">
|
||||
<IndicatorCard
|
||||
status="negative"
|
||||
headline="Houston, we've got a problem here!"
|
||||
showIcon
|
||||
>
|
||||
<div className="flex items-center flex-row">
|
||||
<span>
|
||||
There was an error in fetching the latest latency data.
|
||||
<pre className="w-1/2">{queryResponse.data}</pre>
|
||||
</span>
|
||||
<div className="flex items-center flex-row ml-xs">
|
||||
<Button
|
||||
className="mr-xs"
|
||||
onClick={checkDatabaseLatency}
|
||||
isLoading={queryResponse.isLoading}
|
||||
loadingText="Measuring Latencies..."
|
||||
>
|
||||
Re-check Database Latency
|
||||
</Button>
|
||||
<Button onClick={() => setLatencyButtonVisibility(true)}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showCheckLatencyButton ? (
|
||||
<Button
|
||||
size="md"
|
||||
className="mt-xs mr-xs"
|
||||
icon={<FaHourglassHalf />}
|
||||
onClick={checkDatabaseLatency}
|
||||
isLoading={queryResponse.isLoading}
|
||||
loadingText="Measuring Latencies..."
|
||||
>
|
||||
Check Database Latency
|
||||
</Button>
|
||||
) : null}
|
||||
{showAccelerateProjectSection ? (
|
||||
<div className="mt-xs">
|
||||
<IndicatorCard
|
||||
status="negative"
|
||||
headline="Accelerate your Hasura Project"
|
||||
>
|
||||
<div className="flex items-center flex-row">
|
||||
<span>
|
||||
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 <b>lightning fast</b>, 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.
|
||||
<LearnMoreLink href={KNOW_MORE_PROJECT_REGION_UPDATE} />
|
||||
</span>
|
||||
<div className="flex items-center flex-row ml-xs">
|
||||
<Button
|
||||
className="mr-xs"
|
||||
onClick={checkDatabaseLatency}
|
||||
isLoading={queryResponse.isLoading}
|
||||
loadingText="Measuring Latencies..."
|
||||
icon={<FaRedoAlt />}
|
||||
>
|
||||
Re-check Database Latency
|
||||
</Button>
|
||||
<Button
|
||||
className="mr-xs"
|
||||
onClick={openUpdateProjectRegionPage}
|
||||
icon={<FaExternalLinkAlt />}
|
||||
>
|
||||
Update Project Region
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</IndicatorCard>
|
||||
</div>
|
||||
</IndicatorCard>
|
||||
</div>
|
||||
) : null}
|
||||
<NeonDashboardLink className="mt-lg" />
|
||||
) : null}
|
||||
{showErrorIndicator ? (
|
||||
<div className="mt-xs">
|
||||
<IndicatorCard
|
||||
status="negative"
|
||||
headline="Houston, we've got a problem here!"
|
||||
showIcon
|
||||
>
|
||||
<div className="flex items-center flex-row">
|
||||
<span>
|
||||
There was an error in fetching the latest latency data.
|
||||
<pre className="w-1/2">{queryResponse.data}</pre>
|
||||
</span>
|
||||
<div className="flex items-center flex-row ml-xs">
|
||||
<Button
|
||||
className="mr-xs"
|
||||
onClick={checkDatabaseLatency}
|
||||
isLoading={queryResponse.isLoading}
|
||||
loadingText="Measuring Latencies..."
|
||||
>
|
||||
Re-check Database Latency
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setLatencyButtonVisibility(true)}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</IndicatorCard>
|
||||
</div>
|
||||
) : null}
|
||||
<NeonDashboardLink className="mt-lg" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<hr className="my-md" />
|
||||
<div className="mt-4">
|
||||
|
@ -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> = props => {
|
||||
|
||||
const [databaseInfo, setDatabaseInfo] = useState<DatabaseInfo>({});
|
||||
|
||||
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> = 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> = props => {
|
||||
handleHeadersChange={handleHeadersChange}
|
||||
handleToggleAllColumn={setState.toggleAllColumnChecked}
|
||||
handleAutoCleanupChange={handleAutoCleanupChange}
|
||||
autoCleanupSupport={autoCleanupSupport}
|
||||
/>
|
||||
<ConfigureTransformation
|
||||
transformationType="event"
|
||||
|
@ -1,7 +1,6 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
import React from 'react';
|
||||
import { Collapsible } from '../../../../../new-components/Collapsible';
|
||||
import { isProConsole } from '../../../../../utils/proConsole';
|
||||
import { useSchemas } from '../../../Data/TableInsertItem/hooks/useSchemas';
|
||||
import { LocalEventTriggerState } from '../state';
|
||||
import Headers, { Header } from '../../../../Common/Headers/Headers';
|
||||
@ -22,6 +21,7 @@ import FormLabel from './FormLabel';
|
||||
import { inputStyles, heading } from '../../constants';
|
||||
import { AutoCleanupForm } from '../Common/AutoCleanupForm';
|
||||
import { FaShieldAlt } from 'react-icons/fa';
|
||||
import { EELiteAccessStatus } from '../../../../../features/EETrial';
|
||||
|
||||
type CreateETFormProps = {
|
||||
state: LocalEventTriggerState;
|
||||
@ -40,6 +40,7 @@ type CreateETFormProps = {
|
||||
handleHeadersChange: (h: Header[]) => void;
|
||||
handleToggleAllColumn: () => void;
|
||||
handleAutoCleanupChange: (config: EventTriggerAutoCleanup) => void;
|
||||
autoCleanupSupport: EELiteAccessStatus;
|
||||
};
|
||||
|
||||
const CreateETForm: React.FC<CreateETFormProps> = props => {
|
||||
@ -70,6 +71,7 @@ const CreateETForm: React.FC<CreateETFormProps> = props => {
|
||||
handleHeadersChange,
|
||||
handleToggleAllColumn,
|
||||
handleAutoCleanupChange,
|
||||
autoCleanupSupport,
|
||||
} = props;
|
||||
|
||||
const supportedDrivers = getSupportedDrivers('events.triggers.add');
|
||||
@ -222,7 +224,7 @@ const CreateETForm: React.FC<CreateETFormProps> = props => {
|
||||
<br />
|
||||
</div>
|
||||
<hr className="my-md" />
|
||||
{isProConsole(window.__env) && (
|
||||
{autoCleanupSupport !== 'forbidden' && (
|
||||
<>
|
||||
<div className="mb-md">
|
||||
<div className="mb-md cursor-pointer">
|
||||
|
@ -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 (
|
||||
<div className="w-1/2">
|
||||
<Analytics
|
||||
name="open-event-log-auto-cleanup"
|
||||
passHtmlAttributesToChildren
|
||||
>
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<h2 className="text-lg font-semibold mb-xs flex items-center mb-0">
|
||||
Auto-cleanup Event Logs
|
||||
<div className="flex items-center">
|
||||
<Tooltip
|
||||
side="top"
|
||||
tooltipContentChildren={
|
||||
isCleanupConfigSet &&
|
||||
<Analytics name="open-event-log-auto-cleanup" passHtmlAttributesToChildren>
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<h2 className="text-lg font-semibold mb-xs flex items-center mb-0">
|
||||
Auto-cleanup Event Logs
|
||||
<div className="flex items-center">
|
||||
<Tooltip
|
||||
side="top"
|
||||
tooltipContentChildren={
|
||||
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
|
||||
) ? (
|
||||
<FaCircle className="ml-xs fill-sky-600 text-xs" />
|
||||
) : (
|
||||
<FaCircle className="ml-xs fill-slate-400 text-xs" />
|
||||
)}
|
||||
<Analytics
|
||||
name="event-auto-cleanup-clear-reset-btn"
|
||||
passHtmlAttributesToChildren
|
||||
>
|
||||
<span
|
||||
className="text-sky-500 ml-xs font-thin text-sm"
|
||||
onClick={() => 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
|
||||
) ? (
|
||||
<FaCircle className="ml-xs fill-sky-600 text-xs" />
|
||||
) : (
|
||||
<FaCircle className="ml-xs fill-slate-400 text-xs" />
|
||||
)}
|
||||
<Analytics
|
||||
name="event-auto-cleanup-clear-reset-btn"
|
||||
passHtmlAttributesToChildren
|
||||
>
|
||||
<span
|
||||
className="text-sky-500 ml-xs font-thin text-sm"
|
||||
onClick={() => onChange({})}
|
||||
>
|
||||
{isCleanupConfigSet &&
|
||||
!(
|
||||
cleanupConfig?.paused &&
|
||||
Object.keys(cleanupConfig).length === 1
|
||||
)
|
||||
? 'Clear / Reset'
|
||||
: ''}
|
||||
</span>
|
||||
</Analytics>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</h2>
|
||||
}
|
||||
defaultOpen={!!cleanupConfig}
|
||||
>
|
||||
<div className="flex items-center mb-sm">
|
||||
<Tooltip
|
||||
side="right"
|
||||
tooltipContentChildren={
|
||||
isCleanupConfigSet
|
||||
? 'When not enabled, event log cleanup is paused. To completely remove event log cleanup configuration use Clear/Reset button'
|
||||
: 'When not enabled, event log cleanup is paused'
|
||||
}
|
||||
className="flex items-center ml-0"
|
||||
>
|
||||
<Switch
|
||||
checked={cleanupConfig?.paused === false}
|
||||
onCheckedChange={() => {
|
||||
onChange({
|
||||
...cleanupConfig,
|
||||
paused: cleanupConfig?.paused === false ? true : false,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span className="ml-xs cursor-pointer">
|
||||
Enable event log cleanup
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{
|
||||
<div>
|
||||
<div className="flex items-center mb-sm">
|
||||
<Tooltip
|
||||
side="right"
|
||||
tooltipContentChildren={
|
||||
isDisable
|
||||
? 'Enable event log cleanup to configure'
|
||||
: 'Enabling this will clear event invocation logs along with event logs'
|
||||
}
|
||||
className="flex items-center ml-0"
|
||||
>
|
||||
<Switch
|
||||
checked={cleanupConfig?.clean_invocation_logs}
|
||||
disabled={isDisable}
|
||||
onCheckedChange={() => {
|
||||
onChange({
|
||||
...cleanupConfig,
|
||||
clean_invocation_logs:
|
||||
!cleanupConfig?.clean_invocation_logs,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span className="ml-xs cursor-pointer">
|
||||
Clean invocation logs with event logs
|
||||
? 'Clear / Reset'
|
||||
: ''}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<InputSection
|
||||
label="Clear logs older than (hours)"
|
||||
disabled={isDisable}
|
||||
tooltip={
|
||||
isDisable
|
||||
? `Enable event log cleanup to configure. Clear event logs older than (in hours)`
|
||||
: `Clear event logs older than (in hours)`
|
||||
}
|
||||
placeholder="168"
|
||||
required
|
||||
value={cleanupConfig?.clear_older_than?.toString() ?? ''}
|
||||
onChange={value => {
|
||||
onChange({
|
||||
...cleanupConfig,
|
||||
clear_older_than: value ? parseInt(value, 10) : undefined,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<InputSection
|
||||
label="Cleanup Frequency"
|
||||
disabled={isDisable}
|
||||
tooltip={
|
||||
isDisable
|
||||
? `Enable event log cleanup to configure. Cron expression at which the cleanup should be invoked.`
|
||||
: `Cron expression at which the cleanup should be invoked.`
|
||||
}
|
||||
placeholder="0 0 * * *"
|
||||
required
|
||||
value={cleanupConfig?.schedule?.toString() ?? ''}
|
||||
onChange={value => {
|
||||
onChange({
|
||||
...cleanupConfig,
|
||||
schedule: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="my-sm">
|
||||
<DropdownButton
|
||||
disabled={isDisable}
|
||||
items={[
|
||||
crons.map(cron => (
|
||||
<div
|
||||
key={cron.value}
|
||||
onClick={() => {
|
||||
onChange({
|
||||
...cleanupConfig,
|
||||
schedule: cron.value,
|
||||
});
|
||||
}}
|
||||
className="py-xs cursor-pointer mx-1 px-xs py-1 rounded hover:bg-gray-100"
|
||||
>
|
||||
<p className="mb-0 font-semibold whitespace-nowrap">
|
||||
{cron.label}
|
||||
</p>
|
||||
<p className="mb-0">{cron.value}</p>
|
||||
</div>
|
||||
)),
|
||||
]}
|
||||
>
|
||||
<span className="font-bold">Frequent Frequencies</span>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
<Analytics
|
||||
name="open-adv-setting-event-log-cleanup"
|
||||
passHtmlAttributesToChildren
|
||||
>
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<h2 className="text-lg font-semibold mb-xs flex items-center mb-0">
|
||||
Advanced Settings
|
||||
</h2>
|
||||
}
|
||||
>
|
||||
<InputSection
|
||||
label="Timeout (seconds)"
|
||||
disabled={isDisable}
|
||||
tooltip={
|
||||
isDisable
|
||||
? `Enable event log cleanup to configure. Timeout for the query (in seconds, default: 60)`
|
||||
: `Timeout for the query (in seconds, default: 60)`
|
||||
}
|
||||
placeholder="60"
|
||||
value={cleanupConfig?.timeout?.toString() ?? ''}
|
||||
onChange={value => {
|
||||
onChange({
|
||||
...cleanupConfig,
|
||||
timeout: value ? parseInt(value, 10) : undefined,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<InputSection
|
||||
label="Batch Size"
|
||||
disabled={isDisable}
|
||||
tooltip={
|
||||
isDisable
|
||||
? `Enable event log cleanup to configure. Number of event trigger logs to delete in a batch (default: 10,000)`
|
||||
: `Number of event trigger logs to delete in a batch (default: 10,000)`
|
||||
}
|
||||
placeholder="10000"
|
||||
value={cleanupConfig?.batch_size?.toString() ?? ''}
|
||||
onChange={value => {
|
||||
onChange({
|
||||
...cleanupConfig,
|
||||
batch_size: value ? parseInt(value, 10) : undefined,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Collapsible>
|
||||
</Analytics>
|
||||
</Analytics>
|
||||
</Tooltip>
|
||||
</div>
|
||||
}
|
||||
</Collapsible>
|
||||
</Analytics>
|
||||
</div>
|
||||
</h2>
|
||||
}
|
||||
defaultOpen
|
||||
>
|
||||
<ETAutoCleanupWrapper>
|
||||
<div className="w-1/2">
|
||||
<div className="flex items-center mb-sm">
|
||||
<Tooltip
|
||||
side="right"
|
||||
tooltipContentChildren={
|
||||
isCleanupConfigSet
|
||||
? 'When not enabled, event log cleanup is paused. To completely remove event log cleanup configuration use Clear/Reset button'
|
||||
: 'When not enabled, event log cleanup is paused'
|
||||
}
|
||||
className="flex items-center ml-0"
|
||||
>
|
||||
<Switch
|
||||
checked={cleanupConfig?.paused === false}
|
||||
onCheckedChange={() => {
|
||||
onChange({
|
||||
...cleanupConfig,
|
||||
paused: cleanupConfig?.paused === false ? true : false,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span className="ml-xs cursor-pointer">
|
||||
Enable event log cleanup
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{
|
||||
<div>
|
||||
<div className="flex items-center mb-sm">
|
||||
<Tooltip
|
||||
side="right"
|
||||
tooltipContentChildren={
|
||||
isDisable
|
||||
? 'Enable event log cleanup to configure'
|
||||
: 'Enabling this will clear event invocation logs along with event logs'
|
||||
}
|
||||
className="flex items-center ml-0"
|
||||
>
|
||||
<Switch
|
||||
checked={cleanupConfig?.clean_invocation_logs}
|
||||
disabled={isDisable}
|
||||
onCheckedChange={() => {
|
||||
onChange({
|
||||
...cleanupConfig,
|
||||
clean_invocation_logs:
|
||||
!cleanupConfig?.clean_invocation_logs,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<span className="ml-xs cursor-pointer">
|
||||
Clean invocation logs with event logs
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<InputSection
|
||||
label="Clear logs older than (hours)"
|
||||
disabled={isDisable}
|
||||
tooltip={
|
||||
isDisable
|
||||
? `Enable event log cleanup to configure. Clear event logs older than (in hours)`
|
||||
: `Clear event logs older than (in hours)`
|
||||
}
|
||||
placeholder="168"
|
||||
required
|
||||
value={cleanupConfig?.clear_older_than?.toString() ?? ''}
|
||||
onChange={value => {
|
||||
onChange({
|
||||
...cleanupConfig,
|
||||
clear_older_than: value ? parseInt(value, 10) : undefined,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<InputSection
|
||||
label="Cleanup Frequency"
|
||||
disabled={isDisable}
|
||||
tooltip={
|
||||
isDisable
|
||||
? `Enable event log cleanup to configure. Cron expression at which the cleanup should be invoked.`
|
||||
: `Cron expression at which the cleanup should be invoked.`
|
||||
}
|
||||
placeholder="0 0 * * *"
|
||||
required
|
||||
value={cleanupConfig?.schedule?.toString() ?? ''}
|
||||
onChange={value => {
|
||||
onChange({
|
||||
...cleanupConfig,
|
||||
schedule: value,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="my-sm">
|
||||
<DropdownButton
|
||||
disabled={isDisable}
|
||||
items={[
|
||||
crons.map(cron => (
|
||||
<div
|
||||
key={cron.value}
|
||||
onClick={() => {
|
||||
onChange({
|
||||
...cleanupConfig,
|
||||
schedule: cron.value,
|
||||
});
|
||||
}}
|
||||
className="py-xs cursor-pointer mx-1 px-xs py-1 rounded hover:bg-gray-100"
|
||||
>
|
||||
<p className="mb-0 font-semibold whitespace-nowrap">
|
||||
{cron.label}
|
||||
</p>
|
||||
<p className="mb-0">{cron.value}</p>
|
||||
</div>
|
||||
)),
|
||||
]}
|
||||
>
|
||||
<span className="font-bold">Frequent Frequencies</span>
|
||||
</DropdownButton>
|
||||
</div>
|
||||
<Analytics
|
||||
name="open-adv-setting-event-log-cleanup"
|
||||
passHtmlAttributesToChildren
|
||||
>
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<h2 className="text-lg font-semibold mb-xs flex items-center mb-0">
|
||||
Advanced Settings
|
||||
</h2>
|
||||
}
|
||||
>
|
||||
<InputSection
|
||||
label="Timeout (seconds)"
|
||||
disabled={isDisable}
|
||||
tooltip={
|
||||
isDisable
|
||||
? `Enable event log cleanup to configure. Timeout for the query (in seconds, default: 60)`
|
||||
: `Timeout for the query (in seconds, default: 60)`
|
||||
}
|
||||
placeholder="60"
|
||||
value={cleanupConfig?.timeout?.toString() ?? ''}
|
||||
onChange={value => {
|
||||
onChange({
|
||||
...cleanupConfig,
|
||||
timeout: value ? parseInt(value, 10) : undefined,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
|
||||
<InputSection
|
||||
label="Batch Size"
|
||||
disabled={isDisable}
|
||||
tooltip={
|
||||
isDisable
|
||||
? `Enable event log cleanup to configure. Number of event trigger logs to delete in a batch (default: 10,000)`
|
||||
: `Number of event trigger logs to delete in a batch (default: 10,000)`
|
||||
}
|
||||
placeholder="10000"
|
||||
value={cleanupConfig?.batch_size?.toString() ?? ''}
|
||||
onChange={value => {
|
||||
onChange({
|
||||
...cleanupConfig,
|
||||
batch_size: value ? parseInt(value, 10) : undefined,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Collapsible>
|
||||
</Analytics>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ETAutoCleanupWrapper>
|
||||
</Collapsible>
|
||||
</Analytics>
|
||||
);
|
||||
};
|
||||
|
@ -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> = 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> = props => {
|
||||
save={saveWrapper('retry_conf')}
|
||||
/>
|
||||
<hr className="my-md" />
|
||||
{isProConsole(window.__env) && (
|
||||
{autoCleanupSupport !== 'forbidden' && (
|
||||
<div className="mb-md">
|
||||
<AutoCleanupForm
|
||||
onChange={setState.cleanupConfig}
|
||||
|
@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||
import { ReactQueryDecorator } from '../../../../storybook/decorators/react-query';
|
||||
import { eeLicenseInfo } from '../../../../features/EETrial/mocks/http';
|
||||
|
||||
import { About } from './About';
|
||||
|
||||
export default {
|
||||
title: 'components/Services/Settings/About',
|
||||
parameters: {
|
||||
Benefits: {
|
||||
source: { type: 'code' },
|
||||
},
|
||||
},
|
||||
component: About,
|
||||
decorators: [ReactQueryDecorator()],
|
||||
} as ComponentMeta<typeof About>;
|
||||
|
||||
export const LoadingServerVersion: ComponentStory<typeof About> = args => (
|
||||
<div className="flex justify-center">
|
||||
<About serverVersion="" consoleAssetVersion="9acd324" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const WithoutEnterpriseAccess: ComponentStory<typeof About> = args => (
|
||||
<div className="flex justify-center">
|
||||
<About serverVersion="v2.17.0" consoleAssetVersion="9acd324" />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const WithoutEnterpriseLicense: ComponentStory<typeof About> = args => (
|
||||
<div className="flex justify-center">
|
||||
<About serverVersion="v2.17.0" consoleAssetVersion="9acd324" />
|
||||
</div>
|
||||
);
|
||||
WithoutEnterpriseLicense.parameters = {
|
||||
msw: [eeLicenseInfo.none],
|
||||
consoleType: 'pro-lite',
|
||||
};
|
||||
|
||||
export const DeactivatedEnterpriseLicense: ComponentStory<
|
||||
typeof About
|
||||
> = args => (
|
||||
<div className="flex justify-center">
|
||||
<About serverVersion="v2.17.0" consoleAssetVersion="9acd324" />
|
||||
</div>
|
||||
);
|
||||
DeactivatedEnterpriseLicense.parameters = {
|
||||
msw: [eeLicenseInfo.deactivated],
|
||||
consoleType: 'pro-lite',
|
||||
};
|
||||
|
||||
export const ExpiredEnterpriseLicense: ComponentStory<typeof About> = args => (
|
||||
<div className="flex justify-center">
|
||||
<About serverVersion="v2.17.0" consoleAssetVersion="9acd324" />
|
||||
</div>
|
||||
);
|
||||
ExpiredEnterpriseLicense.parameters = {
|
||||
msw: [eeLicenseInfo.expired],
|
||||
consoleType: 'pro-lite',
|
||||
};
|
||||
|
||||
export const ActiveEnterpriseLicense: ComponentStory<typeof About> = args => (
|
||||
<div className="flex justify-center">
|
||||
<About serverVersion="v2.17.0" consoleAssetVersion="9acd324" />
|
||||
</div>
|
||||
);
|
||||
ActiveEnterpriseLicense.parameters = {
|
||||
msw: [eeLicenseInfo.active],
|
||||
consoleType: 'pro-lite',
|
||||
};
|
@ -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<StateProps> = props => {
|
||||
const { serverVersion, consoleAssetVersion } = props;
|
||||
|
||||
type AboutState = {
|
||||
consoleAssetVersion?: string;
|
||||
};
|
||||
const spinner = <FaSpinner className="animate-spin" />;
|
||||
|
||||
class About extends Component<ConnectInjectedProps & StateProps> {
|
||||
// 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 = <FaSpinner className="animate-spin" />;
|
||||
|
||||
const getServerVersionSection = () => {
|
||||
return (
|
||||
<div>
|
||||
<b>Current server version: </b>
|
||||
<span className="ml-sm font-light">{serverVersion || spinner}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getConsoleAssetVersionSection = () => {
|
||||
return (
|
||||
<div>
|
||||
<b>Console asset version: </b>
|
||||
<span className="ml-sm font-light">
|
||||
{consoleAssetVersion || 'NA'}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Analytics name="About" {...REDACT_EVERYTHING}>
|
||||
<div className="clear-both pl-sm pt-md mb-sm bootstrap-jail">
|
||||
<div className="text-base font-bold">
|
||||
<Helmet title="About | Hasura" />
|
||||
<h2 className="text-xl font-bold">About</h2>
|
||||
<div className="mt-sm">{getServerVersionSection()}</div>
|
||||
<div className="mt-sm">{getConsoleAssetVersionSection()}</div>
|
||||
return (
|
||||
<Analytics name="About" {...REDACT_EVERYTHING}>
|
||||
<div className="clear-both pl-md pt-md mb-sm bootstrap-jail">
|
||||
<div className="text-base font-bold">
|
||||
<Helmet title="About | Hasura" />
|
||||
<h2 className="text-xl font-bold mb-md">About</h2>
|
||||
<div className="mb-md">
|
||||
<LabelValue
|
||||
label={'Current Server Version'}
|
||||
value={serverVersion || spinner}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-md">
|
||||
<LabelValue
|
||||
label={'Console asset version'}
|
||||
value={consoleAssetVersion || 'NA'}
|
||||
/>
|
||||
</div>
|
||||
<EELicenseInfo className="mb-md" />
|
||||
</div>
|
||||
</Analytics>
|
||||
);
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</Analytics>
|
||||
);
|
||||
};
|
||||
|
||||
const mapStateToProps = (state: ReduxState) => {
|
||||
return {
|
||||
dataHeaders: state.tables.dataHeaders,
|
||||
serverVersion: state.main.serverVersion,
|
||||
source: state.tables.currentDataSource,
|
||||
latestStableServerVersion: state.main.latestStableServerVersion,
|
||||
consoleAssetVersion: globals.consoleAssetVersion,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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 (
|
||||
<a
|
||||
href={EE_TRIAL_CONTACT_US_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={className}
|
||||
>
|
||||
<Button
|
||||
icon={<FaExternalLinkAlt className="text-sm" />}
|
||||
iconPosition="end"
|
||||
className="font-weight-700 text-md"
|
||||
>
|
||||
{text}
|
||||
</Button>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
||||
export const EELicenseInfo: React.VFC<{ className?: string }> = props => {
|
||||
const { className } = props;
|
||||
const eeLite = useEELiteAccess(globals);
|
||||
|
||||
if (eeLite.access === 'forbidden') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<EELicenseInfoUI info={eeLite} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const EELicenseInfoUI: React.VFC<{
|
||||
info: EELiteAccess;
|
||||
}> = props => {
|
||||
const { info } = props;
|
||||
switch (info.access) {
|
||||
case 'eligible': {
|
||||
return (
|
||||
<div className="max-w-3xl">
|
||||
<div className="mb-xs">
|
||||
<LabelValue
|
||||
label="Enterprise Edition"
|
||||
value={
|
||||
<EETrialCard
|
||||
cardTitle="Activate your free Hasura Enterprise trial license"
|
||||
className="mt-xs"
|
||||
cardText="Unlock extra observability, security, and performance features for your Hasura instance."
|
||||
eeAccess={info.access}
|
||||
horizontal
|
||||
id="settings-about-ee"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
case 'active':
|
||||
case 'expired':
|
||||
const expiryDate = moment(info.license.expiry_at);
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-xs">
|
||||
<LabelValue
|
||||
label="Enterprise Edition Expiry Date"
|
||||
value={`${expiryDate.format(
|
||||
'D MMMM, YYYY'
|
||||
)} (${expiryDate.fromNow()})`}
|
||||
/>
|
||||
</div>
|
||||
<EECTAButton
|
||||
text={info.access === 'active' ? 'Get in touch' : 'Renew License'}
|
||||
className="mt-md"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
case 'deactivated':
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-xs">
|
||||
<LabelValue label="Enterprise Edition" value={`Deactivated`} />
|
||||
</div>
|
||||
<EECTAButton text="Get in touch" />
|
||||
</div>
|
||||
);
|
||||
case 'loading':
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
@ -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 (
|
||||
<div className="flex flex-col">
|
||||
<b className="text-muted">{label}: </b>
|
||||
<span className="text-muted font-normal">{value}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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<typeof Sidebar> = args => {
|
||||
MetadataOk.storyName = '💠 Demo Metadata Ok';
|
||||
MetadataOk.args = generateArgs();
|
||||
MetadataOk.parameters = {
|
||||
msw: mockHandlers({}),
|
||||
msw: [...mockHandlers({}), eeLicenseInfo.active],
|
||||
};
|
||||
|
||||
export const MetadataKo: ComponentStory<typeof Sidebar> = args => {
|
||||
@ -162,7 +164,7 @@ export const MetadataKo: ComponentStory<typeof Sidebar> = args => {
|
||||
MetadataKo.storyName = '💠 Demo Metadata Ko';
|
||||
MetadataKo.args = generateArgs(false);
|
||||
MetadataKo.parameters = {
|
||||
msw: mockHandlers({}),
|
||||
msw: [...mockHandlers({}), eeLicenseInfo.active],
|
||||
};
|
||||
|
||||
export const LogoutActive: ComponentStory<typeof Sidebar> = args => {
|
||||
@ -171,7 +173,7 @@ export const LogoutActive: ComponentStory<typeof Sidebar> = 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 <Sidebar {...args} />;
|
||||
};
|
||||
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<typeof Sidebar> = 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 <Sidebar {...args} />;
|
||||
};
|
||||
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',
|
||||
};
|
||||
|
@ -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<string, unknown>[];
|
||||
inconsistentInheritedRoles: Record<string, unknown>[];
|
||||
@ -32,10 +34,11 @@ type SectionDataKey =
|
||||
| 'about';
|
||||
|
||||
const Sidebar: React.FC<SidebarProps> = ({ location, metadata }) => {
|
||||
const eeLiteAccess = useEELiteAccess(globals);
|
||||
|
||||
const sectionsData: Partial<
|
||||
Record<SectionDataKey, NavigationSidebarSection>
|
||||
> = {};
|
||||
|
||||
sectionsData.metadata = {
|
||||
key: 'metadata',
|
||||
label: 'Metadata',
|
||||
@ -105,7 +108,8 @@ const Sidebar: React.FC<SidebarProps> = ({ 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<SidebarProps> = ({ 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<SidebarProps> = ({ 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',
|
||||
|
@ -82,7 +82,7 @@ export const supportedFeatures: DeepRequired<SupportedFeaturesType> = {
|
||||
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',
|
||||
|
@ -765,7 +765,7 @@ export const supportedFeatures: DeepRequired<SupportedFeaturesType> = {
|
||||
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
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -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 (
|
||||
<div>
|
||||
<div>
|
||||
|
@ -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<AllowListSidebarProps> = 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<AllowListSidebarProps> = props => {
|
||||
return (
|
||||
<div>
|
||||
<AllowListSidebarHeader
|
||||
onQueryCollectionCreate={onQueryCollectionCreate}
|
||||
onQueryCollectionCreate={
|
||||
allowQueryCollectionsCreation ? onQueryCollectionCreate : undefined
|
||||
}
|
||||
/>
|
||||
<AllowListSidebarSearchForm
|
||||
setSearch={(searchString: string) => debouncedSearch(searchString)}
|
||||
|
@ -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) => {
|
||||
<div className="pb-4">
|
||||
{isCreateModalOpen && (
|
||||
<QueryCollectionCreateDialog
|
||||
onCreate={onQueryCollectionCreate}
|
||||
onCreate={name => {
|
||||
if (onQueryCollectionCreate) {
|
||||
onQueryCollectionCreate(name);
|
||||
}
|
||||
}}
|
||||
onClose={() => setIsCreateModalOpen(false)}
|
||||
/>
|
||||
)}
|
||||
@ -30,7 +33,7 @@ export const AllowListSidebarHeader = (props: AllowListSidebarHeaderProps) => {
|
||||
<AllowListStatus />
|
||||
</div>
|
||||
</div>
|
||||
{isProConsole(window.__env) && (
|
||||
{onQueryCollectionCreate && (
|
||||
<div className="mt-2 2xl:mt-0 2xl:ml-auto">
|
||||
<Button
|
||||
icon={<FaFolderPlus />}
|
||||
|
@ -2,6 +2,8 @@ import type { RedactOptions } from './heap/types';
|
||||
import type { HtmlAnalyticsAttributes, HtmlNameAttributes } from '../types';
|
||||
import { getRedactAttributes } from './heap/getRedactAttributes';
|
||||
|
||||
export const DATA_ANALYTICS_ATTRIBUTE = 'data-analytics-name';
|
||||
|
||||
export type AnalyticsOptions = RedactOptions & {
|
||||
/**
|
||||
* @deprecated It is meant for the old components that already had a `data-trackid` attribute
|
||||
@ -17,7 +19,7 @@ export function getAnalyticsAttributes(
|
||||
name: string,
|
||||
options?: AnalyticsOptions
|
||||
): HtmlAnalyticsAttributes {
|
||||
let htmlAttributes: HtmlNameAttributes = { 'data-analytics-name': name };
|
||||
let htmlAttributes: HtmlNameAttributes = { [DATA_ANALYTICS_ATTRIBUTE]: name };
|
||||
|
||||
if (!options) return htmlAttributes;
|
||||
|
||||
|
@ -0,0 +1,16 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
useSetupTelemetryEventListeners,
|
||||
UserEventTracker,
|
||||
} from '../htmlEvents';
|
||||
|
||||
// This component exists so that the logic of the `useSetupTelemetryEventListeners` hook can be run in class components
|
||||
// This is important because the entrypoint of both CE and EE console is `Main.js`, which is a class component
|
||||
export const InitializeTelemetry = (props: {
|
||||
tracker: UserEventTracker;
|
||||
skip: boolean;
|
||||
}) => {
|
||||
const { tracker, skip } = props;
|
||||
useSetupTelemetryEventListeners(tracker, skip);
|
||||
return null;
|
||||
};
|
@ -0,0 +1,237 @@
|
||||
import * as React from 'react';
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
import { Analytics } from '../../../Analytics';
|
||||
import { setupTelemetryEventListeners } from './htmlEvents';
|
||||
|
||||
let mockTracker = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
mockTracker = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
const useSetupTelemetryEventListeners = () => {
|
||||
React.useEffect(() => {
|
||||
const listenersCleaner = setupTelemetryEventListeners(mockTracker);
|
||||
return listenersCleaner;
|
||||
}, []);
|
||||
};
|
||||
|
||||
test('tracks button clicks with analytics attribute correctly', async () => {
|
||||
const TestComponent = () => {
|
||||
useSetupTelemetryEventListeners();
|
||||
return (
|
||||
<Analytics name="button-component">
|
||||
<button onClick={() => null} data-testid="click-me">
|
||||
Click Me
|
||||
</button>
|
||||
</Analytics>
|
||||
);
|
||||
};
|
||||
|
||||
await render(<TestComponent />);
|
||||
|
||||
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 (
|
||||
<Analytics name="nested-button-component">
|
||||
<button onClick={() => null}>
|
||||
<div>
|
||||
<span data-testid="click-me" onClick={() => null}>
|
||||
Click me
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</Analytics>
|
||||
);
|
||||
};
|
||||
|
||||
await render(<TestComponent />);
|
||||
|
||||
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 (
|
||||
<button onClick={() => null} data-testid="click-me">
|
||||
Click Me
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
await render(<TestComponent />);
|
||||
|
||||
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 (
|
||||
<Analytics name="text-component">
|
||||
<input
|
||||
onChange={e => setValue(e.target.value)}
|
||||
value={value}
|
||||
type="text"
|
||||
data-testid="type-here"
|
||||
/>
|
||||
</Analytics>
|
||||
);
|
||||
};
|
||||
|
||||
await render(<TestComponent />);
|
||||
|
||||
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 (
|
||||
<Analytics name="text-component">
|
||||
<div>
|
||||
<input
|
||||
onChange={e => setValue(e.target.value)}
|
||||
value={value}
|
||||
type="text"
|
||||
data-testid="type-here"
|
||||
/>
|
||||
</div>
|
||||
</Analytics>
|
||||
);
|
||||
};
|
||||
|
||||
await render(<TestComponent />);
|
||||
|
||||
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 (
|
||||
<div>
|
||||
<input
|
||||
onChange={e => setValue(e.target.value)}
|
||||
value={value}
|
||||
type="text"
|
||||
data-testid="type-here"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
await render(<TestComponent />);
|
||||
|
||||
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 (
|
||||
<Analytics name="radio-component">
|
||||
<input
|
||||
type="radio"
|
||||
onChange={() => setChecked(c => !c)}
|
||||
checked={checked}
|
||||
data-testid="toggle-radio"
|
||||
/>
|
||||
</Analytics>
|
||||
);
|
||||
};
|
||||
|
||||
await render(<TestComponent />);
|
||||
|
||||
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 (
|
||||
<Analytics name="checkbox-component">
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={() => setChecked(c => !c)}
|
||||
checked={checked}
|
||||
data-testid="toggle-checkbox"
|
||||
/>
|
||||
</Analytics>
|
||||
);
|
||||
};
|
||||
|
||||
await render(<TestComponent />);
|
||||
|
||||
fireEvent.change(screen.getByTestId('toggle-checkbox'), {
|
||||
target: { value: 'text' },
|
||||
});
|
||||
|
||||
expect(mockTracker).toHaveBeenLastCalledWith('checkbox-component', 'change');
|
||||
});
|
||||
|
||||
test('tracks onchange with of <select> with analytics attribute', async () => {
|
||||
const TestComponent = () => {
|
||||
const [value, setValue] = React.useState('');
|
||||
useSetupTelemetryEventListeners();
|
||||
return (
|
||||
<Analytics name="select-component">
|
||||
<select
|
||||
onChange={e => setValue(e.target.value)}
|
||||
value={value}
|
||||
data-testid="select-option"
|
||||
>
|
||||
<option value="value1">First</option>
|
||||
<option value="value2">Second</option>
|
||||
</select>
|
||||
</Analytics>
|
||||
);
|
||||
};
|
||||
|
||||
await render(<TestComponent />);
|
||||
|
||||
fireEvent.change(screen.getByTestId('select-option'), {
|
||||
target: { value: 'value2' },
|
||||
});
|
||||
|
||||
expect(mockTracker).toHaveBeenLastCalledWith('select-component', 'change');
|
||||
});
|
@ -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]);
|
||||
};
|
@ -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
|
||||
|
@ -82,37 +82,42 @@ export const EditConnection = () => {
|
||||
if (!schema) return <>Could not find schema</>;
|
||||
|
||||
return (
|
||||
<Form onSubmit={submit} className="p-0 pl-sm">
|
||||
<div className="max-w-5xl">
|
||||
<InputField type="text" name="name" label="Database Display Name" />
|
||||
|
||||
<Select
|
||||
options={[{ label: driver || '', value: driver }]}
|
||||
name="driver"
|
||||
label="Data Source Driver"
|
||||
disabled
|
||||
/>
|
||||
|
||||
<div className="max-w-xl">
|
||||
<Configuration name="configuration" />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<CustomizationForm />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Button type="submit" mode="primary" isLoading={submitIsLoading}>
|
||||
Edit Connection
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{!!Object(formState.errors)?.keys?.length && (
|
||||
<div className="mt-6 max-w-xl">
|
||||
<IndicatorCard status="negative">
|
||||
Error submitting form, see error messages above
|
||||
</IndicatorCard>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="text-xl text-gray-600 font-semibold p-4">
|
||||
Edit {name} database connection
|
||||
</div>
|
||||
</Form>
|
||||
<Form onSubmit={submit} className="p-0 pl-sm">
|
||||
<div className="max-w-5xl">
|
||||
<InputField type="text" name="name" label="Database Display Name" />
|
||||
|
||||
<Select
|
||||
options={[{ label: driver || '', value: driver }]}
|
||||
name="driver"
|
||||
label="Data Source Driver"
|
||||
disabled
|
||||
/>
|
||||
|
||||
<div className="max-w-xl">
|
||||
<Configuration name="configuration" />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<CustomizationForm />
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<Button type="submit" mode="primary" isLoading={submitIsLoading}>
|
||||
Edit Connection
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{!!Object(formState.errors)?.keys?.length && (
|
||||
<div className="mt-6 max-w-xl">
|
||||
<IndicatorCard status="negative">
|
||||
Error submitting form, see error messages above
|
||||
</IndicatorCard>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -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 <ConnectDatabaseV2 {...env} />;
|
||||
};
|
@ -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<typeof ConnectDatabase>;
|
||||
} as ComponentMeta<typeof ConnectDatabaseV2>;
|
||||
|
||||
export const Primary: ComponentStory<typeof ConnectDatabase> = args => {
|
||||
const [, updateArgs] = useArgs();
|
||||
const Template: ComponentStory<typeof ConnectDatabaseV2> = args => {
|
||||
return <ConnectDatabaseV2 {...args} />;
|
||||
};
|
||||
|
||||
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<typeof ConnectDatabaseV2> = () => {
|
||||
const env = useEnvironmentState();
|
||||
const cloud = isCloudConsole(globals);
|
||||
return (
|
||||
<ConnectDatabase
|
||||
{...args}
|
||||
onEnableEnterpriseTrial={() => {
|
||||
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' });
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<div className="my-3">
|
||||
This component attempts to read Console Type, and EE License Info from
|
||||
the environment
|
||||
</div>
|
||||
<div>isCloud: {cloud.toString()}</div>
|
||||
<div>Console Type: {globals.consoleType}</div>
|
||||
<div>Tenant Id: {globals.hasuraCloudTenantId}</div>
|
||||
<ConnectDatabaseV2 {...env} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
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();
|
||||
// };
|
||||
|
@ -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<DriverInfo>(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 (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="py-lg border-b border-slate-300 w-full flex justify-center">
|
||||
<div className="max-w-3xl w-full">
|
||||
<div className="text-xl font-bold">Connect Your First Database</div>
|
||||
<div className="text-muted">
|
||||
Connect your first database to access your database objects in your
|
||||
GraphQL API.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-3xl py-lg w-full">
|
||||
<SelectDatabase {...props} />
|
||||
</div>
|
||||
</div>
|
||||
<ConnectDatabaseWrapper>
|
||||
<FancyRadioCards
|
||||
items={cardData}
|
||||
value={selectedDriver?.name}
|
||||
onChange={val => {
|
||||
setSelectedDriver(
|
||||
prev => allDrivers?.find(d => d.name === val) || prev
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<ConnectDbBody
|
||||
consoleType={consoleType}
|
||||
selectedDriver={selectedDriver}
|
||||
eeLicenseInfo={eeLicenseInfo}
|
||||
isDriverAvailable={isDriverAvailable}
|
||||
/>
|
||||
</ConnectDatabaseWrapper>
|
||||
);
|
||||
};
|
||||
|
@ -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 <Oss selectedDriver={selectedDriver} />;
|
||||
case 'pro-lite':
|
||||
return (
|
||||
<ProLite
|
||||
selectedDriver={selectedDriver}
|
||||
eeLicenseInfo={eeLicenseInfo}
|
||||
isDriverAvailable={isDriverAvailable}
|
||||
/>
|
||||
);
|
||||
case 'pro':
|
||||
return (
|
||||
<Pro
|
||||
selectedDriver={selectedDriver}
|
||||
isDriverAvailable={isDriverAvailable}
|
||||
/>
|
||||
);
|
||||
case 'cloud':
|
||||
return (
|
||||
<Cloud
|
||||
selectedDriver={selectedDriver}
|
||||
isDriverAvailable={isDriverAvailable}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
@ -0,0 +1 @@
|
||||
export { ConnectDbBody } from './ConnectDbBody';
|
@ -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' && (
|
||||
<div className="mt-3" data-testid="neon-connect">
|
||||
<NeonConnect
|
||||
allDatabases={sourceNames ?? []}
|
||||
dispatch={dispatch}
|
||||
connectDbUrl={'data/v2/manage/connect'}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isDriverAvailable ? (
|
||||
<div className="mt-3" data-testid="cloud-driver-not-available">
|
||||
<IndicatorCard
|
||||
status="negative"
|
||||
headline="Cannot find the corresponding driver info"
|
||||
>
|
||||
The response from<code>list_source_kinds</code>did not return your
|
||||
selected driver. Please verify if the data connector agent is
|
||||
reachable from your Hasura instance.
|
||||
</IndicatorCard>
|
||||
</div>
|
||||
) : (
|
||||
<ConnectButton driverName={selectedDriverName} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,6 @@
|
||||
import { DriverInfo } from '../../../DataSource';
|
||||
import { ConnectButton } from '../../components/ConnectButton';
|
||||
|
||||
export const Oss = ({ selectedDriver }: { selectedDriver: DriverInfo }) => (
|
||||
<ConnectButton driverName={selectedDriver?.name} />
|
||||
);
|
@ -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 ? (
|
||||
<ConnectButton driverName={selectedDriver?.name} />
|
||||
) : (
|
||||
<div className="mt-3" data-testid="setup-connector">
|
||||
<SetupConnector
|
||||
selectedDriver={selectedDriver}
|
||||
onSetupSuccess={() => {
|
||||
pushRoute(
|
||||
`/data/v2/manage/database/add?driver=${selectedDriver?.name}`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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 ? (
|
||||
<div className="mt-3" data-testid="setup-connector">
|
||||
<SetupConnector
|
||||
selectedDriver={selectedDriver}
|
||||
onSetupSuccess={() => {
|
||||
pushRoute(
|
||||
`/data/v2/manage/database/add?driver=${selectedDriver?.name}`
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null;
|
||||
case 'forbidden':
|
||||
/**
|
||||
*
|
||||
* The only way "forbidden" happens here is if the licensing API is not reachable.
|
||||
*
|
||||
*/
|
||||
return (
|
||||
<div className="mt-3" data-testid="license-forbidden-api-error">
|
||||
<IndicatorCard
|
||||
status="negative"
|
||||
headline="Error Loading License"
|
||||
>
|
||||
Unable to determine your Enterprise License status.
|
||||
</IndicatorCard>
|
||||
</div>
|
||||
);
|
||||
|
||||
/**
|
||||
* Being verbose for state clarity
|
||||
*/
|
||||
case 'loading':
|
||||
case 'deactivated':
|
||||
case 'expired':
|
||||
case 'eligible':
|
||||
return (
|
||||
<div className="mt-3" data-testid="ee-trial-card">
|
||||
<EETrialCard
|
||||
eeAccess={eeLicenseInfo}
|
||||
id={dbWithArticle.replace(' ', '-')}
|
||||
horizontal
|
||||
{...eeCardContentMap(dbWithArticle)[eeLicenseInfo]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})()}
|
||||
|
||||
{(!selectedDriver?.enterprise ||
|
||||
(eeLicenseInfo === 'active' && isDriverAvailable)) && (
|
||||
<ConnectButton driverName={selectedDriver?.name} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,4 @@
|
||||
export { Cloud } from './Cloud';
|
||||
export { Oss } from './Oss';
|
||||
export { Pro } from './Pro';
|
||||
export { ProLite } from './ProLite';
|
@ -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<typeof SelectDatabase>;
|
||||
|
||||
export const Primary: ComponentStory<typeof SelectDatabase> = () => (
|
||||
<div className="max-w-3xl">
|
||||
Note: This container has a max width set. When rendering this component keep
|
||||
width in mind to avoid it growing too large.
|
||||
<SelectDatabase />
|
||||
</div>
|
||||
);
|
@ -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<DatabaseKind>('snowflake');
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<img
|
||||
src={DbConnectSVG}
|
||||
className={`mb-md w-full`}
|
||||
//
|
||||
alt="Database Connection Diagram"
|
||||
/>
|
||||
<FancyRadioCards
|
||||
items={databases}
|
||||
value={selectedDb}
|
||||
onChange={val => {
|
||||
console.log('selected value', val);
|
||||
setSelectedDb(val);
|
||||
}}
|
||||
/>
|
||||
{selectedDb === 'postgres' && (
|
||||
<div className="mt-3">
|
||||
<NeonBanner
|
||||
onClickConnect={() =>
|
||||
window.alert('todo: implement Neon integration')
|
||||
}
|
||||
status={{ status: 'default' }}
|
||||
buttonText="Create a Neon Database"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{enterpriseDbs.includes(selectedDb) && (
|
||||
<div className="border border-gray-300 mt-3 shadow-md rounded bg-white p-6">
|
||||
<div className="flex items-center">
|
||||
<div className="flex flex-col w-3/4">
|
||||
<div className="text-[21px]">
|
||||
Looking to connect to{' '}
|
||||
{selectedDb === 'snowflake' ? 'a Snowflake' : 'an Athena'}{' '}
|
||||
database?
|
||||
</div>
|
||||
<div className="text-md text-gray-700">
|
||||
Deploy data connectors to add data sources such as Snowflake,
|
||||
Amazon Athena, and more to your GraphQL API.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-1/4 justify-end">
|
||||
<Button
|
||||
//data-testid="onboarding-wizard-neon-connect-db-button"
|
||||
mode={'primary'}
|
||||
//isLoading={status.status === 'loading'}
|
||||
//loadingText={buttonText}
|
||||
size="md"
|
||||
//icon={icon ? iconMap[icon] : undefined}
|
||||
onClick={() => {}}
|
||||
//disabled={isButtonDisabled}
|
||||
>
|
||||
<div className="text-black font-semibold text-md">
|
||||
Enable Enterprise
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Button className="mt-6 self-end">Connect Existing Database</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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) => {
|
||||
<div className="text-xl text-gray-600 font-semibold">
|
||||
{isEditMode ? 'Edit BigQuery Connection' : 'Connect BigQuery Database'}
|
||||
</div>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<InputField
|
||||
name="name"
|
||||
label="Database name"
|
||||
placeholder="Database name"
|
||||
/>
|
||||
<Configuration name="configuration" />
|
||||
|
||||
<div className="mt-sm">
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div className="font-semibold text-muted">
|
||||
GraphQL Customization
|
||||
<Tabs
|
||||
value={tab}
|
||||
onValueChange={value => setTab(value)}
|
||||
items={[
|
||||
{
|
||||
value: 'connectionDetails',
|
||||
label: 'Connection Details',
|
||||
content: (
|
||||
<div className="mt-sm">
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<InputField
|
||||
name="name"
|
||||
label="Database name"
|
||||
placeholder="Database name"
|
||||
/>
|
||||
<Configuration name="configuration" />
|
||||
|
||||
<div className="mt-sm">
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div className="font-semibold text-muted">
|
||||
GraphQL Customization
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<GraphQLCustomization name="customization" />
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
mode="primary"
|
||||
isLoading={isLoading}
|
||||
loadingText="Saving"
|
||||
>
|
||||
{isEditMode ? 'Update Connection' : 'Connect Database'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<GraphQLCustomization name="customization" />
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
mode="primary"
|
||||
isLoading={isLoading}
|
||||
loadingText="Saving"
|
||||
>
|
||||
{isEditMode ? 'Update Connection' : 'Connect Database'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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,
|
||||
}: {
|
||||
|
@ -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 (
|
||||
<Button
|
||||
className="mt-6 self-end"
|
||||
data-testid="connect-existing-button"
|
||||
onClick={() =>
|
||||
pushRoute(`/data/v2/manage/database/add?driver=${driverName}`)
|
||||
}
|
||||
>
|
||||
Connect Existing Database
|
||||
</Button>
|
||||
);
|
||||
};
|
@ -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 (
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="py-lg border-b border-slate-300 w-full flex justify-center">
|
||||
<div className="max-w-3xl w-full">
|
||||
<div className="text-xl font-bold">
|
||||
{metadataSources?.length
|
||||
? 'Connect Database'
|
||||
: 'Connect Your First Database'}
|
||||
</div>
|
||||
{metadataSources?.length ? (
|
||||
<div className="text-muted">
|
||||
Connect a database to access your database objects in your GraphQL
|
||||
API.
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-muted">
|
||||
Connect your first database to access your database objects in
|
||||
your GraphQL API.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-3xl py-lg w-full">
|
||||
<div className="flex flex-col">
|
||||
<img
|
||||
src={DbConnectSVG}
|
||||
className={`mb-md w-full`}
|
||||
alt="Database Connection Diagram"
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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}
|
||||
/>
|
||||
|
||||
<div className="mt-sm">
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div className="font-semibold text-muted">
|
||||
GraphQL Customization
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<GraphQLCustomization name="customization" />
|
||||
</Collapsible>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'customization',
|
||||
label: 'GraphQL Customization',
|
||||
content: <GraphQLCustomization name="customization" />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
|
@ -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) => {
|
||||
<div className="text-xl text-gray-600 font-semibold">
|
||||
{isEditMode ? 'Edit MSSQL Connection' : 'Connect MSSQL Database'}
|
||||
</div>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<InputField
|
||||
name="name"
|
||||
label="Database name"
|
||||
placeholder="Database name"
|
||||
/>
|
||||
<ConnectionString name="configuration.connectionInfo.connectionString" />
|
||||
|
||||
<div className="mt-sm">
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div className="font-semibold text-muted">Advanced Settings</div>
|
||||
}
|
||||
>
|
||||
<PoolSettings name="configuration.connectionInfo.poolSettings" />
|
||||
</Collapsible>
|
||||
</div>
|
||||
<Tabs
|
||||
value={tab}
|
||||
onValueChange={value => setTab(value)}
|
||||
items={[
|
||||
{
|
||||
value: 'connectionDetails',
|
||||
label: 'Connection Details',
|
||||
content: (
|
||||
<div className="mt-sm">
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<InputField
|
||||
name="name"
|
||||
label="Database name"
|
||||
placeholder="Database name"
|
||||
/>
|
||||
<ConnectionString name="configuration.connectionInfo.connectionString" />
|
||||
|
||||
{areReadReplicasEnabled() && (
|
||||
<div className="mt-sm">
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div className="font-semibold text-muted">Read Replicas</div>
|
||||
}
|
||||
>
|
||||
<ReadReplicas name="configuration.readReplicas" />
|
||||
</Collapsible>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-sm">
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div className="font-semibold text-muted">
|
||||
Advanced Settings
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<PoolSettings name="configuration.connectionInfo.poolSettings" />
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
<div className="mt-sm">
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div className="font-semibold text-muted">
|
||||
GraphQL Customization
|
||||
<div className="mt-sm">
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div className="font-semibold text-muted">
|
||||
GraphQL Customization
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<GraphQLCustomization name="customization" />
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
<div className="mt-sm">
|
||||
<LimitedFeatureWrapper
|
||||
title="Looking to add Read Replicas?"
|
||||
id="read-replicas"
|
||||
description="Get production-ready today with a 30-day free trial of Hasura EE, no credit card required."
|
||||
>
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div className="font-semibold text-muted">
|
||||
Read Replicas
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ReadReplicas name="configuration.readReplicas" />
|
||||
</Collapsible>
|
||||
</LimitedFeatureWrapper>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-sm">
|
||||
<Button
|
||||
type="submit"
|
||||
mode="primary"
|
||||
isLoading={isLoading}
|
||||
loadingText="Saving"
|
||||
>
|
||||
{isEditMode ? 'Update Connection' : 'Connect Database'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<GraphQLCustomization name="customization" />
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
type="submit"
|
||||
mode="primary"
|
||||
isLoading={isLoading}
|
||||
loadingText="Saving"
|
||||
>
|
||||
{isEditMode ? 'Update Connection' : 'Connect Database'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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: (
|
||||
<div className="mt-sm">
|
||||
<DynamicDBRouting sourceName={dataSourceName} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
<div className="text-xl text-gray-600 font-semibold">
|
||||
{isEditMode
|
||||
? `Edit ${overrideDisplayName ?? 'Postgres'} Connection`
|
||||
: `Connect ${overrideDisplayName ?? 'Postgres'} Database`}
|
||||
</div>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<InputField
|
||||
name="name"
|
||||
label="Database name"
|
||||
placeholder="Database name"
|
||||
/>
|
||||
<Tabs
|
||||
value={tab}
|
||||
onValueChange={value => setTab(value)}
|
||||
items={[
|
||||
{
|
||||
value: 'connectionDetails',
|
||||
label: 'Connection Details',
|
||||
content: (
|
||||
<div className="mt-sm">
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<InputField
|
||||
name="name"
|
||||
label="Database name"
|
||||
placeholder="Database name"
|
||||
/>
|
||||
|
||||
<div className="bg-white border border-hasGray-300 rounded-md shadow-sm overflow-hidden p-4">
|
||||
<DatabaseUrl
|
||||
name="configuration.connectionInfo.databaseUrl"
|
||||
hideOptions={hiddenOptions}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-sm">
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div className="font-semibold text-muted">Advanced Settings</div>
|
||||
}
|
||||
>
|
||||
<PoolSettings name={`configuration.connectionInfo.poolSettings`} />
|
||||
<IsolationLevel
|
||||
name={`configuration.connectionInfo.isolationLevel`}
|
||||
/>
|
||||
<UsePreparedStatements
|
||||
name={`configuration.connectionInfo.usePreparedStatements`}
|
||||
/>
|
||||
<ExtensionSchema name="configuration.extensionSchema" />
|
||||
{areSSLSettingsEnabled() && (
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div className="font-semibold text-muted">
|
||||
SSL Certificates Settings
|
||||
<span className="px-1.5 italic font-light">
|
||||
(Certificates will be loaded from{' '}
|
||||
<a href="https://hasura.io/docs/latest/graphql/cloud/projects/create.html#existing-database">
|
||||
environment variables
|
||||
</a>
|
||||
)
|
||||
</span>
|
||||
<div className="bg-white border border-hasGray-300 rounded-md shadow-sm overflow-hidden p-4">
|
||||
<DatabaseUrl
|
||||
name="configuration.connectionInfo.databaseUrl"
|
||||
hideOptions={hiddenOptions}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<SslSettings
|
||||
name={`configuration.connectionInfo.sslSettings`}
|
||||
/>
|
||||
</Collapsible>
|
||||
)}
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
{areReadReplicasEnabled() && (
|
||||
<div className="mt-sm">
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div className="font-semibold text-muted">Read Replicas</div>
|
||||
}
|
||||
>
|
||||
<ReadReplicas
|
||||
name="configuration.readReplicas"
|
||||
hideOptions={hiddenOptions}
|
||||
/>
|
||||
</Collapsible>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-sm">
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div className="font-semibold text-muted">
|
||||
Advanced Settings
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<PoolSettings
|
||||
name={`configuration.connectionInfo.poolSettings`}
|
||||
/>
|
||||
<IsolationLevel
|
||||
name={`configuration.connectionInfo.isolationLevel`}
|
||||
/>
|
||||
<UsePreparedStatements
|
||||
name={`configuration.connectionInfo.usePreparedStatements`}
|
||||
/>
|
||||
<ExtensionSchema name="configuration.extensionSchema" />
|
||||
<LimitedFeatureWrapper
|
||||
title="Looking to add SSL Settings?"
|
||||
id="db-ssl-settings"
|
||||
description="Get production-ready today with a 30-day free trial of Hasura EE, no credit card required."
|
||||
>
|
||||
<div className="mt-sm">
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div className="font-semibold text-muted">
|
||||
SSL Certificates Settings
|
||||
<span className="px-1.5 italic font-light">
|
||||
(Certificates will be loaded from{' '}
|
||||
<a href="https://hasura.io/docs/latest/graphql/cloud/projects/create.html#existing-database">
|
||||
environment variables
|
||||
</a>
|
||||
)
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<SslSettings
|
||||
name={`configuration.connectionInfo.sslSettings`}
|
||||
/>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</LimitedFeatureWrapper>
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
<div className="mt-sm">
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div className="font-semibold text-muted">
|
||||
GraphQL Customization
|
||||
<div className="mt-sm">
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div className="font-semibold text-muted">
|
||||
GraphQL Customization
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<GraphQLCustomization name="customization" />
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
<div className="mt-sm">
|
||||
<LimitedFeatureWrapper
|
||||
id="read-replicas"
|
||||
title="Improve performance and handle increased traffic with read replicas"
|
||||
description="Scale your database by offloading read queries to
|
||||
read-only replicas, allowing for better performance
|
||||
and availability for users."
|
||||
>
|
||||
<Collapsible
|
||||
triggerChildren={
|
||||
<div className="font-semibold text-muted">
|
||||
Read Replicas
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ReadReplicas
|
||||
name="configuration.readReplicas"
|
||||
hideOptions={hiddenOptions}
|
||||
/>
|
||||
</Collapsible>
|
||||
</LimitedFeatureWrapper>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-sm">
|
||||
<Button
|
||||
type="submit"
|
||||
mode="primary"
|
||||
isLoading={isLoading}
|
||||
loadingText="Saving"
|
||||
>
|
||||
{isEditMode ? 'Update Connection' : 'Connect Database'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<GraphQLCustomization name="customization" />
|
||||
</Collapsible>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end mt-sm">
|
||||
<Button
|
||||
type="submit"
|
||||
mode="primary"
|
||||
isLoading={isLoading}
|
||||
loadingText="Saving"
|
||||
>
|
||||
{isEditMode ? 'Update Connection' : 'Connect Database'}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
...dynamicDBRoutingTab,
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -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`}
|
||||
>
|
||||
<FaExclamationCircle className="fill-current self-start h-md text-muted" />
|
||||
<div className="ml-xs">
|
||||
<div className="ml-xs max-w-2xl">
|
||||
<strong>Dynamic Routing Precedence</strong>
|
||||
<p>
|
||||
{' '}
|
||||
|
@ -29,7 +29,7 @@ export const ReadReplicas = ({
|
||||
>({
|
||||
name,
|
||||
});
|
||||
const { watch, setValue } =
|
||||
const { watch, setValue, trigger } =
|
||||
useFormContext<Record<string, ConnectionInfoSchema[]>>();
|
||||
|
||||
const [mode, setMode] = useState<'idle' | 'add' | 'edit'>('idle');
|
||||
@ -141,9 +141,14 @@ export const ReadReplicas = ({
|
||||
</Collapsible>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setMode('idle');
|
||||
setActiveRow(undefined);
|
||||
onClick={async () => {
|
||||
// validate the current open read replica state before closing.
|
||||
const result = await trigger(`${name}.${activeRow}`);
|
||||
|
||||
if (result) {
|
||||
setMode('idle');
|
||||
setActiveRow(undefined);
|
||||
}
|
||||
}}
|
||||
mode="primary"
|
||||
className="my-2"
|
||||
|
@ -0,0 +1,97 @@
|
||||
import BreadCrumb from '../../../../components/Common/Layout/BreadCrumb/BreadCrumb';
|
||||
import { getRoute } from '../../../../utils/getDataRoute';
|
||||
import { ConnectBigQueryWidget } from '../ConnectBigQueryWidget/ConnectBigQueryWidget';
|
||||
import { ConnectGDCSourceWidget } from '../ConnectGDCSourceWidget/ConnectGDCSourceWidget';
|
||||
import { ConnectMssqlWidget } from '../ConnectMssqlWidget/ConnectMssqlWidget';
|
||||
import { ConnectPostgresWidget } from '../ConnectPostgresWidget/ConnectPostgresWidget';
|
||||
|
||||
const getEditDatasourceName = (): string | undefined => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
const database = urlParams.get('database');
|
||||
|
||||
return database ?? undefined;
|
||||
};
|
||||
|
||||
const getDriverName = (): string | undefined => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
const driver = urlParams.get('driver');
|
||||
|
||||
return driver ?? undefined;
|
||||
};
|
||||
|
||||
const ConnectDatabaseWrapper = () => {
|
||||
const dataSourceName = getEditDatasourceName();
|
||||
const driver = getDriverName();
|
||||
|
||||
if (!driver) return <div>Error. No driver found.</div>;
|
||||
|
||||
if (driver === 'postgres')
|
||||
return <ConnectPostgresWidget dataSourceName={dataSourceName} />;
|
||||
|
||||
if (driver === 'citus')
|
||||
return (
|
||||
<ConnectPostgresWidget
|
||||
dataSourceName={dataSourceName}
|
||||
overrideDisplayName="Citus"
|
||||
overrideDriver="citus"
|
||||
/>
|
||||
);
|
||||
|
||||
if (driver === 'alloy')
|
||||
return (
|
||||
<ConnectPostgresWidget
|
||||
dataSourceName={dataSourceName}
|
||||
overrideDisplayName="AlloyDB"
|
||||
/>
|
||||
);
|
||||
|
||||
if (driver === 'cockroach')
|
||||
return (
|
||||
<ConnectPostgresWidget
|
||||
dataSourceName={dataSourceName}
|
||||
overrideDisplayName="CockroachDB"
|
||||
overrideDriver="cockroach"
|
||||
/>
|
||||
);
|
||||
|
||||
if (driver === 'bigquery')
|
||||
return <ConnectBigQueryWidget dataSourceName={dataSourceName} />;
|
||||
|
||||
if (driver === 'mssql')
|
||||
return <ConnectMssqlWidget dataSourceName={dataSourceName} />;
|
||||
|
||||
return (
|
||||
<ConnectGDCSourceWidget dataSourceName={dataSourceName} driver={driver} />
|
||||
);
|
||||
};
|
||||
|
||||
export const ConnectUIContainer = () => {
|
||||
const driver = getDriverName();
|
||||
return (
|
||||
<div className="p-4">
|
||||
<BreadCrumb
|
||||
breadCrumbs={[
|
||||
{
|
||||
url: '/data',
|
||||
title: 'Data',
|
||||
},
|
||||
{
|
||||
url: '/data/manage',
|
||||
title: 'Manage',
|
||||
},
|
||||
{
|
||||
url: '/data/v2/manage/connect',
|
||||
title: 'Connect',
|
||||
},
|
||||
{
|
||||
url: getRoute().connectDatabase(driver),
|
||||
title: driver ?? '',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ConnectDatabaseWrapper />
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1 @@
|
||||
export { ConnectUIContainer } from './ConnectUIContainer';
|
@ -8,7 +8,7 @@ export const DatabaseLogo: React.FC<{ title: string; image: string }> = ({
|
||||
<div className="flex flex-col mt-2 items-center">
|
||||
<img
|
||||
src={image}
|
||||
className="h-[16px] w-[16px] mb-2"
|
||||
className="h-[24px] w-[24px] mb-2 object-contain"
|
||||
alt={`${title} logo`}
|
||||
/>
|
||||
<div className="text-black text-base">{title}</div>
|
||||
|
@ -0,0 +1,56 @@
|
||||
import { expect } from '@storybook/jest';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
import { userEvent, within } from '@storybook/testing-library';
|
||||
import React from 'react';
|
||||
import { ReactQueryDecorator } from '../../../storybook/decorators/react-query';
|
||||
import { handlers } from '../mocks/handlers.mock';
|
||||
|
||||
import { useDatabaseConnectDrivers } from '../hooks';
|
||||
import { FancyRadioCards } from './FancyRadioCards';
|
||||
import { Badge } from '../../../new-components/Badge';
|
||||
|
||||
export default {
|
||||
component: FancyRadioCards,
|
||||
parameters: {
|
||||
msw: handlers(),
|
||||
},
|
||||
decorators: [ReactQueryDecorator()],
|
||||
} as ComponentMeta<typeof FancyRadioCards>;
|
||||
|
||||
const Template: ComponentStory<typeof FancyRadioCards> = () => {
|
||||
// using this hook as it has a handy card data return so the story has an example of icons + text
|
||||
|
||||
const { cardData } = useDatabaseConnectDrivers({
|
||||
showEnterpriseDrivers: true,
|
||||
});
|
||||
|
||||
const [value, setValue] = React.useState('postgres');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-3">
|
||||
Selected Value:
|
||||
<Badge className="ml-2">
|
||||
<span data-testid="value">{value}</span>
|
||||
</Badge>
|
||||
</div>
|
||||
<FancyRadioCards value={value} items={cardData} onChange={setValue} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Primary = Template.bind({});
|
||||
|
||||
Primary.play = async ({ canvasElement }) => {
|
||||
const c = within(canvasElement);
|
||||
|
||||
// test click on label element
|
||||
await userEvent.click(await c.findByTestId('fancy-label-snowflake'));
|
||||
|
||||
expect(c.getByTestId('value')).toHaveTextContent('snowflake');
|
||||
|
||||
// test click on radio button
|
||||
await userEvent.click(await c.findByTestId('fancy-radio-mssql'));
|
||||
|
||||
expect(c.getByTestId('value')).toHaveTextContent('mssql');
|
||||
};
|
@ -1,10 +1,8 @@
|
||||
import { DatabaseKind } from '../types';
|
||||
import * as RadioGroup from '@radix-ui/react-radio-group';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import React, { VFC } from 'react';
|
||||
|
||||
const twRadioStyles = {
|
||||
//root: `flex flex-row gap-3 flex-wrap w-full`,
|
||||
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]`,
|
||||
@ -16,24 +14,25 @@ const twRadioStyles = {
|
||||
label: `text-base whitespace-nowrap cursor-pointer flex-[1] h-full w-full flex justify-center items-center`,
|
||||
};
|
||||
|
||||
export const FancyRadioCards: React.VFC<{
|
||||
export const FancyRadioCards: VFC<{
|
||||
value: string;
|
||||
items: {
|
||||
value: string;
|
||||
content: React.ReactNode | string;
|
||||
}[];
|
||||
onChange: (value: DatabaseKind) => void;
|
||||
onChange: (value: string) => void;
|
||||
}> = ({ value, items, onChange }) => {
|
||||
return (
|
||||
<RadioGroup.Root
|
||||
className={twRadioStyles.root}
|
||||
defaultValue={value}
|
||||
aria-label="View density"
|
||||
value={value}
|
||||
aria-label="Radio cards"
|
||||
onValueChange={onChange}
|
||||
>
|
||||
{items.map((item, i) => {
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={clsx(
|
||||
twRadioStyles.itemContainer.default,
|
||||
value === item.value && twRadioStyles.itemContainer.active
|
||||
@ -42,12 +41,14 @@ export const FancyRadioCards: React.VFC<{
|
||||
<RadioGroup.Item
|
||||
className={twRadioStyles.radioButton}
|
||||
value={item.value}
|
||||
data-testid={`fancy-radio-${item.value}`}
|
||||
id={`radio-item-${item.value}`}
|
||||
>
|
||||
<RadioGroup.Indicator className={twRadioStyles.indicator} />
|
||||
</RadioGroup.Item>
|
||||
<label
|
||||
className={twRadioStyles.label}
|
||||
data-testid={`fancy-label-${item.value}`}
|
||||
htmlFor={`radio-item-${item.value}`}
|
||||
>
|
||||
{item.content}
|
||||
|
@ -1,7 +1,5 @@
|
||||
export { GraphQLCustomization } from './GraphQLCustomization';
|
||||
export { adaptGraphQLCustomization } from './utils/adaptResponse';
|
||||
export { generateGraphQLCustomizationInfo } from './utils/generateRequest';
|
||||
export {
|
||||
GraphQLCustomizationSchema,
|
||||
graphQLCustomizationSchema,
|
||||
} from './schema';
|
||||
export { graphQLCustomizationSchema } from './schema';
|
||||
export type { GraphQLCustomizationSchema } from './schema';
|
||||
|
@ -17,4 +17,6 @@ export const graphQLCustomizationSchema = z.object({
|
||||
namingConvention: z.string().optional(),
|
||||
});
|
||||
|
||||
export type GraphQLCustomization = z.infer<typeof graphQLCustomizationSchema>;
|
||||
export type GraphQLCustomizationSchema = z.infer<
|
||||
typeof graphQLCustomizationSchema
|
||||
>;
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { SourceCustomization } from '../../../../hasura-metadata-types';
|
||||
import { GraphQLCustomization } from '../schema';
|
||||
import { GraphQLCustomizationSchema } from '../schema';
|
||||
|
||||
export const adaptGraphQLCustomization = (
|
||||
sourceCustomization: SourceCustomization
|
||||
): GraphQLCustomization => {
|
||||
): GraphQLCustomizationSchema => {
|
||||
return {
|
||||
rootFields: {
|
||||
namespace: sourceCustomization.root_fields?.namespace,
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { GraphQLCustomization } from '../schema';
|
||||
import { GraphQLCustomizationSchema } from '../schema';
|
||||
|
||||
export const generateGraphQLCustomizationInfo = (
|
||||
values: GraphQLCustomization
|
||||
values: GraphQLCustomizationSchema
|
||||
) => {
|
||||
return {
|
||||
root_fields: {
|
||||
|
@ -0,0 +1,47 @@
|
||||
import globals from '../../../../Globals';
|
||||
import { isProConsole } from '../../../../utils';
|
||||
import { EETrialCard, useEELiteAccess } from '../../../EETrial';
|
||||
|
||||
export const LimitedFeatureWrapper = ({
|
||||
children,
|
||||
title,
|
||||
description,
|
||||
id,
|
||||
}: //
|
||||
{
|
||||
title: string;
|
||||
description: string;
|
||||
id: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const { access: eeLiteAccess } = useEELiteAccess(globals);
|
||||
|
||||
/**
|
||||
* There are three cases here.
|
||||
* 1. If it's OSS - do not show the children at all. (there is no point in using this wrapper for oss features)
|
||||
* 2. If it's pro lite
|
||||
* - show the "Try pro-lite" license form if license is not active.
|
||||
* - show the children if license is active.
|
||||
* 3. If it's cloud/pro just show the children
|
||||
*
|
||||
*/
|
||||
|
||||
// this will tell us if console is pro or cloud
|
||||
const isPro = isProConsole(window.__env);
|
||||
|
||||
if (eeLiteAccess === 'active' || isPro) return <div>{children}</div>;
|
||||
|
||||
// this is to return nothing for oss
|
||||
if (eeLiteAccess === 'forbidden') return null;
|
||||
|
||||
return (
|
||||
<EETrialCard
|
||||
id={id}
|
||||
cardTitle={title}
|
||||
cardText={description}
|
||||
buttonType="default"
|
||||
eeAccess={eeLiteAccess}
|
||||
horizontal
|
||||
/>
|
||||
);
|
||||
};
|
@ -14,7 +14,6 @@ import { useMetadata } from '../../../MetadataAPI';
|
||||
import _push from '../../../../components/Services/Data/push';
|
||||
import { useReloadSource } from '../../hooks/useReloadSource';
|
||||
import { useDropSource } from '../../hooks/useDropSource';
|
||||
import { getRoute } from '../../../../utils/getDataRoute';
|
||||
import { IndicatorCard } from '../../../../new-components/IndicatorCard';
|
||||
import Skeleton from 'react-loading-skeleton';
|
||||
import { useInconsistentSources } from '../../hooks/useInconsistentSources';
|
||||
@ -129,12 +128,7 @@ export const ListConnectedDatabases = (props?: { className?: string }) => {
|
||||
const columns = ['database', 'driver', '', ''];
|
||||
|
||||
const rowData = (databaseList ?? []).map((databaseItem, index) => [
|
||||
<a
|
||||
href={getRoute().database(databaseItem.dataSourceName)}
|
||||
className="text-secondary"
|
||||
>
|
||||
{databaseItem.dataSourceName}
|
||||
</a>,
|
||||
<div>{databaseItem.dataSourceName}</div>,
|
||||
databaseItem.driver,
|
||||
isDatabaseVersionLoading || isInconsistentFetchCallLoading ? (
|
||||
<Skeleton />
|
||||
@ -178,7 +172,7 @@ export const ListConnectedDatabases = (props?: { className?: string }) => {
|
||||
onClick={() => {
|
||||
dispatch(
|
||||
_push(
|
||||
`/data/v2/database/edit?database=${databaseItem.dataSourceName}`
|
||||
`/data/v2/manage/database/edit?driver=${databaseItem.driver}&database=${databaseItem.dataSourceName}`
|
||||
)
|
||||
);
|
||||
}}
|
||||
|
@ -23,7 +23,7 @@ export const DisplayDetails = ({
|
||||
<div className="flex justify-start">
|
||||
<div
|
||||
className={clsx(
|
||||
'max-w-xs',
|
||||
'max-w-2xl',
|
||||
isExpanded
|
||||
? 'whitespace-pre-line'
|
||||
: 'overflow-hidden text-ellipsis whitespace-nowrap'
|
||||
|
@ -1,40 +0,0 @@
|
||||
import { SelectDatabase } from '.';
|
||||
import { hasuraToast } from '../../../../new-components/Toasts';
|
||||
import { useArgs } from '@storybook/client-api';
|
||||
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||
|
||||
export default {
|
||||
component: SelectDatabase,
|
||||
argTypes: {
|
||||
onEnableEnterpriseTrial: { action: 'Enable Enterprise Clicked' },
|
||||
onContactSales: { action: 'Contact Sales Clicked' },
|
||||
},
|
||||
} as ComponentMeta<typeof SelectDatabase>;
|
||||
|
||||
export const Primary: ComponentStory<typeof SelectDatabase> = args => {
|
||||
const [, updateArgs] = useArgs();
|
||||
return (
|
||||
<div className="max-w-3xl">
|
||||
Note: This container has a max width set. When rendering this component
|
||||
keep width in mind to avoid it growing too large.
|
||||
<SelectDatabase
|
||||
{...args}
|
||||
onEnableEnterpriseTrial={() => {
|
||||
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' });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Primary.args = {
|
||||
eeState: 'inactive',
|
||||
initialDb: 'snowflake',
|
||||
};
|
@ -1,67 +0,0 @@
|
||||
import { Button } from '../../../../../new-components/Button';
|
||||
import { InputField } from '../../../../../new-components/Form';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { useFormContext } from 'react-hook-form';
|
||||
import { FaRegCopy } from 'react-icons/fa';
|
||||
|
||||
const twStyles = {
|
||||
alignToTopOfInput: `top-[32px]`,
|
||||
inputHeight: `h-[36px]`,
|
||||
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: `active:opacity-50 border-none bg-transparent absolute right-0 shadow-none bg-none`,
|
||||
};
|
||||
|
||||
export const CopyableInputField: typeof InputField = props => {
|
||||
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<NodeJS.Timeout>();
|
||||
|
||||
const handleCopyButton = () => {
|
||||
// clear timer if already going...
|
||||
if (copyTimer.current) {
|
||||
clearTimeout(copyTimer.current);
|
||||
}
|
||||
|
||||
// copy text to clipboard
|
||||
navigator.clipboard.writeText(fieldValue);
|
||||
|
||||
// show confirmation
|
||||
setShowCopiedConfirmation(true);
|
||||
|
||||
// hide after 1.5s
|
||||
copyTimer.current = setTimeout(() => {
|
||||
setShowCopiedConfirmation(false);
|
||||
}, 1500);
|
||||
};
|
||||
return (
|
||||
<div className="relative">
|
||||
<InputField {...props} />
|
||||
<Button
|
||||
className={clsx(twStyles.copyButton, twStyles.alignToTopOfInput)}
|
||||
icon={<FaRegCopy />}
|
||||
onClick={handleCopyButton}
|
||||
/>
|
||||
<div
|
||||
className={clsx(
|
||||
twStyles.copyConfirm.base,
|
||||
twStyles.alignToTopOfInput,
|
||||
twStyles.inputHeight,
|
||||
twStyles.copyConfirm.invisible,
|
||||
showCopiedConfirmation && twStyles.copyConfirm.visible
|
||||
)}
|
||||
>
|
||||
Copied!
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
export const DatabaseLogo: React.FC<{ title: string; image: string }> = ({
|
||||
title,
|
||||
image,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex flex-col mt-2 items-center">
|
||||
<img
|
||||
src={image}
|
||||
className="h-[16px] w-[16px] mb-2"
|
||||
alt={`${title} logo`}
|
||||
/>
|
||||
<div className="text-black text-base">{title}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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 (
|
||||
<InformationCard blueLeftBorder>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center pb-3 mb-3 border-b border-slate-300">
|
||||
<div className="flex flex-col w-3/4">
|
||||
<div className="font-bold">
|
||||
{dbDisplayNames[selectedDb]} Connector Required
|
||||
</div>
|
||||
<div className="text-md text-gray-700">
|
||||
{`The Hasura GraphQL Data Connector Service is required to connect to ${dbWithArticle} database.`}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-1/4 justify-end">
|
||||
<Button
|
||||
icon={<FaExternalLinkAlt />}
|
||||
iconPosition="end"
|
||||
onClick={() => {
|
||||
alert('need link to docs here');
|
||||
}}
|
||||
>
|
||||
Deployment Methods
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-bold text-muted flex items-center">
|
||||
<GrDocker />
|
||||
<div className="ml-1">Docker Initialization</div>
|
||||
</div>
|
||||
<SimpleForm
|
||||
schema={z.object({
|
||||
docker_command: z.string(),
|
||||
agent_path: z.string(),
|
||||
})}
|
||||
options={{
|
||||
defaultValues: {
|
||||
docker_command:
|
||||
'docker run -p 127.0.0.1:1234:1234 hasura/graphql-data-connector',
|
||||
agent_path: 'http://host.docker.internal:1234',
|
||||
},
|
||||
}}
|
||||
onSubmit={values => {
|
||||
console.log(values);
|
||||
}}
|
||||
>
|
||||
<CopyableInputField
|
||||
className="mt-1"
|
||||
label="Run GraphQL Data Connector Service"
|
||||
tooltip="This is a really great tooltip for this field"
|
||||
disabled={true}
|
||||
name="docker_command"
|
||||
learnMoreLink="https://hasura.io/docs"
|
||||
/>
|
||||
<InputField
|
||||
name="agent_path"
|
||||
label="Connect to GraphQL Data Connector URI"
|
||||
tooltip="This is a really great tooltip for this field"
|
||||
/>
|
||||
<div className="flex justify-end w-full">
|
||||
<Button
|
||||
type="submit"
|
||||
mode="primary"
|
||||
onClick={() => {
|
||||
hasuraToast({
|
||||
title: 'Not Implemented',
|
||||
message:
|
||||
'This feature will be implemented once the dc_add_agent check is merged.',
|
||||
toastOptions: {
|
||||
duration: 3000,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Validate And Connect Database
|
||||
</Button>
|
||||
</div>
|
||||
</SimpleForm>
|
||||
</div>
|
||||
</div>
|
||||
</InformationCard>
|
||||
);
|
||||
};
|
@ -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 (
|
||||
<InformationCard>
|
||||
<div className="flex items-center">
|
||||
<div className="flex flex-col w-3/4">
|
||||
<div className="text-[21px] flex items-center gap-2">
|
||||
<FiAlertTriangle color="rgb(220 38 38)" /> Enterprise Trial Expired
|
||||
</div>
|
||||
<div className="text-md text-gray-700">
|
||||
With an Enterprise Edition license you can add data sources such as
|
||||
Snowflake, Amazon Athena, and more to your GraphQL API.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-1/4 justify-end">
|
||||
<Button size="md" onClick={onContactSales}>
|
||||
Contact Sales
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</InformationCard>
|
||||
);
|
||||
};
|
@ -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 (
|
||||
<InformationCard>
|
||||
<div className="flex items-center">
|
||||
<div className="flex flex-col w-3/4">
|
||||
<div className="text-[21px]">
|
||||
{`Looking to connect to ${dbWithArticle} database?`}
|
||||
</div>
|
||||
<div className="text-md text-gray-700">
|
||||
Deploy data connectors to add data sources such as Snowflake, Amazon
|
||||
Athena, and more to your GraphQL API.
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-1/4 justify-end">
|
||||
<Button mode={'primary'} size="md" onClick={onEnableEnterpriseTrial}>
|
||||
Enable Enterprise
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</InformationCard>
|
||||
);
|
||||
};
|
@ -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 (
|
||||
<RadioGroup.Root
|
||||
className={twRadioStyles.root}
|
||||
defaultValue={value}
|
||||
aria-label="Radio cards"
|
||||
onValueChange={onChange}
|
||||
>
|
||||
{items.map((item, i) => {
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
twRadioStyles.itemContainer.default,
|
||||
value === item.value && twRadioStyles.itemContainer.active
|
||||
)}
|
||||
>
|
||||
<RadioGroup.Item
|
||||
className={twRadioStyles.radioButton}
|
||||
value={item.value}
|
||||
id={`radio-item-${item.value}`}
|
||||
>
|
||||
<RadioGroup.Indicator className={twRadioStyles.indicator} />
|
||||
</RadioGroup.Item>
|
||||
<label
|
||||
className={twRadioStyles.label}
|
||||
htmlFor={`radio-item-${item.value}`}
|
||||
>
|
||||
{item.content}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</RadioGroup.Root>
|
||||
);
|
||||
};
|
@ -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 (
|
||||
<div
|
||||
className={clsx(
|
||||
twStyles.container,
|
||||
blueLeftBorder && twStyles.blueBorder,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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';
|
@ -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<DatabaseKind, string> = {
|
||||
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: (
|
||||
<DatabaseLogo title={dbDisplayNames.postgres} image={postgresLogo} />
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'citus',
|
||||
content: <DatabaseLogo title={dbDisplayNames.citus} image={citusLogo} />,
|
||||
},
|
||||
{
|
||||
value: 'cockroach',
|
||||
content: (
|
||||
<DatabaseLogo title={dbDisplayNames.cockroach} image={cockroachLogo} />
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'alloydb',
|
||||
content: <DatabaseLogo title={dbDisplayNames.alloydb} image={googleLogo} />,
|
||||
},
|
||||
{
|
||||
value: 'mssql',
|
||||
content: (
|
||||
<DatabaseLogo title={dbDisplayNames.mssql} image={microsoftLogo} />
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'bigquery',
|
||||
content: (
|
||||
<DatabaseLogo title={dbDisplayNames.bigquery} image={googleLogo} />
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'snowflake',
|
||||
content: (
|
||||
<DatabaseLogo title={dbDisplayNames.snowflake} image={snowflakeLogo} />
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'athena',
|
||||
content: <DatabaseLogo title={dbDisplayNames.athena} image={amazonLogo} />,
|
||||
},
|
||||
];
|
@ -1,9 +0,0 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<rect x="0.9375" width="16" height="16" fill="url(#pattern0)"/>
|
||||
<defs>
|
||||
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||
<use xlink:href="#image0_44_21233" transform="scale(0.0625)"/>
|
||||
</pattern>
|
||||
<image id="image0_44_21233" width="16" height="16" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABdUlEQVQ4jc2TO04jQRCGv+rxjDDaFZiAERkSsNqQG7BH4ABIiARENjEiMBIHmAzYZM0NOALcYEMEC7KEQJqAh7HxGM90FwEv29gWZPuH1fV/XV1dJQxQvvdjUZAYQNGosHZy0C9PegP6e27eWhOLsNAVV448z0Wyevq3L0D/TI+7VhAjsjyoqhfSvhlpR7JSvXsD2J2fZUQjYGyo+V01VGJv/bgs9c3ZX2L0ICgVfK/I6Gfcj9eu2TjPMvHNUgFAHWOP1znGlySYMKHxP7QGgPxBqZ9liX3QEMBa1yh0JrhMw1Zi8YqSBCUvFPPy7BzqZ1nSvnEhEHZ6ugCvsqmGaStP/W+m3a450isboN3GoYDnaylmdVdML+3AFAAz9PQT+g8A37f/HYJsAbUv+GoIW1OV6uHbh9+Wp8e93IsFuka5edHTRGE/gKhU6RjlTjU25ubVuBiel+kVoMKRgWiyUu2/TL2635xZFGW3eWFbGKKwUu27zk/aOpGGPDxYiQAAAABJRU5ErkJggg=="/>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.0 KiB |
@ -1,9 +0,0 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<rect x="0.6875" width="16" height="16" fill="url(#pattern0)"/>
|
||||
<defs>
|
||||
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||
<use xlink:href="#image0_44_21195" transform="scale(0.03125)"/>
|
||||
</pattern>
|
||||
<image id="image0_44_21195" width="32" height="32" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAE5UlEQVR42rVXa2yUVRDdbh87QkstaNMqz9m2KIVWo9TSat3t2HY02hASKkT9oZhoZov4iI9UoiBGbRSJ6A+MJoOloikhEBNMTKMI0ag8FB8oMYKoiIGgUq0EoVBzm1ny+fnttsWyyfzZvXfOmblnzr0bCo3QB0gygGQUkFwEJJcAyQwgmQYk4933oXPxAZIcICkDkluBZCWQdAPJbiD5EUh+AZKfgOQbIHkHSJ4EklogiYwE8DggaQGSLiA5ACR9QNI/hDgKJGuAZPrZAhcAyQIg+QhI/h4iaFC4rsSHA5wFJAwk7wHJCU8iR+IwkOwBkp1Ash1IvgaSQ0BychASu5xWhgJeCCTt1r5kG98HkmVA0gwk5UBSBCTnA0m+rXffzQOSTiA5koLAaSBZPhh4hQnLLd4PJM8DSU1O7cLRQ+xcNpDEgGRLChJfpNscs7NyLX4mEkuUhkIrMyq21IajHU0Xo3Icle9A5ftReREq34bKMVQej8qZvlwTbBL8BA6lAm8Eku+AZBPEE1Wh0PKMkrUN41B5Hip3ovJWVO5C5UdReQ4qV6FyGSpPQOWxfgKW043grz4C3weBX2OCeiz7qkV5pV3XuYStqLwTlb9C5WWofDkqwzAnKDfgKN72L5oKJBuhXuYXL27JxtVNjMofovJvqLwClTGFA55nInROmJFmkjZ4wN00tXoXOAW3R+KJ60vfaBiDyktRuQeVv0XlZlTO8iXMA5IbgeQlINkMJDtsOp4CkokpOrDVN4aTvFXMjcQTTdHOxiJUfh2VT6Hyp6h8ZUCy6gF9kBwLENZfQLIkYM9l5g/95hGt07rrcpI/TorEE/WTV93g1L0Rlfut8qqARLOB5IcAYCewp4FkFpAU+/aEgeRZz9q38m+5vTD6WlO1+zEzEkuUFbW1uMrXGHivU3wAeE0K8ONAclea82/2GNKXkbrW8mhHUw0qzw7lXL0wK3fOnaNQeTEq9xmBDlSO+JIAkKxPYShuasYGALvKbwKSfbZubySeuLaks7EYlV9A5ckDC1G5DpUPG7gTXiwgmbt2f07j60WetZlAUmqCTFa+G+olVvpmQwSVX0TltiS4q36dgbv4AJXHBBAoN1cMIuBEtc5uylYgWW3W7Sz81IAL1ktF5bbqMCo/gMqfo3I0ScDN+u8eAi+nOMe8FHaaLpzqn4B6uXDmgcoMVF5gnvLgmcSo/IoH3EV7GjertRfPYMBH7OExK+uKe8PRzsYsVL4blY9ahwu9BPb4CLw6iKVeCiQrnJpt9HqBpMeeYc6QlgLJTPdcs/z5Zmq9Vj3/KyEqH/MR2IXKRYOQCNu9X2ldcYAIJKN9uWeg8npUPmkT9jgqh/0ETvsIuIUP/2fhMD6uxah8Hyrv8+Rd67oRtLjHR6DfWnUPKucOA9QpfCIqCypvs6qT+brdVZ1q4ycBBFwcR+VN9gaYYuMa9uzLsvOdispzUXmV2XefL8+7qFySjnlbwCZvnEDl/ai82dropkbtznCVHvRVmwx3mW0443ZpCEyxW69/BONPVH4OlS8Y6vm5Nv8xAsBO0J/ZkeQMR7XZqPyQMT9bYKf4JSnFNkQSN6Py9hRnGhTOXD5G5UdQuRSVM/73/z6b4fnOES35XhPaQatyh11ebfYMLzgn/3xtrgvcQxSVp1tEUdk9z7NHCucflO7Q/CFSv7QAAAAASUVORK5CYII="/>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 2.2 KiB |
@ -1,9 +0,0 @@
|
||||
<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<rect x="0.3125" width="16" height="16" fill="url(#pattern0)"/>
|
||||
<defs>
|
||||
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||
<use xlink:href="#image0_44_21201" transform="scale(0.03125)"/>
|
||||
</pattern>
|
||||
<image id="image0_44_21201" width="32" height="32" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAAXNSR0IArs4c6QAAAKhQTFRFR3BMaDL/aTP/aDP/aTP/ajP/aTL/aDD/cDD/aTP/ajP/aDT/aTL/ajL/aTP/aDL/aDL/aDP/fU3/aDH/ajT/vaX/s5b/aTP/////2cz/9vL/xrL/7OX/j2b/s5n/ckD/mHP/vaX/tZn/qov/ooD/0L//jmb/q43/4tn/x7P/l3L/hVn/fEz/fE3/7Ob/e0z/vKX/s5j/0MD/vab/oH7/hln/4tj/qoz/OmmteQAAADh0Uk5TAGDfoM+f3yAQ78+A739fgJCQ76DP+vr////////////////////////////////////////////R1fvCAAABX0lEQVQ4y31T53rCMAx0ICEDKKND8iJh7+7x/m9Wm2EpoV/1I/l0Ovt0li1EiCxKihwgL5K7TNxGOnLFEEnaKGd9aES/vvwBbqLFNklbAP8xfH1srd1fSy+2qsaM4ddLtdkopS3A6kOhekbpwOLshvr72hotUVblFRicBLjuFHHKcy/SZvkEZ2ucMGDoToClC5z5jyUkz0RE2cHok4wqCYu4wgQr/9vhGztzEVNi8NP/VqgIiwWNyBW8edgq1kUuiDxX1jtwQmZOKCNMJTiPzim8a0YgCS29R+9UL5lEzHdwMzFQI8Tinnowl21AvjKbdFAWywtBVQG8E1loolTzM2GHdJRu4MNaE54gNVPg4z6qH09Y4LE2bjEI6VrttTyYWQC65ytPTpdGGvMd0s710nZCn0tEGTrs0LUODHh6hNt6jRGiqL++brPebT7glF/e3ij9431nUTvuuWKcRGz1L1LVS43OWNCRAAAAAElFTkSuQmCC"/>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.3 KiB |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user