console: add comments to tracked functions

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

PR-URL: https://github.com/hasura/graphql-engine-mono/pull/2745
Co-authored-by: Vijay Prasanna <11921040+vijayprasanna13@users.noreply.github.com>
Co-authored-by: Alberto Francesco Motta <36401353+afmotta@users.noreply.github.com>
GitOrigin-RevId: 8a9c89a4175c5d0a48dada30a12cfdc0f4bf4250
This commit is contained in:
Matt Hardman 2021-11-09 19:17:04 +01:00 committed by hasura-bot
parent e8023afaaf
commit 55c7be6b61
11 changed files with 238 additions and 4 deletions

View File

@ -2,8 +2,8 @@
## Next release
(Add highlights/major features below)
- server: log locking DB queries during source catalog migration
- console: add comments to tracked functions
### Bug fixes and improvements
(Add entries below in the order of server, console, cli, docs, others)

View File

@ -0,0 +1,82 @@
import React from 'react';
import { saveFunctionComment, setEditing } from '../customFunctionReducer';
interface FunctionCommentEditorProps {
isEditing: boolean;
defaultValue: string;
readOnly: boolean;
dispatch: (input: any) => void;
}
export const FunctionCommentEditor: React.FC<FunctionCommentEditorProps> = ({
isEditing,
defaultValue,
readOnly,
dispatch,
}) => {
const [comment, setComment] = React.useState('');
const dispatchIsEditing = React.useCallback(
(editing: boolean) => {
dispatch(setEditing(editing));
},
[dispatch]
);
const commentEditSave = () => {
dispatch(saveFunctionComment(comment));
};
const commentEditCancel = () => {
setComment(defaultValue);
dispatchIsEditing(false);
};
React.useEffect(() => {
setComment(defaultValue);
}, [defaultValue]);
if (isEditing) {
return (
<div className="flex items-center">
<input
onChange={event => setComment(event.target.value)}
className="form-control"
type="text"
value={comment}
placeholder="Function comment..."
/>
<div onClick={commentEditSave} className="ml-sm cursor-pointer">
Save
</div>
<div onClick={commentEditCancel} className="ml-sm cursor-pointer">
Cancel
</div>
</div>
);
}
return (
<div>
{comment && (
<div className="rounded bg-secondary-light border border-gray-300 border-l-4 border-l-secondary py-sm px-md mb-sm">
{comment}
</div>
)}
{!readOnly && (
<button
onClick={() => dispatchIsEditing(true)}
className="flex items-center cursor-pointer"
>
<i className="fa fa-edit mr-xs" />
<span className="font-semibold">
{comment ? 'Edit Comment' : 'Add a Comment'}
</span>
</button>
)}
</div>
);
};
export default FunctionCommentEditor;

View File

