console: allow tracking of custom SQL functions having composite type (rowtype) input arguments

Closes: https://github.com/hasura/graphql-engine/issues/6858

Description:

Following two things are done:

1. Show compatible functions in the Untracked custom functions list (i.e. even those with ROWTYPE arg)
![Screenshot from 2021-05-11 16-13-21](https://user-images.githubusercontent.com/68095256/117803100-dd606f80-b273-11eb-8d02-1ea55b31863d.png)
2. When a function with ROWTYPE argument is tracked, a confirmatory dialogue box is shown. The text reads `This function can be added as a root field or a computed field inside a table` and the buttons reflect the options: `Add as root field`, `Add as computed field` (this will take the user to the Modify -> Add a computed field section of the first-row type argument)
![Screenshot from 2021-06-03 17-28-28](https://user-images.githubusercontent.com/68095256/120641377-28dff500-c491-11eb-80ea-cc60e6f37f23.png)

Affected Component:

- [x] Console

Co-authored-by: Vijay Prasanna <11921040+vijayprasanna13@users.noreply.github.com>
Co-authored-by: Abhijeet Khangarot <26903230+abhi40308@users.noreply.github.com>
GitOrigin-RevId: 089944aadba73f7a77e220a49489846ff1cb9540
This commit is contained in:
Varun Choudhary 2021-06-08 20:51:29 +05:30 committed by hasura-bot
parent b83ba51fa3
commit 3f19e8a3a8
7 changed files with 126 additions and 45 deletions

View File

@ -5,6 +5,7 @@
### Bug fixes and improvements
(Add entries below in the order of server, console, cli, docs, others)
- console: add foreign key CRUD functionality to ms sql server tables
- console: allow tracking of custom SQL functions having composite type (rowtype) input arguments
## v2.0.0-beta.1

View File

@ -64,6 +64,8 @@ export const trackFunction = () => {
cy.get(
getElementFromAlias(`add-track-function-${getCustomFunctionName(1)}`)
).click();
cy.get(getElementFromAlias(`track-as-mutation`)).should('exist');
cy.get(getElementFromAlias(`track-as-mutation`)).click();
cy.wait(5000);
validateCFunc(getCustomFunctionName(1), getSchema(), ResultType.SUCCESS);
cy.wait(5000);
@ -148,7 +150,10 @@ export const deleteCustomFunction = () => {
export const trackVolatileFunction = () => {
const fN = 'customVolatileFunc'.toLowerCase();
dataRequest(dropTableIfExists({ name: 'text_result', schema: 'public'}), ResultType.SUCCESS);
dataRequest(
dropTableIfExists({ name: 'text_result', schema: 'public' }),
ResultType.SUCCESS
);
cy.wait(5000);
dataRequest(createSampleTable(), ResultType.SUCCESS);
cy.wait(5000);
@ -168,7 +173,10 @@ export const trackVolatileFunction = () => {
export const trackVolatileFunctionAsQuery = () => {
const fN = 'customVolatileFunc'.toLowerCase();
dataRequest(dropTableIfExists({ name: 'text_result', schema: 'public'}), ResultType.SUCCESS);
dataRequest(
dropTableIfExists({ name: 'text_result', schema: 'public' }),
ResultType.SUCCESS
);
cy.wait(5000);
dataRequest(createSampleTable(), ResultType.SUCCESS);
cy.wait(5000);

View File

@ -1,47 +1,78 @@
import React, { useReducer } from 'react';
import { isEmpty } from '../../../Common/utils/jsUtils';
import { Dispatch } from '../../../../types';
import { Dispatch, ReduxState } from '../../../../types';
import { addExistingFunction } from '../Add/AddExistingTableViewActions';
import Button from '../../../Common/Button';
import RawSqlButton from '../Common/Components/RawSqlButton';
import { Note } from '../../../Common/Note';
import styles from './styles.scss';
import { PGFunction } from '../../../../dataSources/services/postgresql/types';
import { isTrackableAndComputedField } from './utils';
import _push from '../push';
import { Table } from '../../../../dataSources/types';
type VolatileFuncNoteProps = {
showQueryNote: boolean;
type FuncNoteProps = {
showTrackVolatileNote?: boolean;
onCancelClick: () => void;
onTrackAsMutationClick: () => void;
onTrackAsQueryClick: () => void;
onTrackAsQueryConfirmClick: () => void;
onClickMainAction: () => void;
onClickSecondaryAction: () => void;
showQueryNote?: boolean;
onTrackAsQueryConfirmClick?: () => void;
};
const VolatileFuncNote: React.FC<VolatileFuncNoteProps> = ({
showQueryNote,
const FuncNote: React.FC<FuncNoteProps> = ({
showQueryNote = false,
showTrackVolatileNote = false,
onCancelClick,
onTrackAsMutationClick,
onTrackAsQueryClick,
onClickMainAction,
onClickSecondaryAction,
onTrackAsQueryConfirmClick,
}) => {
const mainButtonText = showTrackVolatileNote
? 'Track As Mutation'
: 'Add As Root Field';
const secondaryActionText = showTrackVolatileNote
? 'Track As Query'
: 'Cancel';
const thirdActionText = showTrackVolatileNote
? 'Cancel'
: 'Add As Computed Field';
return (
<Note type="warn">
This function will be tracked as a <b>Mutation</b> because the function is{' '}
<b>VOLATILE</b>
{showTrackVolatileNote ? (
<>
This function will be tracked as a <b>Mutation</b> because the
function is <b>VOLATILE</b>
</>
) : (
<>
This function can be added as a <b>root field</b> or a{' '}
<b>computed field</b> inside a table.
</>
)}
<div className={styles.buttonsSection}>
<Button
color="yellow"
onClick={onTrackAsMutationClick}
onClick={onClickMainAction}
data-test="track-as-mutation"
>
Track as Mutation
{mainButtonText}
</Button>
<Button
onClick={
showTrackVolatileNote ? onCancelClick : onClickSecondaryAction
}
>
{thirdActionText}
</Button>
<Button onClick={onCancelClick}>Cancel</Button>
<button
className={`${styles.btnBlank} ${styles.queryButton}`}
onClick={onTrackAsQueryClick}
onClick={
showTrackVolatileNote ? onClickSecondaryAction : onCancelClick
}
data-test="track-as-query"
>
Track as Query
{secondaryActionText}
</button>
</div>
{showQueryNote ? (
@ -65,18 +96,21 @@ const VolatileFuncNote: React.FC<VolatileFuncNoteProps> = ({
type State = {
volatileNoteOpen: boolean;
queryWarningOpen: boolean;
stableImmutableNoteOpen: boolean;
};
const defaultState: State = {
volatileNoteOpen: false,
queryWarningOpen: false,
stableImmutableNoteOpen: false,
};
type Action =
| 'click-track-volatile-func'
| 'click-track-volatile-func-as-query'
| 'track-volatile-func'
| 'cancel';
| 'cancel'
| 'click-track-stable-immutable-func';
const reducer = (state: State, action: Action): State => {
switch (action) {
@ -85,6 +119,11 @@ const reducer = (state: State, action: Action): State => {
...state,
volatileNoteOpen: true,
};
case 'click-track-stable-immutable-func':
return {
...state,
stableImmutableNoteOpen: true,
};
case 'click-track-volatile-func-as-query':
return {
...state,
@ -107,6 +146,7 @@ type TrackableEntryProps = {
func: PGFunction;
index: number;
source: string;
allSchemas: Table[];
};
const TrackableEntry: React.FC<TrackableEntryProps> = ({
readOnlyMode,
@ -114,14 +154,18 @@ const TrackableEntry: React.FC<TrackableEntryProps> = ({
func,
index,
source,
allSchemas,
}) => {
const [state, dispatchR] = useReducer(reducer, defaultState);
const isVolatile = func.function_type.toLowerCase() === 'volatile';
const isTrackableAndUseInComputedField = isTrackableAndComputedField(func);
const handleTrackFn = (e: React.MouseEvent) => {
e.preventDefault();
if (isVolatile) {
dispatchR('click-track-volatile-func');
} else if (isTrackableAndUseInComputedField) {
dispatchR('click-track-stable-immutable-func');
} else {
dispatch(addExistingFunction(func.function_name));
}
@ -164,13 +208,47 @@ const TrackableEntry: React.FC<TrackableEntryProps> = ({
<span>{func.function_name}</span>
</div>
{isTrackableAndUseInComputedField && state.stableImmutableNoteOpen && (
<div className={styles.volatileNote}>
<FuncNote
onCancelClick={() => dispatchR('cancel')}
onClickMainAction={() =>
dispatch(addExistingFunction(func.function_name))
}
onClickSecondaryAction={() => {
const currentFuncInfo = func.input_arg_types?.find(
fn => fn.type === 'c'
);
if (!currentFuncInfo) {
return;
}
if (
!allSchemas?.find(
schemaInfo =>
schemaInfo?.table_name === currentFuncInfo.name &&
schemaInfo?.table_schema === currentFuncInfo.schema
)
) {
return;
}
dispatch(
_push(
`/data/${source}/schema/${currentFuncInfo.schema}/tables/${currentFuncInfo.name}/modify`
)
);
}}
/>
</div>
)}
{isVolatile && state.volatileNoteOpen && (
<div className={styles.volatileNote}>
<VolatileFuncNote
<FuncNote
showTrackVolatileNote
showQueryNote={state.queryWarningOpen}
onCancelClick={() => dispatchR('cancel')}
onTrackAsMutationClick={() => handleTrackFunction('mutation')}
onTrackAsQueryClick={() =>
onClickMainAction={() => handleTrackFunction('mutation')}
onClickSecondaryAction={() =>
dispatchR('click-track-volatile-func-as-query')
}
onTrackAsQueryConfirmClick={() => handleTrackFunction('query')}
@ -186,13 +264,13 @@ type FunctionsListProps = {
readOnlyMode: boolean;
dispatch: Dispatch;
source: string;
allSchemas: ReduxState['tables']['allSchemas'];
};
export const TrackableFunctionsList: React.FC<FunctionsListProps> = ({
funcs,
...props
}) => {
const noTrackableFunctions = isEmpty(funcs);
if (noTrackableFunctions) {
return (
<div key="no-untracked-fns">

View File

@ -225,7 +225,6 @@ class Schema extends Component {
trackedFunctions,
currentDataSource,
} = this.props;
const getSectionHeading = (headingText, tooltip, actionElement = null) => {
return (
<div>
@ -572,6 +571,7 @@ class Schema extends Component {
funcs={trackableFuncs}
readOnlyMode={readOnlyMode}
source={currentDataSource}
allSchemas={schema}
/>
) : (
`Currently unsupported for ${(

View File

@ -1,23 +1,19 @@
import {
ArgType,
PGFunction,
PGInputArgType,
} from '../../../../dataSources/services/postgresql/types';
import { PGFunction } from '../../../../dataSources/services/postgresql/types';
export const getTrackableFunctions = (
functionsList: PGFunction[],
trackedFunctions: PGFunction[]
): PGFunction[] => {
const trackedFuncNames = trackedFunctions.map(fn => fn.function_name);
const containsTableArgs = (arg: PGInputArgType): boolean =>
arg.type.toLowerCase() === ArgType.CompositeType;
// Assuming schema for both function and tables are same
// return function which are tracked
const filterCondition = (func: PGFunction) => {
return (
!trackedFuncNames.includes(func.function_name) &&
!func.input_arg_types?.some(containsTableArgs)
);
};
const filterCondition = (func: PGFunction) =>
!trackedFuncNames.includes(func.function_name);
return functionsList.filter(filterCondition);
};
export const isTrackableAndComputedField = (func: PGFunction) => {
return (
func.return_type_type === 'c' &&
(func.function_type === 'STABLE' || func.function_type === 'IMMUTABLE') &&
func.returns_set
);
};

View File

@ -97,7 +97,7 @@ export const getFetchTablesListQuery = (options: {
COALESCE(json_agg(DISTINCT row_to_json(ist) :: jsonb || jsonb_build_object('comment', obj_description(pgt.oid))) filter (WHERE ist.trigger_name IS NOT NULL), '[]' :: json) AS triggers,
row_to_json(isv) AS view_info
FROM partitions, pg_class as pgc
FROM pg_class as pgc
INNER JOIN pg_namespace as pgn
ON pgc.relnamespace = pgn.oid
@ -179,7 +179,6 @@ export const getFetchTablesListQuery = (options: {
WHERE
pgc.relkind IN ('r', 'v', 'f', 'm', 'p')
and NOT (pgc.relname = ANY (partitions.names))
${whereQuery}
GROUP BY pgc.oid, pgn.nspname, pgc.relname, table_type, isv.*
) AS info;
@ -400,7 +399,7 @@ export const getCreateTableQueries = (
// add comment
if (tableComment && tableComment !== '') {
sqlCreateTable += `COMMENT ON TABLE "${currentSchema}"."${tableName}" IS ${sqlEscapeText(
sqlCreateTable += `COMMENT ON TABLE "${currentSchema}".${tableName} IS ${sqlEscapeText(
tableComment
)};`;
}
@ -797,8 +796,6 @@ export const getCreatePkSql = ({
primary key (${selectedPkColumns.map(pkc => `"${pkc}"`).join(', ')});`;
};
export const getFunctionsWhereQuery = () => {};
const trackableFunctionsWhere = `
AND has_variadic = FALSE
AND returns_set = TRUE

View File

@ -17,6 +17,7 @@ export type PGFunction = {
return_type_type: string;
function_type: string;
input_arg_types?: PGInputArgType[];
returns_set: boolean;
};
export interface PostgresTable {