console: implement new design for naming convention and edit functionality for Graphql Field Customization

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/4859
Co-authored-by: Rikin Kachhia <54616969+rikinsk@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Hardman <28978422+mattshardman@users.noreply.github.com>
GitOrigin-RevId: ebe25491b90caf9d1de091072727503d666469fe
This commit is contained in:
Varun Choudhary 2022-07-01 18:29:03 +05:30 committed by hasura-bot
parent b9fb7d8720
commit 4a129042fa
16 changed files with 349 additions and 203 deletions

View File

@ -56,7 +56,7 @@
- objects now have a title and, when available, the same description as in the GraphQL schema
- server: fix dropping column from a table that has update permissions (fix #8415)
- console: Hide TimescaleDB internal schema from data tab
- console: support naming convention in source customization for postgres DB [CON-297]
- console: support naming conventions under GraphQL customizations while connecting postgres DB
## v2.8.3

View File

@ -1579,7 +1579,7 @@ code {
}
.connect_form_width {
width: 50%;
width: 66%;
}
/* container height subtracting top header and bottom scroll bar */

View File

@ -137,7 +137,6 @@ const ConnectDatabaseForm: React.FC<ConnectDatabaseFormProps> = ({
changeConnectionType(driverToLabel[value].defaultConnection);
}
};
return (
<>
<div className={styles.connect_form_layout}>
@ -447,15 +446,14 @@ const ConnectDatabaseForm: React.FC<ConnectDatabaseFormProps> = ({
TODO: remove the edit state condition when the BE issue is solved
https://github.com/hasura/graphql-engine-mono/issues/4700
*/}
{!isEditState && (
<GraphQLFieldCustomizationContainer
rootFields={connectionDBState.customization?.rootFields}
typeNames={connectionDBState.customization?.typeNames}
namingConvention={connectionDBState.customization?.namingConvention}
connectionDBStateDispatch={connectionDBStateDispatch}
connectionDBState={connectionDBState}
/>
)}
<GraphQLFieldCustomizationContainer
rootFields={connectionDBState.customization?.rootFields}
typeNames={connectionDBState.customization?.typeNames}
namingConvention={connectionDBState.customization?.namingConvention}
connectionDBStateDispatch={connectionDBStateDispatch}
connectionDBState={connectionDBState}
/>
</div>
<hr className={styles.line_width} />
</>

View File

@ -0,0 +1,34 @@
import React from 'react';
type Props = {
name: string;
label: string;
placeholder: string;
value?: string;
onChange: (value: string) => void;
};
export const FormRow = (props: Props) => {
const { name, label, placeholder, value = '', onChange } = props;
return (
<>
<label
htmlFor={name}
className="font-normal px-sm py-xs text-gray-600 w-1/3"
>
{label}
</label>
<span className="px-sm py-xs">
<input
type="text"
name={name}
aria-label={name}
placeholder={placeholder}
className="form-control font-normal"
defaultValue={value}
onChange={e => onChange(e.target.value)}
/>
</span>
</>
);
};

View File

@ -1,188 +1,84 @@
import { getSupportedDrivers } from '@/dataSources';
import { namingConventionOptions } from '@/metadata/types';
import { Collapse } from '@/new-components/Collapse';
import { ToolTip } from '@/new-components/Tooltip';
import React from 'react';
import { ConnectDBState } from '../state';
import { FormRow } from './FormRow';
import { NamingConvention } from './NamingConvention';
import {
CustomizationFieldName,
GraphQLFieldCustomizationProps,
RootFields,
TypeNames,
} from './types';
type FormRowProps = {
name: string;
label: string;
placeholder: string;
value?: string;
onChange: (value: string) => void;
};
const FormRow: React.FC<FormRowProps> = ({
name,
label,
placeholder,
value = '',
onChange,
}) => (
<>
<label
htmlFor={name}
className="font-normal px-sm py-xs text-gray-600 w-1/3"
>
{label}
</label>
<span className="px-sm py-xs">
<input
type="text"
name={name}
aria-label={name}
placeholder={placeholder}
className="form-control font-normal"
defaultValue={value}
onChange={e => onChange(e.target.value)}
/>
</span>
</>
);
type TypeNamesField = {
id: 'prefix' | 'suffix';
label: string;
placeholder: string;
};
const TypeNames: TypeNamesField[] = [
{ label: 'Prefix', placeholder: 'prefix_', id: 'prefix' },
{ label: 'Suffix', placeholder: '_suffix', id: 'suffix' },
];
type RootFieldsField = Omit<TypeNamesField, 'id'> & {
id: TypeNamesField['id'] | 'namespace';
};
const RootFields: RootFieldsField[] = [
{ label: 'Namespace', placeholder: 'Namespace...', id: 'namespace' },
...TypeNames,
];
export type CustomizationFieldName =
| 'rootFields.namespace'
| 'rootFields.prefix'
| 'rootFields.suffix'
| 'typeNames.prefix'
| 'typeNames.suffix'
| 'namingConvention';
export type GraphQLFieldCustomizationProps = {
rootFields?: {
namespace?: string;
prefix?: string;
suffix?: string;
};
typeNames?: {
prefix?: string;
suffix?: string;
};
namingConvention?: namingConventionOptions;
onChange: (fieldName: CustomizationFieldName, fieldValue: string) => void;
connectionDBState?: ConnectDBState;
};
export const GraphQLFieldCustomization: React.FC<GraphQLFieldCustomizationProps> = ({
export const GraphQLFieldCustomization = ({
rootFields,
typeNames,
namingConvention,
onChange,
namingConvention,
connectionDBState,
}) => {
}: GraphQLFieldCustomizationProps) => {
return (
<div>
<div className="w-full mb-md">
<div>
<div className="cursor-pointer w-full flex-initial align-middle">
<div className="font-semibold">
<Collapse
title="GraphQL Field Customization"
defaultOpen={false}
tooltip="Set a namespace or add a prefix / suffix to the root fields and types for the database's objects in the GraphQL API"
>
<Collapse.Content>
{connectionDBState?.dbType &&
getSupportedDrivers(
'connectDbForm.namingConvention'
).includes(connectionDBState?.dbType) && (
<div className="grid gap-0 grid-cols-2 grid-rows">
<label className="p-sm text-gray-600 font-semibold py-xs w-1/3">
Naming Convention
</label>
<span className="px-sm py-xs">
<select
className="form-control font-normal cursor-pointer"
onChange={e => {
if (namingConvention) {
onChange('namingConvention', e.target.value);
}
}}
value={namingConvention}
>
<option value="hasura-default">
hasura-default
</option>
<option value="graphql-default">
graphql-default
</option>
</select>
</span>
</div>
)}
<div>
<div className="p-sm text-gray-600 font-semibold">
Root Fields
</div>
<div className="mb-md">
<div className="w-full flex-initial align-middle">
<div className="font-semibold">
<Collapse title="GraphQL Field Customization" defaultOpen={false}>
<Collapse.Content>
<NamingConvention
onChange={onChange}
namingConvention={namingConvention}
connectionDBState={connectionDBState}
/>
<div>
<div className="flex items-center p-sm text-gray-600 font-semibold">
Root Fields
<ToolTip message="Set a namespace or add a prefix / suffix to the root fields for the database's objects in the GraphQL API" />
</div>
<form
aria-label="rootFields"
className={`grid gap-0 grid-cols-2 grid-rows-${RootFields.length}`}
>
{RootFields.map(({ label, placeholder, id }) => {
const inputName: CustomizationFieldName = `rootFields.${id}`;
return (
<FormRow
key={id}
name={inputName}
label={label}
placeholder={placeholder}
value={rootFields?.[id]}
onChange={(value: string) =>
onChange(inputName, value)
}
/>
);
})}
</form>
<div>
<div className="p-sm text-gray-600 font-semibold">
Type Names
</div>
</div>
<form
aria-label="rootFields"
className={`grid gap-0 grid-cols-2 grid-rows-${RootFields.length}`}
>
{RootFields.map(({ label, placeholder, id }) => {
const inputName: CustomizationFieldName = `rootFields.${id}`;
return (
<FormRow
key={id}
name={inputName}
label={label}
placeholder={placeholder}
value={rootFields?.[id]}
onChange={(value: string) => onChange(inputName, value)}
/>
);
})}
</form>
<div>
<div className="flex items-center p-sm text-gray-600 font-semibold">
Type Names
<ToolTip message="Add a prefix / suffix to the types for the database's objects in the GraphQL API" />
</div>
<form
aria-label="typeNames"
className={`grid gap-0 grid-cols-2 grid-rows-${TypeNames.length}`}
>
{TypeNames.map(({ label, placeholder, id }) => {
const inputName: CustomizationFieldName = `typeNames.${id}`;
return (
<FormRow
key={id}
name={inputName}
label={label}
placeholder={placeholder}
value={typeNames?.[id]}
onChange={(value: string) =>
onChange(inputName, value)
}
/>
);
})}
</form>
</Collapse.Content>
</Collapse>
</div>
</div>
<form
aria-label="typeNames"
className={`grid gap-0 grid-cols-2 grid-rows-${TypeNames.length}`}
>
{TypeNames.map(({ label, placeholder, id }) => {
const inputName: CustomizationFieldName = `typeNames.${id}`;
return (
<FormRow
key={id}
name={inputName}
label={label}
placeholder={placeholder}
value={typeNames?.[id]}
onChange={(value: string) => onChange(inputName, value)}
/>
);
})}
</form>
</Collapse.Content>
</Collapse>
</div>
</div>
</div>

View File

@ -1,10 +1,10 @@
import React, { Dispatch } from 'react';
import { ConnectDBActions } from '../state';
import { GraphQLFieldCustomization } from './GraphQLFieldCustomization';
import {
CustomizationFieldName,
GraphQLFieldCustomization,
GraphQLFieldCustomizationProps,
} from './GraphQLFieldCustomization';
} from './types';
type GraphQLFieldCustomizationContainerProps = Omit<
GraphQLFieldCustomizationProps,

View File

@ -0,0 +1,102 @@
import React from 'react';
import { NamingConventionOptions } from '@/metadata/types';
import { ToolTip } from '@/new-components/Tooltip';
import { useServerConfig } from '@/hooks';
import { getSupportedDrivers } from '@/dataSources';
import { CardRadioGroup } from '@/new-components/CardRadioGroup';
import { GraphQLFieldCustomizationProps } from './types';
const namingConventionRadioGroupItems: {
value: NamingConventionOptions;
title: string;
body: React.ReactNode;
}[] = [
{
value: 'hasura-default',
title: 'hasura-default',
body: (
<div className="font-thin">
<li>All names will use snake_case</li>
<li> Enum values will not be changed</li>
</div>
),
},
{
value: 'graphql-default',
title: 'graphql-default',
body: (
<div className="font-thin">
<li>
Field names, argument names, and <br />{' '}
<p className="pl-6 pb-0">boolean operators will be camelCased</p>
</li>
<li>Type names will be PascalCased</li>
<li>Enum values will be UPPERCASED</li>
</div>
),
},
];
export const NamingConvention: React.FC<GraphQLFieldCustomizationProps> = ({
onChange,
namingConvention,
connectionDBState,
}) => {
const { data: configData, isLoading, isError } = useServerConfig();
if (isError) {
return <div>Error in fetching server configuration</div>;
}
if (isLoading) {
return <div>Loading...</div>;
}
const isNamingConventionEnabled = configData?.experimental_features.includes(
'naming_convention'
);
const isNamingConventionSupported =
connectionDBState?.dbType &&
getSupportedDrivers('connectDbForm.namingConvention').includes(
connectionDBState?.dbType
);
return (
<div>
{isNamingConventionSupported ? (
<div className="p-sm box-border">
<p className="flex items-center text-gray-600 font-semibold">
Naming Convention
<ToolTip message="Choose a default naming convention for your auto-generated GraphQL schema objects (fields, types, arguments, etc.)" />
<a
href="https://hasura.io/docs/latest/graphql/core/databases/postgres/schema/naming-convention/#set-naming-convention-for-a-particular-source"
target="_blank"
rel="noopener noreferrer"
>
<span className="italic font-thin text-sm pl-1">(Know More)</span>
</a>
</p>
{!isNamingConventionEnabled ? (
<div className="font-thin">
Naming convention is not enabled. To enable naming convention,
start the Hasura server with environment variable
<code>
HASURA_GRAPHQL_EXPERIMENTAL_FEATURES:
&quot;naming_convention&quot;
</code>
</div>
) : (
<span className="p-sm py-8">
<CardRadioGroup
items={namingConventionRadioGroupItems}
onChange={ncType => onChange('namingConvention', ncType)}
value={namingConvention}
/>
</span>
)}
</div>
) : null}
</div>
);
};

View File

@ -0,0 +1,48 @@
import { NamingConventionOptions } from '@/metadata/types';
import { ConnectDBState } from '../state';
export type TypeNamesField = {
id: 'prefix' | 'suffix';
label: string;
placeholder: string;
};
export const TypeNames: TypeNamesField[] = [
{ label: 'Prefix', placeholder: 'prefix_', id: 'prefix' },
{ label: 'Suffix', placeholder: '_suffix', id: 'suffix' },
];
export type RootFieldsField = {
id: 'prefix' | 'suffix' | 'namespace';
label: string;
placeholder: string;
};
export const RootFields: RootFieldsField[] = [
{ label: 'Namespace', placeholder: 'Namespace...', id: 'namespace' },
...TypeNames,
];
export type CustomizationFieldName =
| 'rootFields.namespace'
| 'rootFields.prefix'
| 'rootFields.suffix'
| 'typeNames.prefix'
| 'typeNames.suffix'
| 'namingConvention'
| string;
export type GraphQLFieldCustomizationProps = {
rootFields?: {
namespace?: string;
prefix?: string;
suffix?: string;
};
typeNames?: {
prefix?: string;
suffix?: string;
};
onChange: (fieldName: CustomizationFieldName, fieldValue: string) => void;
connectionDBState?: ConnectDBState;
namingConvention?: NamingConventionOptions;
};

View File

@ -1,10 +1,11 @@
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { fireEvent, screen } from '@testing-library/react';
import { GraphQLFieldCustomization } from '../GraphQLFieldCustomization/GraphQLFieldCustomization';
import { renderWithClient } from '../../../../../hooks/__tests__/common/decorator';
describe('component GraphQLFieldCustomization', () => {
it('renders', () => {
render(<GraphQLFieldCustomization onChange={() => null} />);
renderWithClient(<GraphQLFieldCustomization onChange={() => null} />);
fireEvent.click(screen.getByText('GraphQL Field Customization'));
@ -21,7 +22,7 @@ describe('component GraphQLFieldCustomization', () => {
});
it('passes props', () => {
render(
renderWithClient(
<GraphQLFieldCustomization
rootFields={{
namespace: 'name',
@ -53,7 +54,7 @@ describe('component GraphQLFieldCustomization', () => {
describe('when the user provides the values', () => {
it('calls the on change callback', () => {
const onChange = jest.fn();
render(<GraphQLFieldCustomization onChange={onChange} />);
renderWithClient(<GraphQLFieldCustomization onChange={onChange} />);
fireEvent.click(screen.getByText('GraphQL Field Customization'));

View File

@ -1,5 +1,5 @@
import { CustomizationFieldName } from '../GraphQLFieldCustomization/GraphQLFieldCustomization';
import { getActionType } from '../GraphQLFieldCustomization/GraphQLFieldCustomizationContainer';
import { CustomizationFieldName } from '../GraphQLFieldCustomization/types';
describe('getActionType', () => {
const tests = [

View File

@ -10,7 +10,7 @@ import {
SSLConfigOptions,
IsolationLevelOptions,
GraphQLFieldCustomization,
namingConventionOptions,
NamingConventionOptions,
} from '../../../../metadata/types';
export const connectionTypes = {
@ -260,7 +260,7 @@ export type ConnectDBActions =
| { type: 'RESET_INPUT_STATE' }
| {
type: 'UPDATE_CUSTOMIZATION_NAMING_CONVENTION';
data: namingConventionOptions;
data: NamingConventionOptions;
}
| { type: 'UPDATE_CUSTOMIZATION_ROOT_FIELDS_NAMESPACE'; data: string }
| { type: 'UPDATE_CUSTOMIZATION_ROOT_FIELDS_PREFIX'; data: string }

View File

@ -71,6 +71,23 @@ export const addSource = (
const adaptedCustomizations = adaptCustomizations(payload?.customization);
if (driver === 'mssql') {
if (payload?.replace_configuration) {
return {
type: 'mssql_update_source',
args: {
name: payload.name,
configuration: {
connection_info: {
connection_string: payload.dbUrl,
pool_settings: payload.connection_pool_settings,
},
read_replicas: replicas?.length ? replicas : null,
},
replace_configuration,
...adaptedCustomizations,
},
};
}
return {
type: 'mssql_add_source',
args: {
@ -93,6 +110,22 @@ export const addSource = (
typeof payload.dbUrl === 'string'
? JSON.parse(payload.dbUrl)
: payload.dbUrl;
if (payload?.replace_configuration) {
return {
type: 'bigquery_update_source',
args: {
name: payload.name,
configuration: {
service_account,
global_select_limit: payload.bigQuery.global_select_limit,
project_id: payload.bigQuery.projectId,
datasets: payload.bigQuery.datasets.split(',').map(d => d.trim()),
},
replace_configuration,
...adaptedCustomizations,
},
};
}
return {
type: 'bigquery_add_source',
args: {
@ -109,6 +142,32 @@ export const addSource = (
};
}
if (
(driver === 'postgres' || driver === 'citus') &&
payload?.replace_configuration
) {
return {
type: `${driver === 'postgres' ? 'pg' : 'citus'}_update_source`,
args: {
name: payload.name,
configuration: {
connection_info: {
database_url: payload.connection_parameters
? { connection_parameters: payload.connection_parameters }
: payload.dbUrl,
use_prepared_statements: payload.preparedStatements,
isolation_level: payload.isolationLevel,
pool_settings: payload.connection_pool_settings,
ssl_configuration: payload.sslConfiguration,
},
read_replicas: replicas?.length ? replicas : null,
},
replace_configuration,
...adaptedCustomizations,
},
};
}
return {
type: `${driver === 'postgres' ? 'pg' : 'citus'}_add_source`,
args: {

View File

@ -1010,7 +1010,7 @@ export type IsolationLevelOptions =
| 'repeatable-read'
| 'serializable';
export type namingConventionOptions = 'hasura-default' | 'graphql-default';
export type NamingConventionOptions = 'hasura-default' | 'graphql-default';
export interface SSLConfigOptions {
sslmode?: SSLModeOptions;
@ -1054,7 +1054,7 @@ export type GraphQLFieldCustomization = {
prefix?: string;
suffix?: string;
};
namingConvention?: namingConventionOptions;
namingConvention?: NamingConventionOptions;
};
export interface SourceConnectionInfo {

View File

@ -4,7 +4,7 @@ import React from 'react';
interface CardRadioGroupItem<T> {
value: T;
title: string;
body: string;
body: string | React.ReactNode;
}
interface CardRadioGroupProps<T> {
@ -58,7 +58,7 @@ export const CardRadioGroup = <T extends string = string>(
<label
htmlFor={`radio-select-${iValue}`}
className={clsx(
'mb-sm font-semibold',
'mb-sm font-semibold mt-0.5',
disabled ? 'cursor-not-allowed' : 'cursor-pointer'
)}
>

View File

@ -13,6 +13,7 @@ keywords:
---
import Tabs from '@theme/Tabs';
import Thumbnail from "@site/src/components/Thumbnail";
import TabItem from '@theme/TabItem';
import GraphiQLIDE from '@site/src/components/GraphiQLIDE';
@ -153,19 +154,26 @@ For the above schema, a sample GraphQL query will look like the following with t
`}
/>
## Set default naming convention for all sources {#pg-default-naming-convention}
## Set default naming convention for all database sources {#pg-default-naming-convention}
For setting the default naming convention for all sources, set the environment variable
For setting the default naming convention for all database sources, set the environment variable
`HASURA_GRAPHQL_DEFAULT_NAMING_CONVENTION` to one of `hasura-default` or `graphql-default`.
This means any database source will follow this naming convention unless explicitly set to something else.
## Set naming convention for a particular source {#pg-source-naming-convention}
## Set naming convention for a particular database source {#pg-source-naming-convention}
<Tabs className="api-tabs">
<TabItem value="console" label="Console">
Console support will be added soon.
Currently setting the database naming convention is only allowed at the time of connecting your database.
Head to the `Data -> Manage -> Connect Database` page. Under the `GraphQL Field Customization` section you can
choose the naming convention of your choice.
<Thumbnail
src="/img/graphql/core/schema/naming-convention.png"
alt="Naming Convention"
/>
</TabItem>
<TabItem value="cli" label="CLI">

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB