diff --git a/.gitignore b/.gitignore index b4c738e20ce..b204f1b15e7 100644 --- a/.gitignore +++ b/.gitignore @@ -110,3 +110,10 @@ localStorage.json .scannerwork .sonar packages/manager/test-report.xml + +# cypress files/folders +**/manager/config/development.json +**/manager/config/staging.json +**/manager/cypress/videos/ +**/manager/cypress/screenshots/ + diff --git a/.travis.yml b/.travis.yml index 1078b4ac031..0415070883a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,13 +12,11 @@ before_install: sudo apt-get install libcairo2-dev libjpeg8-dev libpango1.0-dev build-essential g++ google-chrome-stable curl install: - yarn install:all +- if [ "${TRAVIS_PULL_REQUEST}" != "false" ]; then audit-ci --low; fi -#before_script: -#- lerna run --stream --scope linode-manager storybook & script: - yarn build - yarn test -#- lerna run --stream --scope linode-manager storybook:e2e #- 'pkill -f selenium-standalone || :' #- pkill -f storybook env: diff --git a/CHANGELOG.md b/CHANGELOG.md index 5bf21075bcf..3ce4222d397 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,74 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [v1.0.0] - 2020-02-10 + +### Added: +- New One-Click Apps: + - MEAN + - MongoDB + - Flask + - Django + - Redis + - Ruby on Rails + - PostgreSQL + +### Changed: +- Change default distro to Debian 10 +- Fix changelog to match GitHub release +- Update graph units on Linode Details page +- Fetch backups after selecting Linode in Linode Create +- Toast notifications for Image related events +- Unify graph colors across the app +- LKE: Warn users before allowing a single-node cluster +- LKE: Update typings for node pools +- Show Domains Import Zone Drawer button when a user has no Domains +- Improve compile time +- Cleanup axios version management and aligning +- Prevent unneeded requests when loading Lish window + +### Fixed: +- Updating a Linode causes in-progress events to stop being displayed +- Safari: Open ticket button issue +- Remove plural for hour on DNS manager +- 'Show More' tooltip accessibility fix + +## [0.84.1] - 2020-02-04 + +### Fixed: +- Fix issue where only 100 Images were displayed + + +## [v0.84.0] - 2020-01-28 + +### Added: +- Add Domain Transfers to Domain Drawer for slave Domains +- “Delete” button to Domain Drawer +- Improve Form context help/info in Configuration Edit +- Ability to delete a Domain from Domain Detail +- Show a banner when one or more Regions experience outages +- New One-Click App: phpMyAdmin +- Show progress on the target Linode while cloning + +### Changed: +- Add link to Resizing a Linode Guide +- [LKE] Node pools should have 3 nodes by default +- Longview Process Arrow Rework +- StackScript author links from StackScript Detail page +- Sort Kubernetes versions by label descending in dropdown +- OAuth Scopes can be space separated +- Store Longview time selection in user preferences + +### Fixed: +- Longview Overview graphs were incorrectly showing data as “today” +- Refactor LineGraphs to allow mixed units for network graphs +- Routing on Search Landing page for slave Domains +- Fix Linode network graph units +- Display updated credit card info in Billing Summary when credit card is updated +- Visual regression on Clone Configs/Disks +- Loading state for Longview landing page (visual bug) + + ## [v0.83.0] - 2020-01-17 ### Added: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d865c17a736..6422f47e9f0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,12 +20,6 @@ In order to contribute to Linode UI, we recommend the following minimum version 2. Node v10.16.0 3. Yarn 1.16.0 -You must also have [Lerna](https://lerna.js.org/) installed globally, so please run the following to install the package to your local machine: - -``` -yarn global add lerna -``` - ## Development ### Coding Style diff --git a/Dockerfile b/Dockerfile index 4a44381bbc3..02457b79b0a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,14 +8,10 @@ WORKDIR /home/node/app RUN chown -R node:node /home/node/app USER node -RUN yarn global add lerna - # Copy the root level package.json and run yarn if anything changes COPY --chown=node:node package.json yarn.lock tslint.json ./ RUN yarn -# Copy lerna.json -COPY --chown=node:node lerna.json . COPY --chown=node:node scripts ./scripts/ # Copy Cloud Manager deps @@ -26,7 +22,7 @@ COPY --chown=node:node packages/manager/patches ./packages/manager/patches/ COPY --chown=node:node packages/linode-js-sdk/package.json ./packages/linode-js-sdk/ # Runs "yarn install" for all child packages -RUN npx lerna bootstrap +RUN yarn install:all # Copy the rest of the files that don't require installation COPY --chown=node:node packages/linode-js-sdk ./packages/linode-js-sdk/ diff --git a/GETTING_STARTED.md b/GETTING_STARTED.md index ceefd69824c..19eef048c8e 100644 --- a/GETTING_STARTED.md +++ b/GETTING_STARTED.md @@ -4,10 +4,18 @@ This document explains the instructions for setting up Linode UI locally, step-b ## Preface -This repository uses [Lerna](https://lerna.js.org/), a solution to transform JavaScript-based repositories +This repository uses [Yarn Workspaces](https://legacy.yarnpkg.com/lang/en/docs/workspaces/), a solution to transform JavaScript-based repositories into one, combined monorepo. This is useful because it allows us to maintain multiple different projects in one place, with shared dependencies. -You can assume that as long as you are `cd`ed into the root of this project, any [Lerna commands](https://github.com/lerna/lerna/tree/master/commands) are fair game. Feel free to start any individual package or multiple. but please keep in mind that if you plan on running the Cloud Manager locally, you'll want to start all projects. +There is 3 `package.json` files, 1 for the root, and one for each folder in `packages/`. running `yarn` will install everything, and "hoist" as much as possible in the root `node_modules` folder. There is also 1 unique `yarn.lock` at the root. + +Most of the time you will directly use the commands from this documentation. If you have to run a specific command the rule is: +- To run a command in both sub packages `yarn workspace ` +- To run a command in 1 subpackage `yarn workspace linode-js-sdk `, or `yarn workspace linode-manager ` + +Note the workspace names are defined in the root `package.json` +- linode-js-sdk: /packages/linode-js-sdk +- linode-js-sdk: /packages/linode-manager ## Starting All Projects @@ -16,7 +24,7 @@ If your intention is to start a development server for all projects, you have a To start all projects: While in the root... -1. Run `yarn install:all` to install all dependencies and setup `lerna` +1. Run `yarn install:all` to install all dependencies 2. Run `yarn start:all` to start a development server for all projects Alternatively, you can run `yarn up` which runs all previous commands. @@ -25,8 +33,8 @@ Alternatively, you can run `yarn up` which runs all previous commands. Starting a single project is similar to the previous instructions with the exception of adding a `--scope` flag to to the command. So for example, starting the Cloud Manager project looks like: -1. Run `yarn install:all` to install all dependencies and setup `lerna` -2. Run `npx lerna run start --scope linode-js-sdk` to start a development server for the JavaScript SDK +1. Run `yarn install:all` to install all dependencies +2. Run `yarn workspace linode-js-sdk start` to start a development server for the JavaScript SDK * `linode-js-sdk` is the name located in `packages/linode-js-sdk/package.json` ### To do the same thing with Yarn @@ -43,7 +51,6 @@ See [this document](./TESTING.md) ## Helper Scripts * `yarn clean` is an alias that will remove both top-level and package-level `node_modules`. - * Please note - this also bypasses the confirmation Lerna gives by default to delete package-level `node_modules` * `yarn test` is an alias that will run a test suite in the Cloud Manager project * `yarn test packages/manager/src/App.test.tsx` for example diff --git a/README.md b/README.md index b9ab50e3bce..40a9664cfa4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@

Build status Code coverage -