@ -1,4 +1,5 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import Helmet from 'react-helmet';
@ -9,6 +10,7 @@ import globals from '../../../../../Globals';
import Button from '../../../../Common/Button/Button';
import styles from './ModifyCustomFunction.scss';
import TextAreaWithCopy from '../../../../Common/TextAreaWithCopy/TextAreaWithCopy';
import FunctionCommentEditor from './FunctionCommentEditor';
import {
fetchCustomFunction,
unTrackCustomFunction,
@ -23,7 +25,8 @@ import {
} from '../../../../Common/utils/routesUtils';
import SessionVarSection from './SessionVarSection';
import RawSqlButton from '../../Common/Components/RawSqlButton';
import { connect } from 'react-redux';
import { isFeatureSupported } from '@/dataSources';
export const pageTitle = 'Custom Function';
@ -122,6 +125,8 @@ class ModifyCustomFunction extends React.Component {
functionSchema: schema,
functionName,
functionDefinition,
functionComment,
isEditingComment,
isRequesting,
isDeleting,
isUntracking,
@ -220,7 +225,24 @@ class ModifyCustomFunction extends React.Component {
showLoader={isFetching}
testPrefix={'functions'}
/>
<br />
{isFeatureSupported('functions.modify.comments.view') && (
<div className="w-full sm:w-6/12 mb-lg">
<h4 className="flex items-center text-gray-600 font-semibold mb-formlabel">
Function Comments
</h4>
<FunctionCommentEditor
isEditing={isEditingComment}
defaultValue={functionComment}
dispatch={dispatch}
readOnly={!isFeatureSupported('functions.modify.comments.edit')}
dispatch={dispatch}
/>
</div>
)}
<div className="w-full sm:w-6/12 mb-md">
<h4 className="flex items-center text-gray-600 font-semibold mb-formlabel">
Function Definition:

View File

@ -1,3 +1,4 @@
import Migration from '@/utils/migration/Migration';
import { dataSource } from '../../../../dataSources';
import Endpoints, { globalCookiePolicy } from '../../../../Endpoints';
import { exportMetadata } from '../../../../metadata/actions';
@ -47,6 +48,12 @@ const PERMISSION_CUSTOM_FUNCTION_DROP_SUCCESS =
const PERMISSION_CUSTOM_FUNCTION_DROP_FAIL =
'@customFunction/PERMISSION_CUSTOM_FUNCTION_DROP_FAIL';
const FUNCTION_COMMENT_SET_SUCCESS =
'@customFunction/FUNCTION_COMMENT_SET_SUCCESS';
const CUSTOM_FUNCTION_COMMENT_EDITING =
'@customFunction/CUSTOM_FUNCTION_COMMENT_EDITING';
const FUNCTION_COMMENT_SET_FAIL = '@customFunction/FUNCTION_COMMENT_SET_FAIL';
/* Action creators */
const fetchCustomFunction = (functionName, schema, source) => {
return (dispatch, getState) => {
@ -262,6 +269,69 @@ const updateSessVar = session_argument => {
};
};
const setEditing = editing => dispatch =>
dispatch({ type: CUSTOM_FUNCTION_COMMENT_EDITING, data: editing });
const saveFunctionComment = updatedComment => (dispatch, getState) => {
const source = getState().tables.currentDataSource;
const { currentSchema } = getState().tables;
const { functionName } = getState().functions;
const upQuery = dataSource.getSetCommentSql(
'function',
'',
currentSchema,
updatedComment || null,
null,
functionName
);
const downQuery = dataSource.getSetCommentSql(
'function',
'',
currentSchema,
'NULL',
null,
functionName
);
const migration = new Migration();
migration.add(
getRunSqlQuery(upQuery, source),
getRunSqlQuery(downQuery, source)
);
const migrationName =
'alter_function_' + currentSchema + '_' + functionName + '_update_comment';
const requestMsg = 'Updating Comment...';
const successMsg = 'Comment Updated';
const errorMsg = 'Updating comment failed';
const customOnSuccess = () => {
dispatch({
type: FUNCTION_COMMENT_SET_SUCCESS,
data: { functionComment: updatedComment },
});
};
const customOnError = err => {
dispatch({ type: FUNCTION_COMMENT_SET_FAIL, data: err });
};
return dispatch(
makeRequest(
migration.upMigration,
migration.downMigration,
migrationName,
customOnSuccess,
customOnError,
requestMsg,
successMsg,
errorMsg
)
);
};
const setFunctionPermission = (userRole, onSuccessCb) => (
dispatch,
getState
@ -377,6 +447,7 @@ const customFunctionReducer = (state = functionData, action) => {
setOffTableSchema: action?.data?.return_type_schema || null,
inputArgNames: action?.data?.input_arg_names || null,
inputArgTypes: action?.data?.input_arg_types || null,
functionComment: action?.data?.comment || '',
isFetching: false,
isUpdating: false,
isFetchError: null,
@ -453,6 +524,16 @@ const customFunctionReducer = (state = functionData, action) => {
isPermissionDrop: false,
isError: action.data,
};
case CUSTOM_FUNCTION_COMMENT_EDITING:
return { ...state, isEditingComment: action?.data };
case FUNCTION_COMMENT_SET_SUCCESS:
return {
...state,
isEditingComment: false,
functionComment: action?.data?.functionComment,
};
case FUNCTION_COMMENT_SET_FAIL:
return { ...state, isEditingComment: false, isError: action?.data };
default:
return state;
}
@ -461,7 +542,9 @@ const customFunctionReducer = (state = functionData, action) => {
export {
deleteFunction,
dropFunctionPermission,
saveFunctionComment,
fetchCustomFunction,
setEditing,
RESET,
setFunctionPermission,
unTrackCustomFunction,

View File

@ -14,6 +14,8 @@ const functionData = {
functionName: '',
functionSchema: '',
functionDefinition: '',
functionComment: '',
isEditingComment: false,
configuration: {},
permissions: {},
setOffTable: '',

View File

@ -170,6 +170,13 @@ export const supportedFeatures: DeepRequired<SupportedFeaturesType> = {
nonTrackableFunctions: {
enabled: false,
},
modify: {
enabled: false,
comments: {
view: false,
edit: false,
},
},
},
events: {
triggers: {

View File

@ -68,6 +68,13 @@ export const supportedFeatures: DeepRequired<SupportedFeaturesType> = {
nonTrackableFunctions: {
enabled: true,
},
modify: {
enabled: true,
comments: {
view: true,
edit: true,
},
},
},
};

View File

@ -188,6 +188,13 @@ export const supportedFeatures: DeepRequired<SupportedFeaturesType> = {
nonTrackableFunctions: {
enabled: false,
},
modify: {
enabled: false,
comments: {
view: false,
edit: false,
},
},
},
events: {
triggers: {

View File

@ -647,6 +647,13 @@ export const supportedFeatures: DeepRequired<SupportedFeaturesType> = {
nonTrackableFunctions: {
enabled: true,
},
modify: {
enabled: true,
comments: {
view: true,
edit: true,
},
},
},
events: {
triggers: {

View File

@ -663,12 +663,21 @@ export const getSetColumnDefaultSql = (
};
export const getSetCommentSql = (
on: 'column' | 'table' | string,
on: string,
tableName: string,
schemaName: string,
comment: string | null,
columnName?: string
columnName?: string,
functionName?: string
) => {
if (functionName) {
return `
comment on ${on} "${schemaName}"."${functionName}" is ${
comment ? sqlEscapeText(comment) : 'NULL'
}
`;
}
if (columnName) {
return `
comment on ${on} "${schemaName}"."${tableName}"."${columnName}" is ${
@ -857,6 +866,7 @@ pg_get_functiondef(p.oid) AS function_definition,
rtn.nspname::text AS return_type_schema,
rt.typname::text AS return_type_name,
rt.typtype::text AS return_type_type,
obj_description(p.oid) AS comment,
p.proretset AS returns_set,
( SELECT COALESCE(json_agg(json_build_object('schema', q.schema, 'name', q.name, 'type', q.type)), '[]'::json) AS "coalesce"
FROM ( SELECT pt.typname AS name,

View File

@ -323,6 +323,13 @@ export type SupportedFeaturesType = {
nonTrackableFunctions: {
enabled: boolean;
};
modify: {
enabled: boolean;
comments: {
view: boolean;
edit: boolean;
};
};
};
events: {
triggers: {