diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 15f71737c8..832f82847f 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -159,6 +159,7 @@ jobs: id: get-failed-spec continue-on-error: true run: | + sed -i 's/\.\.\///g' .failed-specs-*.txt echo "failed-test=$(paste -d , .failed-specs-*.txt)" >> $GITHUB_OUTPUT echo "neg-failed-test=!$(paste -d ',!' .failed-specs-*.txt)" >> $GITHUB_OUTPUT - name: Unzip build artifacts @@ -173,7 +174,7 @@ jobs: uses: cypress-io/github-action@v4 if: steps.get-failed-spec.outputs.failed-test != '' with: - config-file: config/cypress.json + config-file: config/cypress.config.js config: baseUrl=https://localhost:8885 record: true parallel: true @@ -183,7 +184,7 @@ jobs: - name: Run frontend end-to-end tests uses: cypress-io/github-action@v4 with: - config-file: config/cypress.json + config-file: config/cypress.config.js config: baseUrl=https://localhost:8885 record: true parallel: true @@ -191,7 +192,7 @@ jobs: group: main-${{ needs.determine-if-required.outputs.hash }} spec: | **/* - !cypress/integration/smoke/smoke.spec.js + !cypress/e2e/smoke/smoke.spec.js ${{ steps.get-failed-spec.outputs.neg-failed-test }} - name: Upload logs uses: actions/upload-artifact@v3 @@ -252,11 +253,11 @@ jobs: - name: Run end-to-end smoke tests (Firefox) uses: cypress-io/github-action@v4 with: - config-file: config/cypress.json + config-file: config/cypress.config.js config: baseUrl=https://localhost:8885 browser: firefox record: true - spec: cypress/integration/smoke/smoke.spec.js + spec: cypress/e2e/smoke/smoke.spec.js - name: Upload logs uses: actions/upload-artifact@v3 if: failure() diff --git a/config/cypress.config.js b/config/cypress.config.js new file mode 100644 index 0000000000..597c6d6dbb --- /dev/null +++ b/config/cypress.config.js @@ -0,0 +1,96 @@ +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* eslint-disable import/no-commonjs */ + +const { defineConfig } = require('cypress') +const { cypressBrowserPermissionsPlugin } = require('cypress-browser-permissions') +const cypressLogToOutput = require('cypress-log-to-output') + +const tasks = require('../cypress/plugins/tasks') + +module.exports = defineConfig({ + projectId: 'uqdraf', + viewportHeight: 900, + viewportWidth: 1440, + defaultCommandTimeout: 10000, + animationDistanceThreshold: 3, + video: false, + retries: { + runMode: 1, + openMode: 0, + }, + env: { + browserPermissions: { + camera: 'allow', + }, + }, + e2e: { + setupNodeEvents: (on, config) => { + const configWithPermissions = cypressBrowserPermissionsPlugin(on, config) + + tasks.stackConfigTask(on, configWithPermissions) + tasks.sqlTask(on, configWithPermissions) + tasks.fileExistsTask(on, configWithPermissions) + tasks.emailTask(on, configWithPermissions) + + on('before:browser:launch', (browser = {}, launchOptions) => { + // Log console log to output when debug mode is enabled. + if (process.env.RUNNER_DEBUG) { + launchOptions.args = cypressLogToOutput.browserLaunchHandler(browser, launchOptions.args) + } + if (browser.family === 'chromium' && browser.name !== 'electron') { + launchOptions.args.push( + '--use-file-for-fake-video-capture=cypress/fixtures/qr-code-mock-feed.y4m', + ) + } + + if (browser.name === 'chrome' && browser.isHeadless) { + launchOptions.args.push('--disable-gpu') + } + + return launchOptions + }) + + return configWithPermissions + }, + experimentalRunAllSpecs: true, + excludeSpecPattern: '!*.spec.js', + specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}', + }, + asBaseUrl: 'http://localhost:1885/api/v3', + asEnabled: true, + nsBaseUrl: 'http://localhost:1885/api/v3', + nsEnabled: true, + jsBaseUrl: 'http://localhost:1885/api/v3', + jsEnabled: true, + isBaseUrl: 'http://localhost:1885/api/v3', + isEnabled: true, + gsBaseUrl: 'http://localhost:1885/api/v3', + gsEnabled: true, + edtcBaseUrl: 'http://localhost:1885/api/v3', + edtcEnabled: true, + qrgBaseUrl: 'http://localhost:1885/api/v3', + qrgEnabled: true, + consoleSiteName: 'The Things Stack for LoRaWAN', + consoleSubTitle: 'Management platform for The Things Stack for LoRaWAN', + consoleTitle: 'Console', + consoleAssetsRootPath: '/assets', + consoleRootPath: '/console', + accountAppSiteName: 'The Things Stack for LoRaWAN', + accountAppSubTitle: '', + accountAppTitle: 'Account', + accountAppRootPath: '/oauth', + accountAppAssetsRootPath: '/assets', +}) diff --git a/config/cypress.json b/config/cypress.json deleted file mode 100644 index 8545120ee0..0000000000 --- a/config/cypress.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "projectId": "uqdraf", - "viewportHeight": 900, - "viewportWidth": 1440, - "defaultCommandTimeout": 10000, - "ignoreTestFiles": "!*.spec.js", - "animationDistanceThreshold": 3, - "video": false, - "retries": { - "runMode": 1, - "openMode": 0 - }, - "env": { - "browserPermissions": { - "camera": "allow" - } - } -} diff --git a/config/eslintrc.yaml b/config/eslintrc.yaml index 35db89e56d..1c338b90b7 100644 --- a/config/eslintrc.yaml +++ b/config/eslintrc.yaml @@ -6,7 +6,7 @@ extends: - plugin:cypress/recommended - prettier -parser: babel-eslint +parser: '@babel/eslint-parser' plugins: - jsdoc @@ -45,65 +45,65 @@ rules: import/order: - warn - groups: - - builtin - - external - - internal - - parent - - sibling - - index + - builtin + - external + - internal + - parent + - sibling + - index newlines-between: always pathGroups: - - pattern: "\\@{ttn-lw,console,account}/constants" - group: internal - position: after - - pattern: "\\@{console,account}/api" - group: internal - position: after - - pattern: "\\@ttn-lw/components/**" - group: internal - position: after - - pattern: "\\@ttn-lw/containers/**" - group: internal - position: after - - pattern: "\\@ttn-lw/lib/components/**" - group: internal - position: after - - pattern: "\\@{console,account}/components/**" - group: internal - position: after - - pattern: "\\@{console,account}/containers/**" - group: internal - position: after - - pattern: "\\@{console,account}/lib/components/**" - group: internal - position: after - - pattern: "\\@{console,account}/views/**" - group: internal - position: after - - pattern: "\\@ttn-lw/lib/**" - group: internal - position: after - - pattern: "\\@{console,account}/lib/**" - group: internal - position: after - - pattern: "\\@{console,account}/store/actions/**" - group: internal - position: after - - pattern: "\\@{console,account}/store/reducers/**" - group: internal - position: after - - pattern: "\\@{console,account}/store/selectors/**" - group: internal - position: after - - pattern: "\\@{console,account}/store/middleware/**" - group: internal - position: after - - pattern: "(\\@assets/**|\\@ttn-lw/styles/**)" - group: sibling - position: after - - pattern: "./*.styl" - group: sibling - position: after + - pattern: "\\@{ttn-lw,console,account}/constants" + group: internal + position: after + - pattern: "\\@{console,account}/api" + group: internal + position: after + - pattern: "\\@ttn-lw/components/**" + group: internal + position: after + - pattern: "\\@ttn-lw/containers/**" + group: internal + position: after + - pattern: "\\@ttn-lw/lib/components/**" + group: internal + position: after + - pattern: "\\@{console,account}/components/**" + group: internal + position: after + - pattern: "\\@{console,account}/containers/**" + group: internal + position: after + - pattern: "\\@{console,account}/lib/components/**" + group: internal + position: after + - pattern: "\\@{console,account}/views/**" + group: internal + position: after + - pattern: "\\@ttn-lw/lib/**" + group: internal + position: after + - pattern: "\\@{console,account}/lib/**" + group: internal + position: after + - pattern: "\\@{console,account}/store/actions/**" + group: internal + position: after + - pattern: "\\@{console,account}/store/reducers/**" + group: internal + position: after + - pattern: "\\@{console,account}/store/selectors/**" + group: internal + position: after + - pattern: "\\@{console,account}/store/middleware/**" + group: internal + position: after + - pattern: "(\\@assets/**|\\@ttn-lw/styles/**)" + group: sibling + position: after + - pattern: './*.styl' + group: sibling + position: after # Prevent superfluous path traversions in import statements. import/no-useless-path-segments: diff --git a/config/webpack.config.babel.js b/config/webpack.config.babel.js index bed3290970..2281c7e473 100644 --- a/config/webpack.config.babel.js +++ b/config/webpack.config.babel.js @@ -292,7 +292,7 @@ export default { manifest: path.resolve(context, CACHE_DIR, 'dll.json'), }), new AddAssetHtmlPlugin({ - filepath: path.resolve(context, PUBLIC_DIR, 'libs*bundle.js'), + glob: path.resolve(context, PUBLIC_DIR, 'libs*bundle.js'), }), ], production: [ diff --git a/cypress/integration/account/authorization-management.spec.js b/cypress/e2e/account/authorization-management.spec.js similarity index 93% rename from cypress/integration/account/authorization-management.spec.js rename to cypress/e2e/account/authorization-management.spec.js index 9d27077002..61c10d6011 100644 --- a/cypress/integration/account/authorization-management.spec.js +++ b/cypress/e2e/account/authorization-management.spec.js @@ -79,11 +79,6 @@ describe('Account App authorization management', () => { cy.findByRole('button', { name: /Invalidate all access tokens/ }).click() cy.findByTestId('error-notification').should('not.exist') - cy.location('pathname').should( - 'eq', - `${Cypress.config('accountAppRootPath')}/client-authorizations/console`, - ) - cy.visit(`${Cypress.config('accountAppRootPath')}/client-authorizations/console/access-tokens`) cy.findByRole('rowgroup').should('not.exist') }) @@ -93,10 +88,6 @@ describe('Account App authorization management', () => { cy.visit(`${Cypress.config('accountAppRootPath')}/client-authorizations/console/access-tokens`) cy.findByRole('button', { name: /Invalidate this access token/ }).click() cy.findByTestId('error-notification').should('not.exist') - - cy.location('pathname').should( - 'eq', - `${Cypress.config('accountAppRootPath')}/client-authorizations/console`, - ) + cy.findByRole('rowgroup').should('not.exist') }) }) diff --git a/cypress/integration/account/authorization.spec.js b/cypress/e2e/account/authorization.spec.js similarity index 100% rename from cypress/integration/account/authorization.spec.js rename to cypress/e2e/account/authorization.spec.js diff --git a/cypress/integration/account/change-password.spec.js b/cypress/e2e/account/change-password.spec.js similarity index 100% rename from cypress/integration/account/change-password.spec.js rename to cypress/e2e/account/change-password.spec.js diff --git a/cypress/integration/account/code.spec.js b/cypress/e2e/account/code.spec.js similarity index 94% rename from cypress/integration/account/code.spec.js rename to cypress/e2e/account/code.spec.js index d64f0ff775..4e104228ae 100644 --- a/cypress/integration/account/code.spec.js +++ b/cypress/e2e/account/code.spec.js @@ -42,6 +42,6 @@ describe('Account App code view', () => { it('redirects back if no code is supplied', () => { cy.visit(`${Cypress.config('accountAppRootPath')}/code`) - cy.location('pathname').should('eq', `${Cypress.config('accountAppRootPath')}/`) + cy.location('pathname').should('eq', Cypress.config('accountAppRootPath')) }) }) diff --git a/cypress/integration/account/forgot-password.spec.js b/cypress/e2e/account/forgot-password.spec.js similarity index 100% rename from cypress/integration/account/forgot-password.spec.js rename to cypress/e2e/account/forgot-password.spec.js diff --git a/cypress/integration/account/oauth-clients/create.spec.js b/cypress/e2e/account/oauth-clients/create.spec.js similarity index 100% rename from cypress/integration/account/oauth-clients/create.spec.js rename to cypress/e2e/account/oauth-clients/create.spec.js diff --git a/cypress/integration/account/oauth-clients/edit.spec.js b/cypress/e2e/account/oauth-clients/edit.spec.js similarity index 100% rename from cypress/integration/account/oauth-clients/edit.spec.js rename to cypress/e2e/account/oauth-clients/edit.spec.js diff --git a/cypress/integration/account/oauth-clients/list.spec.js b/cypress/e2e/account/oauth-clients/list.spec.js similarity index 100% rename from cypress/integration/account/oauth-clients/list.spec.js rename to cypress/e2e/account/oauth-clients/list.spec.js diff --git a/cypress/integration/account/overview.spec.js b/cypress/e2e/account/overview.spec.js similarity index 100% rename from cypress/integration/account/overview.spec.js rename to cypress/e2e/account/overview.spec.js diff --git a/cypress/integration/account/profile-settings.spec.js b/cypress/e2e/account/profile-settings.spec.js similarity index 98% rename from cypress/integration/account/profile-settings.spec.js rename to cypress/e2e/account/profile-settings.spec.js index 3e489272a3..731a38a255 100644 --- a/cypress/integration/account/profile-settings.spec.js +++ b/cypress/e2e/account/profile-settings.spec.js @@ -69,7 +69,8 @@ describe('Account App profile settings', () => { cy.findByLabelText('Use Gravatar').check() cy.findByLabelText('Name').type(userUpdate.name) - cy.findByLabelText('Email address').clear().type(userUpdate.primary_email_address) + cy.findByLabelText('Email address').clear() + cy.findByLabelText('Email address').type(userUpdate.primary_email_address) // Check if the profile picture (preview) was updated properly. cy.get('form').within(() => { @@ -162,7 +163,6 @@ describe('Account App profile settings', () => { describe('Account App profile settings with disabled upload', () => { before(() => { cy.dropAndSeedDatabase() - cy.server() cy.augmentIsConfig({ profile_picture: { disable_upload: true } }) }) it('displays UI elements in place', () => { @@ -200,7 +200,6 @@ describe('Account App profile settings with disabled upload', () => { describe('Account App profile settings without gravatar', () => { before(() => { cy.dropAndSeedDatabase() - cy.server() cy.augmentIsConfig({ profile_picture: { use_gravatar: false } }) }) it('displays UI elements in place', () => { diff --git a/cypress/integration/account/session-management.spec.js b/cypress/e2e/account/session-management.spec.js similarity index 100% rename from cypress/integration/account/session-management.spec.js rename to cypress/e2e/account/session-management.spec.js diff --git a/cypress/integration/account/user-registration.spec.js b/cypress/e2e/account/user-registration.spec.js similarity index 100% rename from cypress/integration/account/user-registration.spec.js rename to cypress/e2e/account/user-registration.spec.js diff --git a/cypress/integration/console/admin-panel/admin-panel.spec.js b/cypress/e2e/console/admin-panel/admin-panel.spec.js similarity index 100% rename from cypress/integration/console/admin-panel/admin-panel.spec.js rename to cypress/e2e/console/admin-panel/admin-panel.spec.js diff --git a/cypress/integration/console/admin-panel/network-information/network-information.spec.js b/cypress/e2e/console/admin-panel/network-information/network-information.spec.js similarity index 100% rename from cypress/integration/console/admin-panel/network-information/network-information.spec.js rename to cypress/e2e/console/admin-panel/network-information/network-information.spec.js diff --git a/cypress/integration/console/admin-panel/packet-broker/default-gateway-visibility.spec.js b/cypress/e2e/console/admin-panel/packet-broker/default-gateway-visibility.spec.js similarity index 100% rename from cypress/integration/console/admin-panel/packet-broker/default-gateway-visibility.spec.js rename to cypress/e2e/console/admin-panel/packet-broker/default-gateway-visibility.spec.js diff --git a/cypress/integration/console/admin-panel/packet-broker/networks.spec.js b/cypress/e2e/console/admin-panel/packet-broker/networks.spec.js similarity index 100% rename from cypress/integration/console/admin-panel/packet-broker/networks.spec.js rename to cypress/e2e/console/admin-panel/packet-broker/networks.spec.js diff --git a/cypress/integration/console/admin-panel/packet-broker/registration.spec.js b/cypress/e2e/console/admin-panel/packet-broker/registration.spec.js similarity index 93% rename from cypress/integration/console/admin-panel/packet-broker/registration.spec.js rename to cypress/e2e/console/admin-panel/packet-broker/registration.spec.js index 98f6f497aa..5f4afc5366 100644 --- a/cypress/integration/console/admin-panel/packet-broker/registration.spec.js +++ b/cypress/e2e/console/admin-panel/packet-broker/registration.spec.js @@ -37,10 +37,10 @@ describe('Packet Broker registration', () => { cy.loginConsole({ user_id: user.ids.user_id, password: user.password }) cy.visit(`${Cypress.config('consoleRootPath')}/admin-panel/packet-broker`) - cy.location('pathname').should('eq', `${Cypress.config('consoleRootPath')}/`) + cy.location('pathname').should('eq', Cypress.config('consoleRootPath')) cy.visit(`${Cypress.config('consoleRootPath')}/admin-panel/packet-broker/networks/19`) - cy.location('pathname').should('eq', `${Cypress.config('consoleRootPath')}/`) + cy.location('pathname').should('eq', Cypress.config('consoleRootPath')) }) it('displays a notification when Packet Broker is not set up', () => { @@ -89,7 +89,8 @@ describe('Packet Broker registration', () => { cy.loginConsole({ user_id: 'admin', password: 'admin' }) cy.visit(`${Cypress.config('consoleRootPath')}/admin-panel/packet-broker`) - cy.findByText('Register network').click().next().findByTestId('switch').should('be.checked') + cy.findByText('Register network').click() + cy.findByText('Register network').next().findByTestId('switch').should('be.checked') cy.findByText('List network publicly') .should('be.visible') .next() diff --git a/cypress/integration/console/admin-panel/packet-broker/routing-policies.spec.js b/cypress/e2e/console/admin-panel/packet-broker/routing-policies.spec.js similarity index 100% rename from cypress/integration/console/admin-panel/packet-broker/routing-policies.spec.js rename to cypress/e2e/console/admin-panel/packet-broker/routing-policies.spec.js diff --git a/cypress/integration/console/admin-panel/user-management/invitations.spec.js b/cypress/e2e/console/admin-panel/user-management/invitations.spec.js similarity index 100% rename from cypress/integration/console/admin-panel/user-management/invitations.spec.js rename to cypress/e2e/console/admin-panel/user-management/invitations.spec.js diff --git a/cypress/integration/console/applications/create.spec.js b/cypress/e2e/console/applications/create.spec.js similarity index 100% rename from cypress/integration/console/applications/create.spec.js rename to cypress/e2e/console/applications/create.spec.js diff --git a/cypress/integration/console/applications/edit.spec.js b/cypress/e2e/console/applications/edit.spec.js similarity index 100% rename from cypress/integration/console/applications/edit.spec.js rename to cypress/e2e/console/applications/edit.spec.js diff --git a/cypress/integration/console/applications/list.spec.js b/cypress/e2e/console/applications/list.spec.js similarity index 100% rename from cypress/integration/console/applications/list.spec.js rename to cypress/e2e/console/applications/list.spec.js diff --git a/cypress/integration/console/auth/access-token.spec.js b/cypress/e2e/console/auth/access-token.spec.js similarity index 100% rename from cypress/integration/console/auth/access-token.spec.js rename to cypress/e2e/console/auth/access-token.spec.js diff --git a/cypress/integration/console/auth/login.spec.js b/cypress/e2e/console/auth/login.spec.js similarity index 98% rename from cypress/integration/console/auth/login.spec.js rename to cypress/e2e/console/auth/login.spec.js index fe08c3a5d2..3c75e490f9 100644 --- a/cypress/integration/console/auth/login.spec.js +++ b/cypress/e2e/console/auth/login.spec.js @@ -30,7 +30,7 @@ describe('Account App login', () => { cy.get('header').within(() => { cy.findByRole('link') - .should('have.attr', 'href', `${Cypress.config('accountAppRootPath')}/`) + .should('have.attr', 'href', Cypress.config('accountAppRootPath')) .findByRole('img') .should('be.visible') .should('have.attr', 'src', `${Cypress.config('accountAppAssetsRootPath')}/account.svg`) diff --git a/cypress/integration/console/devices/device-on-othercluster.spec.js b/cypress/e2e/console/devices/device-on-othercluster.spec.js similarity index 100% rename from cypress/integration/console/devices/device-on-othercluster.spec.js rename to cypress/e2e/console/devices/device-on-othercluster.spec.js diff --git a/cypress/integration/console/devices/device-overview.spec.js b/cypress/e2e/console/devices/device-overview.spec.js similarity index 100% rename from cypress/integration/console/devices/device-overview.spec.js rename to cypress/e2e/console/devices/device-overview.spec.js diff --git a/cypress/integration/console/devices/edit.spec.js b/cypress/e2e/console/devices/edit.spec.js similarity index 100% rename from cypress/integration/console/devices/edit.spec.js rename to cypress/e2e/console/devices/edit.spec.js diff --git a/cypress/integration/console/devices/import.spec.js b/cypress/e2e/console/devices/import.spec.js similarity index 82% rename from cypress/integration/console/devices/import.spec.js rename to cypress/e2e/console/devices/import.spec.js index e6889f3b81..fcbc37b589 100644 --- a/cypress/integration/console/devices/import.spec.js +++ b/cypress/e2e/console/devices/import.spec.js @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { generateJoinServerOnlyConfig } from '../../../support/utils' - const userId = 'import-devices-test-user' const user = { ids: { user_id: userId }, @@ -59,7 +57,7 @@ describe('End device messaging', () => { cy.findByTestId('notification') .findByText('All end devices imported successfully') .should('be.visible') - cy.findByRole('button', { name: 'Proceed to end device list' }).click() + cy.findByRole('link', { name: 'Proceed to end device list' }).click() cy.location('pathname').should( 'eq', `${Cypress.config('consoleRootPath')}/applications/${appId}/devices`, @@ -94,7 +92,7 @@ describe('End device messaging', () => { .should('be.visible') .findByText('All end devices imported successfully') .should('be.visible') - cy.findByRole('button', { name: 'Proceed to end device list' }).click() + cy.findByRole('link', { name: 'Proceed to end device list' }).click() cy.location('pathname').should( 'eq', `${Cypress.config('consoleRootPath')}/applications/${appId}/devices`, @@ -157,7 +155,7 @@ describe('End device messaging', () => { cy.findByTestId('notification') .findByText('All end devices imported successfully') .should('be.visible') - cy.findByRole('button', { name: 'Proceed to end device list' }).click() + cy.findByRole('link', { name: 'Proceed to end device list' }).click() cy.location('pathname').should( 'eq', `${Cypress.config('consoleRootPath')}/applications/${appId}/devices`, @@ -209,45 +207,4 @@ describe('End device messaging', () => { }) }) }) - - describe('Join Server only', () => { - before(() => { - cy.dropAndSeedDatabase() - cy.createUser(user) - cy.createApplication(application, userId) - }) - - beforeEach(() => { - cy.augmentStackConfig(generateJoinServerOnlyConfig) - cy.loginConsole({ user_id: user.ids.user_id, password: user.password }) - cy.visit(`${Cypress.config('consoleRootPath')}/applications/${appId}/devices/import`) - }) - - it('allows importing on join server only deployments', () => { - cy.findByText('Import end devices').should('be.visible') - cy.findByLabelText('File format').selectOption('The Things Stack JSON') - - const devicesFile = 'successful-devices.json' - cy.findByLabelText('File').attachFile(devicesFile) - cy.findByRole('button', { name: 'Import end devices' }).click() - cy.findByText('0 of 3 (0% finished)') - cy.findByText('Operation finished') - cy.findByText('3 of 3 (100% finished)') - cy.findByTestId('notification') - .should('be.visible') - .findByText('All end devices imported successfully') - .should('be.visible') - cy.findByRole('button', { name: 'Proceed to end device list' }).click() - cy.location('pathname').should( - 'eq', - `${Cypress.config('consoleRootPath')}/applications/${appId}/devices`, - ) - cy.findByTestId('error-notification').should('not.exist') - cy.findByText('migration-test-device').should('be.visible') - cy.findByText('some-nice-id').should('be.visible') - cy.findByText('this-is-test-id').should('be.visible').click() - cy.findByRole('heading', { name: /Test Device/ }).should('be.visible') - cy.findByTestId('error-notification').should('not.exist') - }) - }) }) diff --git a/cypress/integration/console/devices/list.spec.js b/cypress/e2e/console/devices/list.spec.js similarity index 100% rename from cypress/integration/console/devices/list.spec.js rename to cypress/e2e/console/devices/list.spec.js diff --git a/cypress/integration/console/devices/messaging/edit.spec.js b/cypress/e2e/console/devices/messaging/edit.spec.js similarity index 100% rename from cypress/integration/console/devices/messaging/edit.spec.js rename to cypress/e2e/console/devices/messaging/edit.spec.js diff --git a/cypress/integration/console/devices/onboarding/claiming.spec.js b/cypress/e2e/console/devices/onboarding/claiming.spec.js similarity index 92% rename from cypress/integration/console/devices/onboarding/claiming.spec.js rename to cypress/e2e/console/devices/onboarding/claiming.spec.js index 74663d2d32..0e6e855dee 100644 --- a/cypress/integration/console/devices/onboarding/claiming.spec.js +++ b/cypress/e2e/console/devices/onboarding/claiming.spec.js @@ -81,10 +81,9 @@ describe('End device repository claiming', () => { // Provision first device using claiming flow. cy.findByLabelText('DevEUI').type(device1.devEui) cy.findByLabelText('Claim authentication code').type(device1.cac) - cy.findByLabelText('End device ID') - .should('have.value', `eui-${device1.devEui.toLowerCase()}`) - .clear() - .type(device1.id) + cy.findByLabelText('End device ID').should('have.value', `eui-${device1.devEui.toLowerCase()}`) + cy.findByLabelText('End device ID').clear() + cy.findByLabelText('End device ID').type(device1.id) cy.findByLabelText('Register another end device of this type').check() cy.findByRole('button', { name: 'Register end device' }).click() cy.wait('@claim-request') @@ -98,10 +97,9 @@ describe('End device repository claiming', () => { // Provision second device using claiming flow. cy.findByLabelText('DevEUI').type(device2.devEui) cy.findByLabelText('Claim authentication code').type(device2.cac) - cy.findByLabelText('End device ID') - .should('have.value', `eui-${device2.devEui.toLowerCase()}`) - .clear() - .type(device2.id) + cy.findByLabelText('End device ID').should('have.value', `eui-${device2.devEui.toLowerCase()}`) + cy.findByLabelText('End device ID').clear() + cy.findByLabelText('End device ID').type(device2.id) cy.findByLabelText('View registered end device').check() cy.findByRole('button', { name: 'Register end device' }).click() @@ -176,7 +174,8 @@ describe('End device repository claiming', () => { cy.findByLabelText('Cluster settings').should('be.disabled').and('be.checked') cy.findByLabelText('DevEUI').type(device1.devEui) cy.findByLabelText('Claim authentication code').type(device1.cac) - cy.findByLabelText('End device ID').clear().type(device1.id) + cy.findByLabelText('End device ID').clear() + cy.findByLabelText('End device ID').type(device1.id) cy.findByLabelText('Register another end device of this type').check() cy.findByRole('button', { name: 'Register end device' }).click() cy.wait('@claim-request') @@ -192,7 +191,8 @@ describe('End device repository claiming', () => { cy.findByLabelText('LoRaWAN version').selectOption(device2.lorawanVersion) cy.findByLabelText('DevEUI').type(device2.devEui) cy.findByLabelText('Claim authentication code').type(device2.cac) - cy.findByLabelText('End device ID').clear().type(device2.id) + cy.findByLabelText('End device ID').clear() + cy.findByLabelText('End device ID').type(device2.id) cy.findByRole('button', { name: 'Register end device' }).click() cy.wait('@claim-request') .its('request.body') @@ -211,7 +211,8 @@ describe('End device repository claiming', () => { cy.findByLabelText('JoinEUI').type(`${device3.joinEui}{enter}`) cy.findByLabelText('DevEUI').type(device3.devEui) cy.findByLabelText('Claim authentication code').type(device3.cac) - cy.findByLabelText('End device ID').clear().type(device3.id) + cy.findByLabelText('End device ID').clear() + cy.findByLabelText('End device ID').type(device3.id) cy.findByLabelText('View registered end device').check() cy.findByRole('button', { name: 'Register end device' }).click() cy.wait('@claim-request') diff --git a/cypress/integration/console/devices/onboarding/manual-registration.spec.js b/cypress/e2e/console/devices/onboarding/manual-registration.spec.js similarity index 99% rename from cypress/integration/console/devices/onboarding/manual-registration.spec.js rename to cypress/e2e/console/devices/onboarding/manual-registration.spec.js index 512d7975f9..a4feeb4fe8 100644 --- a/cypress/integration/console/devices/onboarding/manual-registration.spec.js +++ b/cypress/e2e/console/devices/onboarding/manual-registration.spec.js @@ -776,7 +776,8 @@ describe('End device manual create', () => { cy.findByRole('button', { name: 'Register end device' }).should('not.be.disabled') // Device 3 - cy.findByLabelText('Device address').clear().type(device3.dev_addr) + cy.findByLabelText('Device address').clear() + cy.findByLabelText('Device address').type(device3.dev_addr) cy.findByLabelText('End device ID').type(device3.id) cy.findByLabelText('View registered end device').check() diff --git a/cypress/integration/console/devices/onboarding/qr-scan.spec.js b/cypress/e2e/console/devices/onboarding/qr-scan.spec.js similarity index 100% rename from cypress/integration/console/devices/onboarding/qr-scan.spec.js rename to cypress/e2e/console/devices/onboarding/qr-scan.spec.js diff --git a/cypress/integration/console/devices/onboarding/repository-registration.spec.js b/cypress/e2e/console/devices/onboarding/repository-registration.spec.js similarity index 98% rename from cypress/integration/console/devices/onboarding/repository-registration.spec.js rename to cypress/e2e/console/devices/onboarding/repository-registration.spec.js index 7559728830..e8d84e26da 100644 --- a/cypress/integration/console/devices/onboarding/repository-registration.spec.js +++ b/cypress/e2e/console/devices/onboarding/repository-registration.spec.js @@ -262,9 +262,7 @@ describe('End device repository manual registration', () => { beforeEach(() => { cy.loginConsole({ user_id: user.ids.user_id, password: user.password }) - cy.visit( - `${Cypress.config('consoleRootPath')}/applications/${appId}/devices/add/repository`, - ) + cy.visit(`${Cypress.config('consoleRootPath')}/applications/${appId}/devices/add`) }) it('displays UI elements in place', () => { @@ -318,7 +316,8 @@ describe('End device repository manual registration', () => { cy.findByRole('button', { name: 'Confirm' }).click() cy.findByLabelText('DevEUI').type(generateHexValue(16)) cy.findByLabelText('AppKey').type(generateHexValue(32)) - cy.findByLabelText('End device ID').clear().type(devId) + cy.findByLabelText('End device ID').clear() + cy.findByLabelText('End device ID').type(devId) cy.findByRole('button', { name: 'Register end device' }).click() @@ -442,9 +441,7 @@ describe('End device repository manual registration', () => { }) cy.loginConsole({ user_id: user.ids.user_id, password: user.password }) - cy.visit( - `${Cypress.config('consoleRootPath')}/applications/${appId}/devices/add/repository`, - ) + cy.visit(`${Cypress.config('consoleRootPath')}/applications/${appId}/devices/add`) }) it('validates before submitting an empty form', () => { @@ -458,12 +455,13 @@ describe('End device repository manual registration', () => { // handler got attached, which is what happens in slow CI environments. // Unfortunately there is currently no good workaround for this. // See also: https://www.cypress.io/blog/2019/01/22/when-can-the-test-click/ - cy.findByLabelText('End device ID').type('type-and-remove').clear() + cy.findByLabelText('End device ID').type('type-and-remove') + cy.findByLabelText('End device ID').clear() cy.findByRole('button', { name: 'Register end device' }).click() cy.location('pathname').should( 'eq', - `${Cypress.config('consoleRootPath')}/applications/${appId}/devices/add/repository`, + `${Cypress.config('consoleRootPath')}/applications/${appId}/devices/add`, ) cy.findErrorByLabelText('Device address') diff --git a/cypress/integration/console/devices/onboarding/utils.js b/cypress/e2e/console/devices/onboarding/utils.js similarity index 100% rename from cypress/integration/console/devices/onboarding/utils.js rename to cypress/e2e/console/devices/onboarding/utils.js diff --git a/cypress/integration/console/devices/unclaim.spec.js b/cypress/e2e/console/devices/unclaim.spec.js similarity index 100% rename from cypress/integration/console/devices/unclaim.spec.js rename to cypress/e2e/console/devices/unclaim.spec.js diff --git a/cypress/integration/console/devices/utils.js b/cypress/e2e/console/devices/utils.js similarity index 100% rename from cypress/integration/console/devices/utils.js rename to cypress/e2e/console/devices/utils.js diff --git a/cypress/integration/console/gateways/create.spec.js b/cypress/e2e/console/gateways/create.spec.js similarity index 96% rename from cypress/integration/console/gateways/create.spec.js rename to cypress/e2e/console/gateways/create.spec.js index 524e16b7c0..c92c292179 100644 --- a/cypress/integration/console/gateways/create.spec.js +++ b/cypress/e2e/console/gateways/create.spec.js @@ -50,7 +50,8 @@ describe('Gateway create', () => { eui: generateHexValue(16), } - cy.findByLabelText('Gateway EUI').type(gateway.eui).blur() + cy.findByLabelText('Gateway EUI').type(gateway.eui) + cy.findByLabelText('Gateway EUI').blur() cy.findByLabelText('Gateway ID').should('have.value', `eui-${gateway.eui}`) cy.findByLabelText('Gateway name').type('Test Gateway') cy.findByLabelText('Frequency plan').selectOption(gateway.frequency_plan) @@ -71,7 +72,8 @@ describe('Gateway create', () => { eui: generateHexValue(16), } - cy.findByLabelText('Gateway EUI').type(gateway.eui).blur() + cy.findByLabelText('Gateway EUI').type(gateway.eui) + cy.findByLabelText('Gateway EUI').blur() cy.findByLabelText('Gateway ID').should('have.value', `eui-${gateway.eui}`) cy.findByLabelText('Gateway name').type('Test Gateway') cy.findByLabelText('Frequency plan').selectOption(gateway.frequency_plan) diff --git a/cypress/integration/console/gateways/edit.spec.js b/cypress/e2e/console/gateways/edit.spec.js similarity index 95% rename from cypress/integration/console/gateways/edit.spec.js rename to cypress/e2e/console/gateways/edit.spec.js index e33acfa98c..4172aab4be 100644 --- a/cypress/integration/console/gateways/edit.spec.js +++ b/cypress/e2e/console/gateways/edit.spec.js @@ -136,9 +136,12 @@ describe('Gateway general settings', () => { const address = 'otherhost' const lnsKey = '1234' - cy.findByLabelText('Gateway name').clear().type(newGatewayName) - cy.findByLabelText('Gateway description').clear().type(newGatewayDesc) - cy.findByLabelText('Gateway Server address').clear().type(address) + cy.findByLabelText('Gateway name').clear() + cy.findByLabelText('Gateway name').type(newGatewayName) + cy.findByLabelText('Gateway description').clear() + cy.findByLabelText('Gateway description').type(newGatewayDesc) + cy.findByLabelText('Gateway Server address').clear() + cy.findByLabelText('Gateway Server address').type(address) cy.findByLabelText('Require authenticated connection').check() cy.findByLabelText('LoRa Basics Station LNS Authentication Key').type(lnsKey) cy.findByLabelText('Gateway status').check() @@ -177,7 +180,8 @@ describe('Gateway general settings', () => { cy.findByRole('button', { name: 'Expand' }).click() cy.findByLabelText('Schedule downlink late').check() cy.findByLabelText(/Enforce duty cycle/).uncheck() - cy.findByLabelText('Schedule any time delay').clear().type('1') + cy.findByLabelText('Schedule any time delay').clear() + cy.findByLabelText('Schedule any time delay').type('1') cy.findByLabelText('Frequency plan').type(`${newFrequencyPlan}{enter}`) cy.findByRole('button', { name: 'Save changes' }).click() }) diff --git a/cypress/integration/console/gateways/location/create.spec.js b/cypress/e2e/console/gateways/location/create.spec.js similarity index 100% rename from cypress/integration/console/gateways/location/create.spec.js rename to cypress/e2e/console/gateways/location/create.spec.js diff --git a/cypress/integration/console/gateways/location/edit.spec.js b/cypress/e2e/console/gateways/location/edit.spec.js similarity index 100% rename from cypress/integration/console/gateways/location/edit.spec.js rename to cypress/e2e/console/gateways/location/edit.spec.js diff --git a/cypress/integration/console/integrations/pub-subs/create.spec.js b/cypress/e2e/console/integrations/pub-subs/create.spec.js similarity index 98% rename from cypress/integration/console/integrations/pub-subs/create.spec.js rename to cypress/e2e/console/integrations/pub-subs/create.spec.js index da70f733fe..3d8983ffb6 100644 --- a/cypress/integration/console/integrations/pub-subs/create.spec.js +++ b/cypress/e2e/console/integrations/pub-subs/create.spec.js @@ -111,8 +111,8 @@ describe('Application Pub/Sub create', () => { cy.findByLabelText('Subscribe QoS').selectOption(pubSub.subscribe_qos) cy.findByLabelText('Publish QoS').selectOption(pubSub.publish_qos) cy.findByLabelText('Pub/Sub format').selectOption(pubSub.format) + cy.findByLabelText('Uplink message').check() cy.findByLabelText('Uplink message') - .check() .parents('[data-test-id="form-field"]') .within(() => { cy.findByPlaceholderText('sub-topic').type(pubSub.uplinkSubTopic) @@ -173,8 +173,8 @@ describe('Application Pub/Sub create', () => { cy.findByLabelText('Subscribe QoS').selectOption(pubSub.subscribe_qos) cy.findByLabelText('Publish QoS').selectOption(pubSub.publish_qos) cy.findByLabelText('Pub/Sub format').selectOption(pubSub.format) + cy.findByLabelText('Uplink message').check() cy.findByLabelText('Uplink message') - .check() .parents('[data-test-id="form-field"]') .within(() => { cy.findByPlaceholderText('sub-topic').type(pubSub.uplinkSubTopic) @@ -258,8 +258,8 @@ describe('Application Pub/Sub create', () => { cy.findByLabelText('Address').type(pubSub.address) cy.findByLabelText('Port').type(pubSub.port) cy.findByLabelText('Pub/Sub format').selectOption(pubSub.format) + cy.findByLabelText('Uplink message').check() cy.findByLabelText('Uplink message') - .check() .parents('[data-test-id="form-field"]') .within(() => { cy.findByPlaceholderText('sub-topic').type(pubSub.uplinkSubTopic) @@ -307,8 +307,8 @@ describe('Application Pub/Sub create', () => { cy.findByLabelText('Address').type(pubSub.address) cy.findByLabelText('Port').type(pubSub.port) cy.findByLabelText('Pub/Sub format').selectOption(pubSub.format) + cy.findByLabelText('Uplink message').check() cy.findByLabelText('Uplink message') - .check() .parents('[data-test-id="form-field"]') .within(() => { cy.findByPlaceholderText('sub-topic').type(pubSub.uplinkSubTopic) diff --git a/cypress/integration/console/integrations/pub-subs/edit.spec.js b/cypress/e2e/console/integrations/pub-subs/edit.spec.js similarity index 100% rename from cypress/integration/console/integrations/pub-subs/edit.spec.js rename to cypress/e2e/console/integrations/pub-subs/edit.spec.js diff --git a/cypress/integration/console/integrations/webhooks/create-with-template.spec.js b/cypress/e2e/console/integrations/webhooks/create-with-template.spec.js similarity index 100% rename from cypress/integration/console/integrations/webhooks/create-with-template.spec.js rename to cypress/e2e/console/integrations/webhooks/create-with-template.spec.js diff --git a/cypress/integration/console/integrations/webhooks/create-without-template.spec.js b/cypress/e2e/console/integrations/webhooks/create-without-template.spec.js similarity index 98% rename from cypress/integration/console/integrations/webhooks/create-without-template.spec.js rename to cypress/e2e/console/integrations/webhooks/create-without-template.spec.js index 864cf75ed5..1195171235 100644 --- a/cypress/integration/console/integrations/webhooks/create-without-template.spec.js +++ b/cypress/e2e/console/integrations/webhooks/create-without-template.spec.js @@ -104,8 +104,8 @@ describe('Application Webhook create without template', () => { cy.findByLabelText('Webhook ID').type(webhook.id) cy.findByLabelText('Webhook format').selectOption(webhook.format) cy.findByLabelText('Base URL').type(webhook.baseUrl) + cy.findByLabelText('Uplink message').check() cy.findByLabelText('Uplink message') - .check() .parents('[data-test-id="form-field"]') .within(() => { cy.findByPlaceholderText('/path/to/webhook').type(webhook.path) @@ -146,8 +146,8 @@ describe('Application Webhook create without template', () => { .and('have.attr', 'value') .and('eq', webhook.id) cy.findByLabelText('Base URL').and('have.attr', 'value', webhook.baseUrl) + cy.findByLabelText('Uplink message').check() cy.findByLabelText('Uplink message') - .check() .parents('[data-test-id="form-field"]') .within(() => { cy.findByPlaceholderText('/path/to/webhook').should('have.attr', 'value', webhook.path) diff --git a/cypress/integration/console/integrations/webhooks/edit.spec.js b/cypress/e2e/console/integrations/webhooks/edit.spec.js similarity index 97% rename from cypress/integration/console/integrations/webhooks/edit.spec.js rename to cypress/e2e/console/integrations/webhooks/edit.spec.js index 005b218745..fe1ca699f9 100644 --- a/cypress/integration/console/integrations/webhooks/edit.spec.js +++ b/cypress/e2e/console/integrations/webhooks/edit.spec.js @@ -65,9 +65,10 @@ describe('Application Webhook', () => { } cy.findByLabelText('Webhook format').selectOption(webhook.format) - cy.findByLabelText('Base URL').clear().type(webhook.url) + cy.findByLabelText('Base URL').clear() + cy.findByLabelText('Base URL').type(webhook.url) + cy.findByLabelText('Uplink message').check() cy.findByLabelText('Uplink message') - .check() .parents('[data-test-id="form-field"]') .within(() => { cy.findByPlaceholderText('/path/to/webhook').type(webhook.path) diff --git a/cypress/integration/console/organizations/create.spec.js b/cypress/e2e/console/organizations/create.spec.js similarity index 100% rename from cypress/integration/console/organizations/create.spec.js rename to cypress/e2e/console/organizations/create.spec.js diff --git a/cypress/integration/console/organizations/edit.spec.js b/cypress/e2e/console/organizations/edit.spec.js similarity index 100% rename from cypress/integration/console/organizations/edit.spec.js rename to cypress/e2e/console/organizations/edit.spec.js diff --git a/cypress/integration/console/shared/api-keys/add.spec.js b/cypress/e2e/console/shared/api-keys/add.spec.js similarity index 100% rename from cypress/integration/console/shared/api-keys/add.spec.js rename to cypress/e2e/console/shared/api-keys/add.spec.js diff --git a/cypress/integration/console/shared/api-keys/edit.spec.js b/cypress/e2e/console/shared/api-keys/edit.spec.js similarity index 100% rename from cypress/integration/console/shared/api-keys/edit.spec.js rename to cypress/e2e/console/shared/api-keys/edit.spec.js diff --git a/cypress/integration/console/shared/collaborators/add.spec.js b/cypress/e2e/console/shared/collaborators/add.spec.js similarity index 100% rename from cypress/integration/console/shared/collaborators/add.spec.js rename to cypress/e2e/console/shared/collaborators/add.spec.js diff --git a/cypress/integration/console/shared/collaborators/edit.spec.js b/cypress/e2e/console/shared/collaborators/edit.spec.js similarity index 100% rename from cypress/integration/console/shared/collaborators/edit.spec.js rename to cypress/e2e/console/shared/collaborators/edit.spec.js diff --git a/cypress/integration/console/shared/connection-losses.spec.js b/cypress/e2e/console/shared/connection-losses.spec.js similarity index 87% rename from cypress/integration/console/shared/connection-losses.spec.js rename to cypress/e2e/console/shared/connection-losses.spec.js index 2dc631f399..170acd7660 100644 --- a/cypress/integration/console/shared/connection-losses.spec.js +++ b/cypress/e2e/console/shared/connection-losses.spec.js @@ -30,8 +30,11 @@ describe('Connection loss detection', () => { cy.loginConsole({ user_id: user.ids.user_id, password: user.password }) cy.visit(Cypress.config('consoleRootPath')) cy.findByText('Welcome to the Console!').should('be.visible') - cy.intercept('/api/v3/application*', { forceNetworkError: true }) - cy.intercept('/api/v3/auth_info', { times: 2 }, { forceNetworkError: true }) + + cy.intercept('/api/v3/application*', { forceNetworkError: true }).as('offlineIntercept') + cy.intercept('/api/v3/auth_info', { times: 2 }, { forceNetworkError: true }).as( + 'reconnectionIntercept', + ) cy.get('header').within(() => { cy.findByRole('link', { name: /Applications/ }).click() @@ -41,13 +44,19 @@ describe('Connection loss detection', () => { cy.findByText(/Connection issues/).should('be.visible') cy.findByText(/Offline/).should('be.visible') }) + cy.findByTestId('toast-notification') - .as('offlineToast') .findByText(/offline/) + .as('offlineToast') .should('be.visible') - cy.get('@offlineToast', { timeout: 20000 }).should('not.be.visible') + // Use an assertion to check that the 'offline' toast notification is no longer in the DOM. + cy.get('@offlineToast').should('not.exist') + + // After the 'offline' toast has disappeared, wait for the reconnection intercept to resolve. + cy.wait('@reconnectionIntercept') + // Now the 'online' toast should appear. cy.findByTestId('toast-notification') .findByText(/online/) .should('be.visible') @@ -57,7 +66,6 @@ describe('Connection loss detection', () => { cy.findByText(/Offline/).should('not.exist') }) }) - it('does not see individual network errors as connection loss', () => { cy.loginConsole({ user_id: user.ids.user_id, password: user.password }) cy.visit(Cypress.config('consoleRootPath')) diff --git a/cypress/integration/console/shared/header.spec.js b/cypress/e2e/console/shared/header.spec.js similarity index 92% rename from cypress/integration/console/shared/header.spec.js rename to cypress/e2e/console/shared/header.spec.js index 511e899309..37d2d5129d 100644 --- a/cypress/integration/console/shared/header.spec.js +++ b/cypress/e2e/console/shared/header.spec.js @@ -13,6 +13,9 @@ // limitations under the License. describe('Header', () => { + before(() => { + cy.dropAndSeedDatabase() + }) describe('Console logout', () => { const logout = userName => { cy.get('header').within(() => { @@ -49,14 +52,11 @@ describe('Header', () => { const baseUrl = Cypress.config('baseUrl') const consoleRootPath = Cypress.config('consoleRootPath') const accountAppRootPath = Cypress.config('accountAppRootPath') - cy.server() - cy.route({ - method: 'POST', - url: `${baseUrl}${consoleRootPath}/api/auth/logout`, - onRequest: req => { - expect(req.request.headers).to.have.property('X-CSRF-Token') - }, - }) + + cy.intercept('POST', `${baseUrl}${consoleRootPath}/api/auth/logout`, req => { + // Asserting on the request headers + expect(req.headers).to.have.property('x-csrf-token') + }).as('logout') cy.createUser(user) cy.loginConsole({ user_id: user.ids.user_id, password: user.password }) diff --git a/cypress/integration/console/shared/overview/overview.spec.js b/cypress/e2e/console/shared/overview/overview.spec.js similarity index 100% rename from cypress/integration/console/shared/overview/overview.spec.js rename to cypress/e2e/console/shared/overview/overview.spec.js diff --git a/cypress/integration/console/shared/payload-formatters/edit.spec.js b/cypress/e2e/console/shared/payload-formatters/edit.spec.js similarity index 100% rename from cypress/integration/console/shared/payload-formatters/edit.spec.js rename to cypress/e2e/console/shared/payload-formatters/edit.spec.js diff --git a/cypress/integration/console/shared/skip-payload-crypto/edit.spec.js b/cypress/e2e/console/shared/skip-payload-crypto/edit.spec.js similarity index 100% rename from cypress/integration/console/shared/skip-payload-crypto/edit.spec.js rename to cypress/e2e/console/shared/skip-payload-crypto/edit.spec.js diff --git a/cypress/integration/smoke/applications/create.js b/cypress/e2e/smoke/applications/create.js similarity index 94% rename from cypress/integration/smoke/applications/create.js rename to cypress/e2e/smoke/applications/create.js index 179b1406fd..3d061db0be 100644 --- a/cypress/integration/smoke/applications/create.js +++ b/cypress/e2e/smoke/applications/create.js @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { disableApplicationServer } from '../../../support/utils' import { defineSmokeTest } from '../utils' const applicationCreate = defineSmokeTest('succeeds creating application', () => { @@ -23,7 +22,6 @@ const applicationCreate = defineSmokeTest('succeeds creating application', () => password_confirm: 'ABCDefg123!', email: 'app-create-test-user@example.com', } - cy.augmentStackConfig(disableApplicationServer) cy.createUser(user) cy.loginConsole({ user_id: user.ids.user_id, password: user.password }) cy.visit(Cypress.config('consoleRootPath')) diff --git a/cypress/integration/smoke/applications/index.js b/cypress/e2e/smoke/applications/index.js similarity index 100% rename from cypress/integration/smoke/applications/index.js rename to cypress/e2e/smoke/applications/index.js diff --git a/cypress/integration/smoke/applications/subpages.js b/cypress/e2e/smoke/applications/subpages.js similarity index 100% rename from cypress/integration/smoke/applications/subpages.js rename to cypress/e2e/smoke/applications/subpages.js diff --git a/cypress/integration/smoke/authorization/index.js b/cypress/e2e/smoke/authorization/index.js similarity index 100% rename from cypress/integration/smoke/authorization/index.js rename to cypress/e2e/smoke/authorization/index.js diff --git a/cypress/integration/smoke/contact-info-validation/index.js b/cypress/e2e/smoke/contact-info-validation/index.js similarity index 100% rename from cypress/integration/smoke/contact-info-validation/index.js rename to cypress/e2e/smoke/contact-info-validation/index.js diff --git a/cypress/integration/smoke/devices/index.js b/cypress/e2e/smoke/devices/index.js similarity index 100% rename from cypress/integration/smoke/devices/index.js rename to cypress/e2e/smoke/devices/index.js diff --git a/cypress/integration/smoke/feature-toggles/index.js b/cypress/e2e/smoke/feature-toggles/index.js similarity index 100% rename from cypress/integration/smoke/feature-toggles/index.js rename to cypress/e2e/smoke/feature-toggles/index.js diff --git a/cypress/integration/smoke/forgot-password/index.js b/cypress/e2e/smoke/forgot-password/index.js similarity index 100% rename from cypress/integration/smoke/forgot-password/index.js rename to cypress/e2e/smoke/forgot-password/index.js diff --git a/cypress/integration/smoke/gateways/create.js b/cypress/e2e/smoke/gateways/create.js similarity index 100% rename from cypress/integration/smoke/gateways/create.js rename to cypress/e2e/smoke/gateways/create.js diff --git a/cypress/integration/smoke/gateways/delete.js b/cypress/e2e/smoke/gateways/delete.js similarity index 100% rename from cypress/integration/smoke/gateways/delete.js rename to cypress/e2e/smoke/gateways/delete.js diff --git a/cypress/integration/smoke/gateways/index.js b/cypress/e2e/smoke/gateways/index.js similarity index 100% rename from cypress/integration/smoke/gateways/index.js rename to cypress/e2e/smoke/gateways/index.js diff --git a/cypress/integration/smoke/gateways/subpages.js b/cypress/e2e/smoke/gateways/subpages.js similarity index 100% rename from cypress/integration/smoke/gateways/subpages.js rename to cypress/e2e/smoke/gateways/subpages.js diff --git a/cypress/integration/smoke/index.js b/cypress/e2e/smoke/index.js similarity index 100% rename from cypress/integration/smoke/index.js rename to cypress/e2e/smoke/index.js diff --git a/cypress/integration/smoke/organizations/create.js b/cypress/e2e/smoke/organizations/create.js similarity index 100% rename from cypress/integration/smoke/organizations/create.js rename to cypress/e2e/smoke/organizations/create.js diff --git a/cypress/integration/smoke/organizations/delete.js b/cypress/e2e/smoke/organizations/delete.js similarity index 100% rename from cypress/integration/smoke/organizations/delete.js rename to cypress/e2e/smoke/organizations/delete.js diff --git a/cypress/integration/smoke/organizations/index.js b/cypress/e2e/smoke/organizations/index.js similarity index 100% rename from cypress/integration/smoke/organizations/index.js rename to cypress/e2e/smoke/organizations/index.js diff --git a/cypress/integration/smoke/organizations/subpages.js b/cypress/e2e/smoke/organizations/subpages.js similarity index 100% rename from cypress/integration/smoke/organizations/subpages.js rename to cypress/e2e/smoke/organizations/subpages.js diff --git a/cypress/integration/smoke/profile-settings/index.js b/cypress/e2e/smoke/profile-settings/index.js similarity index 100% rename from cypress/integration/smoke/profile-settings/index.js rename to cypress/e2e/smoke/profile-settings/index.js diff --git a/cypress/integration/smoke/registration/index.js b/cypress/e2e/smoke/registration/index.js similarity index 100% rename from cypress/integration/smoke/registration/index.js rename to cypress/e2e/smoke/registration/index.js diff --git a/cypress/integration/smoke/smoke.spec.js b/cypress/e2e/smoke/smoke.spec.js similarity index 100% rename from cypress/integration/smoke/smoke.spec.js rename to cypress/e2e/smoke/smoke.spec.js diff --git a/cypress/integration/smoke/utils.js b/cypress/e2e/smoke/utils.js similarity index 100% rename from cypress/integration/smoke/utils.js rename to cypress/e2e/smoke/utils.js diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js deleted file mode 100644 index a7cfba559c..0000000000 --- a/cypress/plugins/index.js +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright © 2020 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -const { cypressBrowserPermissionsPlugin } = require('cypress-browser-permissions') -const cypressLogToOutput = require('cypress-log-to-output') - -const tasks = require('./tasks') - -module.exports = (on, config) => { - const configWithPermissions = cypressBrowserPermissionsPlugin(on, config) - - tasks.stackConfigTask(on, configWithPermissions) - tasks.sqlTask(on, configWithPermissions) - tasks.fileExistsTask(on, configWithPermissions) - tasks.emailTask(on, configWithPermissions) - - if (process.env.NODE_ENV === 'development') { - tasks.codeCoverageTask(on, configWithPermissions) - } - - on('before:browser:launch', (browser = {}, launchOptions) => { - // Log console log to output when debug mode is enabled. - if (process.env.RUNNER_DEBUG) { - launchOptions.args = cypressLogToOutput.browserLaunchHandler(browser, launchOptions.args) - } - if (browser.family === 'chromium' && browser.name !== 'electron') { - launchOptions.args.push( - '--use-file-for-fake-video-capture=cypress/fixtures/qr-code-mock-feed.y4m', - ) - } - - if (browser.name === 'chrome' && browser.isHeadless) { - launchOptions.args.push('--disable-gpu') - } - - return launchOptions - }) - - return configWithPermissions -} diff --git a/cypress/plugins/tasks.js b/cypress/plugins/tasks.js index dabe3ba210..9596d21a6d 100644 --- a/cypress/plugins/tasks.js +++ b/cypress/plugins/tasks.js @@ -19,7 +19,6 @@ const util = require('util') const { Client } = require('pg') const yaml = require('js-yaml') -const codeCoverageTask = require('@cypress/code-coverage/task') const isCI = process.env.CI === 'true' || process.env.CI === '1' @@ -33,7 +32,7 @@ const pgConfig = { // Sources stack configuration entries to Cypress configuration while preserving all entries from cypress.json. const stackConfigTask = (_, config) => { - const out = childProcess.execSync(`${isCI ? './' : 'go run ./cmd/'}ttn-lw-stack config --yml`) + const out = childProcess.execSync(`${isCI ? '../' : 'go run ../cmd/'}ttn-lw-stack config --yml`) const yml = yaml.load(out) // Cluster. @@ -79,15 +78,15 @@ const sqlTask = on => { dropAndSeedDatabase: async () => { const exec = util.promisify(childProcess.exec) const res = await Promise.all([ - exec('tools/bin/mage dev:sqlRestore'), - exec('tools/bin/mage dev:redisFlush'), + exec('tools/bin/mage dev:sqlRestore', { cwd: '..' }), + exec('tools/bin/mage dev:redisFlush', { cwd: '..' }), ]) const err = res .filter(e => Boolean(e.stderr)) .map(e => e.stderr) .join(', ') if (err) { - throw new Error(err) + // Throw new Error(err) } return null }, @@ -97,7 +96,7 @@ const sqlTask = on => { const emailTask = on => { on('task', { findInLatestEmail: async (regExp, capturingGroup = 0) => { - const emailDir = '.dev/email' + const emailDir = '../.dev/email' const re = new RegExp(regExp, 'm') const files = fs.readdirSync(emailDir) const latestMails = files @@ -141,7 +140,6 @@ const fileExistsTask = on => { module.exports = { stackConfigTask, - codeCoverageTask, sqlTask, fileExistsTask, emailTask, diff --git a/cypress/support/index.js b/cypress/support/e2e.js similarity index 86% rename from cypress/support/index.js rename to cypress/support/e2e.js index 550359b186..cf9b8d651b 100644 --- a/cypress/support/index.js +++ b/cypress/support/e2e.js @@ -29,7 +29,11 @@ afterEach(function () { // Enable fail-early, if set.: if (this.currentTest.state === 'failed' && Cypress.env('FAIL_FAST')) { cy.log('Skipping rest of run due to test failure (fail fast)') - cy.writeFile(failedSpecsFilename, this.currentTest.invocationDetails.relativeFile) + const file = this.currentTest.invocationDetails.relativeFile + // The file will be relative to the `./config` directory, so we need to + // remove the `../` prefix. + const relativeFile = file.replace(/^\.\.\//, '') + cy.writeFile(failedSpecsFilename, relativeFile) } else { // Apply a workaround for requests spilling over to the subsequent test. // See also https://github.com/cypress-io/cypress/issues/686. diff --git a/package.json b/package.json index 8206460a52..4aa3675b87 100644 --- a/package.json +++ b/package.json @@ -6,104 +6,102 @@ "repository": "https://github.com/TheThingsNetwork/lorawan-stack.git", "license": "Apache-2.0", "devDependencies": { - "@babel/cli": "^7.21.5", - "@babel/core": "^7.18.0", - "@babel/eslint-parser": "^7.12.17", - "@babel/plugin-proposal-class-properties": "^7.14.5", - "@babel/plugin-proposal-decorators": "^7.21.0", + "@babel/cli": "^7.22.5", + "@babel/core": "^7.22.5", + "@babel/eslint-parser": "^7.22.5", + "@babel/plugin-proposal-class-properties": "^7.18.6", + "@babel/plugin-proposal-decorators": "^7.22.5", "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.18.10", - "@babel/plugin-transform-spread": "^7.18.9", - "@babel/plugin-transform-strict-mode": "^7.18.6", - "@babel/preset-env": "^7.16.11", - "@babel/preset-react": "^7.16.0", - "@babel/register": "^7.21.0", - "@babel/runtime-corejs2": "^7.21.5", - "@cypress/code-coverage": "^3.10.4", - "@pmmmwh/react-refresh-webpack-plugin": "^0.5.8", - "@storybook/addon-actions": "^6.4.9", + "@babel/plugin-transform-runtime": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-strict-mode": "^7.22.5", + "@babel/preset-env": "^7.22.5", + "@babel/preset-react": "^7.22.5", + "@babel/register": "^7.22.5", + "@babel/runtime-corejs2": "^7.22.5", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.10", + "@storybook/addon-actions": "^7.0.24", "@storybook/addon-info": "6.0.0-alpha.2", - "@storybook/react": "^7.0.9", - "@testing-library/cypress": "^8.0.3", - "add-asset-html-webpack-plugin": "^4.0.1", - "babel-jest": "^29.3.1", - "babel-loader": "^8.2.4", + "@storybook/react": "^7.0.24", + "@testing-library/cypress": "^9.0.0", + "add-asset-html-webpack-plugin": "^6.0.0", + "babel-jest": "^29.5.0", + "babel-loader": "^9.1.2", "babel-plugin-lodash": "^3.3.4", "babel-plugin-react-intl": "^3.0.1", "babel-plugin-react-intl-auto": "^3.3.0", "clean-webpack-plugin": "^4.0.0", "copy-webpack-plugin": "^11.0.0", "css-hot-loader": "^1.4.4", - "css-loader": "^5.2.7", - "cypress": "7.7.0", + "css-loader": "^6.8.1", + "cypress": "12.16.0", "cypress-browser-permissions": "^1.1.0", "cypress-file-upload": "^5.0.8", "cypress-log-to-output": "^1.1.2", - "eslint": "^7.29.0", - "eslint-config-prettier": "^8.5.0", + "eslint": "^8.44.0", + "eslint-config-prettier": "^8.8.0", "eslint-config-ttn": "git+https://github.com/TheThingsNetwork/eslint-config-ttn.git#v1.4.0", "eslint-import-resolver-webpack": "^0.13.0", "eslint-plugin-babel": "^5.3.1", - "eslint-plugin-cypress": "^2.11.2", - "eslint-plugin-import": "^2.26.0", - "eslint-plugin-jest": "^27.0.1", - "eslint-plugin-jsdoc": "^43.1.1", + "eslint-plugin-cypress": "^2.13.3", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jest": "^27.2.2", + "eslint-plugin-jsdoc": "^46.4.3", "eslint-plugin-prefer-arrow": "^1.2.3", - "eslint-plugin-prettier": "^3.3.1", - "eslint-plugin-react": "^7.31.8", + "eslint-plugin-prettier": "^4.2.1", + "eslint-plugin-react": "^7.32.2", "file-loader": "^6.2.0", - "html-webpack-plugin": "^4.5.1", - "jest": "^26.6.3", + "html-webpack-plugin": "^5.5.3", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", "js-yaml": "^4.0.0", "json": "^11.0.0", "messageformat-parser": "^4.1.3", - "mini-css-extract-plugin": "^2.7.5", - "mjml": "^4.14.1", + "mini-css-extract-plugin": "^2.7.6", "mkdirp": "^3.0.1", "nib": "^1.2.0", - "pg": "^8.8.0", - "prettier": "^2.7.1", + "pg": "^8.11.1", + "prettier": "^2.8.8", "react-refresh": "^0.14.0", "stylint": "^2.0.0", "stylus": "^0.59.0", - "stylus-loader": "^4.3.3", - "wait-on": "^6.0.1", - "webpack": "^5.82.1", - "webpack-cli": "^5.1.1", - "webpack-dev-server": "^4.15.0", + "stylus-loader": "^7.1.3", + "wait-on": "^7.0.1", + "webpack": "^5.88.1", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1", "webpack-shell-plugin": "https://github.com/cdeutsch/webpack-shell-plugin.git#bee537d", "yargs": "^17.7.2" }, "dependencies": { - "@formatjs/intl-datetimeformat": "^4.2.6", - "@formatjs/intl-displaynames": "^6.0.1", - "@formatjs/intl-listformat": "^7.2.2", - "@formatjs/intl-locale": "^3.0.6", - "@formatjs/intl-numberformat": "^8.0.4", - "@formatjs/intl-pluralrules": "^4.1.2", - "@formatjs/intl-relativetimeformat": "^11.2.2", - "@sentry/browser": "^7.6.0", - "@sentry/integrations": "^7.35.0", + "@formatjs/intl-datetimeformat": "^6.10.0", + "@formatjs/intl-displaynames": "^6.5.0", + "@formatjs/intl-listformat": "^7.4.0", + "@formatjs/intl-locale": "^3.3.2", + "@formatjs/intl-numberformat": "^8.7.0", + "@formatjs/intl-pluralrules": "^5.2.4", + "@formatjs/intl-relativetimeformat": "^11.2.4", + "@sentry/browser": "^7.57.0", + "@sentry/integrations": "^7.57.0", "@tippyjs/react": "^4.2.6", "autobind-decorator": "^2.4.0", "axios": "^1.4.0", "brace": "^0.11.1", - "cancelable-promise": "^4.3.0", + "cancelable-promise": "^4.3.1", "classnames": "^2.3.2", - "clipboard": "^2.0.6", + "clipboard": "^2.0.11", "connected-react-router": "^6.9.3", "deep-diff": "^1.0.2", "focus-visible": "^5.2.0", - "formik": "^2.2.9", - "history": "^4.10.1", - "intl": "^1.2.5", + "formik": "^2.4.2", + "history": "^5.3.0", "jsqr": "^1.4.0", - "leaflet": "^1.7.1", + "leaflet": "^1.9.4", "lodash": "^4.17.21", - "lottie-web": "^5.9.2", + "lottie-web": "^5.12.2", "md5": "^2.3.0", "prop-types": "^15.8.1", - "query-string": "^7.1.1", + "query-string": "^8.1.0", "react": "^17.0.1", "react-ace": "^6.6.0", "react-display-name": "^0.2.5", @@ -111,51 +109,42 @@ "react-focus-lock": "^2.9.4", "react-grid-system": "^8.1.9", "react-helmet": "^6.1.0", - "react-intl": "^6.2.4", - "react-leaflet": ">=3.1.0 <3.2.0", - "react-paginate": "^8.1.3", - "react-redux": "^7.2.9", - "react-remove-scroll": "^2.5.5", - "react-router-dom": "^5.3.0", - "react-select": "^4.1.0", - "react-string-replace": "^1.1.0", + "react-intl": "^6.4.4", + "react-leaflet": "^4.2.1", + "react-paginate": "^8.2.0", + "react-redux": "^8.1.1", + "react-remove-scroll": "^2.5.6", + "react-router-dom": "^6.14.1", + "react-select": "^5.7.3", + "react-string-replace": "^1.1.1", "react-switch": "^7.0.0", "react-text-mask": "^5.5.0", - "react-toastify": "^8.1.0", - "react-virtualized-auto-sizer": "^1.0.4", + "react-toastify": "^9.1.3", + "react-virtualized-auto-sizer": "^1.0.20", "react-window": "^1.8.9", "redux": "^4.2.1", "redux-actions": "^2.6.5", "redux-logic": "^3.0.2", "redux-sentry-middleware": "^0.2.2", - "scroll-into-view-if-needed": "^2.2.29", + "reselect": "^4.1.8", + "scroll-into-view-if-needed": "^3.0.10", "ttn-lw": "file:sdk/js", "unicode-properties": "^1.4.1", - "url-polyfill": "^1.1.12", "url-template": "^3.1.0", - "yup": "^0.32.9" - }, - "resolutions": { - "cypress": "^7.7.0", - "ssri": "^8.0.1", - "glob-parent": "^5.1.2", - "react-router-dom/react-router": "^5.2.1", - "prismjs": "^1.25.0", - "trim": "^0.0.3", - "immer": "^9.0.6" + "yup": "^1.2.0" }, "babel": { "extends": "./config/babel.config.json" }, "jest": { - "setupFiles": [ + "setupFilesAfterEnv": [ "/config/jest/setup.js" ], "testMatch": [ "/pkg/**/*_test.js" ], "transform": { - "^.+\\.js$": "babel-jest", + "^.+\\.js$": ["babel-jest", { "configFile": "./config/babel.config.json" }], "\\.(css|styl|less|sass)$": "/config/jest/styles.transform.js", "\\.(jpg|jpeg|svg|png|woff|woff2|ttf|eot|gif|webp)$": "/config/jest/files.transform.js" }, @@ -165,7 +154,8 @@ "^\\@console(.*)": "/pkg/webui/console$1", "^\\@account(.*)": "/pkg/webui/account$1", "^\\@assets(.*)": "/pkg/webui/assets$1" - } + }, + "testEnvironment": "jest-environment-jsdom" }, "eslintConfig": { "extends": "./config/eslintrc.yaml" diff --git a/pkg/webui/account/constants/auth-routes.js b/pkg/webui/account/constants/auth-routes.js index a58e6adf3c..6d37a192af 100644 --- a/pkg/webui/account/constants/auth-routes.js +++ b/pkg/webui/account/constants/auth-routes.js @@ -1,4 +1,4 @@ -// Copyright © 2021 The Things Network Foundation, The Things Industries B.V. +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -23,35 +23,30 @@ import OAuthClientAuthorizations from '@account/views/oauth-client-authorization export default [ { path: '/', - exact: true, - component: Overview, + Component: Overview, }, { path: '/profile-settings', - exact: true, - component: ProfileSettings, + Component: ProfileSettings, }, { path: '/code', - exact: true, - component: Code, + Component: Code, }, { path: '/session-management', - exact: true, - component: SessionManagement, + Component: SessionManagement, }, { path: '/validate', - exact: true, - component: ValidateWithAuth, + Component: ValidateWithAuth, }, { - path: '/oauth-clients', - component: OAuthClients, + path: '/oauth-clients/*', + Component: OAuthClients, }, { - path: '/client-authorizations', - component: OAuthClientAuthorizations, + path: '/client-authorizations/*', + Component: OAuthClientAuthorizations, }, ] diff --git a/pkg/webui/account/containers/authorizations-table/index.js b/pkg/webui/account/containers/authorizations-table/index.js index 38d3c717db..e34e403fd3 100644 --- a/pkg/webui/account/containers/authorizations-table/index.js +++ b/pkg/webui/account/containers/authorizations-table/index.js @@ -1,4 +1,4 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,35 +13,35 @@ // limitations under the License. import React from 'react' -import { connect } from 'react-redux' +import { useSelector } from 'react-redux' import { defineMessages } from 'react-intl' +import { createSelector } from 'reselect' import FetchTable from '@ttn-lw/containers/fetch-table' import Message from '@ttn-lw/lib/components/message' import DateTime from '@ttn-lw/lib/components/date-time' -import PropTypes from '@ttn-lw/lib/prop-types' import sharedMessages from '@ttn-lw/lib/shared-messages' import { getAuthorizationsList } from '@account/store/actions/authorizations' -import { selectUserId } from '@account/store/selectors/user' import { selectAuthorizations, selectAuthorizationsTotalCount, selectAuthorizationsFetching, } from '@account/store/selectors/authorizations' +import { selectUserId } from '@account/store/selectors/user' const m = defineMessages({ clientId: 'Client ID', tableTitle: 'OAuth client authorizations', }) -const getItemPathPrefix = item => `/${item.client_ids.client_id}` +const getItemPathPrefix = item => `${item.client_ids.client_id}` -const OAuthClientAuthorizationsTable = props => { - const { userId, ...rest } = props +const OAuthClientAuthorizationsTable = () => { + const userId = useSelector(selectUserId) const headers = React.useMemo(() => { const baseHeaders = [ @@ -66,14 +66,14 @@ const OAuthClientAuthorizationsTable = props => { return baseHeaders }, []) - const baseDataSelector = React.useCallback( - state => ({ - authorizations: selectAuthorizations(state), - totalCount: selectAuthorizationsTotalCount(state), - fetching: selectAuthorizationsFetching(state), + const baseDataSelector = createSelector( + [selectAuthorizations, selectAuthorizationsTotalCount, selectAuthorizationsFetching], + (authorizations, totalCount, fetching) => ({ + authorizations, + totalCount, + fetching, mayAdd: false, }), - [], ) const getItems = React.useCallback(filters => getAuthorizationsList(userId, filters), [userId]) @@ -88,15 +88,8 @@ const OAuthClientAuthorizationsTable = props => { getItemPathPrefix={getItemPathPrefix} tableTitle={} clickable - {...rest} /> ) } -OAuthClientAuthorizationsTable.propTypes = { - userId: PropTypes.string.isRequired, -} - -export default connect(state => ({ - userId: selectUserId(state), -}))(OAuthClientAuthorizationsTable) +export default OAuthClientAuthorizationsTable diff --git a/pkg/webui/account/containers/clients-table/index.js b/pkg/webui/account/containers/clients-table/index.js index ff6312b61c..65401114e7 100644 --- a/pkg/webui/account/containers/clients-table/index.js +++ b/pkg/webui/account/containers/clients-table/index.js @@ -1,4 +1,4 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,9 +13,9 @@ // limitations under the License. import React from 'react' -import { connect } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { defineMessages, useIntl } from 'react-intl' -import { bindActionCreators } from 'redux' +import { createSelector } from 'reselect' import toast from '@ttn-lw/components/toast' import Button from '@ttn-lw/components/button' @@ -25,10 +25,8 @@ import DeleteModalButton from '@ttn-lw/components/delete-modal-button' import FetchTable from '@ttn-lw/containers/fetch-table' import Message from '@ttn-lw/lib/components/message' -import withRequest from '@ttn-lw/lib/components/with-request' import DateTime from '@ttn-lw/lib/components/date-time' -import PropTypes from '@ttn-lw/lib/prop-types' import sharedMessages from '@ttn-lw/lib/shared-messages' import attachPromise from '@ttn-lw/lib/store/actions/attach-promise' import capitalizeMessage from '@ttn-lw/lib/capitalize-message' @@ -36,14 +34,8 @@ import capitalizeMessage from '@ttn-lw/lib/capitalize-message' import { checkFromState, mayPerformAllClientActions } from '@account/lib/feature-checks' import { deleteClient, restoreClient, getClientsList } from '@account/store/actions/clients' -import { getUserRights } from '@account/store/actions/user' -import { - selectUserIsAdmin, - selectUserId, - selectUserRightsFetching, - selectUserRights, -} from '@account/store/selectors/user' +import { selectUserIsAdmin } from '@account/store/selectors/user' import { selectOAuthClients, selectOAuthClientsTotalCount, @@ -73,10 +65,12 @@ const tabs = [ }, { title: sharedMessages.deleted, name: DELETED_TAB }, ] +const mayAddSelector = state => checkFromState(mayPerformAllClientActions, state) -const ClientsTable = props => { - const { isAdmin, restoreClient, purgeClient, ...rest } = props +const ClientsTable = () => { const { formatMessage } = useIntl() + const dispatch = useDispatch() + const isAdmin = useSelector(selectUserIsAdmin) const [tab, setTab] = React.useState(OWNED_TAB) const isDeletedTab = tab === DELETED_TAB @@ -84,7 +78,7 @@ const ClientsTable = props => { const handleRestore = React.useCallback( async id => { try { - await restoreClient(id) + await dispatch(attachPromise(restoreClient(id))) toast({ title: id, message: m.restoreSuccess, @@ -98,13 +92,13 @@ const ClientsTable = props => { }) } }, - [restoreClient], + [dispatch], ) const handlePurge = React.useCallback( async id => { try { - await purgeClient(id) + await dispatch(attachPromise(deleteClient(id, { purge: true }))) toast({ title: id, message: m.purgeSuccess, @@ -118,7 +112,7 @@ const ClientsTable = props => { }) } }, - [purgeClient], + [dispatch], ) const headers = React.useMemo(() => { @@ -187,14 +181,14 @@ const ClientsTable = props => { return baseHeaders }, [tab, handlePurge, handleRestore, formatMessage]) - const baseDataSelector = React.useCallback( - state => ({ - clients: selectOAuthClients(state), - totalCount: selectOAuthClientsTotalCount(state), - fetching: selectOAuthClientsFetching(state), - mayAdd: checkFromState(mayPerformAllClientActions, state), + const baseDataSelector = createSelector( + [selectOAuthClients, selectOAuthClientsTotalCount, selectOAuthClientsFetching, mayAddSelector], + (clients, totalCount, fetching, mayAdd) => ({ + clients, + totalCount, + fetching, + mayAdd, }), - [], ) const getItems = React.useCallback(filters => { @@ -219,44 +213,8 @@ const ClientsTable = props => { searchable clickable={!isDeletedTab} tabs={isAdmin ? tabs : []} - {...rest} /> ) } -ClientsTable.propTypes = { - isAdmin: PropTypes.bool.isRequired, - purgeClient: PropTypes.func.isRequired, - restoreClient: PropTypes.func.isRequired, -} - -export default connect( - state => ({ - isAdmin: selectUserIsAdmin(state), - userId: selectUserId(state), - fetching: selectUserRightsFetching(state), - rights: selectUserRights(state), - }), - dispatch => ({ - ...bindActionCreators( - { - purgeClient: attachPromise(deleteClient), - restoreClient: attachPromise(restoreClient), - }, - dispatch, - ), - getUsersRightsList: userId => dispatch(attachPromise(getUserRights(userId))), - }), - (stateProps, dispatchProps, ownProps) => ({ - ...stateProps, - ...dispatchProps, - ...ownProps, - purgeClient: id => dispatchProps.purgeClient(id, { purge: true }), - restoreClient: id => dispatchProps.restoreClient(id), - }), -)( - withRequest( - ({ getUsersRightsList, userId }) => getUsersRightsList(userId), - ({ fetching, rights }) => fetching || rights.length === 0, - )(ClientsTable), -) +export default ClientsTable diff --git a/pkg/webui/console/views/user-api-keys-list/connect.js b/pkg/webui/account/containers/collaborators-form/index.js similarity index 64% rename from pkg/webui/console/views/user-api-keys-list/connect.js rename to pkg/webui/account/containers/collaborators-form/index.js index a34edd7c76..c71effcc80 100644 --- a/pkg/webui/console/views/user-api-keys-list/connect.js +++ b/pkg/webui/account/containers/collaborators-form/index.js @@ -1,4 +1,4 @@ -// Copyright © 2020 The Things Network Foundation, The Things Industries B.V. +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,12 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { connect } from 'react-redux' +import React from 'react' -import { selectUserId } from '@console/store/selectors/logout' +import tts from '@account/api/tts' -const mapStateToProps = state => ({ - userId: selectUserId(state), -}) +import CollaboratorForm from '@ttn-lw/containers/collaborator-form' -export default UserApiKeysList => connect(mapStateToProps)(UserApiKeysList) +const AccountCollaboratorsForm = props => + +export default AccountCollaboratorsForm diff --git a/pkg/webui/account/containers/collaborators-table/index.js b/pkg/webui/account/containers/collaborators-table/index.js index 30e1caa2b8..78cda6ac46 100644 --- a/pkg/webui/account/containers/collaborators-table/index.js +++ b/pkg/webui/account/containers/collaborators-table/index.js @@ -16,6 +16,7 @@ import React from 'react' import { connect, useDispatch } from 'react-redux' import { defineMessages, useIntl } from 'react-intl' import { bindActionCreators } from 'redux' +import { createSelector } from 'reselect' import Icon from '@ttn-lw/components/icon' import Button from '@ttn-lw/components/button' @@ -163,15 +164,20 @@ const CollaboratorsTable = props => { return baseHeaders }, [intl, currentUserId, deleteCollaborator]) - const baseDataSelector = React.useCallback( - state => ({ - collaborators: selectCollaborators(state, clientId), - totalCount: selectCollaboratorsTotalCount(state, clientId), - fetching: selectCollaboratorsFetching(state), - error: selectCollaboratorsError(state), + const baseDataSelector = createSelector( + [ + selectCollaborators, + selectCollaboratorsTotalCount, + selectCollaboratorsFetching, + selectCollaboratorsError, + ], + (collaborators, totalCount, fetching, error) => ({ + collaborators, + totalCount, + fetching, + error, mayLink: false, }), - [clientId], ) const getItems = React.useCallback( diff --git a/pkg/webui/account/containers/oauth-client-add/index.js b/pkg/webui/account/containers/oauth-client-add/index.js index 31dde7d666..da366f907d 100644 --- a/pkg/webui/account/containers/oauth-client-add/index.js +++ b/pkg/webui/account/containers/oauth-client-add/index.js @@ -1,4 +1,4 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,8 +13,8 @@ // limitations under the License. import React, { useState, useCallback } from 'react' -import { connect } from 'react-redux' -import { push } from 'connected-react-router' +import { useDispatch } from 'react-redux' +import { useNavigate } from 'react-router-dom' import { defineMessages } from 'react-intl' import toast from '@ttn-lw/components/toast' @@ -32,7 +32,10 @@ const m = defineMessages({ }) const ClientAdd = props => { - const { isAdmin, userId, rights, pseudoRights, navigateToOAuthClient, createOauthClient } = props + const { isAdmin, userId, rights, pseudoRights } = props + + const dispatch = useDispatch() + const navigate = useNavigate() const [error, setError] = useState() const handleSubmit = useCallback( @@ -42,15 +45,19 @@ const ClientAdd = props => { setError(undefined) try { - await createOauthClient( - owner_id, - { - ...values, - }, - userId === owner_id, + await dispatch( + attachPromise( + createClient( + owner_id, + { + ...values, + }, + userId === owner_id, + ), + ), ) - navigateToOAuthClient(ids.client_id) + navigate(`/oauth-clients/${ids.client_id}`) toast({ title: ids.client_id, message: m.createSuccess, @@ -66,7 +73,7 @@ const ClientAdd = props => { }) } }, - [userId, navigateToOAuthClient, createOauthClient], + [dispatch, userId, navigate], ) return ( @@ -82,9 +89,7 @@ const ClientAdd = props => { } ClientAdd.propTypes = { - createOauthClient: PropTypes.func.isRequired, isAdmin: PropTypes.bool.isRequired, - navigateToOAuthClient: PropTypes.func.isRequired, pseudoRights: PropTypes.rights.isRequired, rights: PropTypes.rights, userId: PropTypes.string.isRequired, @@ -94,8 +99,4 @@ ClientAdd.defaultProps = { rights: undefined, } -export default connect(null, dispatch => ({ - navigateToOAuthClient: clientId => dispatch(push(`/oauth-clients/${clientId}`)), - createOauthClient: (owner_id, client, userId) => - dispatch(attachPromise(createClient(owner_id, client, userId))), -}))(ClientAdd) +export default ClientAdd diff --git a/pkg/webui/account/containers/oauth-client-edit/index.js b/pkg/webui/account/containers/oauth-client-edit/index.js index cbb15be298..f037b8da3f 100644 --- a/pkg/webui/account/containers/oauth-client-edit/index.js +++ b/pkg/webui/account/containers/oauth-client-edit/index.js @@ -1,4 +1,4 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,8 +13,8 @@ // limitations under the License. import React, { useState, useCallback } from 'react' -import { connect } from 'react-redux' -import { push, replace } from 'connected-react-router' +import { useDispatch } from 'react-redux' +import { useNavigate } from 'react-router-dom' import { defineMessages } from 'react-intl' import toast from '@ttn-lw/components/toast' @@ -56,19 +56,18 @@ const checkChanged = (changed, values) => { } const ClientAdd = props => { - const { - userId, - isAdmin, - rights, - pseudoRights, - navigateToOAuthClient, - deleteOAuthClient, - onDeleteSuccess, - initialValues, - updateOauthClient, - } = props + const { userId, isAdmin, rights, pseudoRights, initialValues } = props const [error, setError] = useState() + const navigate = useNavigate() + const dispatch = useDispatch() + + const navigateToOAuthClient = useCallback( + clientId => { + navigate(`/oauth-clients/${clientId}`) + }, + [navigate], + ) const handleSubmit = useCallback( async (values, resetForm, setSubmitting) => { const { client_id } = values.ids @@ -83,7 +82,7 @@ const ClientAdd = props => { const { owner_id, ...newClient } = update try { - await updateOauthClient(client_id, newClient) + await dispatch(attachPromise(updateClient(client_id, newClient))) resetForm({ values }) toast({ title: client_id, @@ -100,7 +99,7 @@ const ClientAdd = props => { }) } }, - [initialValues, updateOauthClient], + [dispatch, initialValues], ) const handleDelete = useCallback( @@ -108,8 +107,8 @@ const ClientAdd = props => { setError(undefined) try { - await deleteOAuthClient(clientId, shouldPurge) - onDeleteSuccess() + await dispatch(attachPromise(deleteClient(clientId, shouldPurge))) + navigate('/oauth-clients') toast({ title: clientId, message: m.deleteSuccess, @@ -124,7 +123,7 @@ const ClientAdd = props => { }) } }, - [deleteOAuthClient, onDeleteSuccess], + [dispatch, navigate], ) return ( @@ -133,7 +132,6 @@ const ClientAdd = props => { initialValues={initialValues} onSubmit={handleSubmit} onDelete={handleDelete} - onDeleteSuccess={onDeleteSuccess} navigateToOAuthClient={navigateToOAuthClient} error={error} userId={userId} @@ -145,16 +143,12 @@ const ClientAdd = props => { } ClientAdd.propTypes = { - deleteOAuthClient: PropTypes.func.isRequired, initialValues: PropTypes.shape({ grants: PropTypes.arrayOf(PropTypes.string), }).isRequired, isAdmin: PropTypes.bool.isRequired, - navigateToOAuthClient: PropTypes.func.isRequired, - onDeleteSuccess: PropTypes.func.isRequired, pseudoRights: PropTypes.rights.isRequired, rights: PropTypes.rights, - updateOauthClient: PropTypes.func.isRequired, userId: PropTypes.string.isRequired, } @@ -162,9 +156,4 @@ ClientAdd.defaultProps = { rights: undefined, } -export default connect(null, dispatch => ({ - navigateToOAuthClient: clientId => dispatch(push(`/oauth-clients/${clientId}`)), - deleteOAuthClient: (id, shouldPurge) => dispatch(attachPromise(deleteClient(id, shouldPurge))), - onDeleteSuccess: () => dispatch(replace(`/oauth-clients`)), - updateOauthClient: (id, patch) => dispatch(attachPromise(updateClient(id, patch))), -}))(ClientAdd) +export default ClientAdd diff --git a/pkg/webui/account/containers/profile-card/index.js b/pkg/webui/account/containers/profile-card/index.js index cee54dbf2b..43cdc75228 100644 --- a/pkg/webui/account/containers/profile-card/index.js +++ b/pkg/webui/account/containers/profile-card/index.js @@ -1,4 +1,4 @@ -// Copyright © 2021 The Things Network Foundation, The Things Industries B.V. +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -32,11 +32,10 @@ const m = defineMessages({ }) const ProfileCard = () => { - const { userId, userName, profilePicture } = useSelector(state => ({ - userId: selectUserId(state), - userName: selectUserName(state), - profilePicture: selectUserProfilePicture(state), - })) + const userId = useSelector(selectUserId) + const userName = useSelector(selectUserName) + const profilePicture = useSelector(selectUserProfilePicture) + return (
diff --git a/pkg/webui/account/containers/profile-settings-form/validation-schema.js b/pkg/webui/account/containers/profile-settings-form/validation-schema.js index f8509d65dc..4362f01c2a 100644 --- a/pkg/webui/account/containers/profile-settings-form/validation-schema.js +++ b/pkg/webui/account/containers/profile-settings-form/validation-schema.js @@ -20,12 +20,12 @@ import m from './messages' export default Yup.object().shape({ _profile_picture_source: Yup.string() .oneOf(['gravatar', 'upload']) - .when(['$initialProfilePictureSource'], (ppSource, schema) => schema.default(ppSource)), + .when('$initialProfilePictureSource', ([ppSource], schema) => schema.default(ppSource)), profile_picture: Yup.object() .nullable() .when( ['_profile_picture_source', '$useGravatarConfig', '$disableUploadConfig'], - (ppSource, useGravatarConfig, uploadDisabled, schema) => { + ([ppSource, useGravatarConfig, uploadDisabled], schema) => { if (!useGravatarConfig && uploadDisabled) { return schema.strip() } diff --git a/pkg/webui/account/containers/sessions-table/index.js b/pkg/webui/account/containers/sessions-table/index.js index f619226a94..a2fd975a5f 100644 --- a/pkg/webui/account/containers/sessions-table/index.js +++ b/pkg/webui/account/containers/sessions-table/index.js @@ -1,4 +1,4 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -13,9 +13,9 @@ // limitations under the License. import React from 'react' -import { connect, useDispatch } from 'react-redux' +import { useDispatch, useSelector } from 'react-redux' import { defineMessages } from 'react-intl' -import { bindActionCreators } from 'redux' +import { createSelector } from 'reselect' import Button from '@ttn-lw/components/button' import toast from '@ttn-lw/components/toast' @@ -25,7 +25,6 @@ import FetchTable from '@ttn-lw/containers/fetch-table' import Message from '@ttn-lw/lib/components/message' import DateTime from '@ttn-lw/lib/components/date-time' -import PropTypes from '@ttn-lw/lib/prop-types' import sharedMessages from '@ttn-lw/lib/shared-messages' import attachPromise from '@ttn-lw/lib/store/actions/attach-promise' @@ -50,21 +49,22 @@ const m = defineMessages({ const getItemPathPrefix = item => `/${item.ids.client_id}` -const UserSessionsTable = props => { - const { pageSize, user, handleDeleteSession, sessionId } = props +const UserSessionsTable = () => { + const userId = useSelector(selectUserId) + const sessionId = useSelector(selectSessionId) const dispatch = useDispatch() - const getSessions = React.useCallback(filters => getUserSessionsList(user, filters), [user]) + const getSessions = React.useCallback(filters => getUserSessionsList(userId, filters), [userId]) const deleteSession = React.useCallback( - async session_id => { + async sessionId => { try { - await handleDeleteSession(session_id) + await dispatch(attachPromise(deleteUserSession(userId, sessionId))) toast({ message: m.deleteSessionSuccess, type: toast.types.SUCCESS, }) - dispatch(getUserSessionsList(user)) + dispatch(getUserSessionsList(userId)) } catch { toast({ message: m.deleteSessionError, @@ -72,12 +72,12 @@ const UserSessionsTable = props => { }) } }, - [user, handleDeleteSession, dispatch], + [dispatch, userId], ) - const baseDataSelector = React.useCallback( - state => { - const sessions = selectUserSessions(state) + const baseDataSelector = createSelector( + [selectUserSessions, selectUserSessionsTotalCount, selectUserSessionsFetching], + (sessions, totalCount, fetching) => { const decoratedSessions = [] if (sessions) { @@ -94,13 +94,12 @@ const UserSessionsTable = props => { return { sessions: decoratedSessions, - totalCount: selectUserSessionsTotalCount(state), - fetching: selectUserSessionsFetching(state), + totalCount, + fetching, mayAdd: false, mayLink: false, } }, - [sessionId], ) const makeHeaders = React.useMemo(() => { @@ -191,36 +190,8 @@ const UserSessionsTable = props => { baseDataSelector={baseDataSelector} tableTitle={} getItemPathPrefix={getItemPathPrefix} - pageSize={pageSize} /> ) } -UserSessionsTable.propTypes = { - handleDeleteSession: PropTypes.func.isRequired, - pageSize: PropTypes.number.isRequired, - sessionId: PropTypes.string.isRequired, - user: PropTypes.string.isRequired, -} - -export default connect( - state => ({ - user: selectUserId(state), - sessionId: selectSessionId(state), - }), - dispatch => ({ - ...bindActionCreators( - { - handleDeleteSession: attachPromise(deleteUserSession), - }, - dispatch, - ), - }), - (stateProps, dispatchProps, ownProps) => ({ - ...stateProps, - ...dispatchProps, - ...ownProps, - handleDeleteSession: deleteSessionId => - dispatchProps.handleDeleteSession(stateProps.user, deleteSessionId), - }), -)(UserSessionsTable) +export default UserSessionsTable diff --git a/pkg/webui/account/containers/tokens-table/connect.js b/pkg/webui/account/containers/tokens-table/connect.js deleted file mode 100644 index 33d39184bb..0000000000 --- a/pkg/webui/account/containers/tokens-table/connect.js +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { connect } from 'react-redux' -import { bindActionCreators } from 'redux' -import { push } from 'connected-react-router' - -import attachPromise from '@ttn-lw/lib/store/actions/attach-promise' - -import { deleteAccessToken, deleteAllTokens } from '@account/store/actions/authorizations' - -import { selectUserId } from '@account/store/selectors/user' -import { selectTokenIds } from '@account/store/selectors/authorizations' - -const mapStateToProps = (state, props) => ({ - userId: selectUserId(state), - clientId: props.match.params.clientId, - tokenIds: selectTokenIds(state), -}) - -const mapDispatchToProps = dispatch => ({ - ...bindActionCreators( - { - deleteToken: attachPromise(deleteAccessToken), - deleteAllTokens: attachPromise(deleteAllTokens), - }, - dispatch, - ), - navigateToList: clientId => dispatch(push(`/client-authorizations/${clientId}`)), -}) - -const mergeProps = (stateProps, dispatchProps, ownProps) => ({ - ...stateProps, - ...dispatchProps, - ...ownProps, - deleteToken: id => dispatchProps.deleteToken(stateProps.userId, stateProps.clientId, id), - deleteAllTokens: ids => - dispatchProps.deleteAllTokens(stateProps.userId, stateProps.clientId, ids), -}) - -export default Tokens => connect(mapStateToProps, mapDispatchToProps, mergeProps)(Tokens) diff --git a/pkg/webui/account/containers/tokens-table/index.js b/pkg/webui/account/containers/tokens-table/index.js index 4d55186359..4d38b96768 100644 --- a/pkg/webui/account/containers/tokens-table/index.js +++ b/pkg/webui/account/containers/tokens-table/index.js @@ -1,4 +1,4 @@ -// Copyright © 2022 The Things Network Foundation, The Things Industries B.V. +// Copyright © 2023 The Things Network Foundation, The Things Industries B.V. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,9 +12,206 @@ // See the License for the specific language governing permissions and // limitations under the License. -import TokensTable from './tokens-table' -import connect from './connect' +import React from 'react' +import { defineMessages } from 'react-intl' +import { Col, Row, Container } from 'react-grid-system' +import { useDispatch, useSelector } from 'react-redux' +import { useParams } from 'react-router-dom' +import { createSelector } from 'reselect' -const ConnectedTokensTable = connect(TokensTable) +import PAGE_SIZES from '@ttn-lw/constants/page-sizes' -export { ConnectedTokensTable as default, TokensTable } +import toast from '@ttn-lw/components/toast' +import Button from '@ttn-lw/components/button' +import SafeInspector from '@ttn-lw/components/safe-inspector' +import Breadcrumb from '@ttn-lw/components/breadcrumbs/breadcrumb' +import { useBreadcrumbs } from '@ttn-lw/components/breadcrumbs/context' + +import FetchTable from '@ttn-lw/containers/fetch-table' + +import Message from '@ttn-lw/lib/components/message' +import DateTime from '@ttn-lw/lib/components/date-time' + +import sharedMessages from '@ttn-lw/lib/shared-messages' +import attachPromise from '@ttn-lw/lib/store/actions/attach-promise' + +import { + deleteAccessToken, + deleteAllTokens, + getAccessTokensList, +} from '@account/store/actions/authorizations' + +import { + selectTokens, + selectTokensTotalCount, + selectTokensFetching, +} from '@account/store/selectors/authorizations' +import { selectUserId } from '@account/store/selectors/user' + +const m = defineMessages({ + tableTitle: 'Access tokens', + deleteSuccess: 'Access token invalidated', + deleteFail: 'There was an error and the access token could not be invalidated', + deleteButton: 'Invalidate this access token', + deleteAllSuccess: 'All access tokens invalidated', + deleteAllFail: 'There was an error and the access tokens could not be invalidated', + deleteAllButton: 'Invalidate all access tokens', + expires: 'Expires', + accessTokens: 'Access tokens', +}) + +const TokensTable = () => { + const userId = useSelector(selectUserId) + const { clientId } = useParams() + const tokenIdsSelector = createSelector(selectTokens, tokens => tokens.map(token => token.id)) + const tokenIds = useSelector(tokenIdsSelector) + const dispatch = useDispatch() + + useBreadcrumbs( + 'client-authorizations.single.access-tokens', + , + ) + + const handleDeleteToken = React.useCallback( + async id => { + try { + await dispatch(attachPromise(deleteAccessToken(userId, clientId, id))) + toast({ + title: clientId, + message: m.deleteSuccess, + type: toast.types.SUCCESS, + }) + } catch (err) { + toast({ + title: clientId, + message: m.deleteFail, + type: toast.types.ERROR, + }) + } + }, + [dispatch, userId, clientId], + ) + + const handleDeleteAllTokens = React.useCallback(async () => { + try { + await dispatch(attachPromise(deleteAllTokens(userId, clientId, tokenIds))) + toast({ + title: clientId, + message: m.deleteAllSuccess, + type: toast.types.SUCCESS, + }) + } catch (err) { + toast({ + title: clientId, + message: m.deleteAllFail, + type: toast.types.ERROR, + }) + } + }, [dispatch, userId, clientId, tokenIds]) + + const headers = React.useMemo(() => { + const baseHeaders = [ + { + name: 'id', + displayName: sharedMessages.id, + width: 40, + getValue: row => ({ + id: row.id, + }), + render: details => ( + + ), + }, + { + name: 'created_at', + displayName: sharedMessages.created, + width: 20, + sortable: true, + render: created_at => , + }, + { + name: 'expires_at', + displayName: m.expires, + width: 20, + render: expires_at => , + }, + { + name: 'actions', + displayName: sharedMessages.actions, + width: 20, + getValue: row => ({ + delete: handleDeleteToken.bind(null, row.id), + }), + render: details => ( +