mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-20 06:58:39 +03:00
console: add GraphQL customisation under Remote schema edit tab
https://github.com/hasura/graphql-engine-mono/pull/2302 GitOrigin-RevId: 60e420298f568779b2732a3fd90388f8adefa599
This commit is contained in:
parent
21254256a1
commit
11d66753be
@ -20,6 +20,7 @@
|
||||
- server: `introspect_remote_schema` API now returns original remote schema instead of customized schema
|
||||
- server: prevent empty subscription roots in the schema (#6898)
|
||||
- console: support tracking of functions with return a single row
|
||||
- console: add GraphQL customisation under Remote schema edit tab
|
||||
|
||||
## v2.0.9
|
||||
|
||||
|
105
console/cypress/integration/remote-schemas/edit-schema/spec.ts
Normal file
105
console/cypress/integration/remote-schemas/edit-schema/spec.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import { getElementFromAlias } from '../../../helpers/eventHelpers';
|
||||
|
||||
type CustomizationSettingsType = {
|
||||
root_fields_namespace: string;
|
||||
type_names: {
|
||||
prefix: string;
|
||||
suffix: string;
|
||||
mapping: Record<string, string>;
|
||||
};
|
||||
field_names: {
|
||||
parent_type: string;
|
||||
prefix: string;
|
||||
suffix: string;
|
||||
mapping: Record<string, string>;
|
||||
}[];
|
||||
};
|
||||
export const modifyCustomization = (
|
||||
customizationSettings: CustomizationSettingsType | undefined
|
||||
) => {
|
||||
cy.get(getElementFromAlias('remote-schema-edit-modify-btn'))
|
||||
.should('exist')
|
||||
.click();
|
||||
cy.get(getElementFromAlias('remote-schema-customization-editor-expand-btn'))
|
||||
.should('exist')
|
||||
.click();
|
||||
|
||||
// add root field name
|
||||
cy.get(getElementFromAlias('remote-schema-customization-root-field-input'))
|
||||
.clear()
|
||||
.type(customizationSettings?.root_fields_namespace || '');
|
||||
|
||||
cy.get(
|
||||
getElementFromAlias('remote-schema-customization-type-name-prefix-input')
|
||||
)
|
||||
.clear()
|
||||
.type(customizationSettings?.type_names.prefix || '');
|
||||
|
||||
cy.get(
|
||||
getElementFromAlias('remote-schema-customization-type-name-suffix-input')
|
||||
)
|
||||
.clear()
|
||||
.type(customizationSettings?.type_names.suffix || '');
|
||||
|
||||
// add type name
|
||||
let key = Object.keys(customizationSettings?.type_names?.mapping || {})[0];
|
||||
cy.get(
|
||||
getElementFromAlias('remote-schema-customization-type-name-lhs-input')
|
||||
).select(key);
|
||||
cy.get(
|
||||
getElementFromAlias('remote-schema-customization-type-name-0-rhs-input')
|
||||
)
|
||||
.clear()
|
||||
.type(customizationSettings?.type_names?.mapping[key] || '');
|
||||
|
||||
cy.get(getElementFromAlias('remote-schema-editor')).should('exist').click();
|
||||
|
||||
cy.get(getElementFromAlias('remote-schema-customization-open-field-mapping'))
|
||||
.should('exist')
|
||||
.click();
|
||||
|
||||
// click the field mapping button
|
||||
cy.get(
|
||||
getElementFromAlias(
|
||||
'remote-schema-customization-field-type-parent-type-input'
|
||||
)
|
||||
).select(customizationSettings?.field_names[0].parent_type || '');
|
||||
|
||||
cy.get(
|
||||
getElementFromAlias(
|
||||
'remote-schema-customization-field-type-field-prefix-input'
|
||||
)
|
||||
)
|
||||
.clear()
|
||||
.type(customizationSettings?.field_names[0].prefix || '');
|
||||
|
||||
cy.get(
|
||||
getElementFromAlias(
|
||||
'remote-schema-customization-field-type-field-suffix-input'
|
||||
)
|
||||
)
|
||||
.clear()
|
||||
.type(customizationSettings?.field_names[0].suffix || '');
|
||||
|
||||
// remote-schema-customization-field-type-lhs-input
|
||||
key = Object.keys(customizationSettings?.field_names[0].mapping || {})[0];
|
||||
cy.get(
|
||||
getElementFromAlias('remote-schema-customization-field-type-lhs-input')
|
||||
).select(key);
|
||||
// remote-schema-customization-field-type-rhs-input
|
||||
cy.get(
|
||||
getElementFromAlias('remote-schema-customization-field-type-0-rhs-input')
|
||||
)
|
||||
.clear()
|
||||
.type(customizationSettings?.field_names[0].mapping[key] || '');
|
||||
|
||||
cy.get(getElementFromAlias('remote-schema-editor')).should('exist').click();
|
||||
|
||||
cy.get(getElementFromAlias('add-field-customization'))
|
||||
.should('exist')
|
||||
.click();
|
||||
|
||||
cy.get(getElementFromAlias('remote-schema-edit-save-btn'))
|
||||
.should('exist')
|
||||
.click();
|
||||
};
|
107
console/cypress/integration/remote-schemas/edit-schema/test.ts
Normal file
107
console/cypress/integration/remote-schemas/edit-schema/test.ts
Normal file
@ -0,0 +1,107 @@
|
||||
/* eslint no-unused-vars: 0 */
|
||||
/* eslint import/prefer-default-export: 0 */
|
||||
import { testMode } from '../../../helpers/common';
|
||||
import { expectNotif } from '../../data/manage-database/common.spec';
|
||||
import { setMetaData } from '../../validators/validators';
|
||||
import { modifyCustomization } from './spec';
|
||||
|
||||
// const visitRoute = () => {
|
||||
// describe('Setup route', () => {
|
||||
// it('Visit the index route', () => {
|
||||
// // Visit the index route
|
||||
// cy.visit('/remote-schemas/manage/schemas');
|
||||
// // Get and set validation metadata
|
||||
// setMetaData();
|
||||
// });
|
||||
// });
|
||||
// };
|
||||
|
||||
const createRemoteSchema = (remoteSchemaName: string) => {
|
||||
const postBody = {
|
||||
type: 'add_remote_schema',
|
||||
args: {
|
||||
name: remoteSchemaName,
|
||||
definition: {
|
||||
url: 'https://graphql-pokemon2.vercel.app',
|
||||
forward_client_headers: true,
|
||||
timeout_seconds: 60,
|
||||
},
|
||||
},
|
||||
};
|
||||
cy.request('POST', 'http://localhost:8080/v1/metadata', postBody).then(
|
||||
response => {
|
||||
expect(response.body).to.have.property('message', 'success'); // true
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const removeRemoteSchema = (remoteSchemaName: string) => {
|
||||
const postBody = {
|
||||
type: 'remove_remote_schema',
|
||||
args: {
|
||||
name: remoteSchemaName,
|
||||
},
|
||||
};
|
||||
cy.request('POST', 'http://localhost:8080/v1/metadata', postBody).then(
|
||||
response => {
|
||||
expect(response.body).to.have.property('message', 'success'); // true
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const editSchemaTests = () => {
|
||||
describe('Modify an existing remote schema', () => {
|
||||
describe('Create a remote schema for testing', () => {
|
||||
it('add a remote schema via the API', () => {
|
||||
createRemoteSchema('test_remote_schema');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edit the remote schema settings', () => {
|
||||
it('Visit the modify page', () => {
|
||||
cy.visit('/remote-schemas/manage/test_remote_schema/modify');
|
||||
setMetaData();
|
||||
});
|
||||
|
||||
it('Modify the remote schema settings', () => {
|
||||
modifyCustomization({
|
||||
root_fields_namespace: 'test_root_namespace',
|
||||
type_names: {
|
||||
prefix: 'test_prefix',
|
||||
suffix: 'test_suffix',
|
||||
mapping: {
|
||||
Pokemon: 'renamed_type_name_mapping',
|
||||
},
|
||||
},
|
||||
field_names: [
|
||||
{
|
||||
parent_type: 'PokemonDimension',
|
||||
prefix: 'test_parent_type_prefix',
|
||||
suffix: 'test_parent_type_suffix',
|
||||
mapping: {
|
||||
minimum: 'test_field_name',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('expect success notification', () => {
|
||||
expectNotif('success', {
|
||||
title: 'Remote schema modified',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Remove remote schema', () => {
|
||||
it('Remove the remote schema via the API', () => {
|
||||
removeRemoteSchema('test_remote_schema');
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (testMode !== 'cli') {
|
||||
// setup();
|
||||
editSchemaTests();
|
||||
}
|
@ -21,6 +21,7 @@ const ENV_URL_CHANGED = '@addRemoteSchema/ENV_URL_CHANGED';
|
||||
const NAME_CHANGED = '@addRemoteSchema/NAME_CHANGED';
|
||||
const TIMEOUT_CONF_CHANGED = '@addRemoteSchema/TIMEOUT_CONF_CHANGED';
|
||||
const COMMENT_CHANGED = '@addRemoteSchema/COMMENT_CHANGED';
|
||||
const CUSTOMIZATION_CHANGED = '@addRemoteSchema/CUSTOMIZATION_CHANGED';
|
||||
// const HEADER_CHANGED = '@addRemoteSchema/HEADER_CHANGED';
|
||||
const ADDING_REMOTE_SCHEMA = '@addRemoteSchema/ADDING_REMOTE_SCHEMA';
|
||||
const ADD_REMOTE_SCHEMA_FAIL = '@addRemoteSchema/ADD_REMOTE_SCHEMA_FAIL';
|
||||
@ -48,6 +49,7 @@ const inputEventMap = {
|
||||
manualUrl: MANUAL_URL_CHANGED,
|
||||
timeoutConf: TIMEOUT_CONF_CHANGED,
|
||||
comment: COMMENT_CHANGED,
|
||||
customization: CUSTOMIZATION_CHANGED,
|
||||
};
|
||||
|
||||
/* Action creators */
|
||||
@ -251,6 +253,7 @@ const modifyRemoteSchema = () => (dispatch, getState) => {
|
||||
timeout_seconds: timeoutSeconds,
|
||||
forward_client_headers: currState.forwardClientHeaders,
|
||||
headers: getReqHeader(getState().remoteSchemas.headerData.headers),
|
||||
customization: currState.customization,
|
||||
};
|
||||
const remoteSchemaComment = currState?.comment;
|
||||
|
||||
@ -281,6 +284,7 @@ const modifyRemoteSchema = () => (dispatch, getState) => {
|
||||
timeout_seconds: oldTimeout,
|
||||
headers: currState.editState.originalHeaders,
|
||||
forward_client_headers: currState.editState.originalForwardClientHeaders,
|
||||
currState: currState.editState.oldCustomization,
|
||||
};
|
||||
|
||||
if (!currState.editState.originalUrl) {
|
||||
@ -360,6 +364,11 @@ const addRemoteSchemaReducer = (state = addState, action) => {
|
||||
...state,
|
||||
comment: action.data,
|
||||
};
|
||||
case CUSTOMIZATION_CHANGED:
|
||||
return {
|
||||
...state,
|
||||
customization: action.data,
|
||||
};
|
||||
case ADDING_REMOTE_SCHEMA:
|
||||
return {
|
||||
...state,
|
||||
@ -404,6 +413,7 @@ const addRemoteSchemaReducer = (state = addState, action) => {
|
||||
: '60',
|
||||
forwardClientHeaders: action.data.definition.forward_client_headers,
|
||||
comment: action.data?.comment || '',
|
||||
customization: action.data.definition?.customization,
|
||||
editState: {
|
||||
...state,
|
||||
isModify: false,
|
||||
@ -414,6 +424,7 @@ const addRemoteSchemaReducer = (state = addState, action) => {
|
||||
originalForwardClientHeaders:
|
||||
action.data.definition.forward_client_headers || false,
|
||||
originalComment: action.data?.comment || '',
|
||||
originalCustomization: action.data.definition?.customization,
|
||||
},
|
||||
isFetching: false,
|
||||
isFetchError: null,
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
} from '../Add/addRemoteSchemaReducer';
|
||||
|
||||
import CommonHeader from '../../../Common/Layout/ReusableHeader/Header';
|
||||
import GraphQLCustomizationEdit from './GraphQLCustomization/GraphQLCustomizationEdit';
|
||||
|
||||
class Common extends React.Component {
|
||||
getPlaceHolderText(valType) {
|
||||
@ -24,6 +25,10 @@ class Common extends React.Component {
|
||||
this.props.dispatch(inputChange(fieldName, e.target.value));
|
||||
}
|
||||
|
||||
handleCustomizationInputChange(updateValue) {
|
||||
this.props.dispatch(inputChange('customization', updateValue));
|
||||
}
|
||||
|
||||
toggleUrlParam(e) {
|
||||
const field = e.target.getAttribute('value');
|
||||
this.props.dispatch(inputChange(field, ''));
|
||||
@ -44,7 +49,9 @@ class Common extends React.Component {
|
||||
forwardClientHeaders,
|
||||
comment,
|
||||
isNew = false,
|
||||
customization,
|
||||
} = this.props;
|
||||
|
||||
const { isModify } = this.props.editState;
|
||||
|
||||
const isDisabled = !isNew && !isModify;
|
||||
@ -252,6 +259,32 @@ class Common extends React.Component {
|
||||
data-test="remote-schema-comment"
|
||||
/>
|
||||
</label>
|
||||
<hr className="my-lg" />
|
||||
{/* <GraphQLCustomization mode="edit" customization={customization} dispatch={this.props.dispatch} /> */}
|
||||
{isNew ? null : (
|
||||
<>
|
||||
<div className="text-lg font-bold">
|
||||
GraphQL Customizations{' '}
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={
|
||||
<Tooltip id="tooltip-cascade">
|
||||
Individual Types and Fields will be editable after saving.
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<i className="fa fa-question-circle" aria-hidden="true" />
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
<GraphQLCustomizationEdit
|
||||
remoteSchemaName={name}
|
||||
graphQLCustomization={customization}
|
||||
dispatch={this.props.dispatch}
|
||||
onChange={this.handleCustomizationInputChange.bind(this)}
|
||||
isDisabled={isDisabled}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -0,0 +1,181 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { OverlayTrigger } from 'react-bootstrap';
|
||||
import Tooltip from 'react-bootstrap/lib/Tooltip';
|
||||
import styles from '../../RemoteSchema.scss';
|
||||
import { Button } from '../../../../Common';
|
||||
import TypeMapping from './TypeMapping';
|
||||
|
||||
type FieldNameType = {
|
||||
parentType?: string;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
mapping?: { type: string; custom_name: string }[];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
types: { typeName: string; fields: string[] }[];
|
||||
fieldName: FieldNameType;
|
||||
mode: 'edit' | 'create';
|
||||
onChange: (updateFieldName: FieldNameType) => void;
|
||||
onDelete?: () => void;
|
||||
onSave?: () => void;
|
||||
onClose?: () => void;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
const tooltip = (
|
||||
<Tooltip id="tooltip-cascade">
|
||||
Field remapping takes precedence to prefixes and suffixes.
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
const SelectOne = ({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
}: {
|
||||
options: string[];
|
||||
value: string | undefined;
|
||||
onChange: (e: any) => void;
|
||||
label?: string;
|
||||
}) => (
|
||||
<select
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="form-control"
|
||||
data-test={label}
|
||||
>
|
||||
<option value="">Select Type ...</option>
|
||||
{options.map((op, i) => (
|
||||
<option value={op} key={i}>
|
||||
{op}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
|
||||
const FieldNames = ({
|
||||
types,
|
||||
fieldName,
|
||||
mode,
|
||||
onChange,
|
||||
onDelete,
|
||||
onSave,
|
||||
onClose,
|
||||
label,
|
||||
}: Props) => {
|
||||
const [fieldNameInput, setFieldNameInput] = useState<
|
||||
FieldNameType | undefined
|
||||
>(undefined);
|
||||
useEffect(() => {
|
||||
setFieldNameInput(fieldName);
|
||||
}, [fieldName]);
|
||||
|
||||
return (
|
||||
<div className={styles.CustomEditor}>
|
||||
{mode === 'edit' ? null : (
|
||||
<div>
|
||||
<Button size="xs" onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center mt-md">
|
||||
<label className="w-1/3">Parent Type</label>
|
||||
<div className="w-2/3">
|
||||
<SelectOne
|
||||
options={types.map(v => v.typeName)}
|
||||
value={fieldNameInput?.parentType}
|
||||
onChange={e => {
|
||||
onChange({
|
||||
...fieldNameInput,
|
||||
parentType: e.target.value,
|
||||
});
|
||||
}}
|
||||
label={`remote-schema-customization-${label}-parent-type-input`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center mt-md">
|
||||
<label className="w-1/3">Field Prefix</label>
|
||||
<div className="w-2/3">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="prefix_"
|
||||
value={fieldNameInput?.prefix}
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...fieldNameInput,
|
||||
prefix: e.target.value,
|
||||
})
|
||||
}
|
||||
data-test={`remote-schema-customization-${label}-field-prefix-input`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center mt-md">
|
||||
<label className="w-1/3">Field Suffix</label>
|
||||
<div className="w-2/3">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="_suffix"
|
||||
value={fieldNameInput?.suffix}
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...fieldNameInput,
|
||||
suffix: e.target.value,
|
||||
})
|
||||
}
|
||||
data-test={`remote-schema-customization-${label}-field-suffix-input`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-lg font-bold mt-md">
|
||||
Remap Field Names{' '}
|
||||
<OverlayTrigger placement="right" overlay={tooltip}>
|
||||
<i className="fa fa-question-circle" aria-hidden="true" />
|
||||
</OverlayTrigger>
|
||||
<TypeMapping
|
||||
types={
|
||||
types.find(x => x.typeName === fieldNameInput?.parentType)
|
||||
?.fields || []
|
||||
}
|
||||
typeMappings={fieldNameInput?.mapping || []}
|
||||
onChange={updatedMaps =>
|
||||
onChange({
|
||||
...fieldNameInput,
|
||||
mapping: updatedMaps,
|
||||
})
|
||||
}
|
||||
label={label}
|
||||
/>
|
||||
</div>
|
||||
{mode === 'edit' ? (
|
||||
<div className="mt-md flex justify-end">
|
||||
<Button color="red" size="sm" onClick={onDelete}>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-md">
|
||||
<Button
|
||||
size="sm"
|
||||
color="yellow"
|
||||
onClick={onSave}
|
||||
data-test="add-field-customization"
|
||||
>
|
||||
Add Field Customization
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FieldNames;
|
@ -0,0 +1,335 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { OverlayTrigger } from 'react-bootstrap';
|
||||
import Tooltip from 'react-bootstrap/lib/Tooltip';
|
||||
import { GraphQLSchema } from 'graphql';
|
||||
|
||||
import styles from '../../RemoteSchema.scss';
|
||||
import { Button } from '../../../../Common';
|
||||
import { graphQLCustomization as GType } from '../../types';
|
||||
import TypeMapping from './TypeMapping';
|
||||
import FieldNames from './FieldNames';
|
||||
import { useIntrospectionSchemaRemote } from '../../graphqlUtils';
|
||||
import globals from '../../../../../Globals';
|
||||
import { Dispatch } from '../../../../../types';
|
||||
import { checkDefaultGQLScalarType } from '../../Permissions/utils';
|
||||
|
||||
type Props = {
|
||||
graphQLCustomization: GType;
|
||||
onChange: (updatedGraphQLCustomization: GType) => void;
|
||||
remoteSchemaName: string;
|
||||
dispatch: Dispatch;
|
||||
isDisabled: boolean;
|
||||
};
|
||||
|
||||
type TypeNamesType = {
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
mapping?: { type: string; custom_name: string }[];
|
||||
};
|
||||
|
||||
type FieldNamesType = { parentType?: string } & TypeNamesType;
|
||||
|
||||
const convertToMapping = (
|
||||
values: { type: string; custom_name: string }[]
|
||||
): Record<string, string> => {
|
||||
const obj: Record<string, string> = {};
|
||||
values.forEach(v => {
|
||||
obj[v.type] = v.custom_name;
|
||||
});
|
||||
return obj;
|
||||
};
|
||||
|
||||
const GraphQLCustomizationEdit = ({
|
||||
graphQLCustomization,
|
||||
remoteSchemaName,
|
||||
dispatch,
|
||||
onChange,
|
||||
isDisabled,
|
||||
}: Props) => {
|
||||
const res = useIntrospectionSchemaRemote(
|
||||
remoteSchemaName,
|
||||
{
|
||||
'x-hasura-admin-secret': globals.adminSecret,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
const schema = res.schema as GraphQLSchema | null;
|
||||
|
||||
const [types, setTypes] = useState<
|
||||
{
|
||||
typeName: string;
|
||||
fields: string[];
|
||||
}[]
|
||||
>([]);
|
||||
const { error } = res;
|
||||
|
||||
useEffect(() => {
|
||||
setTypes(
|
||||
Object.entries(schema?.getTypeMap() || {})
|
||||
.map(([typeName, x]) => ({
|
||||
typeName,
|
||||
// eslint-disable-next-line no-underscore-dangle
|
||||
fields: Object.keys((x as any)._fields || {}),
|
||||
}))
|
||||
.filter(({ typeName }) => !checkDefaultGQLScalarType(typeName))
|
||||
);
|
||||
}, [schema]);
|
||||
|
||||
const [openEditor, setOpenEditor] = useState(false);
|
||||
|
||||
const [rootFieldNamespace, setRootFieldNamespace] = useState<
|
||||
string | undefined
|
||||
>(undefined);
|
||||
const [typeNames, setTypesNames] = useState<undefined | TypeNamesType>(
|
||||
undefined
|
||||
);
|
||||
const [fieldNames, setFieldNames] = useState<undefined | FieldNamesType[]>(
|
||||
undefined
|
||||
);
|
||||
const [showFieldCustomizationBtn, updateShowFieldCustomizationBtn] = useState(
|
||||
true
|
||||
);
|
||||
const [tempFieldName, setTempFieldName] = useState<
|
||||
FieldNamesType | undefined
|
||||
>(undefined);
|
||||
useEffect(() => {
|
||||
setRootFieldNamespace(graphQLCustomization?.root_fields_namespace);
|
||||
}, [graphQLCustomization?.root_fields_namespace]);
|
||||
|
||||
useEffect(() => {
|
||||
setTypesNames({
|
||||
prefix: graphQLCustomization?.type_names?.prefix,
|
||||
suffix: graphQLCustomization?.type_names?.suffix,
|
||||
mapping: Object.entries(
|
||||
graphQLCustomization?.type_names?.mapping || {}
|
||||
).map(([type, custom_name]) => ({
|
||||
type,
|
||||
custom_name,
|
||||
})),
|
||||
});
|
||||
}, [graphQLCustomization?.type_names]);
|
||||
|
||||
useEffect(() => {
|
||||
if (graphQLCustomization?.field_names)
|
||||
setFieldNames(
|
||||
graphQLCustomization?.field_names.map(fieldName => {
|
||||
return {
|
||||
parentType: fieldName.parent_type,
|
||||
prefix: fieldName.prefix,
|
||||
suffix: fieldName.suffix,
|
||||
mapping: Object.entries(fieldName.mapping || {}).map(
|
||||
([type, custom_name]) => ({
|
||||
type,
|
||||
custom_name,
|
||||
})
|
||||
),
|
||||
};
|
||||
})
|
||||
);
|
||||
}, [graphQLCustomization?.field_names]);
|
||||
|
||||
if (error) return <div>Something went wrong with schema introspection</div>;
|
||||
return (
|
||||
<>
|
||||
<br />
|
||||
{!openEditor ? (
|
||||
<Button
|
||||
size="xs"
|
||||
className="mt-md"
|
||||
onClick={() => setOpenEditor(true)}
|
||||
disabled={isDisabled}
|
||||
data-test="remote-schema-customization-editor-expand-btn"
|
||||
>
|
||||
{!graphQLCustomization ? 'Add' : 'Edit'}
|
||||
</Button>
|
||||
) : (
|
||||
<div className={styles.CustomEditor} data-test="remote-schema-editor">
|
||||
<Button size="xs" onClick={() => setOpenEditor(false)}>
|
||||
close
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center mt-md">
|
||||
<label className="w-1/3">Root Field Namespace</label>
|
||||
<div className="w-2/3">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="namespace_"
|
||||
value={rootFieldNamespace}
|
||||
data-test="remote-schema-customization-root-field-input"
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...graphQLCustomization,
|
||||
root_fields_namespace: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-lg font-bold mt-md">Type Names</div>
|
||||
|
||||
<div className="flex items-center mt-md">
|
||||
<label className="w-1/3">Prefix</label>
|
||||
<div className="w-2/3">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="prefix_"
|
||||
value={typeNames?.prefix}
|
||||
data-test="remote-schema-customization-type-name-prefix-input"
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...graphQLCustomization,
|
||||
type_names: {
|
||||
...graphQLCustomization?.type_names,
|
||||
prefix: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center mt-md">
|
||||
<label className="w-1/3">Suffix</label>
|
||||
<div className="w-2/3">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="_suffix"
|
||||
value={typeNames?.suffix}
|
||||
data-test="remote-schema-customization-type-name-suffix-input"
|
||||
onChange={e =>
|
||||
onChange({
|
||||
...graphQLCustomization,
|
||||
type_names: {
|
||||
...graphQLCustomization?.type_names,
|
||||
suffix: e.target.value,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-lg font-bold mt-md">
|
||||
Rename Type Names{' '}
|
||||
<OverlayTrigger
|
||||
placement="right"
|
||||
overlay={
|
||||
<Tooltip id="tooltip-cascade">
|
||||
Type remapping takes precedence to prefixes and suffixes.
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<i className="fa fa-question-circle" aria-hidden="true" />
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
|
||||
<TypeMapping
|
||||
onChange={updatedMaps =>
|
||||
onChange({
|
||||
...graphQLCustomization,
|
||||
type_names: {
|
||||
...graphQLCustomization?.type_names,
|
||||
mapping: convertToMapping(updatedMaps),
|
||||
},
|
||||
})
|
||||
}
|
||||
types={types.map(v => v.typeName)}
|
||||
typeMappings={typeNames?.mapping ?? []}
|
||||
label="type-name"
|
||||
/>
|
||||
|
||||
{/* Existing Field Names */}
|
||||
<div className="text-lg font-bold mt-md">Field Names</div>
|
||||
|
||||
{(fieldNames ?? []).map((fm, i) => (
|
||||
<FieldNames
|
||||
mode="edit"
|
||||
types={types.filter(
|
||||
x =>
|
||||
!fieldNames
|
||||
?.filter(v => v.parentType !== fm.parentType)
|
||||
.map(v => v.parentType)
|
||||
.includes(x.typeName)
|
||||
)}
|
||||
fieldName={fm}
|
||||
onChange={updatedFieldName =>
|
||||
onChange({
|
||||
...graphQLCustomization,
|
||||
field_names: graphQLCustomization.field_names?.map((x, j) => {
|
||||
if (i !== j) return x;
|
||||
return {
|
||||
parent_type: updatedFieldName.parentType,
|
||||
prefix: updatedFieldName.prefix,
|
||||
suffix: updatedFieldName.suffix,
|
||||
mapping: convertToMapping(updatedFieldName.mapping || []),
|
||||
};
|
||||
}),
|
||||
})
|
||||
}
|
||||
onDelete={() => {
|
||||
onChange({
|
||||
...graphQLCustomization,
|
||||
field_names: graphQLCustomization.field_names?.filter(
|
||||
(x, j) => i !== j
|
||||
),
|
||||
});
|
||||
}}
|
||||
label={`field-type-${i}`}
|
||||
/>
|
||||
))}
|
||||
|
||||
{/* Adding a new field name */}
|
||||
{showFieldCustomizationBtn ? (
|
||||
<Button
|
||||
size="xs"
|
||||
className="mt-md"
|
||||
onClick={() => updateShowFieldCustomizationBtn(false)}
|
||||
data-test="remote-schema-customization-open-field-mapping"
|
||||
>
|
||||
Add Field Mapping
|
||||
</Button>
|
||||
) : (
|
||||
<FieldNames
|
||||
mode="create"
|
||||
types={types.filter(
|
||||
x => !fieldNames?.map(v => v.parentType).includes(x.typeName)
|
||||
)}
|
||||
fieldName={tempFieldName || {}}
|
||||
onChange={updatedFieldName => {
|
||||
setTempFieldName(updatedFieldName);
|
||||
}}
|
||||
onClose={() => {
|
||||
updateShowFieldCustomizationBtn(true);
|
||||
setTempFieldName(undefined);
|
||||
}}
|
||||
onSave={() => {
|
||||
const newObj = {
|
||||
parent_type: tempFieldName?.parentType,
|
||||
prefix: tempFieldName?.prefix,
|
||||
suffix: tempFieldName?.suffix,
|
||||
mapping: convertToMapping(tempFieldName?.mapping || []),
|
||||
};
|
||||
onChange({
|
||||
...graphQLCustomization,
|
||||
field_names: [
|
||||
...(graphQLCustomization?.field_names || []),
|
||||
newObj,
|
||||
],
|
||||
});
|
||||
updateShowFieldCustomizationBtn(true);
|
||||
setTempFieldName(undefined);
|
||||
}}
|
||||
label="field-type"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default GraphQLCustomizationEdit;
|
@ -0,0 +1,149 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styles from '../../RemoteSchema.scss';
|
||||
|
||||
type TypeMap = { type: string; custom_name: string };
|
||||
|
||||
type Props = {
|
||||
types: string[];
|
||||
typeMappings: TypeMap[];
|
||||
onChange: (updatedMaps: TypeMap[]) => void;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
const SelectOne = ({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
label,
|
||||
}: {
|
||||
options: Props['types'];
|
||||
value: string;
|
||||
onChange: (e: any) => void;
|
||||
label?: string;
|
||||
}) => (
|
||||
<select
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
className="form-control font-normal"
|
||||
data-test={`remote-schema-customization-${label}-lhs-input`}
|
||||
>
|
||||
<option value="" className="text-base">
|
||||
Select Type ...
|
||||
</option>
|
||||
{options.map((op, i) => (
|
||||
<option value={op} key={i}>
|
||||
{op}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
);
|
||||
|
||||
const TypeMapping = ({ types, typeMappings, onChange, label }: Props) => {
|
||||
const [existingMaps, updateExistingMaps] = useState(typeMappings);
|
||||
const [newMap, setNewMap] = useState<TypeMap>({ type: '', custom_name: '' });
|
||||
|
||||
const onModifyItem = (index: number, newVal: TypeMap) => {
|
||||
const updatedMaps = existingMaps;
|
||||
updatedMaps[index] = newVal;
|
||||
onChange(updatedMaps);
|
||||
};
|
||||
|
||||
const onAddItem = (inputMap: TypeMap) => {
|
||||
onChange([...existingMaps, inputMap]);
|
||||
setNewMap({ type: '', custom_name: '' });
|
||||
};
|
||||
|
||||
const onDeleteItem = (index: number) => {
|
||||
onChange(existingMaps.filter((t, i) => i !== index));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateExistingMaps(typeMappings);
|
||||
}, [typeMappings]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{existingMaps.map(({ type, custom_name }, i) => {
|
||||
return (
|
||||
<div className="flex items-center mt-md" key={i}>
|
||||
<label className="w-2/3">
|
||||
<SelectOne
|
||||
options={[
|
||||
...types.filter(
|
||||
t => !existingMaps.map(x => x.type).includes(t)
|
||||
),
|
||||
type,
|
||||
]}
|
||||
value={type}
|
||||
key={i}
|
||||
onChange={e => {
|
||||
onModifyItem(i, {
|
||||
type: e.target.value,
|
||||
custom_name,
|
||||
});
|
||||
}}
|
||||
label={`${label ?? 'no-value'}-${i}`}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<span className="pl-md pr-md"> : </span>
|
||||
<div className="w-2/3">
|
||||
<input
|
||||
type="text"
|
||||
value={custom_name}
|
||||
className="form-control font-normal"
|
||||
onChange={e => {
|
||||
onModifyItem(i, { type, custom_name: e.target.value });
|
||||
}}
|
||||
data-test={`remote-schema-customization-${
|
||||
label ?? 'no-value'
|
||||
}-${i}-rhs-input`}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<i
|
||||
className={`${styles.fontAwosomeClose} fa-lg fa fa-times`}
|
||||
data-test={`remove-type-map-${i}`}
|
||||
onClick={() => onDeleteItem(i)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="flex items-center mt-md" style={{ maxWidth: '96%' }}>
|
||||
<label className="w-2/3">
|
||||
<SelectOne
|
||||
options={types.filter(
|
||||
t => !existingMaps.map(x => x.type).includes(t)
|
||||
)}
|
||||
label={`${label ?? 'no-value'}`}
|
||||
value={newMap.type}
|
||||
onChange={e => {
|
||||
setNewMap({ ...newMap, type: e.target.value });
|
||||
onAddItem({ ...newMap, type: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<span className="pl-md pr-md"> : </span>
|
||||
<div className="w-2/3">
|
||||
<input
|
||||
type="text"
|
||||
value={newMap.custom_name}
|
||||
className="form-control font-normal"
|
||||
data-test={`remote-schema-customization-${
|
||||
label ?? 'no-value'
|
||||
}-rhs-input`}
|
||||
onChange={e =>
|
||||
setNewMap({ ...newMap, custom_name: e.target.value })
|
||||
}
|
||||
// onBlur={() => {
|
||||
// onAddItem(newMap);
|
||||
// }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TypeMapping;
|
@ -512,7 +512,7 @@ const isEnumType = (type: GraphQLInputType): boolean => {
|
||||
};
|
||||
|
||||
// Check if type belongs to default gql scalar types
|
||||
const checkDefaultGQLScalarType = (typeName: string): boolean => {
|
||||
export const checkDefaultGQLScalarType = (typeName: string): boolean => {
|
||||
const gqlDefaultTypes = ['Boolean', 'Float', 'String', 'Int', 'ID'];
|
||||
if (gqlDefaultTypes.indexOf(typeName) > -1) return true;
|
||||
return false;
|
||||
|
@ -318,3 +318,18 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.CustomEditor {
|
||||
padding: 15px;
|
||||
background-color: white;
|
||||
border: 1px solid #ccc;
|
||||
max-width: 700px;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.fontAwosomeClose {
|
||||
margin-left: 10px;
|
||||
width: 15px;
|
||||
min-width: 15px;
|
||||
cursor: pointer;
|
||||
}
|
@ -23,6 +23,7 @@ const addState: AddState = {
|
||||
name: '',
|
||||
forwardClientHeaders: false,
|
||||
comment: '',
|
||||
customization: undefined,
|
||||
...asyncState,
|
||||
editState: {
|
||||
id: -1,
|
||||
@ -34,6 +35,7 @@ const addState: AddState = {
|
||||
originalTimeoutConf: '',
|
||||
originalForwardClientHeaders: false,
|
||||
originalComment: '',
|
||||
originalCustomization: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -18,6 +18,21 @@ export type AsyncState = {
|
||||
isFetchError: any;
|
||||
};
|
||||
|
||||
export type graphQLCustomization = {
|
||||
root_fields_namespace?: string;
|
||||
type_names?: {
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
mapping?: Record<string, string>;
|
||||
};
|
||||
field_names?: {
|
||||
parent_type?: string;
|
||||
prefix?: string;
|
||||
suffix?: string;
|
||||
mapping?: Record<string, string>;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type EditState = {
|
||||
id: number;
|
||||
isModify: boolean;
|
||||
@ -28,6 +43,7 @@ export type EditState = {
|
||||
originalTimeoutConf: string;
|
||||
originalForwardClientHeaders: boolean;
|
||||
originalComment?: string;
|
||||
originalCustomization?: graphQLCustomization;
|
||||
};
|
||||
|
||||
export type AddState = AsyncState & {
|
||||
@ -39,6 +55,7 @@ export type AddState = AsyncState & {
|
||||
forwardClientHeaders: boolean;
|
||||
editState: EditState;
|
||||
comment?: string;
|
||||
customization?: graphQLCustomization;
|
||||
};
|
||||
|
||||
export type ListState = AsyncState & {
|
||||
|
@ -69,10 +69,9 @@ a[class=''] {
|
||||
background-color: rgb(239, 239, 239);
|
||||
}
|
||||
|
||||
|
||||
/* To remove botstrap html font size from 10px */
|
||||
html {
|
||||
font-size: 14px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* React Select - Fixes Inner Outline in Select Box */
|
||||
@ -82,23 +81,28 @@ input[id*='react-select'][id$='-input']:focus {
|
||||
|
||||
/* ** GraphiQL CSS tweaks to fix compatibility */
|
||||
/* Input fixes from Tailwind */
|
||||
.graphiql-container input, .graphiql-container select{
|
||||
.graphiql-container input,
|
||||
.graphiql-container select {
|
||||
min-width: min-content;
|
||||
padding: 4px 8px;
|
||||
height: 32px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgb(203, 213, 225) !important;
|
||||
box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
||||
box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px,
|
||||
rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
||||
}
|
||||
.graphiql-container input:focus, .graphiql-container select:focus{
|
||||
.graphiql-container input:focus,
|
||||
.graphiql-container select:focus {
|
||||
border: 1px solid rgb(251, 191, 36) !important;
|
||||
box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px, rgb(253, 230, 138) 0px 0px 0px 2px, rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
||||
box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px,
|
||||
rgb(253, 230, 138) 0px 0px 0px 2px, rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
||||
}
|
||||
|
||||
.graphiql-explorer-actions > select {
|
||||
font-family: system, -apple-system, 'San Francisco', '.SFNSDisplay-Regular', 'Helvetica Neue', helvetica, 'Lucida Grande', arial, sans-serif;
|
||||
font-family: system, -apple-system, 'San Francisco', '.SFNSDisplay-Regular',
|
||||
'Helvetica Neue', helvetica, 'Lucida Grande', arial, sans-serif;
|
||||
font-variant-caps: normal;
|
||||
font-weight:400;
|
||||
font-weight: 400;
|
||||
border: 1px solid #cbd5e1;
|
||||
font-size: 14px;
|
||||
padding: 5px 8px;
|
||||
@ -106,44 +110,47 @@ input[id*='react-select'][id$='-input']:focus {
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
height: 32px;
|
||||
box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
||||
box-shadow: rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px,
|
||||
rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
||||
}
|
||||
.graphiql-explorer-actions > select:focus {
|
||||
border: 1px solid rgb(251, 191, 36) !important;
|
||||
box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px, rgb(253, 230, 138) 0px 0px 0px 2px, rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
||||
box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px,
|
||||
rgb(253, 230, 138) 0px 0px 0px 2px, rgba(0, 0, 0, 0.05) 0px 1px 2px 0px;
|
||||
}
|
||||
|
||||
/* While we're fixing the inputs maybe the Explorer header + footer */
|
||||
/* Fixes header spacing */
|
||||
.doc-explorer-title-bar{
|
||||
height:46px !important;
|
||||
.doc-explorer-title-bar {
|
||||
height: 46px !important;
|
||||
}
|
||||
.docExplorerHide{
|
||||
.docExplorerHide {
|
||||
padding: 16px !important;
|
||||
}
|
||||
|
||||
/* Fixes spacing in operation title namer */
|
||||
.graphiql-explorer-root{
|
||||
padding-top:0 !important;
|
||||
.graphiql-explorer-root {
|
||||
padding-top: 0 !important;
|
||||
}
|
||||
.graphiql-operation-title-bar{
|
||||
margin-top:10px;
|
||||
.graphiql-operation-title-bar {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.graphiql-operation-title-bar > span > input{
|
||||
margin-right:4px;
|
||||
.graphiql-operation-title-bar > span > input {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* Fixes footer spacing */
|
||||
.graphiql-explorer-actions > span {
|
||||
font-family: system, -apple-system, 'San Francisco', '.SFNSDisplay-Regular', 'Helvetica Neue', helvetica, 'Lucida Grande', arial, sans-serif;
|
||||
font-family: system, -apple-system, 'San Francisco', '.SFNSDisplay-Regular',
|
||||
'Helvetica Neue', helvetica, 'Lucida Grande', arial, sans-serif;
|
||||
font-variant-caps: normal;
|
||||
font-weight:500;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
letter-spacing:0px;
|
||||
letter-spacing: 0px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
.graphiql-explorer-actions{
|
||||
.graphiql-explorer-actions {
|
||||
margin: 4px 0px 0px -8px !important;
|
||||
width: calc(100% + 16px) !important;
|
||||
padding:10px !important;
|
||||
}
|
||||
padding: 10px !important;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user