diff --git a/cypress/e2e/cloud/scriptQueryBuilder.influxql.test.ts b/cypress/e2e/cloud/scriptQueryBuilder.influxql.test.ts new file mode 100644 index 0000000000..43b4225ed8 --- /dev/null +++ b/cypress/e2e/cloud/scriptQueryBuilder.influxql.test.ts @@ -0,0 +1,321 @@ +import {Organization} from '../../../src/types' + +const DEFAULT_INFLUXQL_EDITOR_TEXT = '/* Start by typing InfluxQL here */' + +const DELAY_FOR_LAZY_LOAD_EDITOR = 30000 +const DELAY_FOR_FILE_DOWNLOAD = 5000 +const NUMBER_OF_ROWS = 5 // see `generateWriteData` for why this number + +describe('Script Builder', () => { + const bucketName = 'bucket-influxql' + const databaseName = 'database-name' + const retentionPolicyName = 'retention-policy-name' + const measurement = 'ndbc' + const fieldName = 'air_degrees' + const fieldName2 = 'humidity' + const tagKey = 'air_station_id' + const tagValue = 'ST01' + const tagValue2 = 'ST02' + let route: string + + const selectScriptDBRP = (dbName: string, rpName: string) => { + const dbrpName: string = `${dbName}/${rpName}` + cy.getByTestID('dbrp-selector--dropdown-button').click() + cy.getByTestID(`dbrp-selector--dropdown--${dbrpName}`).click() + cy.getByTestID('dbrp-selector--dropdown-button').should( + 'contain', + `${dbrpName}` + ) + } + + const selectSchema = () => { + cy.log('select database/retention policy') + selectScriptDBRP(databaseName, retentionPolicyName) + cy.confirmSyncIsOn() // influxql composition is dumb. On bucket selection, it will occasionally drop the sync. + cy.log('writes empty query statement with only the timerange') + cy.getByTestID('influxql-editor', { + timeout: DELAY_FOR_LAZY_LOAD_EDITOR, + }).contains(`SELECT *`) + cy.getByTestID('influxql-editor').contains(`WHERE`) + cy.getByTestID('influxql-editor').contains(`time >= now() - 1h`) + cy.confirmSyncIsOn() // influxql sync sometimes toggles off + + cy.log('select measurement') + cy.selectScriptMeasurement(measurement) + } + + const confirmSchemaComposition = () => { + cy.log('has basic query') + cy.getByTestID('influxql-editor', { + timeout: DELAY_FOR_LAZY_LOAD_EDITOR, + }).contains(`SELECT *`) + cy.getByTestID('influxql-editor').contains(`WHERE`) + cy.getByTestID('influxql-editor').contains(`time >= now() - 1h`) + + cy.log('has measurement chosen as a table') + cy.getByTestID('influxql-editor').contains( + `FROM "${databaseName}"."${retentionPolicyName}"."${measurement}"` + ) + cy.getByTestID('influxql-editor').within(() => { + cy.log('have four lines of query') + cy.get('.composition-sync--on').should('have.length', 4) + }) + + cy.log('does not have other fields or tag filters') + cy.getByTestID('influxql-editor').should('not.contain', 'AND') + } + + const typeInQuery = () => { + cy.log('type in a query') + cy.getByTestID('influxql-editor').monacoType( + `{selectall}{del}SELECT * FROM "${databaseName}"."${retentionPolicyName}"."${measurement}"` + ) + cy.getByTestID('influxql-editor').contains( + `SELECT * FROM "${databaseName}"."${retentionPolicyName}"."${measurement}"` + ) + } + + before(() => { + const generateWriteData = (value: number) => { + // this will generate a table of 5 rows in csv format + // 1 row of table header + 4 rows of data + return [ + `${measurement},${tagKey}=${tagValue} ${fieldName}=${value}`, + `${measurement},${tagKey}=${tagValue} ${fieldName2}=${value}`, + `${measurement},${tagKey}=${tagValue2} ${fieldName}=${value}`, + `${measurement},${tagKey}=${tagValue2} ${fieldName2}=${value}`, + ] + } + + cy.flush().then(() => { + return cy.signin().then(() => { + return cy.get('@org').then(({id, name}: Organization) => { + route = `/orgs/${id}/data-explorer` + + cy.log('add mock data') + cy.createBucket(id, name, bucketName).should(response => { + expect(response.body).to.have.property('id') + const bucketID: string = response.body['id'] + cy.createDBRP( + bucketID, + databaseName, + retentionPolicyName, + id + ).should(response => { + expect(response.status).to.eq(201) + cy.log('a DBRP mapping is created') + }) + }) + + cy.log('create time series, with change of value over time') + cy.writeData(generateWriteData(100), bucketName) + cy.wait(2000) + cy.writeData(generateWriteData(20), bucketName) + }) + }) + }) + }) + + beforeEach(() => { + cy.scriptsLoginWithFlags({ + influxqlUI: true, + }).then(() => { + cy.clearInfluxQLScriptSession() + cy.getByTestID('editor-sync--toggle') + cy.getByTestID('influxql-editor', {timeout: DELAY_FOR_LAZY_LOAD_EDITOR}) + }) + }) + + describe('Schema Composition', () => { + it('can construct a composition with fields and tagValues', () => { + cy.log('start with default text') + cy.getByTestID('influxql-editor').within(() => { + cy.get('textarea.inputarea').should( + 'have.value', + DEFAULT_INFLUXQL_EDITOR_TEXT + ) + }) + + cy.log( + 'disable run button before selecting a database/retention policy mapping' + ) + cy.getByTestID('time-machine-submit-button').should('be.disabled') + + cy.log('select database/retention policy') + selectScriptDBRP(databaseName, retentionPolicyName) + + cy.log('the default text should be gone') + cy.getByTestID('influxql-editor').should( + 'not.contain', + DEFAULT_INFLUXQL_EDITOR_TEXT + ) + + cy.log( + 'enable run button after selecting a database and retention policy mapping' + ) + cy.getByTestID('time-machine-submit-button').should('not.be.disabled') + + cy.log('select measurement') + cy.selectScriptMeasurement(measurement) + + confirmSchemaComposition() + + cy.log('select field --> add to composition') + cy.selectScriptFieldOrTag(fieldName, true) + cy.getByTestID('influxql-editor').contains(`SELECT "${fieldName}"`) + cy.selectScriptFieldOrTag(fieldName2, true) + cy.getByTestID('influxql-editor').contains( + `SELECT "${fieldName}", "${fieldName2}"` + ) + + cy.log('select field --> remove from composition') + cy.selectScriptFieldOrTag(fieldName2, false) + cy.getByTestID('influxql-editor').contains(`SELECT "${fieldName}"`) + cy.getByTestID('influxql-editor').should('not.contain', fieldName2) + + cy.log('select tagValue --> add to composition') + cy.getByTestID('container-side-bar--tag-keys').within(() => { + cy.getByTestID('accordion-header').should('be.visible').click() + }) + cy.selectScriptFieldOrTag(tagValue, true) + cy.getByTestID('influxql-editor').contains( + `("${tagKey}" = '${tagValue}')` + ) + + cy.log('select tagValue --> remove from composition') + cy.selectScriptFieldOrTag(tagValue, false) + cy.getByTestID('influxql-editor').should('not.contain', tagKey) + }) + + it('composition sync functionality', () => { + cy.log('default to be on') + cy.getByTestID('editor-sync--toggle').should('have.class', 'active') + + cy.log('make a composition') + selectSchema() + confirmSchemaComposition() + + cy.log('sync toggles on, with matching styles') + cy.get('.composition-sync--on').should('have.length', 4) + cy.get('.composition-sync--off').should('have.length', 0) + + cy.log('sync toggles off, with matching styles') + cy.getByTestID('editor-sync--toggle') + .should('have.class', 'active') + .click() + .should('not.have.class', 'active') + cy.get('.composition-sync--on').should('have.length', 0) + cy.get('.composition-sync--off').should('have.length', 4) + + cy.log('can still browse schema while not synced, with matching styles') + selectScriptDBRP(databaseName, retentionPolicyName) + cy.selectScriptMeasurement(measurement) + cy.get('.composition-sync--on').should('have.length', 0) + cy.get('.composition-sync--off').should('have.length', 4) + + cy.log('sync toggles on') + cy.getByTestID('editor-sync--toggle') + .click() + .should('have.class', 'active') + cy.get('.composition-sync--on').should('have.length', 4) + cy.get('.composition-sync--off').should('have.length', 0) + }) + }) + + describe('Other Core Features', () => { + const CSV_PARSING: number = 2000 + + it('Run query', () => { + cy.getByTestID('time-machine-submit-button') + .should('be.visible') + .should('be.disabled') + + selectScriptDBRP(databaseName, retentionPolicyName) + typeInQuery() + + cy.log('can execute the query') + cy.getByTestID('time-machine-submit-button') + .should('be.visible') + .should('not.have.class', 'cf-button--disabled') + cy.getByTestID('time-machine-submit-button').click() + + cy.log('result view shows table') + cy.getByTestID('data-explorer-results--view').should('be.visible') + cy.getByTestID('data-explorer-results--view', { + timeout: CSV_PARSING, + }).contains(tagKey) + + cy.log('should not have graph tab') + cy.getByTestID('data-explorer-results--graph-view').should('not.exist') + }) + + it('Save/Load as an InfluxQL Script', () => { + // The save/load functionality works the same for all the languages + // (i.e. Flux, SQL, InfluxQL) at the backend, and the file + // `scriptQueryBuilder.scriptsCrud.test.ts` has already include a + // full coverage in general, so we are just doing a simple test + // for InfluxQL save/load support here + const scriptName: string = 'InfluxQL script' + cy.intercept('POST', '/api/v2/scripts*').as('scripts') + + cy.log('save an InfluxQL query') + typeInQuery() + cy.getByTestID('script-query-builder--save-script') + .should('be.visible') + .click() + cy.getByTestID('overlay--container').within(() => { + cy.getByTestID('save-script-name__input') + .should('be.visible') + .type(scriptName) + cy.getByTestID('script-query-builder--save') + .should('be.visible') + .click() + }) + + cy.log('check the script is saved successfully') + cy.wait('@scripts') + cy.getByTestID('notification-success') + .should('be.visible') + .contains(scriptName) + }) + + it('Download CSV', () => { + // The csv download functionality works the same for all the languages + // (i.e. Flux, SQL, InfluxQL), and the file `scriptQueryBuilder.result.test.ts` + // has already include a full coverage in general, so we are just doing a + // simple test for InfluxQL csv download here + cy.intercept('POST', '/query?*', req => { + req.redirect(route) + }).as('queryDownloadCSV') + + cy.getByTestID('csv-download-button') + .should('be.visible') + .should('be.disabled') + + selectScriptDBRP(databaseName, retentionPolicyName) + typeInQuery() + + cy.log('will download complete csv data') + cy.getByTestID('csv-download-button').should('not.be.disabled').click() + cy.wait('@queryDownloadCSV', {timeout: DELAY_FOR_FILE_DOWNLOAD}) + .its('request', {timeout: DELAY_FOR_FILE_DOWNLOAD}) + .then(req => { + cy.request(req) + .then(({body, headers}) => { + expect(headers).to.have.property( + 'content-type', + 'text/csv; charset=utf-8' + ) + return Promise.resolve(body) + }) + .then((csv: string) => { + cy.wrap(csv) + .then(doc => doc.trim().split('\n')) + .then((list: string[]) => { + expect(list.length).eq(NUMBER_OF_ROWS) + }) + }) + }) + }) + }) +}) diff --git a/cypress/index.d.ts b/cypress/index.d.ts index a87fbf1c15..8af8388ac4 100644 --- a/cypress/index.d.ts +++ b/cypress/index.d.ts @@ -30,6 +30,7 @@ import { createAndAddLabel, createLabel, createBucket, + createDBRP, createScraper, createView, createNotebook, @@ -56,9 +57,11 @@ import { createTaskFromEmpty, createAlertGroup, switchToDataExplorer, + setScriptToInfluxQL, setScriptToFlux, setScriptToSql, confirmSyncIsOn, + clearInfluxQLScriptSession, clearFluxScriptSession, clearSqlScriptSession, selectScriptBucket, @@ -109,6 +112,7 @@ declare global { createAndAddLabel: typeof createAndAddLabel createLabel: typeof createLabel createBucket: typeof createBucket + createDBRP: typeof createDBRP createScraper: typeof createScraper fluxEqual: typeof fluxEqual createTelegraf: typeof createTelegraf @@ -129,9 +133,11 @@ declare global { quartzProvision: typeof quartzProvision createTaskFromEmpty: typeof createTaskFromEmpty switchToDataExplorer: typeof switchToDataExplorer + setScriptToInfluxQL: typeof setScriptToInfluxQL setScriptToFlux: typeof setScriptToFlux setScriptToSql: typeof setScriptToSql confirmSyncIsOn: typeof confirmSyncIsOn + clearInfluxQLScriptSession: typeof clearInfluxQLScriptSession clearFluxScriptSession: typeof clearFluxScriptSession clearSqlScriptSession: typeof clearSqlScriptSession selectScriptBucket: typeof selectScriptBucket diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index b8b5f166b7..f872d183d9 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -356,6 +356,25 @@ export const createBucket = ( }) } +export const createDBRP = ( + bucketID: string, + database: string, + retentionPolicy: string, + orgID?: string +): Cypress.Chainable> => { + return cy.request({ + method: 'POST', + url: '/api/v2/dbrps', + body: { + bucketID, + database, + default: false, + retention_policy: retentionPolicy, + orgID, + }, + }) +} + export const upsertSecret = ( orgID: string, secret: Secret @@ -797,11 +816,18 @@ export const newScriptWithoutLanguageSelection = () => { } const setScriptToLanguage = ( - lang: 'sql' | 'flux', + lang: 'sql' | 'flux' | 'influxql', defaultEditorText: string ) => { return cy.isIoxOrg().then(isIox => { - if (isIox) { + if (lang === 'influxql') { + // give cypress some time to turn on the feature flag `influxqlUI` + // this block can be removed after this feature flag is removed + cy.wait(1000) + } + + // influxql works on both IOx and TSM + if (isIox || lang === 'influxql') { cy.getByTestID('script-query-builder--new-script') .should('be.visible') .click() @@ -820,6 +846,12 @@ const setScriptToLanguage = ( }) } +const DEFAULT_INFLUXQL_EDITOR_TEXT = '/* Start by typing InfluxQL here */' + +export const setScriptToInfluxQL = () => { + return setScriptToLanguage('influxql', DEFAULT_INFLUXQL_EDITOR_TEXT) +} + const DEFAULT_FLUX_EDITOR_TEXT = '// Start by selecting data from the schema browser or typing flux here' @@ -841,6 +873,17 @@ export const confirmSyncIsOn = () => { }) } +export const clearInfluxQLScriptSession = () => { + return cy.setScriptToInfluxQL().then(() => { + return cy.getByTestID('influxql-editor').within(() => { + cy.get('textarea.inputarea').should( + 'have.value', + DEFAULT_INFLUXQL_EDITOR_TEXT + ) + }) + }) +} + export const clearFluxScriptSession = () => { return cy.setScriptToFlux().then(() => { return cy.getByTestID('flux-editor').within(() => { @@ -881,7 +924,9 @@ export const selectScriptMeasurement = (measurement: string) => { .should('be.visible') .should('contain', 'Select measurement') .click() - cy.getByTestID('measurement-selector--dropdown--menu').type(measurement) + cy.getByTestID('measurement-selector--dropdown--menu').type( + `{selectall}${measurement}` + ) cy.getByTestID(`searchable-dropdown--item ${measurement}`) .should('be.visible') .click() @@ -952,6 +997,7 @@ export const scriptsLoginWithFlags = (flags): Cypress.Chainable => { cy.getByTestID('tree-nav') cy.getByTestID('data-explorer-page').should('exist') cy.switchToDataExplorer('new') + cy.log('flags are on for script editor') }) ) }) @@ -1545,6 +1591,9 @@ Cypress.Commands.add('deleteOrg', deleteOrg) // buckets Cypress.Commands.add('createBucket', createBucket) +// database / retention policy (DBRP) +Cypress.Commands.add('createDBRP', createDBRP) + // scrapers Cypress.Commands.add('createScraper', createScraper) @@ -1566,9 +1615,11 @@ Cypress.Commands.add('createNotebook', createNotebook) // scripts Cypress.Commands.add('switchToDataExplorer', switchToDataExplorer) +Cypress.Commands.add('setScriptToInfluxQL', setScriptToInfluxQL) Cypress.Commands.add('setScriptToFlux', setScriptToFlux) Cypress.Commands.add('setScriptToSql', setScriptToSql) Cypress.Commands.add('confirmSyncIsOn', confirmSyncIsOn) +Cypress.Commands.add('clearInfluxQLScriptSession', clearInfluxQLScriptSession) Cypress.Commands.add('clearFluxScriptSession', clearFluxScriptSession) Cypress.Commands.add('clearSqlScriptSession', clearSqlScriptSession) Cypress.Commands.add('selectScriptBucket', selectScriptBucket) diff --git a/src/dataExplorer/components/ScriptQueryBuilder.tsx b/src/dataExplorer/components/ScriptQueryBuilder.tsx index 1cf1239b62..787119dcd2 100644 --- a/src/dataExplorer/components/ScriptQueryBuilder.tsx +++ b/src/dataExplorer/components/ScriptQueryBuilder.tsx @@ -196,7 +196,7 @@ const ScriptQueryBuilder: FC = () => { ) const tsmNewScriptDropDown = - isFlagEnabled('influxqlUI') && hasDBRPs() ? ( + isFlagEnabled('influxqlUI') && hasDBRPs() && CLOUD ? (