diff --git a/cli/commands/migrate_test.go b/cli/commands/migrate_test.go index 7b5df1f800fb0..3dcc2a9b4e1a9 100644 --- a/cli/commands/migrate_test.go +++ b/cli/commands/migrate_test.go @@ -28,7 +28,8 @@ var ravenVersions = []mt.Version{ } var testMetadata = map[string][]byte{ - "metadata": []byte(`query_templates: [] + "metadata": []byte(`functions: [] +query_templates: [] remote_schemas: [] tables: - array_relationships: [] @@ -40,7 +41,8 @@ tables: table: test update_permissions: [] `), - "empty-metadata": []byte(`query_templates: [] + "empty-metadata": []byte(`functions: [] +query_templates: [] remote_schemas: [] tables: [] `), diff --git a/console/cypress/helpers/dataHelpers.js b/console/cypress/helpers/dataHelpers.js index a03bde4a69cdc..b71af934555b5 100644 --- a/console/cypress/helpers/dataHelpers.js +++ b/console/cypress/helpers/dataHelpers.js @@ -38,3 +38,71 @@ export const makeDataAPIOptions = (dataApiUrl, key, body) => ({ body, failOnStatusCode: false, }); + +export const testCustomFunctionDefinition = i => `create function search_posts${'_' + + i} (search text) returns setof post as $$ select * from post where title ilike ('%' || search || '%') or content ilike ('%' || search || '%') $$ language sql stable; +`; + +export const getCustomFunctionName = i => `search_posts${'_' + i}`; + +export const testCustomFunctionSQL = i => { + return { + type: 'bulk', + args: [ + { + type: 'run_sql', + args: { + sql: `CREATE OR REPLACE FUNCTION public.search_posts_${i}(search text)\n RETURNS SETOF post\n LANGUAGE sql\n STABLE\nAS $function$\n select *\n from post\n where\n title ilike ('%' || search || '%') or\n content ilike ('%' || search || '%')\n $function$\n`, + cascade: false, + }, + }, + { + type: 'track_function', + args: { + name: `search_posts_${i}`, + schema: 'public', + }, + }, + ], + }; +}; + +export const createTable = () => { + return { + type: 'bulk', + args: [ + { + type: 'run_sql', + args: { + sql: + 'create table post (\n id serial PRIMARY KEY,\n title TEXT,\n content TEXT\n )', + cascade: false, + }, + }, + { + type: 'add_existing_table_or_view', + args: { + name: 'post', + schema: 'public', + }, + }, + ], + }; +}; + +export const dropTable = () => { + return { + type: 'bulk', + args: [ + { + type: 'run_sql', + args: { + sql: 'DROP table post;', + cascade: false, + }, + }, + ], + }; +}; + +export const getSchema = () => 'public'; diff --git a/console/cypress/integration/data/functions/spec.js b/console/cypress/integration/data/functions/spec.js new file mode 100644 index 0000000000000..f58d2f5c6a6a5 --- /dev/null +++ b/console/cypress/integration/data/functions/spec.js @@ -0,0 +1,104 @@ +import { + getElementFromAlias, + baseUrl, + // testCustomFunctionDefinition, + getCustomFunctionName, + getSchema, + testCustomFunctionSQL, + createTable, + dropTable, +} from '../../../helpers/dataHelpers'; + +import { + dropTableRequest, + dataRequest, + validateCFunc, + validateUntrackedFunc, +} from '../../validators/validators'; + +export const openRawSQL = () => { + // eslint-disable-line + // Open RawSQL + cy.get('a') + .contains('Data') + .click(); + cy.wait(3000); + cy.get(getElementFromAlias('sql-link')).click(); + cy.wait(3000); + // Match URL + cy.url().should('eq', `${baseUrl}/data/sql`); +}; + +export const createCustomFunctionSuccess = () => { + // cy.get('textarea').type(testCustomFunctionDefinition(1), { timeout: 10000, force: true}); + // Round about way to create a function + dataRequest(createTable(1), 'success'); + cy.wait(5000); + dataRequest(testCustomFunctionSQL(1), 'success'); + cy.wait(5000); + // cy.get(getElementFromAlias('run-sql')).click(); + // Check if the track checkbox is clicked or not + validateCFunc(getCustomFunctionName(1), getSchema(), 'success'); + cy.wait(5000); +}; + +export const unTrackFunction = () => { + // Are you absolutely sure?\nThis action cannot be undone. This will permanently delete stitched GraphQL schema. Please type "DELETE" (in caps, without quotes) to confirm.\n + cy.visit(`data/schema/public/functions/${getCustomFunctionName(1)}/modify`); + cy.wait(5000); + cy.get(getElementFromAlias('custom-function-edit-untrack-btn')).click(); + cy.wait(5000); + validateUntrackedFunc(getCustomFunctionName(1), getSchema(), 'success'); + cy.wait(5000); +}; + +export const trackFunction = () => { + // Are you absolutely sure?\nThis action cannot be undone. This will permanently delete stitched GraphQL schema. Please type "DELETE" (in caps, without quotes) to confirm.\n + /* + cy.visit( + `data/schema/public/functions/${getCustomFunctionName(1)}/modify`, + ); + */ + cy.get( + getElementFromAlias(`add-track-function-${getCustomFunctionName(1)}`) + ).should('exist'); + cy.get( + getElementFromAlias(`add-track-function-${getCustomFunctionName(1)}`) + ).click(); + cy.wait(5000); + validateCFunc(getCustomFunctionName(1), getSchema(), 'success'); + cy.wait(5000); +}; + +export const verifyPermissionTab = () => { + // Are you absolutely sure?\nThis action cannot be undone. This will permanently delete stitched GraphQL schema. Please type "DELETE" (in caps, without quotes) to confirm.\n + cy.visit( + `data/schema/public/functions/${getCustomFunctionName(1)}/permissions` + ); + cy.wait(5000); + cy.get(getElementFromAlias('custom-function-permission-btn')).should('exist'); + cy.wait(5000); +}; + +export const deleteCustomFunction = () => { + // Are you absolutely sure?\nThis action cannot be undone. This will permanently delete stitched GraphQL schema. Please type "DELETE" (in caps, without quotes) to confirm.\n + cy.visit(`data/schema/public/functions/${getCustomFunctionName(1)}/modify`, { + onBeforeLoad(win) { + cy.stub(win, 'prompt').returns('DELETE'); + }, + }); + + cy.wait(5000); + + cy.get(getElementFromAlias('custom-function-edit-delete-btn')).click(); + cy.wait(5000); + cy.window() + .its('prompt') + .should('be.called'); + cy.get(getElementFromAlias('delete-confirmation-error')).should('not.exist'); + cy.url().should('eq', `${baseUrl}/data/schema/public`); + cy.wait(5000); + + dropTableRequest(dropTable(1), 'success'); + cy.wait(5000); +}; diff --git a/console/cypress/integration/data/functions/test.js b/console/cypress/integration/data/functions/test.js new file mode 100644 index 0000000000000..15a2d9d6fe137 --- /dev/null +++ b/console/cypress/integration/data/functions/test.js @@ -0,0 +1,44 @@ +/* eslint no-unused-vars: 0 */ +/* eslint import/prefer-default-export: 0 */ +import { testMode } from '../../../helpers/common'; +import { setMetaData } from '../../validators/validators'; + +import { + openRawSQL, + createCustomFunctionSuccess, + deleteCustomFunction, + unTrackFunction, + trackFunction, + verifyPermissionTab, +} from './spec'; + +const setup = () => { + describe('Setup route', () => { + it('Visit the index route', () => { + // Visit the index route + cy.visit('/data'); + cy.wait(5000); + // Get and set validation metadata + setMetaData(); + }); + }); +}; + +export const runCreateCustomFunctionsTableTests = () => { + describe('Create Custom Function', () => { + // it( + // 'Visit Run SQL page', + // openRawSQL, + // ); + it('Create a custom function and track', createCustomFunctionSuccess); + it('Untrack custom function', unTrackFunction); + it('Track custom function', trackFunction); + it('Verify permission tab', verifyPermissionTab); + it('Delete custom function', deleteCustomFunction); + }); +}; + +if (testMode !== 'cli') { + setup(); + runCreateCustomFunctionsTableTests(); +} diff --git a/console/cypress/integration/data/insert-browse/spec.js b/console/cypress/integration/data/insert-browse/spec.js index 6a6cf79652f40..53bbf0f87e352 100644 --- a/console/cypress/integration/data/insert-browse/spec.js +++ b/console/cypress/integration/data/insert-browse/spec.js @@ -438,7 +438,7 @@ export const checkViewRelationship = () => { cy.get(getElementFromAlias('ref-table')).select(getTableName(0, testName)); cy.get(getElementFromAlias('ref-col')).select(getColName(0)); cy.get(getElementFromAlias('save-button')).click(); - cy.wait(300); + cy.wait(1000); // Add relationship cy.get(getElementFromAlias('add-rel-mod')).click(); cy.get(getElementFromAlias('obj-rel-add-0')).click(); @@ -446,19 +446,19 @@ export const checkViewRelationship = () => { .clear() .type('someRel'); cy.get(getElementFromAlias('obj-rel-save-0')).click(); - cy.wait(300); + cy.wait(2000); // Insert a row cy.get(getElementFromAlias('table-insert-rows')).click(); cy.get(getElementFromAlias('typed-input-1')).type('1'); cy.get(getElementFromAlias('insert-save-button')).click(); - cy.wait(300); + cy.wait(1000); cy.get(getElementFromAlias('table-browse-rows')).click(); - cy.wait(300); + cy.wait(1000); cy.get('a') .contains('View') .first() .click(); - cy.wait(300); + cy.wait(1000); cy.get('a') .contains('Close') .first() diff --git a/console/cypress/integration/data/modify/spec.js b/console/cypress/integration/data/modify/spec.js index e0f6f4ce4d202..fa6c38d468686 100644 --- a/console/cypress/integration/data/modify/spec.js +++ b/console/cypress/integration/data/modify/spec.js @@ -100,7 +100,7 @@ export const passMTAddColumn = () => { cy.get(getElementFromAlias('column-name')).type(getColName(0)); cy.get(getElementFromAlias('data-type')).select('integer'); cy.get(getElementFromAlias('add-column-button')).click(); - cy.wait(2500); + cy.wait(5000); // cy.get('.notification-success').click(); validateColumn(getTableName(0, testName), [getColName(0)], 'success'); }; @@ -121,6 +121,7 @@ export const passMCWithRightDefaultValue = () => { .clear() .type('1234'); cy.get(getElementFromAlias('save-button')).click(); + cy.wait(15000); }; export const passCreateForeignKey = () => { @@ -129,11 +130,12 @@ export const passCreateForeignKey = () => { cy.get(getElementFromAlias('ref-table')).select(getTableName(0, testName)); cy.get(getElementFromAlias('ref-col')).select(getColName(0)); cy.get(getElementFromAlias('save-button')).click(); - cy.wait(500); + cy.wait(15000); }; export const passRemoveForeignKey = () => { cy.get(getElementFromAlias('remove-constraint-button')).click(); + cy.wait(10000); }; export const passMTDeleteCol = () => { diff --git a/console/cypress/integration/remote-schemas/create-remote-schema/spec.js b/console/cypress/integration/remote-schemas/create-remote-schema/spec.js index 536fb0f6e3115..44b11fc7e6727 100644 --- a/console/cypress/integration/remote-schemas/create-remote-schema/spec.js +++ b/console/cypress/integration/remote-schemas/create-remote-schema/spec.js @@ -46,7 +46,7 @@ export const createSimpleRemoteSchema = () => { .clear() .type(getRemoteGraphQLURL()); cy.get(getElementFromAlias('add-remote-schema-submit')).click(); - cy.wait(10000); + cy.wait(15000); validateRS(getRemoteSchemaName(1, testName), 'success'); cy.url().should( 'eq', @@ -242,7 +242,7 @@ export const passWithEditRemoteSchema = () => { .type(getRemoteSchemaName(5, testName)); cy.get(getElementFromAlias('remote-schema-edit-save-btn')).click(); - cy.wait(5000); + cy.wait(10000); validateRS(getRemoteSchemaName(5, testName), 'success'); cy.get(getElementFromAlias('remote-schemas-modify')).click(); @@ -252,7 +252,7 @@ export const passWithEditRemoteSchema = () => { getRemoteSchemaName(5, testName) ); cy.get(getElementFromAlias('remote-schema-edit-modify-btn')).should('exist'); - cy.wait(5000); + cy.wait(7000); }; export const deleteRemoteSchema = () => { diff --git a/console/cypress/integration/validators/validators.js b/console/cypress/integration/validators/validators.js index 32273d4772184..c06217ccd0fe7 100644 --- a/console/cypress/integration/validators/validators.js +++ b/console/cypress/integration/validators/validators.js @@ -30,7 +30,7 @@ export const createView = sql => { // ******************* VALIDATION FUNCTIONS ******************************* -// ******************* Remote schema Validator **************************** +// ******************* Remote Schema Validator **************************** export const validateRS = (remoteSchemaName, result) => { const reqBody = { type: 'select', @@ -59,6 +59,97 @@ export const validateRS = (remoteSchemaName, result) => { }); }; +// ******************* Custom Function Validator ************************** +export const validateCFunc = (functionName, functionSchema, result) => { + const reqBody = { + type: 'select', + args: { + table: { + name: 'hdb_function', + schema: 'hdb_catalog', + }, + columns: ['*'], + where: { + function_name: functionName, + function_schema: functionSchema, + }, + }, + }; + const requestOptions = makeDataAPIOptions(dataApiUrl, accessKey, reqBody); + cy.request(requestOptions).then(response => { + if (result === 'success') { + expect( + response.body.length > 0 && + response.body[0].function_name === functionName + ).to.be.true; + } else { + expect( + response.body.length > 0 && + response.body[0].function_name === functionName + ).to.be.false; + } + }); +}; + +export const validateUntrackedFunc = (functionName, functionSchema, result) => { + const reqBody = { + type: 'select', + args: { + table: { + name: 'hdb_function', + schema: 'hdb_catalog', + }, + columns: ['*'], + where: { + function_name: functionName, + function_schema: functionSchema, + }, + }, + }; + const requestOptions = makeDataAPIOptions(dataApiUrl, accessKey, reqBody); + cy.request(requestOptions).then(response => { + if (result === 'success') { + expect(response.body.length === 0).to.be.true; + } else { + expect(response.body.length === 0).to.be.false; + } + }); +}; + +export const dataRequest = (reqBody, result) => { + const requestOptions = makeDataAPIOptions(dataApiUrl, accessKey, reqBody); + cy.request(requestOptions).then(response => { + if (result === 'success') { + expect( + response.body.length > 0 && + response.body[0].result_type === 'CommandOk' && + response.body[1].message === 'success' + ).to.be.true; + } else { + expect( + response.body.length > 0 && + response.body[0].result_type === 'CommandOk' && + response.body[1].message === 'success' + ).to.be.false; + } + }); +}; + +export const dropTableRequest = (reqBody, result) => { + const requestOptions = makeDataAPIOptions(dataApiUrl, accessKey, reqBody); + cy.request(requestOptions).then(response => { + if (result === 'success') { + expect( + response.body.length > 0 && response.body[0].result_type === 'CommandOk' + ).to.be.true; + } else { + expect( + response.body.length > 0 && response.body[0].result_type === 'CommandOk' + ).to.be.false; + } + }); +}; + // ****************** Table Validator ********************* export const validateCT = (tableName, result) => { diff --git a/console/src/components/ApiExplorer/GraphiQL.css b/console/src/components/ApiExplorer/GraphiQL.css index 61de3b318f211..fa45cfc6baf15 100644 --- a/console/src/components/ApiExplorer/GraphiQL.css +++ b/console/src/components/ApiExplorer/GraphiQL.css @@ -1487,8 +1487,7 @@ span.CodeMirror-selectedtext { -ms-user-select: none; user-select: none; } -.doc-explorer-title -{ +.doc-explorer-title { height: 34px; } .graphiql-container .doc-explorer-title, diff --git a/console/src/components/Login/Login.js b/console/src/components/Login/Login.js index 639cd76aff544..60dacce1e0cfa 100644 --- a/console/src/components/Login/Login.js +++ b/console/src/components/Login/Login.js @@ -21,7 +21,8 @@ class Login extends Component { if (loginInProgress) { loginText = ( - Verifying...