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/package.json b/package.json index e94ac28a54..d34accfb34 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "prettier:fix": "pretty-quick --config .prettierrc.json --write '{src,cypress}/**/*.{ts,tsx}'", "tsc": "tsc -p ./tsconfig.json --noEmit --pretty --skipLibCheck", "tsc:watch": "yarn tsc --watch", - "generate": "export SHA=993f6756500aebe47903a3ddaee62f9f75d207c1 && export REMOTE=https://raw.githubusercontent.com/influxdata/openapi/${SHA}/ && yarn generate-meta", + "generate": "export SHA=d05381fbcee0dd5d88833e71057a4af647e0d169 && export REMOTE=https://raw.githubusercontent.com/influxdata/openapi/${SHA}/ && yarn generate-meta", "generate-local": "export REMOTE=../openapi/ && yarn generate-meta", "generate-local-cloud": "export REMOTE=../openapi/ && yarn generate-meta-cloud", "generate-meta": "if [ -z \"${CLOUD_URL}\" ]; then yarn generate-meta-oss; else yarn generate-meta-cloud; fi", diff --git a/src/App.tsx b/src/App.tsx index 6016594342..12870e1e25 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,7 +25,6 @@ import { OverlayController, } from 'src/overlays/components/OverlayController' import PageSpinner from 'src/perf/components/PageSpinner' -import EngagementLink from 'src/cloud/components/onboarding/EngagementLink' import {GlobalHeader} from 'src/identity/components/GlobalHeader/GlobalHeader' const SetOrg = lazy(() => import('src/shared/containers/SetOrg')) @@ -117,7 +116,6 @@ const App: FC = () => { - }> diff --git a/src/cloud/components/onboarding/EngagementLink.tsx b/src/cloud/components/onboarding/EngagementLink.tsx deleted file mode 100644 index ec6f37ccfa..0000000000 --- a/src/cloud/components/onboarding/EngagementLink.tsx +++ /dev/null @@ -1,57 +0,0 @@ -// Libraries -import {FC, useEffect, useState, memo} from 'react' -import {useLocation} from 'react-router-dom' -import {useSelector} from 'react-redux' - -// Utils -import {getOrg} from 'src/organizations/selectors' -import handleGetUserStatus from 'src/cloud/components/onboarding/useGetUserStatus' -import {isFlagEnabled} from 'src/shared/utils/featureFlag' - -// Types -import {AppState} from 'src/types' - -const EngagementLink: FC = () => { - const [dataStates, setDataStates] = useState([]) - const pathname = useLocation().pathname - const userpilot = window['userpilot'] - const org = useSelector(getOrg) - const me = useSelector((state: AppState) => state.me) - - useEffect(() => { - if (isFlagEnabled('newUsageAPI') && org?.id) { - handleGetUserStatus(org.id).then(({usageDataStates}) => - setDataStates(usageDataStates) - ) - } - }, [org?.id]) - - useEffect(() => { - if (userpilot) { - sendToUserPilot() - userpilot.reload() - } - }, [pathname, org, me]) - - const sendToUserPilot = (): void => { - const host = window?.location?.hostname.split('.') - - if (org && me) { - userpilot.identify(me.name, { - email: me.name, // User Email address - orgID: org.id, // Organization ID - region: host[0], // Cloud provider region - provider: host[1], // Cloud provider - company: { - id: org.id, // Organization ID - }, - ...(isFlagEnabled('newUsageAPI') && - dataStates.length && {UsageDataStates: dataStates}), - }) - } - } - - return null -} - -export default memo(EngagementLink) diff --git a/src/cloud/components/onboarding/useGetUserStatus.ts b/src/cloud/components/onboarding/useGetUserStatus.ts deleted file mode 100644 index a78ceb36ab..0000000000 --- a/src/cloud/components/onboarding/useGetUserStatus.ts +++ /dev/null @@ -1,148 +0,0 @@ -import {fromFlux} from '@influxdata/giraffe' -import {event} from 'src/cloud/utils/reporting' -import {isFlagEnabled} from 'src/shared/utils/featureFlag' -import {Table} from '@influxdata/giraffe' -import {CLOUD} from 'src/shared/constants' -import {usageStatsCsv} from 'src/shared/utils/mocks/usageStats.mocks' - -let getUsage = null - -if (CLOUD) { - getUsage = require('src/client').getOrgsUsage -} - -export enum USER_PILOT_USER_STATUS { - NEW_USER = 'NEW_USER', - DELETED_ALL_DATA = 'DELETED_ALL_DATA', - NON_WRITING_USER = 'NON_WRITING_USER', - ACTIVE_USER = 'ACTIVE_USER', - WRITING_NOT_READING_USER = 'WRITING_NOT_READING_USER', -} -export const getUserStatus = (table: Table): USER_PILOT_USER_STATUS[] => { - const dataStates: USER_PILOT_USER_STATUS[] = [] - - const measurement = (table.getColumn('_measurement') as string[]) ?? [] - - // this looks up storage amounts to see if the user has stored data - const storageBytes = measurement.find((_v, i) => { - return ( - measurement[i] === 'storage_usage_bucket_bytes' && - table.getColumn('_value', 'number')[i] > 0 - ) - }) - - // This looks up requests that are POSTs to our query endpoints for writing data - const requestInByte = measurement.find((_v, i) => { - return ( - measurement[i] === 'http_request' && - ['/api/v2/query', '/query'].includes( - table.getColumn('endpoint', 'string')[i] - ) && - table.getColumn('_field')[i] === 'req_bytes' && - table.getColumn('_value', 'number')[i] > 0 - ) - }) - - // This looks up a total of query counts in general to see if the user has read values - const queryCount = measurement.find((_v, i) => { - return ( - measurement[i] === 'query_count' && - table.getColumn('_value', 'number')[i] > 0 - ) - }) - - if (!storageBytes && !requestInByte) { - dataStates.push(USER_PILOT_USER_STATUS.NEW_USER) - } - if (!storageBytes && requestInByte) { - dataStates.push(USER_PILOT_USER_STATUS.DELETED_ALL_DATA) - } - if (storageBytes && !requestInByte) { - dataStates.push(USER_PILOT_USER_STATUS.NON_WRITING_USER) - } - if (storageBytes && requestInByte && !queryCount) { - dataStates.push(USER_PILOT_USER_STATUS.WRITING_NOT_READING_USER) - } - if (storageBytes && requestInByte && queryCount) { - dataStates.push(USER_PILOT_USER_STATUS.ACTIVE_USER) - } - - event('cloud.onboarding.set_user_status_success', { - context: JSON.stringify(dataStates), - }) - - return dataStates -} - -export const queryUsage = async (orgID: string, range?: string) => { - let csvToParse = '' - if (getUsage) { - const usage = await getUsage({ - orgID, - query: { - start: range ?? '-30d', - }, - }) - if (usage?.status === 200) { - csvToParse = usage.data?.trim().replace(/\r\n/g, '\n') - } else { - csvToParse = usageStatsCsv - } - } else { - csvToParse = usageStatsCsv - } - - const {table} = fromFlux(csvToParse) - return table -} - -export const getUserWriteLimitHits = async (orgID: string): Promise => { - try { - const table = await queryUsage(orgID) - - const measurement = (table.getColumn('_measurement') as string[]) ?? [] - - const queryWriteLimitHits = measurement.reduce((a, b, i) => { - if ( - b === 'event' && - table.getColumn('_field')[i] === 'event_type_limited_write' && - table.getColumn('_value', 'number')[i] > 0 - ) { - return a + table.getColumn('_value', 'number')[i] - } else { - return a - } - }, 0) - return queryWriteLimitHits - } catch (err) { - console.error(err) - } -} - -let hasCalledGetStatus = false -const handleGetUserStatus = async (orgID: string) => { - let usageDataStates = [] - - const getUserStatusDefinition = async () => { - const tables = await queryUsage(orgID) - if (tables) { - usageDataStates = getUserStatus(tables) - } - } - - try { - if (!hasCalledGetStatus && isFlagEnabled('newUsageAPI')) { - await getUserStatusDefinition() - hasCalledGetStatus = true - } - } catch (err) { - console.error(err) - event('cloud.onboarding.set_user_status_failure', { - context: JSON.stringify(err), - }) - } - - return {usageDataStates} -} - -export default handleGetUserStatus 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 ? ( { constructor(props) { super(props) @@ -36,20 +34,12 @@ export default class FeedbackBar extends React.Component { if (this.props.selectedFeedback === null) { event(`firstMile.${this.props.wizardEventName}.thumbsUp.clicked`) this.props.onFeedbackSelection(feedbackValue.THUMBS_UP) - - if (window.userpilot) { - window.userpilot.trigger(USERPILOT_FEEDBACK_ID) - } } } private handleThumbsDownClick = () => { if (this.props.selectedFeedback === null) { event(`firstMile.${this.props.wizardEventName}.thumbsDown.clicked`) this.props.onFeedbackSelection(feedbackValue.THUMBS_DOWN) - - if (window.userpilot) { - window.userpilot.trigger(USERPILOT_FEEDBACK_ID) - } } } diff --git a/src/index.tsx b/src/index.tsx index 008136dca2..0bed664f78 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -63,7 +63,6 @@ declare global { interface Window { basepath: string dataLayer: any[] - userpilot: any } } diff --git a/src/operator/account/AccountView.tsx b/src/operator/account/AccountView.tsx index 6e5cd4bbe0..d953f4dfd8 100644 --- a/src/operator/account/AccountView.tsx +++ b/src/operator/account/AccountView.tsx @@ -9,6 +9,7 @@ import AssociatedUsersTable from 'src/operator/account/AssociatedUsersTable' import ConvertAccountToContractOverlay from 'src/operator/account/ConvertAccountToContractOverlay' import CancelAccountOverlay from 'src/operator/account/CancelAccountOverlay' import DeleteAccountOverlay from 'src/operator/account/DeleteAccountOverlay' +import ReactivateAccountOverlay from 'src/operator/account/ReactivateAccountOverlay' import AccountViewHeader from 'src/operator/account/AccountViewHeader' import AccountGrid from 'src/operator/account/AccountGrid' import {AccountContext} from 'src/operator/context/account' @@ -35,6 +36,7 @@ const AccountView: FC = () => { +