This repository is the home for all things related to front-end development at Linode. diff --git a/RELEASING.md b/RELEASING.md index b48d8ed3052..e255c65cdbd 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -38,7 +38,7 @@ When you plan on releasing a new version of Cloud Manager: 8. At this point, run the end-to-end test suite. Please see a team member on instructions how to do so. 9. When you're ready to make the merge to master AKA release to production, you need to do 2 things: Add the git tag, and ensure the changelog has the correct date. 10. Make the date change to CHANGELOG.md if necessary and stage the changes with `git add . && git commit -m "updates changelog date"`. -11. Then, run `git checkout staging && git add . && npx lerna version --no-push` +11. Then, run `git checkout staging && git add . && yarn version --no-push` - This will prompt you for a new version number, apply the Git tags, and update the version number in the `package.json` of each child project. - This will also automatically commit the changes with a generated commit message. 12. Push changes to staging with `git push origin staging && git push origin --tags` @@ -47,7 +47,7 @@ When you plan on releasing a new version of Cloud Manager: 15. Once your new version has being deployed to production, open a PR to merge `master` branch into `develop` branch - **DO NOT SQUASH MERGE** - Seriously...**DO NOT SQUASH MERGE** 16. Finally, on GitHub, create a new release from the Git tag you've just pushed to `master` branch. NOTE: when creating the GitHub release, the tag you pin the release to - should have the format vX.XX.XX. `lerna publish` creates tags such as "linode-manager@X.XX.XX", and GitHub will often autocomplete to these. Using these will break the link from + should have the format vX.XX.XX. `yarn publish` creates tags such as "linode-manager@X.XX.XX", and GitHub will often autocomplete to these. Using these will break the link from the footer in Cloud to the current release. **Do not have an open PR if you plan on hotfixing to master. The build will not succeed if there is an open master -> develop PR** ## Pushing a Hotfix diff --git a/TESTING.md b/TESTING.md index 29acc212f7b..d4d0b19a05a 100644 --- a/TESTING.md +++ b/TESTING.md @@ -39,7 +39,7 @@ yarn test linode To run a test in debug mode, add a `debugger` breakpoint inside one of the test cases, then run ``` -npx lerna run test:debug --stream --scope linode-manager +yarn workspace linode-manager run test:debug ``` Test execution will stop at the debugger statement, and you will be able to use Chrome's normal debugger to step through @@ -209,10 +209,8 @@ The axe-core accessibility testing script has been integrated into the webdriver ``` # Starts the local development environment -yarn && npx lerna bootstrap --scope linode-manager && npx lerna run start --stream --scope linode-manager - - -npx lerna run axe --stream --scope linode-manager +yarn install:all && yarn up +yarn workspace linode-manager run axe ``` The test results will be saved as a JSON file with Critical accessibility violations appearing at the top of the list. diff --git a/generate_changelog.py b/generate_changelog.py index fc8ec9aa366..c12fbd5d075 100644 --- a/generate_changelog.py +++ b/generate_changelog.py @@ -43,14 +43,19 @@ def checkKeyWords(list_keywords, commit): return True return False +# Given a commit message from a merged Pull Request, remove the PR ID. +# Example: "My first commit (#1)" -> "My first commit" +def remove_pull_request_id(commit_message): + regexp = "\(#\d+\)" + return re.sub(regexp, '', commit_message).strip() + def generateChangeLog(release, date, origin): - git_diff=subprocess.Popen(['git', 'log', '--no-merges', '--oneline', "--pretty=split:'%s'", origin+'/master...HEAD'], - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - stdout,stderr = git_diff.communicate() - clean_sdout = str(stdout).replace('b\"', '').replace('\n', '').replace('\\n','').replace("'", '').replace('\"','') - commit_array=clean_sdout.split('split:') - commit_array.pop(0) + git_log_command = ["git", "log", "--no-merges", "--oneline", "--pretty='%s'", "{}/master...HEAD".format(origin)] + + commits = subprocess.check_output(git_log_command, subprocess.STDOUT).decode('utf-8').split('\n') + + # Strip the first and last characters of each line, which are single quotes. + commits = [c[1:-1] for c in commits] breaking=[] added=[] @@ -58,30 +63,32 @@ def generateChangeLog(release, date, origin): fixed=[] jql_query=[] - for i,commit in enumerate(commit_array): - jira_key_regex=re.match('(''|\s)M3(-|\s)\d{1,5}(-|\s|:)', commit) - if( not (jira_key_regex is None) ): + for commit in commits: + commit = remove_pull_request_id(commit) + + jira_key_regex=re.match('M3-\d{4}', commit) + if (jira_key_regex is not None): jira_key=jira_key_regex.group(0) jql_query.append(jira_key) - commit_array[i]=commit.lstrip(jira_key) + commit.lstrip(jira_key) if(checkKeyWords(TEST_KEYWORDS, commit.lower())): - NOT_INCLUDED_IN_LOG.append(commit_array[i]) + NOT_INCLUDED_IN_LOG.append(commit) continue if(checkKeyWords(BREAKING_KEYWORDS, commit.lower())): - breaking.append(commit_array[i]) + breaking.append(commit) continue if(checkKeyWords(CHANGED_KEYWORDS, commit.lower())): - changed.append(commit_array[i]) + changed.append(commit) continue if(checkKeyWords(FIXED_KEYWORDS, commit.lower())): - fixed.append(commit_array[i]) + fixed.append(commit) continue - added.append(commit_array[i]) + added.append(commit) generateJQLQuery(jql_query) diff --git a/lerna.json b/lerna.json deleted file mode 100644 index aec98b4eaea..00000000000 --- a/lerna.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "packages": [ - "packages/*" - ], - "npmClient": "yarn", - "version": "independent", - "useWorkspaces": true - -} \ No newline at end of file diff --git a/package.json b/package.json index 69283093373..71641cbb05a 100644 --- a/package.json +++ b/package.json @@ -3,8 +3,8 @@ "private": true, "license": "Apache-2.0", "devDependencies": { + "audit-ci": "^2.4.2", "husky": "^3.0.1", - "lerna": "^3.18.4", "postinstall": "^0.6.0", "tslint": "^5.20.1", "tslint-config-prettier": "^1.18.0", @@ -15,29 +15,33 @@ }, "husky": { "hooks": { - "pre-commit": "lerna run precommit --stream", - "pre-push": "lerna run prepush --stream --scope linode-manager" + "pre-commit": "yarn workspaces run precommit", + "pre-push": "yarn workspace linode-manager prepush" } }, "scripts": { "cost-of-modules": "yarn global add cost-of-modules && cost-of-modules --less --no-install --include-dev", - "install:all": "yarn install --frozen-lockfile && lerna link", + "install:all": "yarn install --frozen-lockfile", "up": "yarn install:all && yarn start:all", "postinstall": "yarn workspaces run postinstall", "build": "yarn workspaces run build", "up:manager": "yarn install:all && yarn start:manager", - "start:manager": "yarn workspace linode-js-sdk build && lerna run start --stream --scope linode-manager -- --color", + "start:manager": "yarn workspace linode-js-sdk build && yarn workspace linode-manager start --color", "start:docker": "yarn workspace linode-js-sdk build && yarn start:all", - "start:all": "lerna run start --parallel -- --color", - "clean": "rm -rf node_modules && rm -rf packages/linode-js-sdk/node_modules && rm -rf packages/manager/node_modules && lerna clean --yes", + "start:all": "concurrently \"yarn workspace linode-js-sdk start\" \"yarn workspace linode-manager start\"", + "clean": "rm -rf node_modules && rm -rf packages/linode-js-sdk/node_modules && rm -rf packages/manager/node_modules", "test": "yarn workspace linode-manager test", "selenium": "yarn workspace linode-manager selenium", "storybook": "yarn workspace linode-manager storybook", "storybook:e2e": "yarn workspace linode-manager run storybook:e2e", - "storybook:debug": "lerna run storybook:e2e --stream --scope linode-manager -- --color --debug", + "storybook:debug": "yarn workspace linode-manager storybook:e2e --color --debug", "e2e": "yarn workspace linode-manager e2e --color", "e2e:all": "yarn workspace linode-manager e2e:all --color", "e2e:modified": "yarn workspace linode-manager e2e:modified --color", + "cy:stage2e": "yarn workspace linode-manager cy:stage2e", + "cy:stagedebug": "yarn workspace linode-manager cy:stagedebug", + "cy:e2e": "yarn workspace linode-manager cy:e2e", + "cy:debug": "yarn workspace linode-manager cy:debug", "docker:e2e": "docker-compose -f integration-test.yml up --exit-code-from manager-e2e", "docker:test": "docker build -f Dockerfile . -t 'manager' && docker run -it cloud --rm -v $(pwd)/packages/manager/src:/home/node/app/packages/manager/src -v $(pwd)/packages/linode-js-sdk/src:/home/node/app/packages/linode-js-sdk/src manager test", "docker:local": "docker build -f Dockerfile . -t 'manager' -t 'dev' && docker run -it --rm -p 3000:3000 -v $(pwd)/packages/manager/src:/home/node/app/packages/manager/src -v $(pwd)/packages/linode-js-sdk/src:/home/node/app/packages/linode-js-sdk/src manager start:docker", @@ -52,5 +56,6 @@ "packages": [ "packages/*" ] - } + }, + "version": "0.0.0" } diff --git a/packages/linode-js-sdk/package.json b/packages/linode-js-sdk/package.json index a13b3e361d1..61abcb5c006 100644 --- a/packages/linode-js-sdk/package.json +++ b/packages/linode-js-sdk/package.json @@ -1,6 +1,6 @@ { "name": "linode-js-sdk", - "version": "0.17.0-alpha.0", + "version": "0.19.0-alpha.0", "homepage": "https://github.com/linode/manager/tree/develop/packages/linode-js-sdk", "bugs": { "url": "https://github.com/linode/manager/issues", @@ -21,7 +21,7 @@ "types": "./lib/index.d.ts", "unpkg": "./index.js", "dependencies": { - "axios": "^0.19.0", + "axios": "~0.19.0", "querystring": "^0.2.0", "ramda": "^0.26.1", "yup": "^0.27.0" diff --git a/packages/linode-js-sdk/src/account/types.ts b/packages/linode-js-sdk/src/account/types.ts index 710cb3e9a38..e1c5e67042a 100644 --- a/packages/linode-js-sdk/src/account/types.ts +++ b/packages/linode-js-sdk/src/account/types.ts @@ -187,6 +187,13 @@ export type EventAction = | 'domain_record_create' | 'domain_record_updated' | 'domain_record_delete' + | 'firewall_create' + | 'firewall_delete' + | 'firewall_device_add' + | 'firewall_device_remove' + | 'firewall_disable' + | 'firewall_enable' + | 'firewall_update' | 'image_update' | 'image_delete' | 'lassie_reboot' diff --git a/packages/linode-js-sdk/src/firewalls/firewalls.schema.ts b/packages/linode-js-sdk/src/firewalls/firewalls.schema.ts new file mode 100644 index 00000000000..65643ac3282 --- /dev/null +++ b/packages/linode-js-sdk/src/firewalls/firewalls.schema.ts @@ -0,0 +1,20 @@ +import { array, number, object, string } from 'yup'; + +export const CreateFirewallSchema = object({ + label: string(), + tags: array().of(string()), + rules: object().required('You must provide a set of Firewall rules.') +}); + +export const UpdateFirewallSchema = object().shape({ + label: string(), + tags: array().of(string()), + status: string().oneOf(['enabled', 'disabled']) // 'deleted' is also a status but it's not settable +}); + +export const FirewallDeviceSchema = object({ + type: string() + .oneOf(['linode', 'nodebalancer']) + .required('Device type is required.'), + id: number().required('ID is required.') +}); diff --git a/packages/linode-js-sdk/src/firewalls/firewalls.ts b/packages/linode-js-sdk/src/firewalls/firewalls.ts index 710066cdfb6..7af68c51254 100644 --- a/packages/linode-js-sdk/src/firewalls/firewalls.ts +++ b/packages/linode-js-sdk/src/firewalls/firewalls.ts @@ -1,48 +1,221 @@ -// import { API_ROOT } from '../constants'; -// import Request, { -// setData, -// setMethod, -// setParams, -// setURL, -// setXFilter -// } from '../request'; +import { BETA_API_ROOT } from '../constants'; +import Request, { + setData, + setMethod, + setParams, + setURL, + setXFilter +} from '../request'; import { ResourcePage as Page } from '../types'; -import { Firewall, FirewallDevice } from './types'; - -/** - * mocked GET firewalls - */ -export const getFirewalls = ( - mockData: Firewall[], - params: any = {}, - filter: any = {} -): Promise> => { - return new Promise((resolve, reject) => { - setTimeout(() => { - resolve({ - data: mockData, - page: 1, - pages: 1, - results: mockData.length - }); - }, 1000); - }).then((data: any) => { - return data; - }); -}; +import { + CreateFirewallSchema, + FirewallDeviceSchema, + UpdateFirewallSchema +} from './firewalls.schema'; +import { + CreateFirewallPayload, + Firewall, + FirewallDevice, + FirewallDevicePayload, + FirewallRules, + UpdateFirewallPayload +} from './types'; +// FIREWALLS + +/** + * getFirewalls + * + * Returns a paginated list of all Cloud Firewalls on this account. + */ +export const getFirewalls = (params?: any, filters?: any) => + Request>( + setMethod('GET'), + setParams(params), + setXFilter(filters), + setURL(`${BETA_API_ROOT}/networking/firewalls`) + ).then(response => response.data); + +/** + * getFirewall + * + * Get a specific Firewall resource by its ID. The Firewall's Devices will not be + * returned in the response. Use getFirewallDevices() to view the Devices. + * + */ +export const getFirewall = (firewallID: number) => + Request( + setMethod('GET'), + setURL(`${BETA_API_ROOT}/networking/firewalls/${firewallID}`) + ).then(response => response.data); + +/** + * createFirewall + * + * Creates a Firewall to filter network traffic. Use the `rules` property to + * create inbound and outbound access rules. Use the `devices` property to assign the + * Firewall to a Linode service. + * A Firewall can be assigned to multiple Linode services, and up to three active Firewalls + * can be assigned to a single Linode service. Additional disabled Firewalls can be + * assigned to a service, but they cannot be enabled if three other active Firewalls + * are already assigned to the same service. + */ +export const createFirewall = (data: CreateFirewallPayload) => + Request( + setMethod('POST'), + setData(data, CreateFirewallSchema), + setURL(`${BETA_API_ROOT}/networking/firewalls`) + ).then(response => response.data); + +/** + * updateFirewall + * + * Updates the Cloud Firewall with the provided ID. Only label, tags, and status can be updated + * through this method. + * + */ +export const updateFirewall = ( + firewallID: number, + data: UpdateFirewallPayload +) => + Request( + setMethod('PUT'), + setData(data, UpdateFirewallSchema), + setURL(`${BETA_API_ROOT}/networking/firewalls/${firewallID}`) + ).then(response => response.data); + +/** + * enableFirewall + * + * Convenience method for enabling a Cloud Firewall. Calls updateFirewall internally + * with { status: 'enabled' } + * + */ +export const enableFirewall = (firewallID: number) => + updateFirewall(firewallID, { status: 'enabled' }); + +/** + * disableFirewall + * + * Convenience method for disabling a Cloud Firewall. Calls updateFirewall internally + * with { status: 'disabled' } + * + */ +export const disableFirewall = (firewallID: number) => + updateFirewall(firewallID, { status: 'disabled' }); + +/** + * deleteFirewall + * + * Deletes a single Cloud Firewall. + * + */ +export const deleteFirewall = (firewallID: number) => + Request<{}>( + setMethod('DELETE'), + setURL(`${BETA_API_ROOT}/networking/firewalls/${firewallID}`) + ).then(response => response.data); + +// FIREWALL RULES + +/** + * getFirewallRules + * + * Returns the current set of rules for a single Cloud Firewall. + */ +export const getFirewallRules = ( + firewallID: number, + params?: any, + filters?: any +) => + Request>( + setMethod('GET'), + setParams(params), + setXFilter(filters), + setURL(`${BETA_API_ROOT}/networking/firewalls/${firewallID}/rules`) + ).then(response => response.data); + +/** + * updateFirewallRules + * + * Updates the inbound and outbound Rules for a Firewall. Using this endpoint will + * replace all of a Firewall's ruleset with the Rules specified in your request. + */ +export const updateFirewallRules = (firewallID: number, data: FirewallRules) => + Request( + setMethod('PUT'), + setData(data), // Validation is too complicated for these; leave it to the API. + setURL(`${BETA_API_ROOT}/networking/firewalls/${firewallID}/rules`) + ).then(response => response.data); + +// DEVICES + +/** + * getFirewallDevices + * + * Returns a paginated list of a Firewall's Devices. A Firewall Device assigns a + * Firewall to a Linode service (referred to as the Device's `entity`). + */ export const getFirewallDevices = ( - id: number, - mockData: FirewallDevice[] -): Promise> => { - return new Promise(resolve => { - setTimeout(() => { - resolve({ - data: mockData, - page: 1, - pages: 1, - results: mockData.length - }); - }); - }).then((data: any) => data); -}; + firewallID: number, + params?: any, + filters?: any +) => + Request>( + setMethod('GET'), + setParams(params), + setXFilter(filters), + setURL(`${BETA_API_ROOT}/networking/firewalls/${firewallID}/devices`) + ).then(response => response.data); + +/** + * getFirewallDevice + * + * Returns information about a single Firewall Device. A Firewall Device assigns a + * Firewall to a Linode service (referred to as the Device's `entity`). + */ +export const getFirewallDevice = (firewallID: number, deviceID: number) => + Request( + setMethod('GET'), + setURL( + `${BETA_API_ROOT}/networking/firewalls/${firewallID}/devices/${deviceID}` + ) + ).then(response => response.data); + +/** + * addFirewallDevice + * + * Creates a Firewall Device, which assigns a Firewall to a Linode service (referred to + * as the Device's `entity`). + * A Firewall can be assigned to multiple Linode services, and up to three active Firewalls can + * be assigned to a single Linode service. Additional disabled Firewalls can be + * assigned to a service, but they cannot be enabled if three other active Firewalls + * are already assigned to the same service. + * Creating a Firewall Device will apply the Rules from a Firewall to a Linode service. + * A `firewall_device_add` Event is generated when the Firewall Device is added successfully. + */ +export const addFirewallDevice = ( + firewallID: number, + data: FirewallDevicePayload +) => + Request( + setMethod('POST'), + setURL(`${BETA_API_ROOT}/networking/firewalls/${firewallID}/devices`), + setData(data, FirewallDeviceSchema) + ).then(response => response.data); + +/** + * deleteFirewallDevice + * + * Removes a Firewall Device, which removes a Firewall from the Linode service it was + * assigned to by the Device. This will remove all of the Firewall's Rules from the Linode + * service. If any other Firewalls have been assigned to the Linode service, then those Rules + * will remain in effect. + */ +export const deleteFirewallDevice = (firewallID: number, deviceID: number) => + Request<{}>( + setMethod('DELETE'), + setURL( + `${BETA_API_ROOT}/networking/firewalls/${firewallID}/devices/${deviceID}` + ) + ).then(response => response.data); diff --git a/packages/linode-js-sdk/src/firewalls/types.ts b/packages/linode-js-sdk/src/firewalls/types.ts index 37e8e130aab..ae5cb3f7bbe 100644 --- a/packages/linode-js-sdk/src/firewalls/types.ts +++ b/packages/linode-js-sdk/src/firewalls/types.ts @@ -2,6 +2,8 @@ export type FirewallStatus = 'enabled' | 'disabled' | 'deleted'; export type FirewallRuleProtocol = 'ALL' | 'TCP' | 'UDP' | 'ICMP'; +export type FirewallDeviceEntityType = 'linode' | 'nodebalancer'; + export interface Firewall { id: number; status: FirewallStatus; @@ -29,7 +31,7 @@ export interface FirewallRuleType { export interface FirewallDeviceEntity { id: number; - type: 'linode' | 'nodebalancer'; + type: FirewallDeviceEntityType; label: string; url: string; } @@ -38,3 +40,24 @@ export interface FirewallDevice { id: number; entity: FirewallDeviceEntity; } + +export interface CreateFirewallPayload { + label?: string; + tags?: string[]; + rules: FirewallRules; + devices?: { + linodes?: number[]; + nodebalancers?: number[]; + }; +} + +export interface UpdateFirewallPayload { + label?: string; + tags?: string[]; + status?: Omit; +} + +export interface FirewallDevicePayload { + id: number; + type: FirewallDeviceEntityType; +} diff --git a/packages/linode-js-sdk/src/kubernetes/types.ts b/packages/linode-js-sdk/src/kubernetes/types.ts index df4d1ae4cdd..c49e6e30bbf 100644 --- a/packages/linode-js-sdk/src/kubernetes/types.ts +++ b/packages/linode-js-sdk/src/kubernetes/types.ts @@ -10,12 +10,13 @@ export interface KubernetesCluster { export interface KubeNodePoolResponse { count: number; id: number; - linodes: PoolNodeResponse[]; + nodes: PoolNodeResponse[]; type: string; } export interface PoolNodeResponse { - id: number; + id: string; + instance_id: number | null; status: string; } diff --git a/packages/linode-js-sdk/src/profile/profile.ts b/packages/linode-js-sdk/src/profile/profile.ts index 3dac500bf9b..e8617a3ff85 100644 --- a/packages/linode-js-sdk/src/profile/profile.ts +++ b/packages/linode-js-sdk/src/profile/profile.ts @@ -9,7 +9,7 @@ import Request, { } from '../request'; import { ResourcePage } from '../types'; import { updateProfileSchema } from './profile.schema'; -import { Profile, TrustedDevice } from './types'; +import { Profile, TrustedDevice, UserPreferences } from './types'; /** * getProfile @@ -110,8 +110,8 @@ export const getUserPreferences = () => { * Stores an arbitrary JSON blob for the purposes of implementing * conditional logic based on preferences the user chooses */ -export const updateUserPreferences = (payload: Record) => { - return Request>( +export const updateUserPreferences = (payload: UserPreferences) => { + return Request( setURL(`${API_ROOT}/profile/preferences`), setData(payload), setMethod('PUT') diff --git a/packages/linode-js-sdk/src/profile/types.ts b/packages/linode-js-sdk/src/profile/types.ts index 2b7a5308067..7ac6cdeb8d8 100644 --- a/packages/linode-js-sdk/src/profile/types.ts +++ b/packages/linode-js-sdk/src/profile/types.ts @@ -61,3 +61,5 @@ export interface Secret { secret: string; expiry: Date; } + +export type UserPreferences = Record; diff --git a/packages/manager/config/polyfills.js b/packages/manager/config/polyfills.js index 057b6391b80..15d7e59e39b 100644 --- a/packages/manager/config/polyfills.js +++ b/packages/manager/config/polyfills.js @@ -1,13 +1,5 @@ 'use strict'; -if (typeof Promise === 'undefined') { - // Rejection tracking prevents a common issue where React gets into an - // inconsistent state due to an error, but it gets swallowed by a Promise, - // and the user has no idea what causes React's erratic future behavior. - require('promise/lib/rejection-tracking').enable(); - window.Promise = require('promise/lib/es6-extensions.js'); -} - // In tests, polyfill requestAnimationFrame since jsdom doesn't provide it yet. // We don't polyfill it in the browser--this is user's responsibility. if (process.env.NODE_ENV === 'test') { diff --git a/packages/manager/config/testSetup.js b/packages/manager/config/testSetup.js index d2da73b17c6..fb324e34c0a 100644 --- a/packages/manager/config/testSetup.js +++ b/packages/manager/config/testSetup.js @@ -62,3 +62,13 @@ jest.mock('react-chartjs-2', () => ({ } } })); + +// c/f https://github.com/mui-org/material-ui/issues/15726 +window.document.createRange = () => ({ + setStart: () => {}, + setEnd: () => {}, + commonAncestorContainer: { + nodeName: 'BODY', + ownerDocument: document + } +}); diff --git a/packages/manager/config/webpack.config.dev.js b/packages/manager/config/webpack.config.dev.js index c3d7e823ec1..482fbe22dba 100644 --- a/packages/manager/config/webpack.config.dev.js +++ b/packages/manager/config/webpack.config.dev.js @@ -138,6 +138,7 @@ module.exports = { // A missing `test` is equivalent to a match. { test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], + exclude: [/__image_snapshots__/], loader: require.resolve('url-loader'), options: { limit: 10000, @@ -153,12 +154,14 @@ module.exports = { { test: /\.(ts|tsx)$/, include: paths.appSrc, + exclude:[/(stories|test)\.(ts|tsx)$/, /__data__/], use: [ { loader: require.resolve('ts-loader'), options: { // disable type checker - we will use it in fork plugin - transpileOnly: true + transpileOnly: true, + onlyCompileBundledFiles:true } } ] diff --git a/packages/manager/config/webpack.config.prod.js b/packages/manager/config/webpack.config.prod.js index 1fdd0455716..9d35c5676a5 100644 --- a/packages/manager/config/webpack.config.prod.js +++ b/packages/manager/config/webpack.config.prod.js @@ -127,6 +127,7 @@ module.exports = { // assets smaller than specified size as data URLs to avoid requests. { test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], + exclude: [/__image_snapshots__/], loader: require.resolve('url-loader'), options: { limit: 10000, @@ -142,12 +143,14 @@ module.exports = { { test: /\.(ts|tsx)$/, include: paths.appSrc, + exclude:[/(stories|test)\.(ts|tsx)$/, /__data__/], use: [ { loader: require.resolve('ts-loader'), options: { // disable type checker - we will use it in fork plugin - transpileOnly: true + transpileOnly: true, + onlyCompileBundledFiles:true } } ] diff --git a/packages/manager/cypress.json b/packages/manager/cypress.json new file mode 100644 index 00000000000..51e639a9422 --- /dev/null +++ b/packages/manager/cypress.json @@ -0,0 +1,8 @@ +{ + "baseUrl": "http://localhost:3000", + "chromeWebSecurity": false, + "defaultCommandTimeout": 10000, + "pageLoadTimeout": 10000, + "viewportWidth": 1440, + "viewportHeight": 900 +} diff --git a/packages/manager/cypress/integration/local-poc.spec.js b/packages/manager/cypress/integration/local-poc.spec.js new file mode 100644 index 00000000000..5749b995e19 --- /dev/null +++ b/packages/manager/cypress/integration/local-poc.spec.js @@ -0,0 +1,64 @@ +import strings from '../support/cypresshelpers'; + +describe('cypress e2e poc', () => { + beforeEach(() => { + cy.login2(); + }); + + it('checks the dashboard page', () => { + cy.visit('/'); + cy.get('[data-qa-header]').should('have.text', 'Dashboard'); + cy.get('[data-qa-card="Linodes"] h2').should('have.text', 'Linodes'); + cy.get('[data-qa-card="Volumes"] h2').should('have.text', 'Volumes'); + cy.get('[data-qa-card="Domains"] h2').should('have.text', 'Domains'); + cy.get('[data-qa-card="NodeBalancers"] h2').should( + 'have.text', + 'NodeBalancers' + ); + }); + it('creates a volume', () => { + const title = strings.randomTitle(30); + + cy.visit('/volumes/create'); + cy.url().should('contain', '/volumes/create'); + cy.get('[data-testid="link-text"]').should('have.text', 'volumes'); + cy.get('[data-qa-header]').should('have.text', 'Create'); + cy.get('[data-qa-volume-label] [data-testid="textfield-input"]').type( + title + ); + cy.contains('Region') + .click() + .type('new {enter}'); + cy.get('[data-qa-deploy-linode]').click(); + cy.get('[data-qa-drawer-title]').should( + 'have.text', + 'Volume Configuration' + ); + cy.get('[data-qa-mountpoint] input').should( + 'have.value', + `mkdir "/mnt/${title}"` + ); + cy.contains('Close') + .should('be.visible') + .click(); + }); + it('creates a nanode', () => { + const rootpass = strings.randomPass(); + cy.visit('/linodes/create'); + cy.get('[data-testid="link-text"]').should('have.text', 'linodes'); + cy.get('[data-qa-header="Create"]').should('have.text', 'Create'); + cy.contains('Regions') + .click() + .type('new {enter}'); + cy.contains('Nanode').click(); + cy.get('[data-qa-plan-row="Nanode 1GB"]').click(); + cy.get('#root-password').type(rootpass); + cy.get('[data-qa-deploy-linode]').click(); + cy.get('[data-qa-power-control="Busy"]').should('be.visible'); + // skipping this step to save time as we are not currently looking to test the + // systems side of things + // cy.get('[data-qa-power-control="Running"]', { timeout: 120000 }).should( + // 'be.visible' + // ); + }); +}); diff --git a/packages/manager/cypress/integration/poc.spec.js b/packages/manager/cypress/integration/poc.spec.js new file mode 100644 index 00000000000..7404359e8be --- /dev/null +++ b/packages/manager/cypress/integration/poc.spec.js @@ -0,0 +1,63 @@ +import strings from '../support/cypresshelpers'; + +describe('cypress e2e poc', () => { + beforeEach(() => { + cy.login(); + }); + + it('checks the dashboard page', () => { + cy.visit('/'); + cy.get('[data-qa-header]').should('have.text', 'Dashboard'); + cy.get('[data-qa-card="Linodes"] h2').should('have.text', 'Linodes'); + cy.get('[data-qa-card="Volumes"] h2').should('have.text', 'Volumes'); + cy.get('[data-qa-card="Domains"] h2').should('have.text', 'Domains'); + cy.get('[data-qa-card="NodeBalancers"] h2').should( + 'have.text', + 'NodeBalancers' + ); + }); + it('creates a volume', () => { + const title = strings.randomTitle(30); + cy.visit('/volumes/create'); + cy.url().should('contain', '/volumes/create'); + cy.get('[data-testid="link-text"]').should('have.text', 'volumes'); + cy.get('[data-qa-header]').should('have.text', 'Create'); + cy.get('[data-qa-volume-label] [data-testid="textfield-input"]').type( + title + ); + cy.contains('Region') + .click() + .type('new {enter}'); + cy.get('[data-qa-deploy-linode]').click(); + cy.get('[data-qa-drawer-title]').should( + 'have.text', + 'Volume Configuration' + ); + cy.get('[data-qa-mountpoint] input').should( + 'have.value', + `mkdir "/mnt/${title}"` + ); + cy.contains('Close') + .should('be.visible') + .click(); + }); + it('creates a nanode', () => { + const rootpass = strings.randomPass(); + cy.visit('/linodes/create'); + cy.get('[data-testid="link-text"]').should('have.text', 'linodes'); + cy.get('[data-qa-header="Create"]').should('have.text', 'Create'); + cy.contains('Regions') + .click() + .type('new {enter}'); + cy.contains('Nanode').click(); + cy.get('[data-qa-plan-row="Nanode 1GB"]').click(); + cy.get('#root-password').type(rootpass); + cy.get('[data-qa-deploy-linode]').click(); + cy.get('[data-qa-power-control="Busy"]').should('be.visible'); + // skipping this step to save time as we are not currently looking to test the + // systems side of things + // cy.get('[data-qa-power-control="Running"]', { timeout: 120000 }).should( + // 'be.visible' + // ); + }); +}); diff --git a/packages/manager/cypress/plugins/index.js b/packages/manager/cypress/plugins/index.js new file mode 100644 index 00000000000..919cad96ffe --- /dev/null +++ b/packages/manager/cypress/plugins/index.js @@ -0,0 +1,36 @@ +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) +const fs = require('fs-extra'); +const path = require('path'); + +function getConfigurationByFile(file) { + const pathToConfigFile = path.resolve('../manager', 'config', `${file}.json`); + + return fs.readJson(pathToConfigFile); +} + +module.exports = (on, config) => { + // `on` is used to hook into various events Cypress emits + // `config` is the resolved Cypress config + // accept a configFile value or use development by default + + on('task', { + datenow() { + return Date.now(); + } + }); + + const file = config.env.configFile || 'development'; + + return getConfigurationByFile(file); +}; diff --git a/packages/manager/cypress/support/commands.js b/packages/manager/cypress/support/commands.js new file mode 100644 index 00000000000..a9822cbde17 --- /dev/null +++ b/packages/manager/cypress/support/commands.js @@ -0,0 +1,111 @@ +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) +Cypress.Commands.add('login', () => { + Cypress.on('uncaught:exception', (err, runnable) => { + // returning false here prevents Cypress from + // failing the test with newrelic errors + return false; + }); + + cy.clearCookies(); + cy.clearLocalStorage(); + try { + cy.request({ url: '/logout' }); + cy.request({ url: Cypress.env('loginUrl') }); + cy.log('logged out'); + } catch (err) { + cy.log('Could not log out'); + } + let options = { + url: Cypress.env('loginUrl'), + body: { + client_id: Cypress.env('clientId'), + response_type: `token`, + scope: '*', + redirect_uri: Cypress.env('baseUrl') + } + }; + cy.request(options).then(resp => { + expect(resp.status).equals(200); + const xmlResp = Cypress.$.parseHTML(resp.body); + cy.log(resp.body); + const csrfToken = Cypress.$(xmlResp) + .find('#csrf_token') + .attr('value'); + const options = { + method: 'POST', + url: Cypress.env('loginUrl'), + form: true, + body: { + username: Cypress.env('username'), + password: Cypress.env('password'), + csrf_token: csrfToken + } + }; + + cy.request(options).then(resp => { + cy.log(resp); + expect(resp.status).equals(200); + // cy.visit('/'); + }); + }); +}); + +Cypress.Commands.add('login2', () => { + Cypress.on('uncaught:exception', (err, runnable) => { + // returning false here prevents Cypress from + // failing the test with newrelic errors + return false; + }); + cy.visit('/null'); + window.localStorage.setItem( + 'authentication/oauth-token', + Cypress.env('oauthtoken') + ); + window.localStorage.setItem('authentication/scopes', '*'); + cy.log(window.localStorage.getItem('authentication/oauth-token')); + window.localStorage.setItem( + 'authentication/expires', + '2020-04-10T15:10:16.295Z' + ); + //window.localStorage.setItem('authentication/latest-refresh', '1553782241459'); + window.localStorage.setItem( + 'authentication/expire-datetime', + 'Fri Mar 13 2020 11:36:50 GMT-0400 (Eastern Daylight Time)' + ); + window.localStorage.setItem( + 'authentication/token', + 'Bearer ' + Cypress.env('oauthtoken') + ); + // window.localStorage.setItem( + // 'authentication/nonce', + // 'd660f305-25c6-42e1-99ab-91fd04dad159' + // ); + window.localStorage.setItem( + 'authentication/expire', + 'Fri Mar 13 2020 11:54:07 GMT-0500 (Eastern Standard Time)' + ); +}); diff --git a/packages/manager/cypress/support/cypresshelpers.js b/packages/manager/cypress/support/cypresshelpers.js new file mode 100644 index 00000000000..ec4a54cf3d5 --- /dev/null +++ b/packages/manager/cypress/support/cypresshelpers.js @@ -0,0 +1,25 @@ +function randomPass() { + var pass = ''; + var chars = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()1234567890'; + + for (var i = 0; i < 40; i++) { + pass += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return pass; +} + +function randomTitle(count) { + var text = ''; + var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + + for (var i = 0; i < count; i++) { + text += chars.charAt(Math.floor(Math.random() * chars.length)); + } + return text; +} + +export default { + randomPass, + randomTitle +}; diff --git a/packages/manager/cypress/support/index.js b/packages/manager/cypress/support/index.js new file mode 100644 index 00000000000..37a498fb5bf --- /dev/null +++ b/packages/manager/cypress/support/index.js @@ -0,0 +1,20 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/packages/manager/package.json b/packages/manager/package.json index 8ef2f80622a..fa3c8a4fab3 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -2,7 +2,7 @@ "name": "linode-manager", "author": "Linode", "description": "The Linode Manager website", - "version": "0.83.0", + "version": "1.0.0", "private": true, "engines": { "node": ">= 10.16.0" @@ -22,7 +22,7 @@ "@material-ui/styles": "^4.4.1", "@sentry/browser": "~5.10.2", "algoliasearch": "^3.30.0", - "axios": "^0.18.1", + "axios": "~0.19.0", "axios-mock-adapter": "^1.15.0", "bluebird": "^3.5.1", "browser-detect": "^0.2.28", @@ -38,7 +38,7 @@ "jspdf": "^1.5.3", "jspdf-autotable": "^3.2.4", "launchdarkly-react-client-sdk": "^2.11.0", - "linode-js-sdk": "^0.17.0-alpha.0", + "linode-js-sdk": "^0.19.0-alpha.0", "logic-query-parser": "^0.0.5", "md5": "^2.2.1", "memoizee": "^0.4.14", @@ -48,7 +48,6 @@ "novnc-node": "^0.5.3", "patch-package": "^6.1.0", "postinstall-postinstall": "^2.0.0", - "promise": "^8.0.1", "qrcode.react": "^0.8.0", "qs": "^6.6.0", "ramda": "~0.25.0", @@ -62,11 +61,11 @@ "react-ga": "^2.5.3", "react-loadable": "^5.3.1", "react-number-format": "^3.5.0", - "react-redux": "^5.0.7", + "react-redux": "~7.1.3", "react-router-dom": "^5.1.0", "react-select": "^2.0.0", "react-sticky": "^6.0.3", - "react-waypoint": "^8.0.3", + "react-waypoint": "~9.0.2", "recompose": "^0.30.0", "redux": "^4.0.4", "redux-thunk": "^2.3.0", @@ -77,7 +76,7 @@ "showdown": "^1.9.1", "showdown-highlightjs-extension": "^0.1.2", "throttle-debounce": "^2.0.0", - "typescript-fsa": "^3.0.0-beta-2", + "typescript-fsa": "^3.0.0", "typescript-fsa-reducers": "^1.2.0", "xml2js": "^0.4.19", "xterm": "^4.2.0", @@ -103,6 +102,10 @@ "storyshots:update": "export UPDATE=-u && docker-compose -f storyshots.yml up --build --exit-code-from manager-storyshots && unset UPDATE && docker-compose -f storyshots.yml down -v", "storyshots:test": "npx jest --config ./.storybook/storyshots.jest.config.js src/components/Storyshots.test.tsx --env=jsdom", "build-storybook": "build-storybook", + "cy:stage2e": "cypress run --env configFile=staging", + "cy:stagedebug": "cypress open --env configFile=staging", + "cy:e2e": "cypress run", + "cy:debug": "cypress open", "serve": "node testServer.js", "selenium": "npx selenium-standalone install --config=./e2e/config/selenium-config.js && ./node_modules/.bin/selenium-standalone start --config=./e2e/config/selenium-config.js", "e2e": "npx wdio ./e2e/config/wdio.conf.js", @@ -127,12 +130,12 @@ "@babel/preset-typescript": "^7.6.0", "@babel/register": "^7.0.0", "@hapi/hapi": "^18.4.0", - "@storybook/addon-actions": "^5.2.8", - "@storybook/addon-knobs": "^5.2.8", - "@storybook/addon-links": "^5.2.8", - "@storybook/addon-storyshots": "^5.2.8", - "@storybook/addons": "^5.2.8", - "@storybook/react": "^5.2.8", + "@storybook/addon-actions": "~5.3.7", + "@storybook/addon-knobs": "~5.3.7", + "@storybook/addon-links": "~5.3.7", + "@storybook/addon-storyshots": "~5.3.7", + "@storybook/addons": "~5.3.7", + "@storybook/react": "~5.3.7", "@testing-library/jest-dom": "^4.2.3", "@testing-library/react": "^8.0.7", "@testing-library/react-hooks": "^3.2.1", @@ -151,8 +154,6 @@ "@types/memoizee": "^0.4.2", "@types/moment-timezone": "^0.5.4", "@types/node": "^12.7.1", - "@types/normalizr": "^2.0.18", - "@types/pretty-bytes": "^5.1.0", "@types/qrcode.react": "^0.8.0", "@types/qs": "^6.5.1", "@types/ramda": "0.25.16", @@ -160,15 +161,13 @@ "@types/react-currency-formatter": "^1.1.1", "@types/react-dom": "~16.9.4", "@types/react-loadable": "^5.3.3", - "@types/react-redux": "^5.0.15", + "@types/react-redux": "~7.1.7", "@types/react-router-dom": "^5.1.0", "@types/react-select": "^2.0.2", "@types/react-sticky": "^5.0.6", "@types/react-test-renderer": "~16.9.1", "@types/recompose": "^0.30.0", - "@types/redux": "^3.6.0", "@types/redux-mock-store": "^1.0.1", - "@types/redux-thunk": "^2.1.0", "@types/sanitize-html": "1.18.3", "@types/showdown": "^1.9.3", "@types/throttle-debounce": "^1.0.0", @@ -190,7 +189,7 @@ "babel-jest": "^24.8.0", "babel-loader": "^8.0.6", "babel-preset-react-app": "^7.0.0", - "browserslist": "^4.6.6", + "browserslist": "~4.8.5", "case-sensitive-paths-webpack-plugin": "^2.2.0", "chalk": "^2.4.2", "circular-dependency-plugin": "^5.2.0", @@ -198,6 +197,7 @@ "coveralls": "^3.0.2", "css-loader": "^2.1.0", "csstype": "^2.5.5", + "cypress": "3.8.2", "dotenv": "^6.2.0", "enzyme": "^3.10.0", "enzyme-adapter-react-16": "^1.14.0", @@ -230,7 +230,7 @@ "selenium-standalone": "^6.13.0", "set-value": "^3.0.1", "source-map-loader": "^0.2.4", - "storybook-react-router": "^1.0.8", + "storybook-react-router": "~1.0.8", "style-loader": "^0.23.1", "svgr": "^1.9.0", "sw-precache-webpack-plugin": "^0.11.5", diff --git a/packages/manager/public/assets/django.svg b/packages/manager/public/assets/django.svg new file mode 100755 index 00000000000..71fa38e456f --- /dev/null +++ b/packages/manager/public/assets/django.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/packages/manager/public/assets/django_color.svg b/packages/manager/public/assets/django_color.svg new file mode 100755 index 00000000000..160b31516f8 --- /dev/null +++ b/packages/manager/public/assets/django_color.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/packages/manager/public/assets/flask.svg b/packages/manager/public/assets/flask.svg new file mode 100755 index 00000000000..74527a63833 --- /dev/null +++ b/packages/manager/public/assets/flask.svg @@ -0,0 +1,110 @@ + + + + + + + + diff --git a/packages/manager/public/assets/flask_color.svg b/packages/manager/public/assets/flask_color.svg new file mode 100755 index 00000000000..b2d7ff01a92 --- /dev/null +++ b/packages/manager/public/assets/flask_color.svg @@ -0,0 +1,107 @@ + + + + + + + diff --git a/packages/manager/public/assets/mean.svg b/packages/manager/public/assets/mean.svg new file mode 100644 index 00000000000..9a1e16c1fae --- /dev/null +++ b/packages/manager/public/assets/mean.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/manager/public/assets/mean_color.svg b/packages/manager/public/assets/mean_color.svg new file mode 100644 index 00000000000..bbe0932ca7b --- /dev/null +++ b/packages/manager/public/assets/mean_color.svg @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/manager/public/assets/mongodb.svg b/packages/manager/public/assets/mongodb.svg new file mode 100755 index 00000000000..5fee3cc4bd4 --- /dev/null +++ b/packages/manager/public/assets/mongodb.svg @@ -0,0 +1,13 @@ + + + + + + diff --git a/packages/manager/public/assets/mongodb_color.svg b/packages/manager/public/assets/mongodb_color.svg new file mode 100755 index 00000000000..d73c55d8077 --- /dev/null +++ b/packages/manager/public/assets/mongodb_color.svg @@ -0,0 +1,18 @@ + + + + + + + + + + diff --git a/packages/manager/public/assets/phpmyadmin.svg b/packages/manager/public/assets/phpmyadmin.svg new file mode 100755 index 00000000000..35a23ab1ba4 --- /dev/null +++ b/packages/manager/public/assets/phpmyadmin.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/manager/public/assets/phpmyadmin_color.svg b/packages/manager/public/assets/phpmyadmin_color.svg new file mode 100755 index 00000000000..6cf2bac26e4 --- /dev/null +++ b/packages/manager/public/assets/phpmyadmin_color.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/manager/public/assets/postgresql.svg b/packages/manager/public/assets/postgresql.svg new file mode 100755 index 00000000000..7aec5534b72 --- /dev/null +++ b/packages/manager/public/assets/postgresql.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/manager/public/assets/postgresql_color.svg b/packages/manager/public/assets/postgresql_color.svg new file mode 100755 index 00000000000..fbb5d7024a4 --- /dev/null +++ b/packages/manager/public/assets/postgresql_color.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/manager/public/assets/redis.svg b/packages/manager/public/assets/redis.svg new file mode 100755 index 00000000000..14d3aebbd2f --- /dev/null +++ b/packages/manager/public/assets/redis.svg @@ -0,0 +1,27 @@ + + + + + + diff --git a/packages/manager/public/assets/redis_color.svg b/packages/manager/public/assets/redis_color.svg new file mode 100755 index 00000000000..53387da82ac --- /dev/null +++ b/packages/manager/public/assets/redis_color.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/packages/manager/public/assets/rubyonrails.svg b/packages/manager/public/assets/rubyonrails.svg new file mode 100755 index 00000000000..dfcde3155e9 --- /dev/null +++ b/packages/manager/public/assets/rubyonrails.svg @@ -0,0 +1,23 @@ + + + + + + diff --git a/packages/manager/public/assets/rubyonrails_color.svg b/packages/manager/public/assets/rubyonrails_color.svg new file mode 100755 index 00000000000..466c4736318 --- /dev/null +++ b/packages/manager/public/assets/rubyonrails_color.svg @@ -0,0 +1,23 @@ + + + + + + diff --git a/packages/manager/public/index.html b/packages/manager/public/index.html index 0a6a4814b54..451a5ff6c74 100644 --- a/packages/manager/public/index.html +++ b/packages/manager/public/index.html @@ -21,5 +21,11 @@
+ \ No newline at end of file diff --git a/packages/manager/scripts/pre-push.sh b/packages/manager/scripts/pre-push.sh index 5ca3054e0ef..a92b0aae689 100755 --- a/packages/manager/scripts/pre-push.sh +++ b/packages/manager/scripts/pre-push.sh @@ -2,7 +2,7 @@ # Confirm Branch includes M3 Ticket BRANCH_NAME=$(git rev-parse --abbrev-ref HEAD) -BRANCH_INCLUDES_TICKET=$(git rev-parse --abbrev-ref HEAD | egrep -i '(M3-[0-9]*)|feature|staging|develop|testing|master|release-.*|OBJ.*|LKE.*|OCA.*') +BRANCH_INCLUDES_TICKET=$(git rev-parse --abbrev-ref HEAD | egrep -i '(M3-[0-9]*)|feature|patch|staging|develop|testing|master|release-.*|OBJ.*|LKE.*|OCA.*') RED='\033[0;31m' if [ $BRANCH_INCLUDES_TICKET ] diff --git a/packages/manager/src/LinodeThemeWrapper.tsx b/packages/manager/src/LinodeThemeWrapper.tsx index 1453dc98631..4e514a43f5c 100644 --- a/packages/manager/src/LinodeThemeWrapper.tsx +++ b/packages/manager/src/LinodeThemeWrapper.tsx @@ -15,8 +15,8 @@ import { sendThemeToggleEvent } from 'src/utilities/ga'; -type ThemeChoice = 'light' | 'dark'; -type SpacingChoice = 'compact' | 'normal'; +export type ThemeChoice = 'light' | 'dark'; +export type SpacingChoice = 'compact' | 'normal'; type RenderChildren = ( toggle: () => void, @@ -96,7 +96,10 @@ const LinodeThemeWrapper: React.FC = props => { togglePreference: _toggleSpacing }: ToggleProps) => ( { const component = 'Access Panel'; const childStories = ['Password Access', 'Password and SSH Key Access']; - const passwordRegion = '[data-qa-password-input]'; + const passwordLabel = '[data-qa-textfield-label]'; describe('Password Access Suite', () => { - const passwordstrength = '[data-qa-password-strength]'; - const passwordInput = `${passwordRegion} input`; + const passwordInput = '[data-qa-password-input] input'; const hideShowPassword = '[data-qa-hide] svg'; beforeAll(() => { - navigateToStory(component, childStories[0]) - const passwordElem = $(passwordRegion); + navigateToStory(component, childStories[0]); + const passwordElem = $(passwordLabel); passwordElem.waitForDisplayed(constants.wait.normal); }); it('there should be a root password input field', () => { expect($(passwordInput).isDisplayed()) - .withContext(`Password input should be displayed`).toBe(true); - expect($(`${passwordRegion} label`).getText()) - .withContext(`Password input label is incorrect`).toEqual('Root Password') + .withContext(`Password input should be displayed`) + .toBe(true); + expect($(`${passwordLabel}`).getText()) + .withContext(`Password input label is incorrect`) + .toEqual('Root Password'); }); - it('there should be an icon to show plain text password, but should be hidden by default', ()=> { + it('there should be an icon to show plain text password, but should be hidden by default', () => { expect($(hideShowPassword).isDisplayed()) - .withContext(`Password should be hidden`).toBe(true); + .withContext(`Password should be hidden`) + .toBe(true); expect($(passwordInput).getAttribute('type')) - .withContext(`Incorrect password input type`).toEqual('password'); + .withContext(`Incorrect password input type`) + .toEqual('password'); }); - it('password input changes to text type when show password option is selected', ()=> { + it('password input changes to text type when show password option is selected', () => { $(hideShowPassword).click(); expect($(passwordInput).getAttribute('type')) - .withContext(`Incorrect password type`).toEqual('text'); + .withContext(`Incorrect password type`) + .toEqual('text'); //Hide for remaining tests $(hideShowPassword).click(); }); - it('there should be a password strength indicator', () => { - expect($(passwordstrength).isDisplayed()) - .withContext(`Password strength indicator should be displayed`).toBe(true); - }); - - it('password strength indicator updates on input', () => { + it('checks for length and character limitations', () => { const passwords = [ - { password: 'password', strength: 'Weak' }, - { password: '12345test!', strength: 'Fair' }, - { password: '9]%3%7?98+n[', strength: 'Good' } + { password: 'pass', count: 2 }, + { password: 'aaaaaa', count: 1 }, + { password: '9]%3%7?98+n[', count: 0 }, + { password: '2s2s', count: 1 } ]; - passwords.forEach((passwordEntry) => { + passwords.forEach(passwordEntry => { browser.setNewValue(passwordInput, passwordEntry.password); - expect($(passwordstrength).getText()) - .withContext(`Incorrect strength value`) - .toEqual(`Strength: ${passwordEntry.strength}`); + expect($$('circle').length) + .withContext(`password warnings should be ${passwordEntry.count}`) + .toBe(passwordEntry.count); }); }); }); - describe('Password and SSH Key Access Suite', () =>{ - const sshKeysTable = '[data-qa-table=\"SSH Keys\"]'; - const userTableHeader = '[data-qa-table-header=\"User\"]'; - const checkboxAttribute = 'data-qa-checked' + describe('Password and SSH Key Access Suite', () => { + const sshKeysTable = '[data-qa-table="SSH Keys"]'; + const userTableHeader = '[data-qa-table-header="User"]'; + const checkboxAttribute = 'data-qa-checked'; const checkboxes = `[${checkboxAttribute}]`; - function checkAllBoxes(checkOrUnchecked){ - $$(checkboxes).forEach((checkbox) => { + function checkAllBoxes(checkOrUnchecked) { + $$(checkboxes).forEach(checkbox => { checkbox.click(); expect(checkbox.getAttribute(checkboxAttribute)) - .withContext(`Incorrect attribute`).toEqual(checkOrUnchecked.toString()); + .withContext(`Incorrect attribute`) + .toEqual(checkOrUnchecked.toString()); }); } beforeAll(() => { - navigateToStory(component, childStories[1]) - $(passwordRegion).waitForDisplayed(constants.wait.normal); + navigateToStory(component, childStories[1]); + $(passwordLabel).waitForDisplayed(constants.wait.normal); }); it('there should be an ssh key table', () => { expect($(sshKeysTable).isDisplayed()) - .withContext(`ssh key table should be displayed`).toBe(true); + .withContext(`ssh key table should be displayed`) + .toBe(true); expect($(sshKeysTable).getText()) - .withContext(`Incorrect ssh key text`).toEqual('SSH Keys'); + .withContext(`Incorrect ssh key text`) + .toEqual('SSH Keys'); }); it('the table should have a checkbox column, User column, and a SSH Keys column', () => { - const sshKeysHeader = '[data-qa-table-header=\"SSH Keys\"]'; + const sshKeysHeader = '[data-qa-table-header="SSH Keys"]'; expect($(userTableHeader).isDisplayed()) - .withContext(`User header should be displayed`).toBe(true); + .withContext(`User header should be displayed`) + .toBe(true); expect($(userTableHeader).getText()) - .withContext(`Incorrect header text value`).toEqual('User'); + .withContext(`Incorrect header text value`) + .toEqual('User'); expect($(sshKeysHeader).isDisplayed()) - .withContext(`ssh key header should be displayed`).toBe(true); + .withContext(`ssh key header should be displayed`) + .toBe(true); expect($(sshKeysHeader).getText()) - .withContext(`Incorrect ssh key text`).toEqual('SSH Keys'); + .withContext(`Incorrect ssh key text`) + .toEqual('SSH Keys'); expect($$(checkboxes).length) - .withContext(`expected 3 checkboxes`).toEqual(3); + .withContext(`expected 3 checkboxes`) + .toEqual(3); }); it('the checkboxes are clickable', () => { diff --git a/packages/manager/src/components/AccessPanel/AccessPanel.tsx b/packages/manager/src/components/AccessPanel/AccessPanel.tsx index 516d2180457..97e3d495308 100644 --- a/packages/manager/src/components/AccessPanel/AccessPanel.tsx +++ b/packages/manager/src/components/AccessPanel/AccessPanel.tsx @@ -126,9 +126,10 @@ class AccessPanel extends React.Component { className )} > -
+
{error && } { }; makeInitialRequests = async () => { + // When loading lish we avoid all this extra data loading + if (window.location?.pathname?.includes('/lish/')) { return; } + const { nodeBalancerActions: { getAllNodeBalancersWithConfigs } } = this.props; @@ -96,6 +99,7 @@ export class AuthenticationWrapper extends React.Component { */ if (this.props.isAuthenticated) { this.setState({ showChildren: true }); + this.makeInitialRequests(); startEventsInterval(); } @@ -114,6 +118,7 @@ export class AuthenticationWrapper extends React.Component { ) { this.makeInitialRequests(); startEventsInterval(); + return this.setState({ showChildren: true }); } @@ -172,10 +177,7 @@ const mapDispatchToProps: MapDispatchToProps = ( requestClusters: () => dispatch(requestClusters()) }); -const connected = connect( - mapStateToProps, - mapDispatchToProps -); +const connected = connect(mapStateToProps, mapDispatchToProps); export default compose( connected, diff --git a/packages/manager/src/components/Breadcrumb/Breadcrumb.spec.js b/packages/manager/src/components/Breadcrumb/Breadcrumb.spec.js index 513d5b079ff..d6db5e41aeb 100644 --- a/packages/manager/src/components/Breadcrumb/Breadcrumb.spec.js +++ b/packages/manager/src/components/Breadcrumb/Breadcrumb.spec.js @@ -1,129 +1,149 @@ -const { navigateToStory, executeInAllStories } = require('../../../e2e/utils/storybook'); +const { + navigateToStory, + executeInAllStories +} = require('../../../e2e/utils/storybook'); const { constants } = require('../../../e2e/constants'); -describe('Breadcrumb Suite', () => { - const component = 'Breadcrumb'; - const childStories = ['Basic Breadcrumb', 'Breadcrumb with custom label', 'Breadcrumb with editable text']; - const link = '[data-qa-link="true"]'; - const staticText = '[data-qa-label-text="true"]'; - const editableText = '[data-qa-editable-text="true"]'; - const editableTextInput = '[data-testid="textfield-input"]' - const editButton = '[data-qa-edit-button="true"]'; - const saveEditButton = '[data-qa-save-edit="true"]'; - const cancelButton = '[data-qa-cancel-edit="true"]'; - const editBtnMessage = 'edit button should not be displayed' +// These can be fixed at a later time. They have gone through a lot of iterations +// and have broken multiple times. These are now skipped until we decide on keeping them +xdescribe('Breadcrumb Suite', () => { + const component = 'Breadcrumb'; + const childStories = [ + 'Basic Breadcrumb', + 'Breadcrumb with custom label', + 'Breadcrumb with editable text' + ]; + const link = '[data-qa-link="true"]'; + const staticText = '[data-qa-label-text="true"]'; + const editableText = '[data-qa-editable-text="true"]'; + const editableTextInput = '[data-testid="textfield-input"]'; + const editButton = '[data-qa-edit-button="true"]'; + const saveEditButton = '[data-qa-save-edit="true"]'; + const cancelButton = '[data-qa-cancel-edit="true"]'; + const editBtnMessage = 'edit button should not be displayed'; - it('There should be a link in each Breadcrumb story', () => { - executeInAllStories(component, childStories, () => { - $(link).waitForDisplayed(constants.wait.normal); - expect($(link).getAttribute('href')) - .withContext(`href link should not be missing`) - .not.toBeNull(); - expect($$(link)[2].getAttribute('href')) - .withContext('Url should be') - .toEqual(`${browser.options.baseUrl}/linodes/9872893679817/test`) - }); + it('There should be a link in each Breadcrumb story', () => { + executeInAllStories(component, childStories, () => { + $(link).waitForDisplayed(constants.wait.normal); + expect($(link).getAttribute('href')) + .withContext(`href link should not be missing`) + .not.toBeNull(); + expect($$(link)[2].getAttribute('href')) + .withContext('Url should be') + .toEqual(`${browser.options.baseUrl}/linodes/9872893679817/test`); }); + }); - describe('Static text and links', () => { - it('Static text is not editable, and does not contain link', () => { - navigateToStory(component, childStories[0]); - expect($(editButton).isDisplayed()) - .withContext(`${editBtnMessage}`) - .toBe(false); - expect($(staticText).$('..').$('..').getAttribute('href')) - .withContext(`href link should be blank`) - .toBeNull(); - }); - - it('Static text is not editable, and does contain link', () => { - navigateToStory(component, childStories[1]); - expect($(editButton).isDisplayed()) - .withContext(`${editBtnMessage}`).toBe(false); - expect($$(link)[2].getAttribute('href')) - .withContext(`href link should not be blank`).not.toBeNull(); - }); - }) - - describe('Editable breadcrumb text', () => { - it('Editable text header should be editable, but not contain a link', () => { - navigateToStory(component, childStories[2]); - $(editableText).click() - expect($(editButton).isDisplayed()) - .withContext(`edit button should be visible`) - .toBe(true); - expect($(editableText).$('..').getAttribute('href')) - .withContext(`href link should be blank`) - .toBeNull(); - }); + describe('Static text and links', () => { + it('Static text is not editable, and does not contain link', () => { + navigateToStory(component, childStories[0]); + expect($(editButton).isDisplayed()) + .withContext(`${editBtnMessage}`) + .toBe(false); + expect( + $(staticText) + .$('..') + .$('..') + .getAttribute('href') + ) + .withContext(`href link should be blank`) + .toBeNull(); + }); - it('Only clicking the edit icon displays the edit input field', () => { - executeInAllStories(component, [childStories[2]], () => { - $(editableText).waitForDisplayed(true); - $(editableText).click(); - expect($(editableTextInput).isDisplayed()) - .withContext(`text field should not be displayed`) - .toBe(false); - $(editButton).click(); - $(editableTextInput).waitForDisplayed(constants.wait.short); - }); - }); + it('Static text is not editable, and does contain link', () => { + navigateToStory(component, childStories[1]); + expect($(editButton).isDisplayed()) + .withContext(`${editBtnMessage}`) + .toBe(false); + expect($$(link)[2].getAttribute('href')) + .withContext(`href link should not be blank`) + .not.toBeNull(); + }); + }); - it('Text input field should have a save and close button', () => { - navigateToStory(component, childStories[2]); - $(editableText).waitForDisplayed(constants.wait.short); - $(editableText).click(); - $(editButton).click(); - $(editableTextInput).waitForDisplayed() - expect($(saveEditButton).isDisplayed()) - .withContext(`save edit button should be displayed`) - .toBe(true); - expect($(cancelButton).isDisplayed()) - .withContext(`cancel button should be displayed`) - .toBe(true); - $('body').click(); - }); + describe('Editable breadcrumb text', () => { + it('Editable text header should be editable, but not contain a link', () => { + navigateToStory(component, childStories[2]); + $(editableText).click(); + expect($(editButton).isDisplayed()) + .withContext(`edit button should be visible`) + .toBe(true); + expect( + $(editableText) + .$('..') + .getAttribute('href') + ) + .withContext(`href link should be blank`) + .toBeNull(); + }); - it('Clicking the cancel button clears the text input', () => { - navigateToStory(component, childStories[2]); - $(editableText).waitForDisplayed(constants.wait.short); - const originalValue = $(editableText).getText(); + it('Only clicking the edit icon displays the edit input field', () => { + executeInAllStories(component, [childStories[2]], () => { + $(editableText).waitForDisplayed(true); $(editableText).click(); + expect($(editableTextInput).isDisplayed()) + .withContext(`text field should not be displayed`) + .toBe(false); $(editButton).click(); $(editableTextInput).waitForDisplayed(constants.wait.short); - $(editableTextInput).setValue('test clear'); - $(cancelButton).click(); - expect($(editableText).getText()) - .withContext(`text should not be changed on cancel`) - .toEqual(originalValue); }); + }); - it('Clicking the save button saves the text input', () => { - navigateToStory(component, childStories[2]); - $(editableText).waitForDisplayed(constants.wait.short); - const updatedText = 'test save'; - $(editableText).click(); - $(editButton).click(); - $(editableTextInput).waitForDisplayed(constants.wait.short); - browser.setNewValue(editableTextInput, updatedText); - $(saveEditButton).click(); - expect($(editableText).getText()) - .withContext(`text should have been updated`) - .toEqual(updatedText); - }); + it('Text input field should have a save and close button', () => { + navigateToStory(component, childStories[2]); + $(editableText).waitForDisplayed(constants.wait.short); + $(editableText).click(); + $(editButton).click(); + $(editableTextInput).waitForDisplayed(); + expect($(saveEditButton).isDisplayed()) + .withContext(`save edit button should be displayed`) + .toBe(true); + expect($(cancelButton).isDisplayed()) + .withContext(`cancel button should be displayed`) + .toBe(true); + $('body').click(); + }); - it('Clicking out of the editable input without saving does not save the text input', () => { - navigateToStory(component, childStories[2]); - $(editableText).waitForDisplayed(constants.wait.short); - const originalText = $(editableText).getText(); - $(editableText).click(); - $(editButton).click(); - $(editableTextInput).waitForDisplayed(constants.wait.short); - $(editableTextInput).setValue('test do not save input'); - $('body').click(); - expect($(editableText).getText()) - .withContext(`text should not have been saved`) - .toEqual(originalText); - }); - }) + it('Clicking the cancel button clears the text input', () => { + navigateToStory(component, childStories[2]); + $(editableText).waitForDisplayed(constants.wait.short); + const originalValue = $(editableText).getText(); + $(editableText).click(); + $(editButton).click(); + $(editableTextInput).waitForDisplayed(constants.wait.short); + $(editableTextInput).setValue('test clear'); + $(cancelButton).click(); + expect($(editableText).getText()) + .withContext(`text should not be changed on cancel`) + .toEqual(originalValue); + }); + + it('Clicking the save button saves the text input', () => { + navigateToStory(component, childStories[2]); + $(editableText).waitForDisplayed(constants.wait.short); + const updatedText = 'test save'; + $(editableText).click(); + $(editButton).click(); + $(editableTextInput).waitForDisplayed(constants.wait.short); + browser.setNewValue(editableTextInput, updatedText); + $(saveEditButton).click(); + expect($(editableText).getText()) + .withContext(`text should have been updated`) + .toEqual(updatedText); + }); + + it('Clicking out of the editable input without saving does not save the text input', () => { + navigateToStory(component, childStories[2]); + $(editableText).waitForDisplayed(constants.wait.short); + const originalText = $(editableText).getText(); + $(editableText).click(); + $(editButton).click(); + $(editableTextInput).waitForDisplayed(constants.wait.short); + $(editableTextInput).setValue('test do not save input'); + $('body').click(); + expect($(editableText).getText()) + .withContext(`text should not have been saved`) + .toEqual(originalText); + }); + }); }); diff --git a/packages/manager/src/components/ConditionalWrapper/ConditionalWrapper.tsx b/packages/manager/src/components/ConditionalWrapper/ConditionalWrapper.tsx new file mode 100644 index 00000000000..4814ebedba7 --- /dev/null +++ b/packages/manager/src/components/ConditionalWrapper/ConditionalWrapper.tsx @@ -0,0 +1,27 @@ +// This pattern courtesy of: https://blog.hackages.io/conditionally-wrap-an-element-in-react-a8b9a47fab2 +// +// ConditionalWrapper evaluates a given condition and passes its children +// through the given `wrapper` function only if the condition is true. +// +// Example usage: +// +// {children}} +// > +//

Maybe wrapped in a link

+//
+ +import * as React from 'react'; + +interface Props { + condition: boolean; + wrapper: (children: React.ReactNode) => React.ReactElement; +} + +export const ConditionalWrapper: React.FC = props => { + const { condition, wrapper, children } = props; + return condition ? wrapper(children) : <>{children}; +}; + +export default ConditionalWrapper; diff --git a/packages/manager/src/components/ConditionalWrapper/index.ts b/packages/manager/src/components/ConditionalWrapper/index.ts new file mode 100644 index 00000000000..e8427c921c8 --- /dev/null +++ b/packages/manager/src/components/ConditionalWrapper/index.ts @@ -0,0 +1,2 @@ +import ConditionalWrapper from './ConditionalWrapper'; +export default ConditionalWrapper; diff --git a/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.spec.js b/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.spec.js index e2c0e4e3d8e..b798eefbddf 100644 --- a/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.spec.js +++ b/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.spec.js @@ -3,56 +3,60 @@ const { navigateToStory } = require('../../../e2e/utils/storybook'); describe('Confirmation Dialog Suite', () => { const component = 'Confirmation Dialogs'; const childStories = ['Simple Confirmation']; - const confirmButtonTextElem = '[data-qa-dialog-button]'; - const dialogTitleElem = '[data-qa-dialog-title]'; - const confirmButtonElem = '[data-qa-dialog-confirm]'; + const doSomething = '[data-qa-dialog-button]'; + const confirmButton = '[data-qa-buttons] [data-qa-dialog-confirm]'; + const dialogTitle = '.dialog-title'; + const dismissButton = '[data-qa-buttons] [data-qa-dialog-cancel]'; - let dismissButtonElem = '[data-qa-dialog-cancel]'; - let confirmButton, dismissButton, dialogTitle; - - beforeAll(() => { + beforeEach(() => { navigateToStory(component, childStories[0]); + $(doSomething).waitForDisplayed(); }); - it('should display confirm button text', () => { - $(confirmButtonTextElem).waitForDisplayed(); + it('should display Do something!', () => { + expect($(doSomething).getText()) + .withContext(`incorrect text`) + .toBe('Do something!'); }); it('should display dialog on click', () => { - $(confirmButtonTextElem).click(); - dialogTitle = $(dialogTitleElem); - confirmButton = $(confirmButtonElem); - dismissButton = $(dismissButtonElem); - - expect(dialogTitle.getText()) + $(doSomething).click(); + expect($(dialogTitle).getText()) .withContext(`Incorrect dialog title`) .toBe('Are you sure you wanna?'); expect($('[data-qa-dialog-content]').getText()) .withContext(`Incorrect dialog text`) .toBe('stuff stuff stuff'); - expect(confirmButton.isDisplayed()) + expect($(confirmButton).isDisplayed()) .withContext(`Confirm button should be displayed`) .toBe(true); - expect(dismissButton.isDisplayed()) + expect($(dismissButton).isDisplayed()) .withContext(`Dismiss button should be displayed`) .toBe(true); - expect(confirmButton.getTagName()) + expect($(confirmButton).getTagName()) .withContext(`Incorrect tag name`) .toBe('button'); - expect(dismissButton.getTagName()) + expect($(dismissButton).getTagName()) .withContext(`Incorrect tag name`) .toBe('button'); + expect($(confirmButton).getText()) + .withContext(`Incorrect text`) + .toBe(`Continue`); + expect($(dismissButton).getText()) + .withContext(`Incorrect text`) + .toBe(`Cancel`); }); - it('should close dialog on yes', () => { - confirmButton.click(); - dialogTitle.isDisplayed(1500, true); + it('should close dialog with confirm button', () => { + $(doSomething).click(); + $(confirmButton).click(); + $(dialogTitle).isDisplayed(1500, true); }); - it('should close dialog on no', () => { - $(confirmButtonTextElem).click(); - $(dialogTitleElem).waitForDisplayed(); - dismissButton.click(); - dialogTitle.waitForDisplayed(1500, true); + it('should close dialog with cancel button', () => { + $(doSomething).click(); + $(dialogTitle).waitForDisplayed(); + $(dismissButton).click(); + $(dialogTitle).waitForDisplayed(1500, true); }); }); diff --git a/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.tsx b/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.tsx index 001285d8f64..41d8efba711 100644 --- a/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.tsx +++ b/packages/manager/src/components/ConfirmationDialog/ConfirmationDialog.tsx @@ -44,11 +44,7 @@ const ConfirmationDialog: React.FC = props => { PaperProps={{ role: undefined }} role="dialog" > - + {children} {error && ( diff --git a/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx b/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx index 83f42f9f565..fd7403ef03e 100644 --- a/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx +++ b/packages/manager/src/components/CopyTooltip/CopyTooltip.tsx @@ -92,12 +92,12 @@ class CopyTooltip extends React.Component { }; render() { - const { classes, text, className, standAlone, ariaLabel } = this.props; + const { classes, text, className, standAlone } = this.props; const { copied } = this.state; return (