diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 8481709d01..503f7f153e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -13,7 +13,7 @@ jobs: backend_ut: name: Backend Unit Test runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 20 steps: - name: Checkout code uses: actions/checkout@v2 @@ -113,11 +113,8 @@ jobs: ${{ runner.os }}-tiup - name: Install and run TiUP in the background run: | - curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh - source /home/runner/.profile - tiup update playground - source /home/runner/.profile - tiup playground ${{ matrix.tidb_version }} --tiflash=0 & + chmod u+x scripts/start_tiup.sh + scripts/start_tiup.sh ${{ matrix.tidb_version }} - name: Build UI run: | make ui @@ -155,6 +152,13 @@ jobs: SERVER_URL: http://127.0.0.1:12333/dashboard/ CI: true FEATURE_VERSION: ${{ matrix.feature_version }} + TIDB_VERSION: ${{ matrix.tidb_version }} + - name: Archive Test Results + if: always() + run: | + cat ui/start_tiup.log + echo "===============" + cat ui/wait_tiup.log - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 with: diff --git a/Makefile b/Makefile index be32b07674..338dd0847c 100755 --- a/Makefile +++ b/Makefile @@ -77,10 +77,10 @@ run: test_e2e_compat_features: cd ui &&\ - yarn run:e2e-test:compat-features --env FEATURE_VERSION=$(FEATURE_VERSION) + yarn run:e2e-test:compat-features --env FEATURE_VERSION=$(FEATURE_VERSION) TIDB_VERSION=$(TIDB_VERSION) test_e2e_common_features: cd ui &&\ - yarn run:e2e-test:common-features + yarn run:e2e-test:common-features TIDB_VERSION=$(TIDB_VERSION) test_e2e: test_e2e_compat_features test_e2e_common_features \ No newline at end of file diff --git a/scripts/start_tiup.sh b/scripts/start_tiup.sh new file mode 100755 index 0000000000..9e6b52a399 --- /dev/null +++ b/scripts/start_tiup.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -ex +tidb_version=$1 +mode=$2 + +TIUP_BIN_DIR=$HOME/.tiup/bin/tiup + +if [ $mode = "restart" ]; then + # get process id + pid=$(ps -ef | grep -v start_tiup | grep tiup | grep -v grep | awk '{print $2}') + + for id in $pid + do + # kill tiup-playground + echo "killing $id" + kill -9 $id; + done +else + echo "install tiup" + # Install TiUP + curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh + $TIUP_BIN_DIR update playground +fi + +# Run Tiup +$TIUP_BIN_DIR playground ${tidb_version} --tiflash=0 &> start_tiup.log & diff --git a/ui/.gitignore b/ui/.gitignore index 5535d03e5d..6023e7db6e 100644 --- a/ui/.gitignore +++ b/ui/.gitignore @@ -13,6 +13,7 @@ cypress/screeshots cypress/integration/1-getting-started cypress/integration/2-advanced-examples .nyc_output +*.log # production build/ diff --git a/ui/cypress.json b/ui/cypress.json index 35de53e77f..51ae7b579e 100644 --- a/ui/cypress.json +++ b/ui/cypress.json @@ -1,9 +1,14 @@ { - "defaultCommandTimeout": 60000, + "defaultCommandTimeout": 120000, "responseTimeout": 60000, + "requestTimeout": 60000, "screenshotOnRunFailure": false, "video": false, "env": { - "FEATURE_VERSION": "6.0.0" - } + "FEATURE_VERSION": "6.0.0", + "TIDB_VERSION": "nightly" + }, + "experimentalSessionSupport": true, + "viewportWidth": 1500, + "viewportHeight": 1000 } diff --git a/ui/cypress/fixtures/uri.json b/ui/cypress/fixtures/uri.json index 0b506e14d6..bfb6063b0b 100644 --- a/ui/cypress/fixtures/uri.json +++ b/ui/cypress/fixtures/uri.json @@ -1,5 +1,6 @@ { "root": "/", "login": "/signin", - "overview": "/overview" + "overview": "/overview", + "slow_query": "/slow_query" } diff --git a/ui/cypress/integration/common-features/login_session_spec.js b/ui/cypress/integration/login/login_session.spec.js similarity index 89% rename from ui/cypress/integration/common-features/login_session_spec.js rename to ui/cypress/integration/login/login_session.spec.js index 0ed38a91e2..ffe3cb4707 100644 --- a/ui/cypress/integration/common-features/login_session_spec.js +++ b/ui/cypress/integration/login/login_session.spec.js @@ -10,7 +10,7 @@ describe('Login session', () => { it('Redirect to sigin page when user not login', function () { cy.visit(this.uri.overview) expect(localStorage.getItem('dashboard_auth_token')).to.be.null - cy.url().should('include', `${this.uri.login}`) + cy.url().should('include', this.uri.login) }) // Use fake token to indicate session expired. @@ -19,7 +19,7 @@ describe('Login session', () => { localStorage.setItem('dashboard_auth_token', 'invalid_auth_token') cy.visit(this.uri.overview) - cy.url().should('include', `${this.uri.login}`) + cy.url().should('include', this.uri.login) cy.get('.ant-message').should('be.visible') cy.get('.ant-message-error > span:last-child').should( 'has.text', diff --git a/ui/cypress/integration/compatibility-features/user_login_spec.js b/ui/cypress/integration/login/user_login.compat_spec.js similarity index 80% rename from ui/cypress/integration/compatibility-features/user_login_spec.js rename to ui/cypress/integration/login/user_login.compat_spec.js index 88e6f51d8a..c7f4b5e29e 100644 --- a/ui/cypress/integration/compatibility-features/user_login_spec.js +++ b/ui/cypress/integration/login/user_login.compat_spec.js @@ -7,15 +7,21 @@ describe('User Login', () => { if (Cypress.env('FEATURE_VERSION') === '6.0.0') { // Create user test before(() => { - let query = "DROP USER IF EXISTS 'test'@'%'" - let password = '' - cy.task('queryDB', { query, password }) + let queryData = { + query: 'DROP USER IF EXISTS "test"@"%"', + } + cy.task('queryDB', { ...queryData }) - query = "CREATE USER 'test'@'%' IDENTIFIED BY 'test_pwd'" - cy.task('queryDB', { query, password }) + queryData = { + query: "CREATE USER 'test'@'%' IDENTIFIED BY 'test_pwd'", + } - query = "GRANT ALL PRIVILEGES ON *.* TO 'test'@'%' WITH GRANT OPTION" - cy.task('queryDB', { query, password }) + cy.task('queryDB', { ...queryData }) + + queryData = { + query: "GRANT ALL PRIVILEGES ON *.* TO 'test'@'%' WITH GRANT OPTION", + } + cy.task('queryDB', { ...queryData }) }) // Run before each test @@ -38,7 +44,7 @@ describe('User Login', () => { it('nonRoot user with correct password', function () { cy.get('[data-e2e=signin_username_input]').clear().type('test') cy.get('[data-e2e="signin_password_input"]').type('test_pwd{enter}') - cy.url().should('include', `${this.uri.overview}`) + cy.url().should('include', this.uri.overview) }) it('nonRoot user with incorrect password', () => { diff --git a/ui/cypress/integration/common-features/user_login_spec.js b/ui/cypress/integration/login/user_login.spec.js similarity index 78% rename from ui/cypress/integration/common-features/user_login_spec.js rename to ui/cypress/integration/login/user_login.spec.js index e86e5fb005..fb51548a22 100644 --- a/ui/cypress/integration/common-features/user_login_spec.js +++ b/ui/cypress/integration/login/user_login.spec.js @@ -18,7 +18,7 @@ describe('Root User Login', () => { it('root login with no pwd', function () { cy.get('[data-e2e=signin_username_input]').should('have.value', 'root') cy.get('[data-e2e=signin_submit]').click() - cy.url().should('include', `${this.uri.overview}`) + cy.url().should('include', this.uri.overview) }) it('remember last succeeded login username', () => { @@ -40,16 +40,19 @@ describe('Root User Login', () => { it('root login with correct pwd', function () { // set password for root - let query = "SET PASSWORD FOR 'root'@'%' = 'root_pwd'" - let password = '' - cy.task('queryDB', { query, password }) + let queryData = { + query: 'SET PASSWORD FOR "root"@"%" = "root_pwd"', + } + cy.task('queryDB', { ...queryData }) cy.get('[data-e2e="signin_password_input"]').type('root_pwd{enter}') - cy.url().should('include', `${this.uri.overview}`) + cy.url().should('include', this.uri.overview) // set empty password for root - query = "SET PASSWORD FOR 'root'@'%' = ''" - password = 'root_pwd' - cy.task('queryDB', { query, password }) + queryData = { + query: 'SET PASSWORD FOR "root"@"%" = ""', + password: 'root_pwd', + } + cy.task('queryDB', { ...queryData }) }) }) diff --git a/ui/cypress/integration/slow_query/list.spec.js b/ui/cypress/integration/slow_query/list.spec.js new file mode 100644 index 0000000000..4674e5c414 --- /dev/null +++ b/ui/cypress/integration/slow_query/list.spec.js @@ -0,0 +1,243 @@ +// Copyright 2021 PingCAP, Inc. Licensed under Apache-2.0. + +import dayjs from 'dayjs' + +describe('SlowQuery list page', () => { + before(() => { + cy.fixture('uri.json').then(function (uri) { + this.uri = uri + }) + }) + + beforeEach(function () { + cy.login('root') + cy.visit(this.uri.slow_query) + cy.url().should('include', this.uri.slow_query) + }) + + describe('Initialize slow query page', () => { + it('Restart tiup', () => { + cy.exec( + `bash ../scripts/start_tiup.sh ${Cypress.env('TIDB_VERSION')} restart`, + { log: true } + ) + }) + + it('Wait TiUP Playground', () => { + cy.exec('bash ../scripts/wait_tiup_playground.sh 1 300 &> wait_tiup.log') + }) + + it('Slow query side bar highlighted', () => { + cy.get('[data-e2e=menu_item_slow_query]').should( + 'has.class', + 'ant-menu-item-selected' + ) + }) + + it('Has Toolbar', function () { + cy.get('[data-e2e=slow_query_toolbar]').should('be.visible') + }) + + it('Get slow query bad request', () => { + const staticResponse = { + statusCode: 400, + body: { + code: 'common.bad_request', + error: true, + message: 'common.bad_request', + }, + } + + // stub out a response body + cy.intercept( + `${Cypress.env('apiUrl')}slow_query/list*`, + staticResponse + ).as('slow_query_list') + cy.wait('@slow_query_list').then(() => { + cy.get('[data-e2e=alert_error_bar] > span:nth-child(2)').should( + 'has.text', + staticResponse.body.message + ) + }) + }) + }) + + describe('Filter slow query list', () => { + it('Run workload', () => { + let queryData = { + query: 'SET tidb_slow_log_threshold = 500', + } + cy.task('queryDB', { ...queryData }) + + const workloads = [ + 'SELECT SLEEP(0.8);', + 'SELECT SLEEP(0.4);', + 'SELECT SLEEP(1);', + ] + + const waitTwoSecond = (query, idx) => + new Promise((resolve) => { + // run workload every 3 seconds + setTimeout(() => { + resolve(query) + }, 3000 * idx) + }) + + workloads.forEach((query, idx) => { + cy.wrap(waitTwoSecond(query, idx)).then((query) => { + // return a promise to cy.then() that + // is awaited until it resolves + cy.task('queryDB', { query }) + }) + }) + }) + + describe('Filter slow query by changing time range', () => { + const now = dayjs().unix() + let defaultSlowQueryList + let lastSlowQueryTimeStamp + let firstQueryTimeRangeStart, + secondQueryTimeRangeStart, + thirdQueryTimeRangeStart, + thirdQueryTimeRangeEnd + + it('Default time range is 30 mins', () => { + cy.get('[data-e2e=selected_timerange]').should( + 'has.text', + 'Recent 30 min' + ) + }) + + it('Show all slow_query', () => { + const options = { + url: `${Cypress.env('apiUrl')}slow_query/list`, + qs: { + begin_time: now - 1800, + desc: true, + end_time: now + 100, + fields: 'query,timestamp,query_time,memory_max', + limit: 100, + orderBy: 'timestamp', + }, + } + + cy.request(options).as('slow_query') + + cy.get('@slow_query').then((response) => { + defaultSlowQueryList = response.body + if (defaultSlowQueryList.length > 0) { + lastSlowQueryTimeStamp = defaultSlowQueryList[0].timestamp + + firstQueryTimeRangeStart = dayjs + .unix(lastSlowQueryTimeStamp - 7) + .format('YYYY-MM-DD HH:mm:ss') + secondQueryTimeRangeStart = dayjs + .unix(lastSlowQueryTimeStamp - 4) + .format('YYYY-MM-DD HH:mm:ss') + thirdQueryTimeRangeStart = dayjs + .unix(lastSlowQueryTimeStamp - 1) + .format('YYYY-MM-DD HH:mm:ss') + thirdQueryTimeRangeEnd = dayjs + .unix(lastSlowQueryTimeStamp + (3 - 1)) + .format('YYYY-MM-DD HH:mm:ss') + } + }) + }) + + describe('Check slow query', () => { + it('Check slow query in the 1st 3 seconds time range', () => { + cy.get('[data-e2e=timerange-selector]') + .click() + .then(() => { + cy.get('.ant-picker-range').click() + cy.get('.ant-picker-input-active > input').type( + `${firstQueryTimeRangeStart}{leftarrow}{leftarrow}{backspace}{enter}` + ) + cy.get('.ant-picker-input-active > input').type( + `${secondQueryTimeRangeStart}{leftarrow}{leftarrow}{backspace}{enter}` + ) + cy.get('[data-automation-key=query]') + .should('has.length', 1) + .and('has.text', 'SELECT SLEEP(0.8);') + }) + }) + + it('Check slow query in the 2nd 3 seconds time range', () => { + cy.get('[data-e2e=timerange-selector]') + .click() + .then(() => { + cy.get('.ant-picker-range').click() + cy.get('.ant-picker-input-active > input').type( + `${secondQueryTimeRangeStart}{leftarrow}{leftarrow}{backspace}{enter}` + ) + cy.get('.ant-picker-input-active > input').type( + `${thirdQueryTimeRangeStart}{leftarrow}{leftarrow}{backspace}{enter}` + ) + + cy.get('[data-automation-key=query]').should('has.length', 0) + }) + }) + + it('Check slow query in the 3rd 3 seconds time range', () => { + cy.get('[data-e2e=timerange-selector]') + .click() + .then(() => { + cy.get('.ant-picker-range').click() + cy.get('.ant-picker-input-active > input').type( + `${thirdQueryTimeRangeStart}{leftarrow}{leftarrow}{backspace}{enter}` + ) + cy.get('.ant-picker-input-active > input').type( + `${thirdQueryTimeRangeEnd}{leftarrow}{leftarrow}{backspace}{enter}` + ) + + cy.get('[data-automation-key=query]') + .should('has.length', 1) + .and('has.text', 'SELECT SLEEP(1);') + }) + }) + + it('Check slow query in the latest 9 seconds time range', () => { + cy.get('[data-e2e=timerange-selector]') + .click() + .then(() => { + cy.get('.ant-picker-range').click() + cy.get('.ant-picker-input-active > input').type( + `${firstQueryTimeRangeStart}{leftarrow}{leftarrow}{backspace}{enter}` + ) + cy.get('.ant-picker-input-active > input').type( + `${thirdQueryTimeRangeEnd}{leftarrow}{leftarrow}{backspace}{enter}` + ) + + cy.get('[data-automation-key=query]').should('has.length', 2) + }) + }) + }) + }) + + describe('Filter slow query by changing database', () => { + it('No database selected by default', () => { + cy.get('[data-e2e=base_select_input]').should('has.text', '') + }) + + const options = { + url: `${Cypress.env('apiUrl')}info/databases/`, + } + + it('Show all databases', () => { + cy.request(options).as('databases') + + cy.get('@databases').then((response) => { + const databaseList = response.body + cy.get('[data-e2e=base_select_input]') + .click() + .then(() => { + cy.get('[data-e2e=multi_select_options_label]').should( + 'have.length', + databaseList.length + ) + }) + }) + }) + }) + }) +}) diff --git a/ui/cypress/plugins/index.js b/ui/cypress/plugins/index.js index 17465f49e1..ba720a5a65 100644 --- a/ui/cypress/plugins/index.js +++ b/ui/cypress/plugins/index.js @@ -14,11 +14,12 @@ const mysql = require('mysql2') -function queryTestDB(query, password) { +function queryTestDB(query, password, database) { const dbConfig = { host: '127.0.0.1', port: '4000', user: 'root', + database: database, password: password, } // creates a new mysql connection @@ -52,10 +53,10 @@ module.exports = (on, config) => { config.env.apiUrl = 'http://127.0.0.1:12333/dashboard/api/' - // Usage: cy.task('queryDB', {query, password}) + // Usage: cy.task('queryDB', { ...queryData }) on('task', { - queryDB: ({ query, password }) => { - return queryTestDB(query, password) + queryDB: ({ query, password = '', database = 'mysql' }) => { + return queryTestDB(query, password, database) }, }) diff --git a/ui/cypress/support/commands.js b/ui/cypress/support/commands.js index 119ab03f7c..39426a586d 100644 --- a/ui/cypress/support/commands.js +++ b/ui/cypress/support/commands.js @@ -10,7 +10,46 @@ // // // -- This is a parent command -- -// Cypress.Commands.add('login', (email, password) => { ... }) +Cypress.Commands.add('login', (username, password = '') => { + // cy.login will be called inside beforeEach, + // cy.session stores cookies and localStorage when user first login, + // the cookies and localStorage will be reused in the feature beforeEach test. + cy.session( + [username, password], + () => { + // root login + cy.visit('/') + cy.get('[data-e2e=signin_submit]').click() + + // Wait for the post-login redirect to ensure that the + // session actually exists to be cached + cy.url().should('include', '/overview') + }, + { + validate() { + cy.request('/whoami').its('status').should('eq', 200) + }, + } + ) +}) + +// -- This will overwrite an existing command -- +Cypress.Commands.overwrite('request', (originalFn, ...options) => { + const optionsObject = options[0] + const token = localStorage.getItem('dashboard_auth_token') + + if (!!token && optionsObject === Object(optionsObject)) { + optionsObject.headers = { + authorization: 'Bearer ' + token, + ...optionsObject.headers, + } + + return originalFn(optionsObject) + } + + return originalFn(...options) +}) + // // // -- This is a child command -- @@ -21,5 +60,3 @@ // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) // // -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) diff --git a/ui/dashboardApp/layout/main/Sider/index.tsx b/ui/dashboardApp/layout/main/Sider/index.tsx index 5b8d0182d8..9ab2e2b052 100644 --- a/ui/dashboardApp/layout/main/Sider/index.tsx +++ b/ui/dashboardApp/layout/main/Sider/index.tsx @@ -16,7 +16,7 @@ function useAppMenuItem(registry, appId, title?: string, hideIcon?: boolean) { return null } return ( - + {!hideIcon && app.icon ? : null} {title ? title : t(`${appId}.nav_title`, appId)} diff --git a/ui/lib/apps/SlowQuery/pages/List/index.tsx b/ui/lib/apps/SlowQuery/pages/List/index.tsx index db10484a39..44c40c22e8 100644 --- a/ui/lib/apps/SlowQuery/pages/List/index.tsx +++ b/ui/lib/apps/SlowQuery/pages/List/index.tsx @@ -99,7 +99,7 @@ function List() { return (
- + ({ tabIndex={tabIndex} autoFocus={autoFocus} readOnly + data-e2e="base_select_input" />
+ return ( + + ) } else { return ( } + data-e2e="alert_error_bar" /> ) } diff --git a/ui/lib/components/MultiSelect/index.tsx b/ui/lib/components/MultiSelect/index.tsx index 830f618db9..29123afe18 100644 --- a/ui/lib/components/MultiSelect/index.tsx +++ b/ui/lib/components/MultiSelect/index.tsx @@ -79,7 +79,7 @@ function MultiSelect(props: IMultiSelectProps) { return ( - {label} + {label} ) diff --git a/ui/lib/components/TimeRangeSelector/index.tsx b/ui/lib/components/TimeRangeSelector/index.tsx index 0721c4c9ff..b2a9720829 100644 --- a/ui/lib/components/TimeRangeSelector/index.tsx +++ b/ui/lib/components/TimeRangeSelector/index.tsx @@ -174,13 +174,13 @@ function TimeRangeSelector({ >