Associated Users

diff --git a/src/operator/account/AccountViewHeader.tsx b/src/operator/account/AccountViewHeader.tsx index 85760ba582..6596da1a22 100644 --- a/src/operator/account/AccountViewHeader.tsx +++ b/src/operator/account/AccountViewHeader.tsx @@ -25,6 +25,7 @@ const AccountViewHeader: FC = () => { setCancelOverlayVisible, cancelOverlayVisible, setDeleteOverlayVisible, + setReactivateOverlayVisible, deleteOverlayVisible, } = useContext(AccountContext) const {hasWritePermissions} = useContext(OperatorContext) @@ -61,6 +62,16 @@ const AccountViewHeader: FC = () => { Convert to Contract )} + {hasWritePermissions && account?.reactivatable && ( + setReactivateOverlayVisible(true)} + testID="account-reactivate--button" + > + Reactivate Account + + )} {hasWritePermissions && account?.deletable && ( { + const { + account, + organizations, + reactivateStatus, + handleReactivateAccount, + setReactivateOverlayVisible, + reactivateOverlayVisible, + } = useContext(AccountContext) + + const reactivateAccount = () => { + if (account?.reactivatable) { + try { + handleReactivateAccount() + } catch (e) { + setReactivateOverlayVisible(false) + } + } + } + + const message = ` + This action will reactivate the Account + ${account?.id ?? 'N/A'} and unsuspend the organizations:` + + const active = reactivateStatus === RemoteDataState.NotStarted + + return ( + ( + + )} + testID="reactivate-overlay" + transitionDuration={0} + > + + setReactivateOverlayVisible(false)} + /> + + + This action cannot be undone + +

+ Warning +

+ {message} +
    + {organizations.map(o => ( +
  • {o.name ?? 'N/A'}
  • + ))} +
+
+ + + I understand, reactivate account. + + +
+
+ ) +} + +export default ReactivateAccountOverlay diff --git a/src/operator/context/account.tsx b/src/operator/context/account.tsx index 123a51681a..da73d9cef5 100644 --- a/src/operator/context/account.tsx +++ b/src/operator/context/account.tsx @@ -6,6 +6,7 @@ import {useDispatch} from 'react-redux' // Utils import { patchOperatorAccountsConvert, + patchOperatorAccountsReactivate, deleteOperatorAccount, getOperatorAccount, } from 'src/client/unityRoutes' @@ -14,6 +15,7 @@ import { getAccountError, convertAccountError, deleteAccountError, + reactivateAccountError, } from 'src/shared/copy/notifications' // Types @@ -28,9 +30,11 @@ export interface AccountContextType { accountStatus: RemoteDataState convertStatus: RemoteDataState deleteStatus: RemoteDataState + reactivateStatus: RemoteDataState handleConvertAccountToContract: (contractStartDate: string) => void handleDeleteAccount: () => void handleGetAccount: () => void + handleReactivateAccount: () => void organizations: OperatorOrg[] setConvertToContractOverlayVisible: (vis: boolean) => void convertToContractOverlayVisible: boolean @@ -38,6 +42,8 @@ export interface AccountContextType { cancelOverlayVisible: boolean setDeleteOverlayVisible: (vis: boolean) => void deleteOverlayVisible: boolean + setReactivateOverlayVisible: (vis: boolean) => void + reactivateOverlayVisible: boolean } export const DEFAULT_CONTEXT: AccountContextType = { @@ -45,16 +51,20 @@ export const DEFAULT_CONTEXT: AccountContextType = { accountStatus: RemoteDataState.NotStarted, convertStatus: RemoteDataState.NotStarted, deleteStatus: RemoteDataState.NotStarted, + reactivateStatus: RemoteDataState.NotStarted, handleConvertAccountToContract: () => {}, handleDeleteAccount: () => {}, handleGetAccount: () => {}, + handleReactivateAccount: () => {}, organizations: null, setConvertToContractOverlayVisible: (_: boolean) => {}, convertToContractOverlayVisible: false, - cancelOverlayVisible: false, setCancelOverlayVisible: (_: boolean) => {}, setDeleteOverlayVisible: (_: boolean) => {}, + setReactivateOverlayVisible: (_: boolean) => {}, + cancelOverlayVisible: false, deleteOverlayVisible: false, + reactivateOverlayVisible: false, } export const AccountContext = @@ -67,9 +77,14 @@ export const AccountProvider: FC = React.memo(({children}) => { useState(false) const [cancelOverlayVisible, setCancelOverlayVisible] = useState(false) const [deleteOverlayVisible, setDeleteOverlayVisible] = useState(false) + const [reactivateOverlayVisible, setReactivateOverlayVisible] = + useState(false) const [accountStatus, setAccountStatus] = useState(RemoteDataState.NotStarted) const [convertStatus, setConvertStatus] = useState(RemoteDataState.NotStarted) const [deleteStatus, setDeleteStatus] = useState(RemoteDataState.NotStarted) + const [reactivateStatus, setReactivateStatus] = useState( + RemoteDataState.NotStarted + ) const {accountID} = useParams<{accountID: string}>() const history = useHistory() @@ -138,6 +153,21 @@ export const AccountProvider: FC = React.memo(({children}) => { } }, [dispatch, history, accountID]) + const handleReactivateAccount = useCallback(async () => { + try { + setReactivateStatus(RemoteDataState.Loading) + const resp = await patchOperatorAccountsReactivate({accountId: accountID}) + if (resp.status !== 200) { + throw new Error(resp.data.message) + } + setReactivateStatus(RemoteDataState.Done) + history.push('/operator') + } catch (error) { + console.error({error}) + dispatch(notify(reactivateAccountError(accountID))) + } + }, [dispatch, history, accountID]) + return ( = React.memo(({children}) => { accountStatus, convertStatus, deleteStatus, + reactivateStatus, handleConvertAccountToContract, handleDeleteAccount, handleGetAccount, + handleReactivateAccount, organizations, setConvertToContractOverlayVisible, convertToContractOverlayVisible, @@ -155,6 +187,8 @@ export const AccountProvider: FC = React.memo(({children}) => { cancelOverlayVisible, setDeleteOverlayVisible, deleteOverlayVisible, + setReactivateOverlayVisible, + reactivateOverlayVisible, }} > {children} diff --git a/src/shared/copy/notifications/categories/operator.ts b/src/shared/copy/notifications/categories/operator.ts index 5bbfb97b36..11eeeea5f4 100644 --- a/src/shared/copy/notifications/categories/operator.ts +++ b/src/shared/copy/notifications/categories/operator.ts @@ -73,6 +73,12 @@ export const deleteAccountError = (id: string): Notification => ({ message: `Failed to delete the account with the ID ${id}, please try again.`, }) +export const reactivateAccountError = (id: string): Notification => ({ + ...defaultErrorNotification, + duration: FIVE_SECONDS, + message: `Failed to reactivate the account with the ID ${id}, please try again.`, +}) + export const removeUserAccountError = (id: string): Notification => ({ ...defaultErrorNotification, duration: FIVE_SECONDS, diff --git a/src/shared/utils/mocks/usageStats.mocks.ts b/src/shared/utils/mocks/usageStats.mocks.ts deleted file mode 100644 index 215f726e50..0000000000 --- a/src/shared/utils/mocks/usageStats.mocks.ts +++ /dev/null @@ -1,35 +0,0 @@ -export const usageStatsCsv = `#group,false,false,true,true,false,false,true,true,true,true -#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,double,string,string,string,string -#default,_result,,,,,,,,, -,result,table,_start,_stop,_time,_value,_field,_measurement,bucket_id,org_id -,,0,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-14T10:00:00Z,13352776.313888889,gauge,storage_usage_bucket_bytes,d4e6fe5396d18e89,351438333495bd60 -,,0,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-14T11:00:00Z,13360066.18888889,gauge,storage_usage_bucket_bytes,d4e6fe5396d18e89,351438333495bd60 -,,0,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-14T12:00:00Z,13011240.197222224,gauge,storage_usage_bucket_bytes,d4e6fe5396d18e89,351438333495bd60 -,,0,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-14T13:00:00Z,12590111.519444447,gauge,storage_usage_bucket_bytes,d4e6fe5396d18e89,351438333495bd60 - -#group,false,false,true,true,false,false,true,true,true,true,true -#datatype,string,long,dateTime:RFC3339,dateTime:RFC3339,dateTime:RFC3339,long,string,string,string,string,string -#default,_result,,,,,,,,,, -,result,table,_start,_stop,_time,_value,_field,_measurement,endpoint,org_id,status -,,1,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-03T23:00:00Z,21667,req_bytes,http_request,/api/v2/query,351438333495bd60,200 -,,1,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-09T21:00:00Z,89576,req_bytes,http_request,/api/v2/query,351438333495bd60,200 -,,1,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-10T19:00:00Z,5330,req_bytes,http_request,/api/v2/query,351438333495bd60,200 -,,2,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-03T23:00:00Z,778,req_bytes,http_request,/api/v2/query,351438333495bd60,404 -,,2,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-09T21:00:00Z,4279,req_bytes,http_request,/api/v2/query,351438333495bd60,404 -,,2,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-10T19:00:00Z,389,req_bytes,http_request,/api/v2/query,351438333495bd60,404 -,,3,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-10T19:00:00Z,1,req_bytes,query_count,/api/v2/query,351438333495bd60,200 -,,3,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-10T19:00:00Z,1,req_bytes,query_count,/api/v2/query,351438333495bd60,200 -,,3,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-10T19:00:00Z,1,req_bytes,query_count,/api/v2/query,351438333495bd60,200 -,,3,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-05-28T23:00:00Z,31,req_bytes,query_count,/api/v2/query,351438333495bd60,200 -,,3,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-05-29T00:00:00Z,34,req_bytes,query_count,/api/v2/query,351438333495bd60,200 -,,3,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-05-29T01:00:00Z,30,req_bytes,query_count,/api/v2/query,351438333495bd60,200 -,,3,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-05-29T02:00:00Z,15,req_bytes,query_count,/api/v2/query,351438333495bd60,200 -,,4,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-03T23:00:00Z,278,resp_bytes,http_request,/api/v2/query,351438333495bd60,200 -,,4,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-09T21:00:00Z,1790,resp_bytes,http_request,/api/v2/query,351438333495bd60,200 -,,4,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-10T19:00:00Z,145,resp_bytes,http_request,/api/v2/query,351438333495bd60,200 -,,5,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-03T23:00:00Z,212,resp_bytes,http_request,/api/v2/query,351438333495bd60,404 -,,5,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-09T21:00:00Z,1166,resp_bytes,http_request,/api/v2/query,351438333495bd60,404 -,,5,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-10T19:00:00Z,90,event_type_limited_write,event,/api/v2/query,351438333495bd60,400 -,,5,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-10T19:00:00Z,3,event_type_limited_write,event,/api/v2/query,351438333495bd60,400 -,,5,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-10T19:00:00Z,4,event_type_limited_write,event,/api/v2/query,351438333495bd60,400 -,,5,2021-05-15T13:33:29.743615351Z,2021-06-14T13:33:29.743615351Z,2021-06-10T19:00:00Z,1,event_type_limited_write,event,/api/v2/query,351438333495bd60,400`