mirror of
https://github.com/hasura/graphql-engine.git
synced 2024-09-20 06:58:39 +03:00
console: support volatile functions (close #6228)
Close https://github.com/hasura/graphql-engine/issues/6228 GitOrigin-RevId: 814c38846f49abd8c5ee48129e61d1a00b81a41e
This commit is contained in:
parent
3373bbc748
commit
5aedaace28
@ -100,6 +100,7 @@ and be accessible according to the permissions that were configured for the role
|
||||
- console: fix allow-list not getting added to metadata/allow_list.yaml in CLI mode (close #6374)
|
||||
- console: misc bug fixes (close #4785, #6330, #6288)
|
||||
- console: allow setting table custom name (#212)
|
||||
- console: support tracking VOLATILE functions as mutations or queries (close #6228)
|
||||
- cli: add missing global flags for seed command (#5565)
|
||||
- cli: allow seeds as alias for seed command (#5693)
|
||||
- cli: fix action timeouts not being picked up in metadata operations (#6220)
|
||||
|
@ -6,6 +6,21 @@ export const baseUrl = Cypress.config('baseUrl');
|
||||
export const getIndexRoute = (sourceName = 'default', schemaName = 'public') =>
|
||||
`/data/${sourceName}/schema/${schemaName}/`;
|
||||
|
||||
export const createVolatileFunction = (name: string) => {
|
||||
return {
|
||||
type: 'run_sql',
|
||||
args: {
|
||||
sql: `CREATE OR REPLACE FUNCTION public.${name}()
|
||||
RETURNS SETOF text_result
|
||||
LANGUAGE sql
|
||||
AS $function$
|
||||
SELECT * FROM text_result;
|
||||
$function$`,
|
||||
cascade: false,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const dataTypes = [
|
||||
'serial',
|
||||
'bigserial',
|
||||
@ -163,7 +178,7 @@ export const trackCreateFunctionTable = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const createTableForSessionVarTest = () => {
|
||||
export const createSampleTable = () => {
|
||||
return {
|
||||
type: 'run_sql',
|
||||
source: 'default',
|
||||
@ -176,7 +191,7 @@ export const createTableForSessionVarTest = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const getTrackSessionVarTestTableQuery = () => {
|
||||
export const getTrackSampleTableQuery = () => {
|
||||
return {
|
||||
type: 'pg_track_table',
|
||||
source: 'default',
|
||||
@ -196,7 +211,7 @@ export const dropTable = (table = 'post', cascade = false) => {
|
||||
{
|
||||
type: 'run_sql',
|
||||
args: {
|
||||
sql: `DROP table ${table}${cascade ? ' CASCADE;' : ';'}`,
|
||||
sql: `DROP table "public"."${table}"${cascade ? ' CASCADE;' : ';'}`,
|
||||
cascade,
|
||||
},
|
||||
},
|
||||
|
@ -10,8 +10,9 @@ import {
|
||||
trackCreateFunctionTable,
|
||||
getCreateTestFunctionQuery,
|
||||
getTrackTestFunctionQuery,
|
||||
createTableForSessionVarTest,
|
||||
getTrackSessionVarTestTableQuery,
|
||||
createSampleTable,
|
||||
getTrackSampleTableQuery,
|
||||
createVolatileFunction,
|
||||
} from '../../../helpers/dataHelpers';
|
||||
|
||||
import {
|
||||
@ -20,7 +21,6 @@ import {
|
||||
validateCFunc,
|
||||
validateUntrackedFunc,
|
||||
ResultType,
|
||||
createFunctionRequest,
|
||||
trackFunctionRequest,
|
||||
} from '../../validators/validators';
|
||||
import { setPromptValue } from '../../../helpers/common';
|
||||
@ -73,19 +73,12 @@ export const testSessVariable = () => {
|
||||
// Round about way to create a function
|
||||
const fN = 'customFunctionWithSessionArg'.toLowerCase(); // for reading
|
||||
|
||||
dataRequest(createTableForSessionVarTest(), ResultType.SUCCESS, 'query');
|
||||
dataRequest(createSampleTable(), ResultType.SUCCESS, 'query');
|
||||
cy.wait(5000);
|
||||
dataRequest(
|
||||
getTrackSessionVarTestTableQuery(),
|
||||
ResultType.SUCCESS,
|
||||
'metadata'
|
||||
);
|
||||
dataRequest(getTrackSampleTableQuery(), ResultType.SUCCESS, 'metadata');
|
||||
cy.wait(5000);
|
||||
|
||||
createFunctionRequest(
|
||||
testCustomFunctionSQLWithSessArg(fN),
|
||||
ResultType.SUCCESS
|
||||
);
|
||||
dataRequest(testCustomFunctionSQLWithSessArg(fN), ResultType.SUCCESS);
|
||||
cy.wait(1500);
|
||||
|
||||
trackFunctionRequest(getTrackFnPayload(fN), ResultType.SUCCESS);
|
||||
@ -122,6 +115,7 @@ export const testSessVariable = () => {
|
||||
);
|
||||
dropTableRequest(dropTable('text_result', true), ResultType.SUCCESS);
|
||||
cy.wait(2000);
|
||||
cy.visit(`data/default/schema/public/`);
|
||||
};
|
||||
|
||||
export const verifyPermissionTab = () => {
|
||||
@ -148,3 +142,41 @@ export const deleteCustomFunction = () => {
|
||||
dropTableRequest(dropTable(), ResultType.SUCCESS);
|
||||
cy.wait(5000);
|
||||
};
|
||||
|
||||
export const trackVolatileFunction = () => {
|
||||
const fN = 'customVolatileFunc'.toLowerCase();
|
||||
dataRequest(createSampleTable(), ResultType.SUCCESS);
|
||||
cy.wait(1500);
|
||||
dataRequest(getTrackSampleTableQuery(), ResultType.SUCCESS, 'metadata');
|
||||
dataRequest(createVolatileFunction(fN), ResultType.SUCCESS);
|
||||
cy.wait(1500);
|
||||
cy.visit(`data/default/schema/public`);
|
||||
cy.get(getElementFromAlias(`add-track-function-${fN}`)).click();
|
||||
cy.get(getElementFromAlias('track-as-mutation')).click();
|
||||
cy.wait(500);
|
||||
cy.url().should(
|
||||
'eq',
|
||||
`${baseUrl}/data/default/schema/public/functions/${fN}/modify`
|
||||
);
|
||||
dropTableRequest(dropTable('text_result', true), ResultType.SUCCESS);
|
||||
};
|
||||
|
||||
export const trackVolatileFunctionAsQuery = () => {
|
||||
const fN = 'customVolatileFunc'.toLowerCase();
|
||||
dataRequest(createSampleTable(), ResultType.SUCCESS);
|
||||
cy.wait(1500);
|
||||
dataRequest(getTrackSampleTableQuery(), ResultType.SUCCESS, 'metadata');
|
||||
dataRequest(createVolatileFunction(fN), ResultType.SUCCESS);
|
||||
cy.wait(1500);
|
||||
cy.visit(`data/default/schema/public`);
|
||||
cy.get(getElementFromAlias(`add-track-function-${fN}`)).click();
|
||||
cy.get(getElementFromAlias('track-as-query')).click();
|
||||
cy.wait(100);
|
||||
cy.get(getElementFromAlias('track-as-query-confirm')).click();
|
||||
cy.wait(500);
|
||||
cy.url().should(
|
||||
'eq',
|
||||
`${baseUrl}/data/default/schema/public/functions/${fN}/modify`
|
||||
);
|
||||
dropTableRequest(dropTable('text_result', true), ResultType.SUCCESS);
|
||||
};
|
||||
|
@ -8,6 +8,8 @@ import {
|
||||
trackFunction,
|
||||
verifyPermissionTab,
|
||||
testSessVariable,
|
||||
trackVolatileFunction,
|
||||
trackVolatileFunctionAsQuery,
|
||||
} from './spec';
|
||||
import { getIndexRoute } from '../../../helpers/dataHelpers';
|
||||
|
||||
@ -31,6 +33,8 @@ export const runCreateCustomFunctionsTableTests = () => {
|
||||
it('Verify permission tab', verifyPermissionTab);
|
||||
it('Delete custom function', deleteCustomFunction);
|
||||
it('Test custom function with Session Argument', testSessVariable);
|
||||
it('Tracks VOLATILE function as mutation', trackVolatileFunction);
|
||||
it('Tracks VOLATILE function as query', trackVolatileFunctionAsQuery);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -206,25 +206,6 @@ export const dataRequest = (
|
||||
});
|
||||
};
|
||||
|
||||
export const createFunctionRequest = (
|
||||
reqBody: RequestBody,
|
||||
result: ResultType
|
||||
) => {
|
||||
const requestOptions = makeDataAPIOptions(
|
||||
dataApiUrl,
|
||||
adminSecret,
|
||||
reqBody,
|
||||
'query'
|
||||
);
|
||||
cy.request(requestOptions).then(response => {
|
||||
if (result === ResultType.SUCCESS) {
|
||||
expect(response.body.result_type === 'CommandOk').to.be.true;
|
||||
} else {
|
||||
expect(response.body.result_type === 'CommandOk').to.be.false;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const trackFunctionRequest = (
|
||||
reqBody: RequestBody,
|
||||
result: ResultType
|
||||
|
12
console/src/components/Common/Note/index.tsx
Normal file
12
console/src/components/Common/Note/index.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './styles.scss';
|
||||
|
||||
type NoteProps = {
|
||||
type: 'warn';
|
||||
};
|
||||
export const Note: React.FC<NoteProps> = ({ type, children }) => {
|
||||
return (
|
||||
<section className={`${styles.note} ${styles[type]}`}>{children}</section>
|
||||
);
|
||||
};
|
11
console/src/components/Common/Note/styles.scss
Normal file
11
console/src/components/Common/Note/styles.scss
Normal file
@ -0,0 +1,11 @@
|
||||
.note {
|
||||
background: #fff;
|
||||
width: 100%;
|
||||
border: 1px solid #d7d7d7;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.warn {
|
||||
border-left: 3px solid #fec53d;
|
||||
}
|
@ -103,10 +103,14 @@ const addExistingTableSql = (name, customSchema, skipRouting = false) => {
|
||||
};
|
||||
};
|
||||
|
||||
const addExistingFunction = (name, customSchema, skipRouting = false) => {
|
||||
const addExistingFunction = (
|
||||
name,
|
||||
config,
|
||||
customSchema,
|
||||
skipRouting = false
|
||||
) => {
|
||||
return (dispatch, getState) => {
|
||||
dispatch({ type: MAKING_REQUEST });
|
||||
dispatch(showSuccessNotification('Adding an function...'));
|
||||
const currentSchema = customSchema
|
||||
? customSchema
|
||||
: getState().tables.currentSchema;
|
||||
@ -115,7 +119,8 @@ const addExistingFunction = (name, customSchema, skipRouting = false) => {
|
||||
const requestBodyUp = getTrackFunctionQuery(
|
||||
name,
|
||||
currentSchema,
|
||||
currentDataSource
|
||||
currentDataSource,
|
||||
config
|
||||
);
|
||||
const requestBodyDown = getUntrackFunctionQuery(
|
||||
name,
|
||||
|
@ -138,11 +138,11 @@ class ModifyCustomFunction extends React.Component {
|
||||
? { isRequesting, isDeleting, isUntracking, isFetching }
|
||||
: null;
|
||||
|
||||
const { migrationMode, dispatch } = this.props;
|
||||
const { migrationMode, dispatch, currentSource } = this.props;
|
||||
|
||||
const functionBaseUrl = getFunctionBaseRoute(
|
||||
schema,
|
||||
this.props.currentSource,
|
||||
currentSource,
|
||||
functionName
|
||||
);
|
||||
|
||||
@ -230,6 +230,7 @@ class ModifyCustomFunction extends React.Component {
|
||||
sql={functionDefinition}
|
||||
dispatch={dispatch}
|
||||
data-test="modify-view"
|
||||
source={currentSource}
|
||||
>
|
||||
Modify
|
||||
</RawSqlButton>
|
||||
|
@ -15,7 +15,6 @@ import { getRunSqlQuery } from '../../../Common/utils/v1QueryUtils';
|
||||
import {
|
||||
getUntrackFunctionQuery,
|
||||
getTrackFunctionQuery,
|
||||
getTrackFunctionV2Query,
|
||||
} from '../../../../metadata/queryUtils';
|
||||
import { makeRequest } from '../../RemoteSchema/Actions';
|
||||
|
||||
@ -127,7 +126,7 @@ const unTrackCustomFunction = () => {
|
||||
return (dispatch, getState) => {
|
||||
const currentSchema = getState().tables.currentSchema;
|
||||
const currentDataSource = getState().tables.currentDataSource;
|
||||
const functionName = getState().functions.functionName;
|
||||
const { functionName, configuration } = getState().functions;
|
||||
|
||||
const migrationName = 'remove_custom_function_' + functionName;
|
||||
const payload = getUntrackFunctionQuery(
|
||||
@ -138,7 +137,8 @@ const unTrackCustomFunction = () => {
|
||||
const downPayload = getTrackFunctionQuery(
|
||||
functionName,
|
||||
currentSchema,
|
||||
currentDataSource
|
||||
currentDataSource,
|
||||
configuration
|
||||
);
|
||||
|
||||
const requestMsg = 'Deleting custom function...';
|
||||
@ -186,25 +186,25 @@ const updateSessVar = session_argument => {
|
||||
currentSchema,
|
||||
currentDataSource
|
||||
);
|
||||
const retrackPayloadDown = getTrackFunctionV2Query(
|
||||
const retrackPayloadDown = getTrackFunctionQuery(
|
||||
functionName,
|
||||
currentSchema,
|
||||
currentDataSource,
|
||||
{
|
||||
...(oldConfiguration && oldConfiguration),
|
||||
},
|
||||
currentDataSource
|
||||
}
|
||||
);
|
||||
|
||||
// retrack with sess arg config
|
||||
const retrackPayloadUp = getTrackFunctionV2Query(
|
||||
const retrackPayloadUp = getTrackFunctionQuery(
|
||||
functionName,
|
||||
currentSchema,
|
||||
currentDataSource,
|
||||
{
|
||||
...(session_argument && {
|
||||
session_argument,
|
||||
}),
|
||||
},
|
||||
currentDataSource
|
||||
}
|
||||
);
|
||||
|
||||
const untrackPayloadDown = getUntrackFunctionQuery(
|
||||
|
@ -17,7 +17,7 @@ import { dataSource } from '../../../../dataSources';
|
||||
import { getRunSqlQuery } from '../../../Common/utils/v1QueryUtils';
|
||||
import { getDownQueryComments } from '../../../../utils/migration/utils';
|
||||
import {
|
||||
getTrackFunctionV2Query,
|
||||
getTrackFunctionQuery,
|
||||
getTrackTableQuery,
|
||||
} from '../../../../metadata/queryUtils';
|
||||
import globals from '../../../../Globals';
|
||||
@ -49,7 +49,7 @@ const trackAllItems = (sql, isMigration, migrationName) => (
|
||||
objects.forEach(({ type, name, schema }) => {
|
||||
let req = {};
|
||||
if (type === 'function') {
|
||||
req = getTrackFunctionV2Query(name, schema, {}, source);
|
||||
req = getTrackFunctionQuery(name, schema, source, {});
|
||||
} else {
|
||||
req = getTrackTableQuery({ name, schema }, source);
|
||||
}
|
||||
|
211
console/src/components/Services/Data/Schema/FunctionsList.tsx
Normal file
211
console/src/components/Services/Data/Schema/FunctionsList.tsx
Normal file
@ -0,0 +1,211 @@
|
||||
import React, { useReducer } from 'react';
|
||||
|
||||
import { isEmpty } from '../../../Common/utils/jsUtils';
|
||||
import { Dispatch } 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';
|
||||
|
||||
type VolatileFuncNoteProps = {
|
||||
showQueryNote: boolean;
|
||||
onCancelClick: () => void;
|
||||
onTrackAsMutationClick: () => void;
|
||||
onTrackAsQueryClick: () => void;
|
||||
onTrackAsQueryConfirmClick: () => void;
|
||||
};
|
||||
const VolatileFuncNote: React.FC<VolatileFuncNoteProps> = ({
|
||||
showQueryNote,
|
||||
onCancelClick,
|
||||
onTrackAsMutationClick,
|
||||
onTrackAsQueryClick,
|
||||
onTrackAsQueryConfirmClick,
|
||||
}) => {
|
||||
return (
|
||||
<Note type="warn">
|
||||
This function will be tracked as a <b>Mutation</b> because the function is{' '}
|
||||
<b>VOLATILE</b>
|
||||
<div className={styles.buttonsSection}>
|
||||
<Button
|
||||
color="yellow"
|
||||
onClick={onTrackAsMutationClick}
|
||||
data-test="track-as-mutation"
|
||||
>
|
||||
Track as Mutation
|
||||
</Button>
|
||||
<Button onClick={onCancelClick}>Cancel</Button>
|
||||
<button
|
||||
className={`${styles.btnBlank} ${styles.queryButton}`}
|
||||
onClick={onTrackAsQueryClick}
|
||||
data-test="track-as-query"
|
||||
>
|
||||
Track as Query
|
||||
</button>
|
||||
</div>
|
||||
{showQueryNote ? (
|
||||
<div className={styles.nestedBox}>
|
||||
<b>Are you sure?</b> Queries are supposed to be read-only and as such
|
||||
are recommended to be <b>STABLE</b> or <b>IMMUTABLE</b>.
|
||||
<div className={styles.buttonsSection}>
|
||||
<Button
|
||||
onClick={onTrackAsQueryConfirmClick}
|
||||
data-test="track-as-query-confirm"
|
||||
>
|
||||
Track as Query
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</Note>
|
||||
);
|
||||
};
|
||||
|
||||
type State = {
|
||||
volatileNoteOpen: boolean;
|
||||
queryWarningOpen: boolean;
|
||||
};
|
||||
|
||||
const defaultState: State = {
|
||||
volatileNoteOpen: false,
|
||||
queryWarningOpen: false,
|
||||
};
|
||||
|
||||
type Action =
|
||||
| 'click-track-volatile-func'
|
||||
| 'click-track-volatile-func-as-query'
|
||||
| 'track-volatile-func'
|
||||
| 'cancel';
|
||||
|
||||
const reducer = (state: State, action: Action): State => {
|
||||
switch (action) {
|
||||
case 'click-track-volatile-func':
|
||||
return {
|
||||
...state,
|
||||
volatileNoteOpen: true,
|
||||
};
|
||||
case 'click-track-volatile-func-as-query':
|
||||
return {
|
||||
...state,
|
||||
volatileNoteOpen: true,
|
||||
queryWarningOpen: true,
|
||||
};
|
||||
|
||||
case 'track-volatile-func':
|
||||
case 'cancel':
|
||||
return defaultState;
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
type TrackableEntryProps = {
|
||||
readOnlyMode: boolean;
|
||||
dispatch: Dispatch;
|
||||
func: PGFunction;
|
||||
index: number;
|
||||
source: string;
|
||||
};
|
||||
const TrackableEntry: React.FC<TrackableEntryProps> = ({
|
||||
readOnlyMode,
|
||||
dispatch,
|
||||
func,
|
||||
index,
|
||||
source,
|
||||
}) => {
|
||||
const [state, dispatchR] = useReducer(reducer, defaultState);
|
||||
const isVolatile = func.function_type.toLowerCase() === 'volatile';
|
||||
|
||||
const handleTrackFn = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
if (isVolatile) {
|
||||
dispatchR('click-track-volatile-func');
|
||||
} else {
|
||||
dispatch(addExistingFunction(func.function_name));
|
||||
}
|
||||
};
|
||||
|
||||
const handleTrackFunction = (type: 'query' | 'mutation') => {
|
||||
dispatchR('track-volatile-func');
|
||||
dispatch(
|
||||
addExistingFunction(func.function_name, {
|
||||
exposed_as: type,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.padd_bottom} key={`untracked-function-${index}`}>
|
||||
{!readOnlyMode ? (
|
||||
<div className={styles.display_inline}>
|
||||
<Button
|
||||
data-test={`add-track-function-${func.function_name}`}
|
||||
className={`${styles.display_inline} btn btn-xs btn-default`}
|
||||
onClick={handleTrackFn}
|
||||
>
|
||||
Track
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className={`${styles.display_inline} ${styles.add_mar_left_mid}`}>
|
||||
<RawSqlButton
|
||||
dataTestId={`view-function-${func.function_name}`}
|
||||
customStyles={styles.display_inline}
|
||||
sql={func.function_definition}
|
||||
dispatch={dispatch}
|
||||
source={source}
|
||||
>
|
||||
View
|
||||
</RawSqlButton>
|
||||
</div>
|
||||
<div className={`${styles.display_inline} ${styles.add_mar_left}`}>
|
||||
<span>{func.function_name}</span>
|
||||
</div>
|
||||
|
||||
{isVolatile && state.volatileNoteOpen && (
|
||||
<div className={styles.volatileNote}>
|
||||
<VolatileFuncNote
|
||||
showQueryNote={state.queryWarningOpen}
|
||||
onCancelClick={() => dispatchR('cancel')}
|
||||
onTrackAsMutationClick={() => handleTrackFunction('mutation')}
|
||||
onTrackAsQueryClick={() =>
|
||||
dispatchR('click-track-volatile-func-as-query')
|
||||
}
|
||||
onTrackAsQueryConfirmClick={() => handleTrackFunction('query')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type FunctionsListProps = {
|
||||
funcs: PGFunction[];
|
||||
readOnlyMode: boolean;
|
||||
dispatch: Dispatch;
|
||||
source: string;
|
||||
};
|
||||
export const TrackableFunctionsList: React.FC<FunctionsListProps> = ({
|
||||
funcs,
|
||||
...props
|
||||
}) => {
|
||||
const noTrackableFunctions = isEmpty(funcs);
|
||||
|
||||
if (noTrackableFunctions) {
|
||||
return (
|
||||
<div key="no-untracked-fns">
|
||||
<div>There are no untracked functions</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{funcs.map((p, i) => (
|
||||
<TrackableEntry key={p.function_name} {...props} func={p} index={i} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
@ -9,7 +9,6 @@ import {
|
||||
setTableName,
|
||||
addExistingTableSql,
|
||||
addAllUntrackedTablesSql,
|
||||
addExistingFunction,
|
||||
} from '../Add/AddExistingTableViewActions';
|
||||
import {
|
||||
updateSchemaInfo,
|
||||
@ -50,6 +49,7 @@ import {
|
||||
getDataSources,
|
||||
} from '../../../../metadata/selector';
|
||||
import { RightContainer } from '../../../Common/Layout/RightContainer';
|
||||
import { TrackableFunctionsList } from './FunctionsList';
|
||||
|
||||
const DeleteSchemaButton = ({ dispatch, migrationMode, currentDataSource }) => {
|
||||
const successCb = () => {
|
||||
@ -635,74 +635,6 @@ class Schema extends Component {
|
||||
};
|
||||
|
||||
const getUntrackedFunctionsSection = () => {
|
||||
const noTrackableFunctions = isEmpty(trackableFuncs);
|
||||
|
||||
const getTrackableFunctionsList = () => {
|
||||
const trackableFunctionList = [];
|
||||
|
||||
if (noTrackableFunctions) {
|
||||
trackableFunctionList.push(
|
||||
<div key="no-untracked-fns">
|
||||
<div>There are no untracked functions</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
trackableFuncs.forEach((p, i) => {
|
||||
const getTrackBtn = () => {
|
||||
if (readOnlyMode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleTrackFn = e => {
|
||||
e.preventDefault();
|
||||
|
||||
dispatch(addExistingFunction(p.function_name));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.display_inline}>
|
||||
<Button
|
||||
data-test={`add-track-function-${p.function_name}`}
|
||||
className={`${styles.display_inline} btn btn-xs btn-default`}
|
||||
onClick={handleTrackFn}
|
||||
>
|
||||
Track
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
trackableFunctionList.push(
|
||||
<div
|
||||
className={styles.padd_bottom}
|
||||
key={`untracked-function-${i}`}
|
||||
>
|
||||
{getTrackBtn()}
|
||||
<div
|
||||
className={`${styles.display_inline} ${styles.add_mar_left_mid}`}
|
||||
>
|
||||
<RawSqlButton
|
||||
dataTestId={`view-function-${p.function_name}`}
|
||||
customStyles={styles.display_inline}
|
||||
sql={p.function_definition}
|
||||
dispatch={dispatch}
|
||||
>
|
||||
View
|
||||
</RawSqlButton>
|
||||
</div>
|
||||
<div
|
||||
className={`${styles.display_inline} ${styles.add_mar_left}`}
|
||||
>
|
||||
<span>{p.function_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return trackableFunctionList;
|
||||
};
|
||||
|
||||
const heading = getSectionHeading(
|
||||
'Untracked custom functions',
|
||||
'Custom functions that are not exposed over the GraphQL API',
|
||||
@ -717,7 +649,12 @@ class Schema extends Component {
|
||||
testId={'toggle-trackable-functions'}
|
||||
>
|
||||
<div className={`${styles.padd_left_remove} col-xs-12`}>
|
||||
{getTrackableFunctionsList()}
|
||||
<TrackableFunctionsList
|
||||
dispatch={dispatch}
|
||||
funcs={trackableFuncs}
|
||||
readOnlyMode={readOnlyMode}
|
||||
source={currentDataSource}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.clear_fix} />
|
||||
</CollapsibleToggle>
|
||||
@ -752,6 +689,7 @@ class Schema extends Component {
|
||||
customStyles={styles.display_inline}
|
||||
sql={p.function_definition}
|
||||
dispatch={dispatch}
|
||||
source={currentDataSource}
|
||||
>
|
||||
View
|
||||
</RawSqlButton>
|
||||
|
48
console/src/components/Services/Data/Schema/styles.scss
Normal file
48
console/src/components/Services/Data/Schema/styles.scss
Normal file
@ -0,0 +1,48 @@
|
||||
@import '../../../Common/Common';
|
||||
|
||||
.btnBlank {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #4d4d4d;
|
||||
padding: 5px 10px;
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
display: inline-block;
|
||||
margin-bottom: 0;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
border-radius: 4px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.queryButton {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.buttonsSection {
|
||||
display: flex;
|
||||
margin-top: 10px;
|
||||
align-items: center;
|
||||
|
||||
button:first-of-type {
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.nestedBox {
|
||||
background: #fff;
|
||||
width: 100%;
|
||||
border: 1px solid #d7d7d7;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
|
||||
margin-top: 16px;
|
||||
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08), 0 1px 4px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.volatileNote {
|
||||
width: 420px;
|
||||
padding: 8px 0;
|
||||
}
|
@ -16,6 +16,7 @@ const ComputedFields = (props: ComputedFieldsProps) => {
|
||||
nonTrackableFunctions,
|
||||
trackableFunctions,
|
||||
schemaList,
|
||||
currentSource,
|
||||
} = props;
|
||||
|
||||
const allFunctions = nonTrackableFunctions.concat(trackableFunctions);
|
||||
@ -33,6 +34,7 @@ const ComputedFields = (props: ComputedFieldsProps) => {
|
||||
functions={allFunctions} // TODO: fix cross schema functions
|
||||
schemaList={schemaList}
|
||||
dispatch={dispatch}
|
||||
source={currentSource}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
@ -51,6 +53,7 @@ const mapStateToProps = (state: ReduxState, ownProps: OwnProps) => {
|
||||
return {
|
||||
tableSchema: ownProps.tableSchema,
|
||||
currentSchema: state.tables.currentSchema,
|
||||
currentSource: state.tables.currentDataSource,
|
||||
nonTrackableFunctions: state.tables.nonTrackablePostgresFunctions || [],
|
||||
trackableFunctions: state.tables.postgresFunctions || [],
|
||||
schemaList: state.tables.schemaList,
|
||||
|
@ -17,6 +17,7 @@ const ComputedFieldsEditor = ({
|
||||
functions,
|
||||
schemaList,
|
||||
dispatch,
|
||||
source,
|
||||
}) => {
|
||||
const computedFields = table.computed_fields;
|
||||
|
||||
@ -162,6 +163,7 @@ const ComputedFieldsEditor = ({
|
||||
customStyles={`${styles.display_inline} ${styles.add_mar_left}`}
|
||||
sql={computedFieldFunctionDefinition}
|
||||
dispatch={dispatch}
|
||||
source={source}
|
||||
>
|
||||
Modify
|
||||
</RawSqlButton>
|
||||
@ -304,6 +306,7 @@ const ComputedFieldsEditor = ({
|
||||
customStyles={`${styles.display_inline} ${styles.add_mar_left}`}
|
||||
sql={''}
|
||||
dispatch={dispatch}
|
||||
source={source}
|
||||
>
|
||||
Create new
|
||||
</RawSqlButton>
|
||||
|
@ -785,8 +785,6 @@ const trackableFunctionsWhere = `
|
||||
AND has_variadic = FALSE
|
||||
AND returns_set = TRUE
|
||||
AND return_type_type = 'c'
|
||||
AND(function_type ILIKE '%STABLE%'
|
||||
OR function_type ILIKE '%IMMUTABLE%')
|
||||
`;
|
||||
|
||||
const nonTrackableFunctionsWhere = `
|
||||
@ -794,10 +792,6 @@ AND NOT (
|
||||
has_variadic = false
|
||||
AND returns_set = TRUE
|
||||
AND return_type_type = 'c'
|
||||
AND (
|
||||
function_type ilike '%stable%'
|
||||
OR function_type ilike '%immutable%'
|
||||
)
|
||||
)
|
||||
`;
|
||||
|
||||
|
@ -3,6 +3,7 @@ export type PGFunction = {
|
||||
function_schema: string;
|
||||
function_definition: string;
|
||||
return_type_type: string;
|
||||
function_type: string;
|
||||
};
|
||||
|
||||
export interface PostgresTable {
|
||||
|
@ -536,24 +536,24 @@ export const addExistingTableOrView = (
|
||||
export const getTrackFunctionQuery = (
|
||||
name: string,
|
||||
schema: string,
|
||||
source: string
|
||||
) => getMetadataQuery('track_function', source, { function: { name, schema } });
|
||||
|
||||
export const getTrackFunctionV2Query = (
|
||||
name: string,
|
||||
schema: string,
|
||||
configuration: Record<string, string>,
|
||||
source: string
|
||||
) =>
|
||||
getMetadataQuery(
|
||||
'track_function',
|
||||
source,
|
||||
{
|
||||
function: { name, schema },
|
||||
configuration,
|
||||
},
|
||||
{ version: 2 }
|
||||
);
|
||||
source: string,
|
||||
configuration?: Record<string, any>
|
||||
) => {
|
||||
if (configuration) {
|
||||
return getMetadataQuery(
|
||||
'track_function',
|
||||
source,
|
||||
{
|
||||
function: { name, schema },
|
||||
configuration,
|
||||
},
|
||||
{ version: 2 }
|
||||
);
|
||||
}
|
||||
return getMetadataQuery('track_function', source, {
|
||||
function: { name, schema },
|
||||
});
|
||||
};
|
||||
|
||||
export const getUntrackFunctionQuery = (
|
||||
name: string,
|
||||
|
Loading…
Reference in New Issue
Block a user