mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 00:45:19 +00:00
add prettier and have it format all fleet application code (#625)
* add prettier and have it format all js code except website: : * trying running prettier check in CI * fix runs on in CI * change CI job name * fix prettier erros and fix CI
This commit is contained in:
parent
df5fa7f515
commit
efb35b537a
88
.eslintrc.js
88
.eslintrc.js
@ -1,82 +1,82 @@
|
|||||||
var path = require('path');
|
var path = require("path");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: [
|
extends: [
|
||||||
'airbnb',
|
"airbnb",
|
||||||
'plugin:jest/recommended',
|
"plugin:jest/recommended",
|
||||||
'plugin:react-hooks/recommended',
|
"plugin:react-hooks/recommended",
|
||||||
'plugin:@typescript-eslint/recommended',
|
"plugin:@typescript-eslint/recommended",
|
||||||
'plugin:cypress/recommended',
|
"plugin:cypress/recommended",
|
||||||
],
|
"plugin:prettier/recommended",
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
plugins: [
|
|
||||||
'jest',
|
|
||||||
'react',
|
|
||||||
'@typescript-eslint',
|
|
||||||
],
|
],
|
||||||
|
parser: "@typescript-eslint/parser",
|
||||||
|
plugins: ["jest", "react", "@typescript-eslint"],
|
||||||
env: {
|
env: {
|
||||||
node: true,
|
node: true,
|
||||||
mocha: true,
|
mocha: true,
|
||||||
browser: true,
|
browser: true,
|
||||||
'jest/globals': true,
|
"jest/globals": true,
|
||||||
},
|
},
|
||||||
globals: {
|
globals: {
|
||||||
expect: false,
|
expect: false,
|
||||||
describe: false,
|
describe: false,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
camelcase: 'off',
|
camelcase: "off",
|
||||||
'consistent-return': 1,
|
"consistent-return": 1,
|
||||||
'arrow-body-style': 0,
|
"arrow-body-style": 0,
|
||||||
'max-len': 0,
|
"max-len": 0,
|
||||||
'no-unused-expressions': 0,
|
"no-unused-expressions": 0,
|
||||||
'no-console': 0,
|
"no-console": 0,
|
||||||
'space-before-function-paren': 0,
|
"space-before-function-paren": 0,
|
||||||
'react/prefer-stateless-function': 0,
|
"react/prefer-stateless-function": 0,
|
||||||
'react/no-multi-comp': 0,
|
"react/no-multi-comp": 0,
|
||||||
'react/no-unused-prop-types': [1, { customValidators: [], skipShapeProps: true }],
|
"react/no-unused-prop-types": [
|
||||||
'react/require-default-props': 0, // TODO set default props and enable this check
|
1,
|
||||||
'react/jsx-filename-extension': [1, { extensions: ['.jsx', '.tsx'] }],
|
{ customValidators: [], skipShapeProps: true },
|
||||||
'no-param-reassign': 0,
|
],
|
||||||
'new-cap': 0,
|
"react/require-default-props": 0, // TODO set default props and enable this check
|
||||||
'import/no-unresolved': [2, { caseSensitive: false }],
|
"react/jsx-filename-extension": [1, { extensions: [".jsx", ".tsx"] }],
|
||||||
'linebreak-style': 0,
|
"no-param-reassign": 0,
|
||||||
'import/no-named-as-default': 'off',
|
"new-cap": 0,
|
||||||
'import/no-named-as-default-member': 'off',
|
"import/no-unresolved": [2, { caseSensitive: false }],
|
||||||
'import/extensions': 0,
|
"linebreak-style": 0,
|
||||||
'import/no-extraneous-dependencies': 0,
|
"import/no-named-as-default": "off",
|
||||||
'no-underscore-dangle': 0,
|
"import/no-named-as-default-member": "off",
|
||||||
'jsx-a11y/no-static-element-interactions': 'off',
|
"import/extensions": 0,
|
||||||
|
"import/no-extraneous-dependencies": 0,
|
||||||
|
"no-underscore-dangle": 0,
|
||||||
|
"jsx-a11y/no-static-element-interactions": "off",
|
||||||
|
|
||||||
// note you must disable the base rule as it can report incorrect errors. more info here:
|
// note you must disable the base rule as it can report incorrect errors. more info here:
|
||||||
// https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-use-before-define.md
|
// https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-use-before-define.md
|
||||||
'no-use-before-define': 'off',
|
"no-use-before-define": "off",
|
||||||
'@typescript-eslint/no-use-before-define': ['error'],
|
"@typescript-eslint/no-use-before-define": ["error"],
|
||||||
|
|
||||||
// turn off and override to not run this on js and jsx files. More info here:
|
// turn off and override to not run this on js and jsx files. More info here:
|
||||||
// https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md#configuring-in-a-mixed-jsts-codebase
|
// https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/explicit-module-boundary-types.md#configuring-in-a-mixed-jsts-codebase
|
||||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
|
|
||||||
// There is a bug with these rules in our version of jsx-a11y plugin (5.1.1)
|
// There is a bug with these rules in our version of jsx-a11y plugin (5.1.1)
|
||||||
// To upgrade our version of the plugin we would need to make more changes
|
// To upgrade our version of the plugin we would need to make more changes
|
||||||
// with eslint-config-airbnb, so we will just turn off for now.
|
// with eslint-config-airbnb, so we will just turn off for now.
|
||||||
'jsx-a11y/heading-has-content': 'off',
|
"jsx-a11y/heading-has-content": "off",
|
||||||
'jsx-a11y/anchor-has-content': 'off',
|
"jsx-a11y/anchor-has-content": "off",
|
||||||
},
|
},
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ['*.ts', '*.tsx'],
|
files: ["*.ts", "*.tsx"],
|
||||||
rules: {
|
rules: {
|
||||||
// Set to warn for now at the beginning to make migration easier
|
// Set to warn for now at the beginning to make migration easier
|
||||||
// but want to change this to error when we can.
|
// but want to change this to error when we can.
|
||||||
'@typescript-eslint/explicit-module-boundary-types': ['warn'],
|
"@typescript-eslint/explicit-module-boundary-types": ["warn"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
settings: {
|
settings: {
|
||||||
'import/resolver': {
|
"import/resolver": {
|
||||||
webpack: {
|
webpack: {
|
||||||
config: path.join(__dirname, 'webpack.config.js'),
|
config: path.join(__dirname, "webpack.config.js"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
43
.github/workflows/test.yml
vendored
43
.github/workflows/test.yml
vendored
@ -25,18 +25,18 @@ jobs:
|
|||||||
${{ runner.os }}-modules-
|
${{ runner.os }}-modules-
|
||||||
|
|
||||||
# It seems faster not to cache Go dependencies
|
# It seems faster not to cache Go dependencies
|
||||||
|
|
||||||
- name: Install JS Dependencies
|
- name: Install JS Dependencies
|
||||||
run: make deps-js
|
run: make deps-js
|
||||||
|
|
||||||
- name: Install Go Dependencies
|
- name: Install Go Dependencies
|
||||||
run: make deps-go
|
run: make deps-go
|
||||||
|
|
||||||
# Pre-starting dependencies here means they are ready to go when we need them.
|
# Pre-starting dependencies here means they are ready to go when we need them.
|
||||||
- name: Start Infra Dependencies
|
- name: Start Infra Dependencies
|
||||||
# Use & to background this
|
# Use & to background this
|
||||||
run: docker-compose up -d mysql_test redis mailhog saml_idp &
|
run: docker-compose up -d mysql_test redis mailhog saml_idp &
|
||||||
|
|
||||||
- name: Build Fleet
|
- name: Build Fleet
|
||||||
run: |
|
run: |
|
||||||
export PATH=$PATH:~/go/bin
|
export PATH=$PATH:~/go/bin
|
||||||
@ -51,7 +51,7 @@ jobs:
|
|||||||
make e2e-setup
|
make e2e-setup
|
||||||
yarn cypress run --config video=false
|
yarn cypress run --config video=false
|
||||||
|
|
||||||
|
|
||||||
test-js:
|
test-js:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
@ -106,7 +106,33 @@ jobs:
|
|||||||
- name: Run JS Linting
|
- name: Run JS Linting
|
||||||
run: |
|
run: |
|
||||||
make lint-js
|
make lint-js
|
||||||
|
|
||||||
|
check-prettier:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ ubuntu-latest ]
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout Code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: JS Dependency Cache
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
**/node_modules
|
||||||
|
~/.cache/Cypress
|
||||||
|
key: ${{ runner.os }}-modules-${{ hashFiles('**/yarn.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-modules-
|
||||||
|
|
||||||
|
- name: Install JS Dependencies
|
||||||
|
run: make deps-js
|
||||||
|
|
||||||
|
- name: Run prettier formatting check
|
||||||
|
run: |
|
||||||
|
yarn prettier:check
|
||||||
|
|
||||||
test-go:
|
test-go:
|
||||||
strategy:
|
strategy:
|
||||||
@ -122,15 +148,15 @@ jobs:
|
|||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
# Pre-starting dependencies here means they are ready to go when we need them.
|
# Pre-starting dependencies here means they are ready to go when we need them.
|
||||||
- name: Start Infra Dependencies
|
- name: Start Infra Dependencies
|
||||||
# Use & to background this
|
# Use & to background this
|
||||||
run: docker-compose up -d mysql_test redis &
|
run: docker-compose up -d mysql_test redis &
|
||||||
|
|
||||||
# It seems faster not to cache Go dependencies
|
# It seems faster not to cache Go dependencies
|
||||||
- name: Install Go Dependencies
|
- name: Install Go Dependencies
|
||||||
run: make deps-go
|
run: make deps-go
|
||||||
|
|
||||||
- name: Generate static files
|
- name: Generate static files
|
||||||
run: |
|
run: |
|
||||||
export PATH=$PATH:~/go/bin
|
export PATH=$PATH:~/go/bin
|
||||||
@ -158,4 +184,3 @@ jobs:
|
|||||||
- name: Run Go Linting
|
- name: Run Go Linting
|
||||||
run: |
|
run: |
|
||||||
make lint-go
|
make lint-go
|
||||||
|
|
||||||
|
34
.prettierignore
Normal file
34
.prettierignore
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# markdown
|
||||||
|
*.md
|
||||||
|
|
||||||
|
# output directories
|
||||||
|
build
|
||||||
|
vendor
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# generated artifacts
|
||||||
|
assets/bundle*.*
|
||||||
|
assets/*@*.svg
|
||||||
|
assets/*@*.png
|
||||||
|
assets/*@*.eot
|
||||||
|
assets/*@*.woff
|
||||||
|
assets/*@*.woff2
|
||||||
|
assets/*@*.ttf
|
||||||
|
frontend/templates/react.tmpl
|
||||||
|
bindata.go
|
||||||
|
server/bindata/generated.go
|
||||||
|
*.cover
|
||||||
|
*.test
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# typescript generated test files
|
||||||
|
tmp/
|
||||||
|
|
||||||
|
# editors
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
|
||||||
|
# Cypress e2e testing
|
||||||
|
cypress/screenshots
|
||||||
|
cypress/videos
|
||||||
|
cypress/downloads
|
1
.prettierrc.json
Normal file
1
.prettierrc.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{}
|
120
.sass-lint.yml
120
.sass-lint.yml
@ -1,120 +0,0 @@
|
|||||||
# Linter ReadMe: https://github.com/sasstools/sass-lint/tree/master/docs/rules
|
|
||||||
files:
|
|
||||||
include: 'frontend/**/*.s+(a|c)ss'
|
|
||||||
ignore:
|
|
||||||
- 'frontend/styles/global/_fonts.scss'
|
|
||||||
- 'frontend/styles/global/_icons.scss'
|
|
||||||
options:
|
|
||||||
formatter: stylish
|
|
||||||
merge-default-rules: false
|
|
||||||
rules:
|
|
||||||
bem-depth:
|
|
||||||
- 1
|
|
||||||
- max-depth: 1
|
|
||||||
border-zero:
|
|
||||||
- 1
|
|
||||||
- convention: '0'
|
|
||||||
brace-style:
|
|
||||||
- 1
|
|
||||||
- allow-single-line: false
|
|
||||||
class-name-format:
|
|
||||||
- 1
|
|
||||||
- convention: ^(?!js-).*
|
|
||||||
convention-explanation: should not be written in the form js-*
|
|
||||||
clean-import-paths:
|
|
||||||
- 1
|
|
||||||
- filename-extension: true
|
|
||||||
leading-underscore: false
|
|
||||||
empty-line-between-blocks:
|
|
||||||
- 1
|
|
||||||
- ignore-single-line-rulesets: false
|
|
||||||
extends-before-declarations: 1
|
|
||||||
extends-before-mixins: 1
|
|
||||||
final-newline:
|
|
||||||
- 1
|
|
||||||
- include: true
|
|
||||||
force-attribute-nesting: 1
|
|
||||||
force-element-nesting: 1
|
|
||||||
force-pseudo-nesting: 1
|
|
||||||
function-name-format:
|
|
||||||
- 1
|
|
||||||
- convention: '^[\-_a-z]+$'
|
|
||||||
convention-explanation: 'Variables must contain only lowercase letters, hyphens, and underscores'
|
|
||||||
hex-length:
|
|
||||||
- 1
|
|
||||||
- style: short
|
|
||||||
hex-notation:
|
|
||||||
- 1
|
|
||||||
- style: lowercase
|
|
||||||
id-name-format:
|
|
||||||
- 1
|
|
||||||
- convention: hyphenatedbem
|
|
||||||
indentation:
|
|
||||||
- 1
|
|
||||||
- size: 2
|
|
||||||
leading-zero:
|
|
||||||
- 0
|
|
||||||
mixin-name-format:
|
|
||||||
- 1
|
|
||||||
- convention: '^[\-_a-z]+$'
|
|
||||||
convention-explanation: 'Variables must contain only lowercase letters, hyphens, and underscores'
|
|
||||||
mixins-before-declarations:
|
|
||||||
- 1
|
|
||||||
- exclude: ['breakpoint', 'media', 'placeholder']
|
|
||||||
nesting-depth:
|
|
||||||
- 1
|
|
||||||
- max-depth: 4
|
|
||||||
no-color-keywords: 1
|
|
||||||
no-color-literals: 0
|
|
||||||
no-css-comments: 1
|
|
||||||
no-duplicate-properties: 1
|
|
||||||
no-empty-rulesets: 1
|
|
||||||
no-extends: 0
|
|
||||||
no-ids: 1
|
|
||||||
no-important: 1
|
|
||||||
no-invalid-hex: 1
|
|
||||||
no-mergeable-selectors: 1
|
|
||||||
no-misspelled-properties:
|
|
||||||
- 1
|
|
||||||
- extra-properties: []
|
|
||||||
no-qualifying-elements:
|
|
||||||
- 1
|
|
||||||
- allow-element-with-attribute: true
|
|
||||||
allow-element-with-class: false
|
|
||||||
allow-element-with-id: false
|
|
||||||
no-trailing-zero: 1
|
|
||||||
no-transition-all: 1
|
|
||||||
no-url-protocols: 1
|
|
||||||
placeholder-name-format:
|
|
||||||
- 1
|
|
||||||
- convention: hyphenatedbem
|
|
||||||
property-sort-order: 0
|
|
||||||
quotes:
|
|
||||||
- 1
|
|
||||||
- style: single
|
|
||||||
shorthand-values: 1
|
|
||||||
single-line-per-selector: 1
|
|
||||||
space-after-bang:
|
|
||||||
- 1
|
|
||||||
- include: false
|
|
||||||
space-after-colon:
|
|
||||||
- 1
|
|
||||||
- include: true
|
|
||||||
space-after-comma: 1
|
|
||||||
space-before-bang:
|
|
||||||
- 1
|
|
||||||
- include: true
|
|
||||||
space-before-brace:
|
|
||||||
- 1
|
|
||||||
- include: true
|
|
||||||
space-before-colon: 1
|
|
||||||
space-between-parens:
|
|
||||||
- 1
|
|
||||||
- include: false
|
|
||||||
trailing-semicolon: 1
|
|
||||||
url-quotes: 1
|
|
||||||
variable-name-format:
|
|
||||||
- 1
|
|
||||||
- convention: '^[\-_a-z]+$'
|
|
||||||
convention-explanation: 'Variables must contain only lowercase letters, hyphens, and underscores'
|
|
||||||
zero-unit: 1
|
|
@ -1,27 +1,27 @@
|
|||||||
|
import * as path from "path";
|
||||||
|
|
||||||
import * as path from 'path';
|
describe("Hosts page", () => {
|
||||||
|
|
||||||
describe('Hosts page', () => {
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.setup();
|
cy.setup();
|
||||||
cy.login();
|
cy.login();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Add new host', () => {
|
it("Add new host", () => {
|
||||||
cy.visit('/');
|
cy.visit("/");
|
||||||
|
|
||||||
cy.contains('button', /add new host/i)
|
cy.contains("button", /add new host/i).click();
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.contains('a', /download/i).first()
|
cy.contains("a", /download/i)
|
||||||
|
.first()
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.get('a[href*="showSecret"]').click();
|
cy.get('a[href*="showSecret"]').click();
|
||||||
|
|
||||||
// Assert enroll secret downloaded matches the one displayed
|
// Assert enroll secret downloaded matches the one displayed
|
||||||
cy.readFile(path.join(Cypress.config('downloadsFolder'), 'secret.txt'), { timeout: 3000 })
|
cy.readFile(path.join(Cypress.config("downloadsFolder"), "secret.txt"), {
|
||||||
.then((contents) => {
|
timeout: 3000,
|
||||||
cy.get('input[disabled]').should('have.value', contents);
|
}).then((contents) => {
|
||||||
});
|
cy.get("input[disabled]").should("have.value", contents);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,59 +1,59 @@
|
|||||||
describe('Label flow', () => {
|
describe("Label flow", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.setup();
|
cy.setup();
|
||||||
cy.login();
|
cy.login();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Create, edit, and delete a label successfully', () => {
|
it("Create, edit, and delete a label successfully", () => {
|
||||||
cy.visit('/hosts/manage');
|
cy.visit("/hosts/manage");
|
||||||
|
|
||||||
cy.findByRole('button', { name: /add new label/i }).click();
|
cy.findByRole("button", { name: /add new label/i }).click();
|
||||||
|
|
||||||
// Using class selector because third party element doesn't work with Cypress Testing Selector Library
|
// Using class selector because third party element doesn't work with Cypress Testing Selector Library
|
||||||
cy.get('.ace_content')
|
cy.get(".ace_content")
|
||||||
.click()
|
.click()
|
||||||
.type('{selectall}{backspace}SELECT * FROM users;');
|
.type("{selectall}{backspace}SELECT * FROM users;");
|
||||||
|
|
||||||
cy.findByLabelText(/name/i).click().type('Show all users');
|
cy.findByLabelText(/name/i).click().type("Show all users");
|
||||||
|
|
||||||
cy.findByLabelText(/description/i)
|
cy.findByLabelText(/description/i)
|
||||||
.click()
|
.click()
|
||||||
.type('Select all users across platforms.');
|
.type("Select all users across platforms.");
|
||||||
|
|
||||||
// Cannot call cy.select on div disguised as a dropdown
|
// Cannot call cy.select on div disguised as a dropdown
|
||||||
cy.findByText(/select one/i).click();
|
cy.findByText(/select one/i).click();
|
||||||
cy.findByText(/all platforms/i).click();
|
cy.findByText(/all platforms/i).click();
|
||||||
|
|
||||||
cy.findByRole('button', { name: /save label/i }).click();
|
cy.findByRole("button", { name: /save label/i }).click();
|
||||||
|
|
||||||
cy.findByText(/show all users/i).click();
|
cy.findByText(/show all users/i).click();
|
||||||
|
|
||||||
cy.contains('button', /edit/i).click();
|
cy.contains("button", /edit/i).click();
|
||||||
|
|
||||||
// Label SQL not editable to test
|
// Label SQL not editable to test
|
||||||
|
|
||||||
cy.findByLabelText(/name/i)
|
cy.findByLabelText(/name/i)
|
||||||
.click()
|
.click()
|
||||||
.type('{selectall}{backspace}Show all usernames');
|
.type("{selectall}{backspace}Show all usernames");
|
||||||
|
|
||||||
cy.findByLabelText(/description/i)
|
cy.findByLabelText(/description/i)
|
||||||
.click()
|
.click()
|
||||||
.type('{selectall}{backspace}Select all usernames on Mac.');
|
.type("{selectall}{backspace}Select all usernames on Mac.");
|
||||||
|
|
||||||
cy.findByText(/select one/i).click();
|
cy.findByText(/select one/i).click();
|
||||||
|
|
||||||
cy.findAllByText(/macos/i).click();
|
cy.findAllByText(/macos/i).click();
|
||||||
|
|
||||||
cy.findByRole('button', { name: /update label/i }).click();
|
cy.findByRole("button", { name: /update label/i }).click();
|
||||||
|
|
||||||
cy.findByRole('button', { name: /delete/i }).click();
|
cy.findByRole("button", { name: /delete/i }).click();
|
||||||
|
|
||||||
// Can't figure out how attach findByRole onto modal button
|
// Can't figure out how attach findByRole onto modal button
|
||||||
// Can't use findByText because delete button under modal
|
// Can't use findByText because delete button under modal
|
||||||
cy.get('.manage-hosts__modal-buttons > .button--alert')
|
cy.get(".manage-hosts__modal-buttons > .button--alert")
|
||||||
.contains('button', /delete/i)
|
.contains("button", /delete/i)
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.findByText(/show all users/i).should('not.exist');
|
cy.findByText(/show all users/i).should("not.exist");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,25 +1,25 @@
|
|||||||
describe('Pack flow', () => {
|
describe("Pack flow", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.setup();
|
cy.setup();
|
||||||
cy.login();
|
cy.login();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Create, edit, and delete a pack successfully', () => {
|
it("Create, edit, and delete a pack successfully", () => {
|
||||||
cy.visit('/packs/manage');
|
cy.visit("/packs/manage");
|
||||||
|
|
||||||
cy.findByRole('button', { name: /create new pack/i }).click();
|
cy.findByRole("button", { name: /create new pack/i }).click();
|
||||||
|
|
||||||
cy.findByLabelText(/query pack title/i)
|
cy.findByLabelText(/query pack title/i)
|
||||||
.click()
|
.click()
|
||||||
.type('Errors and crashes');
|
.type("Errors and crashes");
|
||||||
|
|
||||||
cy.findByLabelText(/query pack description/i)
|
cy.findByLabelText(/query pack description/i)
|
||||||
.click()
|
.click()
|
||||||
.type('See all user errors and window crashes.');
|
.type("See all user errors and window crashes.");
|
||||||
|
|
||||||
cy.findByRole('button', { name: /save query pack/i }).click();
|
cy.findByRole("button", { name: /save query pack/i }).click();
|
||||||
|
|
||||||
cy.visit('/packs/manage');
|
cy.visit("/packs/manage");
|
||||||
|
|
||||||
cy.findByText(/errors and crashes/i).click();
|
cy.findByText(/errors and crashes/i).click();
|
||||||
|
|
||||||
@ -27,28 +27,28 @@ describe('Pack flow', () => {
|
|||||||
|
|
||||||
cy.findByLabelText(/query pack title/i)
|
cy.findByLabelText(/query pack title/i)
|
||||||
.click()
|
.click()
|
||||||
.type('{selectall}{backspace}Server errors');
|
.type("{selectall}{backspace}Server errors");
|
||||||
|
|
||||||
cy.findByLabelText(/query pack description/i)
|
cy.findByLabelText(/query pack description/i)
|
||||||
.click()
|
.click()
|
||||||
.type('{selectall}{backspace}See all server errors.');
|
.type("{selectall}{backspace}See all server errors.");
|
||||||
|
|
||||||
cy.findByRole('button', { name: /save/i }).click();
|
cy.findByRole("button", { name: /save/i }).click();
|
||||||
|
|
||||||
cy.visit('/packs/manage');
|
cy.visit("/packs/manage");
|
||||||
|
|
||||||
cy.get('#select-pack-1').check({ force: true });
|
cy.get("#select-pack-1").check({ force: true });
|
||||||
|
|
||||||
cy.findByRole('button', { name: /delete/i }).click();
|
cy.findByRole("button", { name: /delete/i }).click();
|
||||||
|
|
||||||
// Can't figure out how attach findByRole onto modal button
|
// Can't figure out how attach findByRole onto modal button
|
||||||
// Can't use findByText because delete button under modal
|
// Can't use findByText because delete button under modal
|
||||||
cy.get('.all-packs-page__modal-btn-wrap > .button--alert')
|
cy.get(".all-packs-page__modal-btn-wrap > .button--alert")
|
||||||
.contains('button', /delete/i)
|
.contains("button", /delete/i)
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.findByText(/successfully deleted/i).should('be.visible');
|
cy.findByText(/successfully deleted/i).should("be.visible");
|
||||||
|
|
||||||
cy.findByText(/server errors/i).should('not.exist');
|
cy.findByText(/server errors/i).should("not.exist");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,63 +1,66 @@
|
|||||||
describe('Query flow', () => {
|
describe("Query flow", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.setup();
|
cy.setup();
|
||||||
cy.login();
|
cy.login();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Create, check, edit, and delete a query successfully', () => {
|
it("Create, check, edit, and delete a query successfully", () => {
|
||||||
cy.visit('/queries/manage');
|
cy.visit("/queries/manage");
|
||||||
|
|
||||||
cy.findByRole('button', { name: /create new query/i }).click();
|
cy.findByRole("button", { name: /create new query/i }).click();
|
||||||
|
|
||||||
cy.findByLabelText(/query title/i).click().type('Query all window crashes');
|
cy.findByLabelText(/query title/i)
|
||||||
|
.click()
|
||||||
|
.type("Query all window crashes");
|
||||||
|
|
||||||
// Using class selector because third party element doesn't work with Cypress Testing Selector Library
|
// Using class selector because third party element doesn't work with Cypress Testing Selector Library
|
||||||
cy.get('.ace_content')
|
cy.get(".ace_content")
|
||||||
.click()
|
.click()
|
||||||
.type('{selectall}{backspace}SELECT * FROM windows_crashes;');
|
.type("{selectall}{backspace}SELECT * FROM windows_crashes;");
|
||||||
|
|
||||||
cy.findByLabelText(/description/i).click().type('See all window crashes');
|
cy.findByLabelText(/description/i)
|
||||||
|
.click()
|
||||||
|
.type("See all window crashes");
|
||||||
|
|
||||||
cy.findByRole('button', { name: /save/i }).click();
|
cy.findByRole("button", { name: /save/i }).click();
|
||||||
|
|
||||||
cy.findByRole('button', { name: /save as new/i }).click();
|
cy.findByRole("button", { name: /save as new/i }).click();
|
||||||
|
|
||||||
// Just refreshes to create new query, needs success alert to user that they created a query
|
// Just refreshes to create new query, needs success alert to user that they created a query
|
||||||
|
|
||||||
cy.visit('/queries/manage');
|
cy.visit("/queries/manage");
|
||||||
|
|
||||||
cy.findByText(/query all/i).click();
|
cy.findByText(/query all/i).click();
|
||||||
|
|
||||||
cy.findByRole('button', { name: /edit or run query/i }).click();
|
cy.findByRole("button", { name: /edit or run query/i }).click();
|
||||||
|
|
||||||
cy.get('.ace_content')
|
cy.get(".ace_content")
|
||||||
.click()
|
.click()
|
||||||
.type(
|
.type(
|
||||||
'{selectall}{backspace}SELECT datetime, username FROM windows_crashes;',
|
"{selectall}{backspace}SELECT datetime, username FROM windows_crashes;"
|
||||||
);
|
);
|
||||||
|
|
||||||
cy.findByRole('button', { name: /save/i }).click();
|
cy.findByRole("button", { name: /save/i }).click();
|
||||||
|
|
||||||
cy.findByRole('button', { name: /save changes/i }).click();
|
cy.findByRole("button", { name: /save changes/i }).click();
|
||||||
|
|
||||||
cy.findByText(/query updated/i).should('be.visible');
|
cy.findByText(/query updated/i).should("be.visible");
|
||||||
|
|
||||||
cy.visit('/queries/manage');
|
cy.visit("/queries/manage");
|
||||||
|
|
||||||
// This element has no label, text, or role
|
// This element has no label, text, or role
|
||||||
cy.get('#query-checkbox-1')
|
cy.get("#query-checkbox-1").check({ force: true });
|
||||||
.check({ force: true });
|
|
||||||
|
|
||||||
cy.findByRole('button', { name: /delete/i }).click();
|
cy.findByRole("button", { name: /delete/i }).click();
|
||||||
|
|
||||||
// Can't figure out how attach findByRole onto modal button
|
// Can't figure out how attach findByRole onto modal button
|
||||||
// Can't use findByText because delete button under modal
|
// Can't use findByText because delete button under modal
|
||||||
cy.get('.manage-queries-page__modal-btn-wrap > .button--alert')
|
cy.get(".manage-queries-page__modal-btn-wrap > .button--alert")
|
||||||
.contains('button', /delete/i)
|
.contains("button", /delete/i)
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
cy.findByText(/successfully deleted/i).should('be.visible');
|
cy.findByText(/successfully deleted/i).should("be.visible");
|
||||||
|
|
||||||
cy.findByText(/query all/i).should('not.exist');
|
cy.findByText(/query all/i).should("not.exist");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,51 +1,43 @@
|
|||||||
describe('Sessions', () => {
|
describe("Sessions", () => {
|
||||||
// Typically we want to use a beforeEach but not much happens in these tests
|
// Typically we want to use a beforeEach but not much happens in these tests
|
||||||
// so sharing some state should be okay and saves a bit of runtime.
|
// so sharing some state should be okay and saves a bit of runtime.
|
||||||
before(() => {
|
before(() => {
|
||||||
cy.setup();
|
cy.setup();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Logs in and out successfully', () => {
|
it("Logs in and out successfully", () => {
|
||||||
cy.visit('/');
|
cy.visit("/");
|
||||||
cy.contains(/forgot password/i);
|
cy.contains(/forgot password/i);
|
||||||
|
|
||||||
// Log in
|
// Log in
|
||||||
cy.get('input').first()
|
cy.get("input").first().type("test@fleetdm.com");
|
||||||
.type('test@fleetdm.com');
|
cy.get("input").last().type("admin123#");
|
||||||
cy.get('input').last()
|
cy.get("button").click();
|
||||||
.type('admin123#');
|
|
||||||
cy.get('button')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Verify dashboard
|
// Verify dashboard
|
||||||
cy.url().should('include', '/hosts/manage');
|
cy.url().should("include", "/hosts/manage");
|
||||||
cy.contains('All Hosts');
|
cy.contains("All Hosts");
|
||||||
|
|
||||||
// Log out
|
// Log out
|
||||||
cy.findByAltText(/user avatar/i)
|
cy.findByAltText(/user avatar/i).click();
|
||||||
.click();
|
cy.contains("button", "Sign out").click();
|
||||||
cy.contains('button', 'Sign out')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.url().should('match', /\/login$/);
|
cy.url().should("match", /\/login$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Fails login with invalid password', () => {
|
it("Fails login with invalid password", () => {
|
||||||
cy.visit('/');
|
cy.visit("/");
|
||||||
cy.get('input').first()
|
cy.get("input").first().type("test@fleetdm.com");
|
||||||
.type('test@fleetdm.com');
|
cy.get("input").last().type("bad_password");
|
||||||
cy.get('input').last()
|
cy.get(".button").click();
|
||||||
.type('bad_password');
|
|
||||||
cy.get('.button')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.url().should('match', /\/login$/);
|
cy.url().should("match", /\/login$/);
|
||||||
cy.contains('Authentication failed');
|
cy.contains("Authentication failed");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Fails to access authenticated resource', () => {
|
it("Fails to access authenticated resource", () => {
|
||||||
cy.visit('/hosts/manage');
|
cy.visit("/hosts/manage");
|
||||||
|
|
||||||
cy.url().should('match', /\/login$/);
|
cy.url().should("match", /\/login$/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,65 +1,59 @@
|
|||||||
describe('SSO Sessions', () => {
|
describe("SSO Sessions", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.setup();
|
cy.setup();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Can login with username/password', () => {
|
it("Can login with username/password", () => {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.setupSSO(enable_idp_login = true);
|
cy.setupSSO((enable_idp_login = true));
|
||||||
cy.logout();
|
cy.logout();
|
||||||
|
|
||||||
cy.visit('/');
|
cy.visit("/");
|
||||||
cy.contains(/forgot password/i);
|
cy.contains(/forgot password/i);
|
||||||
|
|
||||||
// Log in
|
// Log in
|
||||||
cy.get('input').first()
|
cy.get("input").first().type("test@fleetdm.com");
|
||||||
.type('test@fleetdm.com');
|
cy.get("input").last().type("admin123#");
|
||||||
cy.get('input').last()
|
cy.contains("button", "Login").click();
|
||||||
.type('admin123#');
|
|
||||||
cy.contains('button', 'Login')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Verify dashboard
|
// Verify dashboard
|
||||||
cy.url().should('include', '/hosts/manage');
|
cy.url().should("include", "/hosts/manage");
|
||||||
cy.contains('All Hosts');
|
cy.contains("All Hosts");
|
||||||
|
|
||||||
// Log out
|
// Log out
|
||||||
cy.findByAltText(/user avatar/i)
|
cy.findByAltText(/user avatar/i).click();
|
||||||
.click();
|
cy.contains("button", "Sign out").click();
|
||||||
cy.contains('button', 'Sign out')
|
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.url().should('match', /\/login$/);
|
cy.url().should("match", /\/login$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Can login via SSO', () => {
|
it("Can login via SSO", () => {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.setupSSO(enable_idp_login = true);
|
cy.setupSSO((enable_idp_login = true));
|
||||||
cy.logout();
|
cy.logout();
|
||||||
|
|
||||||
|
cy.visit("/");
|
||||||
cy.visit('/');
|
|
||||||
|
|
||||||
// Log in
|
// Log in
|
||||||
cy.contains('button', 'Sign On With SimpleSAML');
|
cy.contains("button", "Sign On With SimpleSAML");
|
||||||
|
|
||||||
cy.loginSSO();
|
cy.loginSSO();
|
||||||
|
|
||||||
cy.contains('All hosts');
|
cy.contains("All hosts");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Fails when IdP login disabled', () => {
|
it("Fails when IdP login disabled", () => {
|
||||||
cy.login();
|
cy.login();
|
||||||
cy.setupSSO();
|
cy.setupSSO();
|
||||||
cy.logout();
|
cy.logout();
|
||||||
|
|
||||||
cy.visit('/');
|
cy.visit("/");
|
||||||
|
|
||||||
cy.contains('button', 'Sign On With SimpleSAML');
|
cy.contains("button", "Sign On With SimpleSAML");
|
||||||
|
|
||||||
cy.loginSSO();
|
cy.loginSSO();
|
||||||
|
|
||||||
// Log in should fail
|
// Log in should fail
|
||||||
cy.contains('Password');
|
cy.contains("Password");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,46 +1,41 @@
|
|||||||
describe('Setup', () => {
|
describe("Setup", () => {
|
||||||
// Different than normal beforeEach because we don't run the fleetctl setup.
|
// Different than normal beforeEach because we don't run the fleetctl setup.
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cy.exec('make e2e-reset-db', { timeout: 5000 });
|
cy.exec("make e2e-reset-db", { timeout: 5000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Completes setup', () => {
|
it("Completes setup", () => {
|
||||||
cy.visit('/');
|
cy.visit("/");
|
||||||
cy.url().should('match', /\/setup$/);
|
cy.url().should("match", /\/setup$/);
|
||||||
cy.contains(/setup/i);
|
cy.contains(/setup/i);
|
||||||
|
|
||||||
// Page 1
|
// Page 1
|
||||||
cy.findByPlaceholderText(/username/i)
|
cy.findByPlaceholderText(/username/i).type("test");
|
||||||
.type('test');
|
|
||||||
|
|
||||||
cy.findByPlaceholderText(/^password/i).first()
|
cy.findByPlaceholderText(/^password/i)
|
||||||
.type('admin123#');
|
.first()
|
||||||
|
.type("admin123#");
|
||||||
|
|
||||||
cy.findByPlaceholderText(/confirm password/i).last()
|
cy.findByPlaceholderText(/confirm password/i)
|
||||||
.type('admin123#');
|
.last()
|
||||||
|
.type("admin123#");
|
||||||
|
|
||||||
cy.findByPlaceholderText(/email/i)
|
cy.findByPlaceholderText(/email/i).type("test@fleetdm.com");
|
||||||
.type('test@fleetdm.com');
|
|
||||||
|
|
||||||
cy.contains('button:enabled', /next/i)
|
cy.contains("button:enabled", /next/i).click();
|
||||||
.click();
|
|
||||||
|
|
||||||
// Page 2
|
// Page 2
|
||||||
cy.findByPlaceholderText(/organization name/i)
|
cy.findByPlaceholderText(/organization name/i).type("Fleet Test");
|
||||||
.type('Fleet Test');
|
|
||||||
|
|
||||||
cy.contains('button:enabled', /next/i)
|
cy.contains("button:enabled", /next/i).click();
|
||||||
.click();
|
|
||||||
|
|
||||||
// Page 3
|
// Page 3
|
||||||
cy.contains('button:enabled', /submit/i)
|
cy.contains("button:enabled", /submit/i).click();
|
||||||
.click();
|
|
||||||
|
|
||||||
// Page 4
|
// Page 4
|
||||||
cy.contains('button:enabled', /finish/i)
|
cy.contains("button:enabled", /finish/i).click();
|
||||||
.click();
|
|
||||||
|
|
||||||
cy.url().should('match', /\/hosts\/manage$/i);
|
cy.url().should("match", /\/hosts\/manage$/i);
|
||||||
cy.contains(/all hosts/i);
|
cy.contains(/all hosts/i);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import '@testing-library/cypress/add-commands';
|
import "@testing-library/cypress/add-commands";
|
||||||
|
|
||||||
// ***********************************************
|
// ***********************************************
|
||||||
// This example commands.js shows you how to
|
// This example commands.js shows you how to
|
||||||
@ -26,85 +26,89 @@ import '@testing-library/cypress/add-commands';
|
|||||||
// -- This will overwrite an existing command --
|
// -- This will overwrite an existing command --
|
||||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
||||||
|
|
||||||
Cypress.Commands.add('setup', () => {
|
Cypress.Commands.add("setup", () => {
|
||||||
cy.exec('make e2e-reset-db e2e-setup', { timeout: 20000 });
|
cy.exec("make e2e-reset-db e2e-setup", { timeout: 20000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('login', (username, password) => {
|
Cypress.Commands.add("login", (username, password) => {
|
||||||
username ||= 'test';
|
username ||= "test";
|
||||||
password ||= 'admin123#';
|
password ||= "admin123#";
|
||||||
cy.request('POST', '/api/v1/fleet/login', { username, password })
|
cy.request("POST", "/api/v1/fleet/login", { username, password }).then(
|
||||||
.then((resp) => {
|
(resp) => {
|
||||||
window.localStorage.setItem('KOLIDE::auth_token', resp.body.token);
|
window.localStorage.setItem("KOLIDE::auth_token", resp.body.token);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('logout', () => {
|
Cypress.Commands.add("logout", () => {
|
||||||
cy.request({
|
cy.request({
|
||||||
url: '/api/v1/fleet/logout',
|
url: "/api/v1/fleet/logout",
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: {},
|
body: {},
|
||||||
auth: {
|
auth: {
|
||||||
bearer: window.localStorage.getItem('KOLIDE::auth_token'),
|
bearer: window.localStorage.getItem("KOLIDE::auth_token"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('setupSSO', (enable_idp_login = false) => {
|
Cypress.Commands.add("setupSSO", (enable_idp_login = false) => {
|
||||||
const body = {
|
const body = {
|
||||||
sso_settings: {
|
sso_settings: {
|
||||||
enable_sso: true,
|
enable_sso: true,
|
||||||
enable_sso_idp_login: enable_idp_login,
|
enable_sso_idp_login: enable_idp_login,
|
||||||
entity_id: 'https://localhost:8080',
|
entity_id: "https://localhost:8080",
|
||||||
idp_name: 'SimpleSAML',
|
idp_name: "SimpleSAML",
|
||||||
issuer_uri: 'http://localhost:8080/simplesaml/saml2/idp/SSOService.php',
|
issuer_uri: "http://localhost:8080/simplesaml/saml2/idp/SSOService.php",
|
||||||
metadata_url: 'http://localhost:9080/simplesaml/saml2/idp/metadata.php',
|
metadata_url: "http://localhost:9080/simplesaml/saml2/idp/metadata.php",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
cy.request({
|
cy.request({
|
||||||
url: '/api/v1/fleet/config',
|
url: "/api/v1/fleet/config",
|
||||||
method: 'PATCH',
|
method: "PATCH",
|
||||||
body,
|
body,
|
||||||
auth: {
|
auth: {
|
||||||
bearer: window.localStorage.getItem('KOLIDE::auth_token'),
|
bearer: window.localStorage.getItem("KOLIDE::auth_token"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Cypress.Commands.add('loginSSO', () => {
|
Cypress.Commands.add("loginSSO", () => {
|
||||||
// Note these requests set cookies that are required for the SSO flow to
|
// Note these requests set cookies that are required for the SSO flow to
|
||||||
// work properly. This is handled automatically by the browser.
|
// work properly. This is handled automatically by the browser.
|
||||||
cy.request({
|
cy.request({
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
url: 'http://localhost:9080/simplesaml/saml2/idp/SSOService.php?spentityid=https://localhost:8080',
|
url:
|
||||||
|
"http://localhost:9080/simplesaml/saml2/idp/SSOService.php?spentityid=https://localhost:8080",
|
||||||
followRedirect: false,
|
followRedirect: false,
|
||||||
}).then((firstResponse) => {
|
}).then((firstResponse) => {
|
||||||
const redirect = firstResponse.headers.location;
|
const redirect = firstResponse.headers.location;
|
||||||
|
|
||||||
cy.request({
|
cy.request({
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
url: redirect,
|
url: redirect,
|
||||||
followRedirect: false,
|
followRedirect: false,
|
||||||
}).then((secondResponse) => {
|
}).then((secondResponse) => {
|
||||||
const el = document.createElement('html');
|
const el = document.createElement("html");
|
||||||
el.innerHTML = secondResponse.body;
|
el.innerHTML = secondResponse.body;
|
||||||
const authState = el.getElementsByTagName('input').namedItem('AuthState').defaultValue;
|
const authState = el.getElementsByTagName("input").namedItem("AuthState")
|
||||||
|
.defaultValue;
|
||||||
|
|
||||||
cy.request({
|
cy.request({
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
url: redirect,
|
url: redirect,
|
||||||
body: `username=user1&password=user1pass&AuthState=${authState}`,
|
body: `username=user1&password=user1pass&AuthState=${authState}`,
|
||||||
form: true,
|
form: true,
|
||||||
followRedirect: false,
|
followRedirect: false,
|
||||||
}).then((finalResponse) => {
|
}).then((finalResponse) => {
|
||||||
el.innerHTML = finalResponse.body;
|
el.innerHTML = finalResponse.body;
|
||||||
const saml = el.getElementsByTagName('input').namedItem('SAMLResponse').defaultValue;
|
const saml = el.getElementsByTagName("input").namedItem("SAMLResponse")
|
||||||
|
.defaultValue;
|
||||||
|
|
||||||
// Load the callback URL with the response from the IdP
|
// Load the callback URL with the response from the IdP
|
||||||
cy.visit({
|
cy.visit({
|
||||||
url: '/api/v1/fleet/sso/callback',
|
url: "/api/v1/fleet/sso/callback",
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: {
|
body: {
|
||||||
SAMLResponse: saml,
|
SAMLResponse: saml,
|
||||||
},
|
},
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
|
|
||||||
// Import commands.js using ES2015 syntax:
|
// Import commands.js using ES2015 syntax:
|
||||||
import './commands';
|
import "./commands";
|
||||||
|
|
||||||
// Alternatively you can use CommonJS syntax:
|
// Alternatively you can use CommonJS syntax:
|
||||||
// require('./commands')
|
// require('./commands')
|
||||||
|
@ -4,7 +4,5 @@
|
|||||||
"lib": ["es5", "dom"],
|
"lib": ["es5", "dom"],
|
||||||
"types": ["cypress", "@testing-library/cypress", "node"]
|
"types": ["cypress", "@testing-library/cypress", "node"]
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["**/*.ts"]
|
||||||
"**/*.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
// __mocks__/fileMock.js
|
// __mocks__/fileMock.js
|
||||||
|
|
||||||
module.exports = 'test-file-stub';
|
module.exports = "test-file-stub";
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export default {
|
export default {
|
||||||
FAKE_PASSWORD: '********',
|
FAKE_PASSWORD: "********",
|
||||||
DEFAULT_SMTP_PORT: '587',
|
DEFAULT_SMTP_PORT: "587",
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import APP_SETTINGS from 'app_constants/APP_SETTINGS';
|
import APP_SETTINGS from "app_constants/APP_SETTINGS";
|
||||||
import HTTP_STATUS from 'app_constants/HTTP_STATUS';
|
import HTTP_STATUS from "app_constants/HTTP_STATUS";
|
||||||
import PATHS from 'router/paths';
|
import PATHS from "router/paths";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
APP_SETTINGS,
|
APP_SETTINGS,
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
import { connect } from 'react-redux';
|
import { connect } from "react-redux";
|
||||||
import { noop } from 'lodash';
|
import { noop } from "lodash";
|
||||||
import classnames from 'classnames';
|
import classnames from "classnames";
|
||||||
|
|
||||||
import { authToken } from 'utilities/local';
|
import { authToken } from "utilities/local";
|
||||||
import { fetchCurrentUser } from 'redux/nodes/auth/actions';
|
import { fetchCurrentUser } from "redux/nodes/auth/actions";
|
||||||
import { getConfig, getEnrollSecret } from 'redux/nodes/app/actions';
|
import { getConfig, getEnrollSecret } from "redux/nodes/app/actions";
|
||||||
import userInterface from 'interfaces/user';
|
import userInterface from "interfaces/user";
|
||||||
|
|
||||||
export class App extends Component {
|
export class App extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -20,47 +20,36 @@ export class App extends Component {
|
|||||||
dispatch: noop,
|
dispatch: noop,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount() {
|
||||||
const { dispatch, user } = this.props;
|
const { dispatch, user } = this.props;
|
||||||
|
|
||||||
if (!user && authToken()) {
|
if (!user && authToken()) {
|
||||||
dispatch(fetchCurrentUser())
|
dispatch(fetchCurrentUser()).catch(() => false);
|
||||||
.catch(() => false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
dispatch(getConfig())
|
dispatch(getConfig()).catch(() => false);
|
||||||
.catch(() => false);
|
dispatch(getEnrollSecret()).catch(() => false);
|
||||||
dispatch(getEnrollSecret())
|
|
||||||
.catch(() => false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
const { dispatch, user } = nextProps;
|
const { dispatch, user } = nextProps;
|
||||||
|
|
||||||
if (user && this.props.user !== user) {
|
if (user && this.props.user !== user) {
|
||||||
dispatch(getConfig())
|
dispatch(getConfig()).catch(() => false);
|
||||||
.catch(() => false);
|
dispatch(getEnrollSecret()).catch(() => false);
|
||||||
dispatch(getEnrollSecret())
|
|
||||||
.catch(() => false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { children } = this.props;
|
const { children } = this.props;
|
||||||
|
|
||||||
const wrapperStyles = classnames(
|
const wrapperStyles = classnames("wrapper");
|
||||||
'wrapper',
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return <div className={wrapperStyles}>{children}</div>;
|
||||||
<div className={wrapperStyles}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,74 +1,77 @@
|
|||||||
import { mount } from 'enzyme';
|
import { mount } from "enzyme";
|
||||||
|
|
||||||
import ConnectedApp from './App';
|
import ConnectedApp from "./App";
|
||||||
import * as authActions from '../../redux/nodes/auth/actions';
|
import * as authActions from "../../redux/nodes/auth/actions";
|
||||||
import helpers from '../../test/helpers';
|
import helpers from "../../test/helpers";
|
||||||
import local from '../../utilities/local';
|
import local from "../../utilities/local";
|
||||||
|
|
||||||
const {
|
const { connectedComponent, reduxMockStore } = helpers;
|
||||||
connectedComponent,
|
|
||||||
reduxMockStore,
|
|
||||||
} = helpers;
|
|
||||||
|
|
||||||
describe('App - component', () => {
|
describe("App - component", () => {
|
||||||
const store = { app: {}, auth: {}, notifications: {} };
|
const store = { app: {}, auth: {}, notifications: {} };
|
||||||
const mockStore = reduxMockStore(store);
|
const mockStore = reduxMockStore(store);
|
||||||
const component = mount(
|
const component = mount(connectedComponent(ConnectedApp, { mockStore }));
|
||||||
connectedComponent(ConnectedApp, { mockStore }),
|
|
||||||
);
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
local.setItem('auth_token', null);
|
local.setItem("auth_token", null);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders', () => {
|
it("renders", () => {
|
||||||
expect(component).toBeTruthy();
|
expect(component).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads the current user if there is an auth token but no user', () => {
|
it("loads the current user if there is an auth token but no user", () => {
|
||||||
local.setItem('auth_token', 'ABC123');
|
local.setItem("auth_token", "ABC123");
|
||||||
|
|
||||||
const spy = jest.spyOn(authActions, 'fetchCurrentUser').mockImplementation(() => {
|
const spy = jest
|
||||||
return (dispatch) => {
|
.spyOn(authActions, "fetchCurrentUser")
|
||||||
dispatch({ type: 'LOAD_USER_ACTION' });
|
.mockImplementation(() => {
|
||||||
return Promise.resolve();
|
return (dispatch) => {
|
||||||
};
|
dispatch({ type: "LOAD_USER_ACTION" });
|
||||||
});
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
});
|
||||||
const application = connectedComponent(ConnectedApp, { mockStore });
|
const application = connectedComponent(ConnectedApp, { mockStore });
|
||||||
|
|
||||||
mount(application);
|
mount(application);
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not load the current user if is it already loaded', () => {
|
it("does not load the current user if is it already loaded", () => {
|
||||||
local.setItem('auth_token', 'ABC123');
|
local.setItem("auth_token", "ABC123");
|
||||||
|
|
||||||
const spy = jest.spyOn(authActions, 'fetchCurrentUser').mockImplementation(() => {
|
const spy = jest
|
||||||
return { type: 'LOAD_USER_ACTION' };
|
.spyOn(authActions, "fetchCurrentUser")
|
||||||
});
|
.mockImplementation(() => {
|
||||||
|
return { type: "LOAD_USER_ACTION" };
|
||||||
|
});
|
||||||
const storeWithUser = {
|
const storeWithUser = {
|
||||||
app: {},
|
app: {},
|
||||||
auth: {
|
auth: {
|
||||||
user: {
|
user: {
|
||||||
id: 1,
|
id: 1,
|
||||||
email: 'hi@thegnar.co',
|
email: "hi@thegnar.co",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
notifications: {},
|
notifications: {},
|
||||||
};
|
};
|
||||||
const mockStoreWithUser = reduxMockStore(storeWithUser);
|
const mockStoreWithUser = reduxMockStore(storeWithUser);
|
||||||
const application = connectedComponent(ConnectedApp, { mockStore: mockStoreWithUser });
|
const application = connectedComponent(ConnectedApp, {
|
||||||
|
mockStore: mockStoreWithUser,
|
||||||
|
});
|
||||||
|
|
||||||
mount(application);
|
mount(application);
|
||||||
expect(spy).not.toHaveBeenCalled();
|
expect(spy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not load the current user if there is no auth token', () => {
|
it("does not load the current user if there is no auth token", () => {
|
||||||
local.clear();
|
local.clear();
|
||||||
|
|
||||||
const spy = jest.spyOn(authActions, 'fetchCurrentUser').mockImplementation(() => {
|
const spy = jest
|
||||||
throw new Error('should not have been called');
|
.spyOn(authActions, "fetchCurrentUser")
|
||||||
});
|
.mockImplementation(() => {
|
||||||
|
throw new Error("should not have been called");
|
||||||
|
});
|
||||||
const application = connectedComponent(ConnectedApp, { mockStore });
|
const application = connectedComponent(ConnectedApp, { mockStore });
|
||||||
|
|
||||||
mount(application);
|
mount(application);
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './App';
|
export { default } from "./App";
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
import { connect } from 'react-redux';
|
import { connect } from "react-redux";
|
||||||
import { push } from 'react-router-redux';
|
import { push } from "react-router-redux";
|
||||||
|
|
||||||
import paths from '../../router/paths';
|
import paths from "../../router/paths";
|
||||||
import userInterface from '../../interfaces/user';
|
import userInterface from "../../interfaces/user";
|
||||||
|
|
||||||
export class AuthenticatedAdminRoutes extends Component {
|
export class AuthenticatedAdminRoutes extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -13,8 +13,11 @@ export class AuthenticatedAdminRoutes extends Component {
|
|||||||
user: userInterface,
|
user: userInterface,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount() {
|
||||||
const { dispatch, user: { admin } } = this.props;
|
const {
|
||||||
|
dispatch,
|
||||||
|
user: { admin },
|
||||||
|
} = this.props;
|
||||||
const { HOME } = paths;
|
const { HOME } = paths;
|
||||||
|
|
||||||
if (!admin) {
|
if (!admin) {
|
||||||
@ -24,18 +27,14 @@ export class AuthenticatedAdminRoutes extends Component {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { children, user } = this.props;
|
const { children, user } = this.props;
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <>{children}</>;
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,35 +1,31 @@
|
|||||||
import { mount } from 'enzyme';
|
import { mount } from "enzyme";
|
||||||
|
|
||||||
import ConnectedAdminRoutes from './AuthenticatedAdminRoutes';
|
import ConnectedAdminRoutes from "./AuthenticatedAdminRoutes";
|
||||||
import { connectedComponent, reduxMockStore } from '../../test/helpers';
|
import { connectedComponent, reduxMockStore } from "../../test/helpers";
|
||||||
|
|
||||||
describe('AuthenticatedAdminRoutes - layout', () => {
|
describe("AuthenticatedAdminRoutes - layout", () => {
|
||||||
const redirectToHomeAction = {
|
const redirectToHomeAction = {
|
||||||
type: '@@router/CALL_HISTORY_METHOD',
|
type: "@@router/CALL_HISTORY_METHOD",
|
||||||
payload: {
|
payload: {
|
||||||
method: 'push',
|
method: "push",
|
||||||
args: ['/'],
|
args: ["/"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
it('redirects to the homepage if the user is not an admin', () => {
|
it("redirects to the homepage if the user is not an admin", () => {
|
||||||
const user = { id: 1, admin: false };
|
const user = { id: 1, admin: false };
|
||||||
const storeWithoutAdminUser = { auth: { user } };
|
const storeWithoutAdminUser = { auth: { user } };
|
||||||
const mockStore = reduxMockStore(storeWithoutAdminUser);
|
const mockStore = reduxMockStore(storeWithoutAdminUser);
|
||||||
mount(
|
mount(connectedComponent(ConnectedAdminRoutes, { mockStore }));
|
||||||
connectedComponent(ConnectedAdminRoutes, { mockStore }),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(mockStore.getActions()).toContainEqual(redirectToHomeAction);
|
expect(mockStore.getActions()).toContainEqual(redirectToHomeAction);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not redirect if the user is an admin', () => {
|
it("does not redirect if the user is an admin", () => {
|
||||||
const user = { id: 1, admin: true };
|
const user = { id: 1, admin: true };
|
||||||
const storeWithAdminUser = { auth: { user } };
|
const storeWithAdminUser = { auth: { user } };
|
||||||
const mockStore = reduxMockStore(storeWithAdminUser);
|
const mockStore = reduxMockStore(storeWithAdminUser);
|
||||||
mount(
|
mount(connectedComponent(ConnectedAdminRoutes, { mockStore }));
|
||||||
connectedComponent(ConnectedAdminRoutes, { mockStore }),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(mockStore.getActions()).not.toContainEqual(redirectToHomeAction);
|
expect(mockStore.getActions()).not.toContainEqual(redirectToHomeAction);
|
||||||
});
|
});
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './AuthenticatedAdminRoutes';
|
export { default } from "./AuthenticatedAdminRoutes";
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
import { connect } from 'react-redux';
|
import { connect } from "react-redux";
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from "lodash";
|
||||||
import { push } from 'react-router-redux';
|
import { push } from "react-router-redux";
|
||||||
|
|
||||||
import paths from 'router/paths';
|
import paths from "router/paths";
|
||||||
import redirectLocationInterface from 'interfaces/redirect_location';
|
import redirectLocationInterface from "interfaces/redirect_location";
|
||||||
import { setRedirectLocation } from 'redux/nodes/redirectLocation/actions';
|
import { setRedirectLocation } from "redux/nodes/redirectLocation/actions";
|
||||||
import userInterface from 'interfaces/user';
|
import userInterface from "interfaces/user";
|
||||||
|
|
||||||
export class AuthenticatedRoutes extends Component {
|
export class AuthenticatedRoutes extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -18,7 +18,7 @@ export class AuthenticatedRoutes extends Component {
|
|||||||
user: userInterface,
|
user: userInterface,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount() {
|
||||||
const { loading, user } = this.props;
|
const { loading, user } = this.props;
|
||||||
const { redirectToLogin, redirectToPasswordReset } = this;
|
const { redirectToLogin, redirectToPasswordReset } = this;
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ export class AuthenticatedRoutes extends Component {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
if (isEqual(this.props, nextProps)) return false;
|
if (isEqual(this.props, nextProps)) return false;
|
||||||
|
|
||||||
const { loading, user } = nextProps;
|
const { loading, user } = nextProps;
|
||||||
@ -56,27 +56,23 @@ export class AuthenticatedRoutes extends Component {
|
|||||||
|
|
||||||
dispatch(setRedirectLocation(locationBeforeTransitions));
|
dispatch(setRedirectLocation(locationBeforeTransitions));
|
||||||
return dispatch(push(LOGIN));
|
return dispatch(push(LOGIN));
|
||||||
}
|
};
|
||||||
|
|
||||||
redirectToPasswordReset = () => {
|
redirectToPasswordReset = () => {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
const { RESET_PASSWORD } = paths;
|
const { RESET_PASSWORD } = paths;
|
||||||
|
|
||||||
return dispatch(push(RESET_PASSWORD));
|
return dispatch(push(RESET_PASSWORD));
|
||||||
}
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { children, user } = this.props;
|
const { children, user } = this.props;
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return <div>{children}</div>;
|
||||||
<div>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,32 +1,32 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { mount } from 'enzyme';
|
import { mount } from "enzyme";
|
||||||
import { Provider } from 'react-redux';
|
import { Provider } from "react-redux";
|
||||||
|
|
||||||
import AuthenticatedRoutes from './index';
|
import AuthenticatedRoutes from "./index";
|
||||||
import helpers from '../../test/helpers';
|
import helpers from "../../test/helpers";
|
||||||
|
|
||||||
describe('AuthenticatedRoutes - component', () => {
|
describe("AuthenticatedRoutes - component", () => {
|
||||||
const redirectToLoginAction = {
|
const redirectToLoginAction = {
|
||||||
type: '@@router/CALL_HISTORY_METHOD',
|
type: "@@router/CALL_HISTORY_METHOD",
|
||||||
payload: {
|
payload: {
|
||||||
method: 'push',
|
method: "push",
|
||||||
args: ['/login'],
|
args: ["/login"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const redirectToPasswordResetAction = {
|
const redirectToPasswordResetAction = {
|
||||||
type: '@@router/CALL_HISTORY_METHOD',
|
type: "@@router/CALL_HISTORY_METHOD",
|
||||||
payload: {
|
payload: {
|
||||||
method: 'push',
|
method: "push",
|
||||||
args: ['/login/reset'],
|
args: ["/login/reset"],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const renderedText = 'This text was rendered';
|
const renderedText = "This text was rendered";
|
||||||
const storeWithUser = {
|
const storeWithUser = {
|
||||||
auth: {
|
auth: {
|
||||||
loading: false,
|
loading: false,
|
||||||
user: {
|
user: {
|
||||||
id: 1,
|
id: 1,
|
||||||
email: 'hi@thegnar.co',
|
email: "hi@thegnar.co",
|
||||||
force_password_reset: false,
|
force_password_reset: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -39,7 +39,7 @@ describe('AuthenticatedRoutes - component', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
user: {
|
user: {
|
||||||
id: 1,
|
id: 1,
|
||||||
email: 'hi@thegnar.co',
|
email: "hi@thegnar.co",
|
||||||
force_password_reset: true,
|
force_password_reset: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -66,7 +66,7 @@ describe('AuthenticatedRoutes - component', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
it('renders if there is a user in state', () => {
|
it("renders if there is a user in state", () => {
|
||||||
const { reduxMockStore } = helpers;
|
const { reduxMockStore } = helpers;
|
||||||
const mockStore = reduxMockStore(storeWithUser);
|
const mockStore = reduxMockStore(storeWithUser);
|
||||||
const component = mount(
|
const component = mount(
|
||||||
@ -74,13 +74,13 @@ describe('AuthenticatedRoutes - component', () => {
|
|||||||
<AuthenticatedRoutes>
|
<AuthenticatedRoutes>
|
||||||
<div>{renderedText}</div>
|
<div>{renderedText}</div>
|
||||||
</AuthenticatedRoutes>
|
</AuthenticatedRoutes>
|
||||||
</Provider>,
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(component.text()).toEqual(renderedText);
|
expect(component.text()).toEqual(renderedText);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('redirects to reset password is force_password_reset is true', () => {
|
it("redirects to reset password is force_password_reset is true", () => {
|
||||||
const { reduxMockStore } = helpers;
|
const { reduxMockStore } = helpers;
|
||||||
const mockStore = reduxMockStore(storeWithUserRequiringPwReset);
|
const mockStore = reduxMockStore(storeWithUserRequiringPwReset);
|
||||||
mount(
|
mount(
|
||||||
@ -88,13 +88,15 @@ describe('AuthenticatedRoutes - component', () => {
|
|||||||
<AuthenticatedRoutes>
|
<AuthenticatedRoutes>
|
||||||
<div>{renderedText}</div>
|
<div>{renderedText}</div>
|
||||||
</AuthenticatedRoutes>
|
</AuthenticatedRoutes>
|
||||||
</Provider>,
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockStore.getActions()).toContainEqual(redirectToPasswordResetAction);
|
expect(mockStore.getActions()).toContainEqual(
|
||||||
|
redirectToPasswordResetAction
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('redirects to login without a user', () => {
|
it("redirects to login without a user", () => {
|
||||||
const { reduxMockStore } = helpers;
|
const { reduxMockStore } = helpers;
|
||||||
const mockStore = reduxMockStore(storeWithoutUser);
|
const mockStore = reduxMockStore(storeWithoutUser);
|
||||||
const component = mount(
|
const component = mount(
|
||||||
@ -102,14 +104,14 @@ describe('AuthenticatedRoutes - component', () => {
|
|||||||
<AuthenticatedRoutes>
|
<AuthenticatedRoutes>
|
||||||
<div>{renderedText}</div>
|
<div>{renderedText}</div>
|
||||||
</AuthenticatedRoutes>
|
</AuthenticatedRoutes>
|
||||||
</Provider>,
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockStore.getActions()).toContainEqual(redirectToLoginAction);
|
expect(mockStore.getActions()).toContainEqual(redirectToLoginAction);
|
||||||
expect(component.html()).toBeFalsy();
|
expect(component.html()).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not redirect to login if the user is loading', () => {
|
it("does not redirect to login if the user is loading", () => {
|
||||||
const { reduxMockStore } = helpers;
|
const { reduxMockStore } = helpers;
|
||||||
const mockStore = reduxMockStore(storeLoadingUser);
|
const mockStore = reduxMockStore(storeLoadingUser);
|
||||||
const component = mount(
|
const component = mount(
|
||||||
@ -117,7 +119,7 @@ describe('AuthenticatedRoutes - component', () => {
|
|||||||
<AuthenticatedRoutes>
|
<AuthenticatedRoutes>
|
||||||
<div>{renderedText}</div>
|
<div>{renderedText}</div>
|
||||||
</AuthenticatedRoutes>
|
</AuthenticatedRoutes>
|
||||||
</Provider>,
|
</Provider>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockStore.getActions()).not.toContainEqual(redirectToLoginAction);
|
expect(mockStore.getActions()).not.toContainEqual(redirectToLoginAction);
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './AuthenticatedRoutes';
|
export { default } from "./AuthenticatedRoutes";
|
||||||
|
@ -1,16 +1,16 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
import fleetLogoText from '../../../assets/images/fleet-logo-text-white.svg';
|
import fleetLogoText from "../../../assets/images/fleet-logo-text-white.svg";
|
||||||
|
|
||||||
const baseClass = 'auth-form-wrapper';
|
const baseClass = "auth-form-wrapper";
|
||||||
|
|
||||||
class AuthenticationFormWrapper extends Component {
|
class AuthenticationFormWrapper extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
children: PropTypes.node,
|
children: PropTypes.node,
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { children } = this.props;
|
const { children } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './AuthenticationFormWrapper';
|
export { default } from "./AuthenticationFormWrapper";
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import classnames from 'classnames';
|
import classnames from "classnames";
|
||||||
|
|
||||||
interface IAvatarUserInterface {
|
interface IAvatarUserInterface {
|
||||||
gravatarURL: string;
|
gravatarURL: string;
|
||||||
@ -11,23 +11,19 @@ interface IAvatarInterface {
|
|||||||
user: IAvatarUserInterface;
|
user: IAvatarUserInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseClass = 'avatar';
|
const baseClass = "avatar";
|
||||||
|
|
||||||
class Avatar extends React.Component<IAvatarInterface, null> {
|
class Avatar extends React.Component<IAvatarInterface, null> {
|
||||||
render(): JSX.Element {
|
render(): JSX.Element {
|
||||||
const { className, size, user } = this.props;
|
const { className, size, user } = this.props;
|
||||||
const isSmall = size !== undefined && size.toLowerCase() === 'small';
|
const isSmall = size !== undefined && size.toLowerCase() === "small";
|
||||||
const avatarClasses = classnames(baseClass, className, {
|
const avatarClasses = classnames(baseClass, className, {
|
||||||
[`${baseClass}--${size}`]: isSmall,
|
[`${baseClass}--${size}`]: isSmall,
|
||||||
});
|
});
|
||||||
const { gravatarURL } = user;
|
const { gravatarURL } = user;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<img
|
<img alt="User Avatar" className={avatarClasses} src={gravatarURL} />
|
||||||
alt="User Avatar"
|
|
||||||
className={avatarClasses}
|
|
||||||
src={gravatarURL}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
.avatar {
|
.avatar {
|
||||||
background: $white url('../assets/images/avatar-default.png') center 100% no-repeat;
|
background: $white url("../assets/images/avatar-default.png") center 100%
|
||||||
|
no-repeat;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './Avatar.tsx';
|
export { default } from "./Avatar.tsx";
|
||||||
|
@ -1,31 +1,34 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import { noop } from 'lodash';
|
import { noop } from "lodash";
|
||||||
|
|
||||||
import { handleClickOutside } from './helpers';
|
import { handleClickOutside } from "./helpers";
|
||||||
|
|
||||||
export default (WrappedComponent, { onOutsideClick = noop, getDOMNode = noop }) => {
|
export default (
|
||||||
|
WrappedComponent,
|
||||||
|
{ onOutsideClick = noop, getDOMNode = noop }
|
||||||
|
) => {
|
||||||
class ClickOutside extends Component {
|
class ClickOutside extends Component {
|
||||||
componentDidMount () {
|
componentDidMount() {
|
||||||
const { componentInstance } = this;
|
const { componentInstance } = this;
|
||||||
const clickHandler = onOutsideClick(componentInstance);
|
const clickHandler = onOutsideClick(componentInstance);
|
||||||
const componentNode = getDOMNode(componentInstance);
|
const componentNode = getDOMNode(componentInstance);
|
||||||
|
|
||||||
this.handleAction = handleClickOutside(clickHandler, componentNode);
|
this.handleAction = handleClickOutside(clickHandler, componentNode);
|
||||||
|
|
||||||
global.document.addEventListener('mousedown', this.handleAction);
|
global.document.addEventListener("mousedown", this.handleAction);
|
||||||
global.document.addEventListener('touchStart', this.handleAction);
|
global.document.addEventListener("touchStart", this.handleAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount() {
|
||||||
global.document.removeEventListener('mousedown', this.handleAction);
|
global.document.removeEventListener("mousedown", this.handleAction);
|
||||||
global.document.removeEventListener('touchStart', this.handleAction);
|
global.document.removeEventListener("touchStart", this.handleAction);
|
||||||
}
|
}
|
||||||
|
|
||||||
setInstance = (instance) => {
|
setInstance = (instance) => {
|
||||||
this.componentInstance = instance;
|
this.componentInstance = instance;
|
||||||
}
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { setInstance } = this;
|
const { setInstance } = this;
|
||||||
return <WrappedComponent {...this.props} ref={setInstance} />;
|
return <WrappedComponent {...this.props} ref={setInstance} />;
|
||||||
}
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './ClickOutside';
|
export { default } from "./ClickOutside";
|
||||||
|
@ -1,9 +1,18 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
const ClickableTableRow = ({ children, className, onClick, onDoubleClick }) => {
|
const ClickableTableRow = ({ children, className, onClick, onDoubleClick }) => {
|
||||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||||
return <tr className={className} onClick={onClick} onDoubleClick={onDoubleClick} tabIndex={-1}>{children}</tr>;
|
return (
|
||||||
|
<tr
|
||||||
|
className={className}
|
||||||
|
onClick={onClick}
|
||||||
|
onDoubleClick={onDoubleClick}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
/* eslint-enable jsx-a11y/no-static-element-interactions */
|
/* eslint-enable jsx-a11y/no-static-element-interactions */
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { mount } from 'enzyme';
|
import { mount } from "enzyme";
|
||||||
|
|
||||||
import ClickableTableRow from './index';
|
import ClickableTableRow from "./index";
|
||||||
|
|
||||||
const clickSpy = jest.fn();
|
const clickSpy = jest.fn();
|
||||||
const dblClickSpy = jest.fn();
|
const dblClickSpy = jest.fn();
|
||||||
@ -11,16 +11,16 @@ const props = {
|
|||||||
onDoubleClick: dblClickSpy,
|
onDoubleClick: dblClickSpy,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('ClickableTableRow - component', () => {
|
describe("ClickableTableRow - component", () => {
|
||||||
it('calls onDblClick when row is double clicked', () => {
|
it("calls onDblClick when row is double clicked", () => {
|
||||||
const queryRow = mount(<ClickableTableRow {...props} />);
|
const queryRow = mount(<ClickableTableRow {...props} />);
|
||||||
queryRow.find('tr').simulate('doubleclick');
|
queryRow.find("tr").simulate("doubleclick");
|
||||||
expect(dblClickSpy).toHaveBeenCalled();
|
expect(dblClickSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls onSelect when row is clicked', () => {
|
it("calls onSelect when row is clicked", () => {
|
||||||
const queryRow = mount(<ClickableTableRow {...props} />);
|
const queryRow = mount(<ClickableTableRow {...props} />);
|
||||||
queryRow.find('tr').simulate('click');
|
queryRow.find("tr").simulate("click");
|
||||||
expect(clickSpy).toHaveBeenCalled();
|
expect(clickSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
import { connect } from 'react-redux';
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
import helpers from 'components/EmailTokenRedirect/helpers';
|
import helpers from "components/EmailTokenRedirect/helpers";
|
||||||
import userInterface from 'interfaces/user';
|
import userInterface from "interfaces/user";
|
||||||
|
|
||||||
export class EmailTokenRedirect extends Component {
|
export class EmailTokenRedirect extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -12,13 +12,13 @@ export class EmailTokenRedirect extends Component {
|
|||||||
user: userInterface,
|
user: userInterface,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount() {
|
||||||
const { dispatch, token, user } = this.props;
|
const { dispatch, token, user } = this.props;
|
||||||
|
|
||||||
return helpers.confirmEmailChange(dispatch, token, user);
|
return helpers.confirmEmailChange(dispatch, token, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
const { dispatch, token: newToken, user: newUser } = nextProps;
|
const { dispatch, token: newToken, user: newUser } = nextProps;
|
||||||
const { token: oldToken, user: oldUser } = this.props;
|
const { token: oldToken, user: oldUser } = this.props;
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ export class EmailTokenRedirect extends Component {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { mount } from 'enzyme';
|
import { mount } from "enzyme";
|
||||||
|
|
||||||
import { connectedComponent, reduxMockStore } from 'test/helpers';
|
import { connectedComponent, reduxMockStore } from "test/helpers";
|
||||||
import ConnectedEmailTokenRedirect, { EmailTokenRedirect } from 'components/EmailTokenRedirect/EmailTokenRedirect';
|
import ConnectedEmailTokenRedirect, {
|
||||||
import Kolide from 'kolide';
|
EmailTokenRedirect,
|
||||||
import { userStub } from 'test/stubs';
|
} from "components/EmailTokenRedirect/EmailTokenRedirect";
|
||||||
|
import Kolide from "kolide";
|
||||||
|
import { userStub } from "test/stubs";
|
||||||
|
|
||||||
describe('EmailTokenRedirect - component', () => {
|
describe("EmailTokenRedirect - component", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(Kolide.users, 'confirmEmailChange')
|
jest
|
||||||
.mockImplementation(() => Promise.resolve({ ...userStub, email: 'new@email.com' }));
|
.spyOn(Kolide.users, "confirmEmailChange")
|
||||||
|
.mockImplementation(() =>
|
||||||
|
Promise.resolve({ ...userStub, email: "new@email.com" })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const authStore = {
|
const authStore = {
|
||||||
@ -17,39 +22,46 @@ describe('EmailTokenRedirect - component', () => {
|
|||||||
user: userStub,
|
user: userStub,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const token = 'KFBR392';
|
const token = "KFBR392";
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
params: {
|
params: {
|
||||||
token,
|
token,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('componentWillMount', () => {
|
describe("componentWillMount", () => {
|
||||||
it('calls the API when a token and user are present', () => {
|
it("calls the API when a token and user are present", () => {
|
||||||
const mockStore = reduxMockStore(authStore);
|
const mockStore = reduxMockStore(authStore);
|
||||||
|
|
||||||
mount(connectedComponent(ConnectedEmailTokenRedirect, {
|
mount(
|
||||||
mockStore,
|
connectedComponent(ConnectedEmailTokenRedirect, {
|
||||||
props: defaultProps,
|
mockStore,
|
||||||
}));
|
props: defaultProps,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
expect(Kolide.users.confirmEmailChange).toHaveBeenCalledWith(userStub, token);
|
expect(Kolide.users.confirmEmailChange).toHaveBeenCalledWith(
|
||||||
|
userStub,
|
||||||
|
token
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not call the API when only a token is present', () => {
|
it("does not call the API when only a token is present", () => {
|
||||||
const mockStore = reduxMockStore({ auth: {} });
|
const mockStore = reduxMockStore({ auth: {} });
|
||||||
|
|
||||||
mount(connectedComponent(ConnectedEmailTokenRedirect, {
|
mount(
|
||||||
mockStore,
|
connectedComponent(ConnectedEmailTokenRedirect, {
|
||||||
props: defaultProps,
|
mockStore,
|
||||||
}));
|
props: defaultProps,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
expect(Kolide.users.confirmEmailChange).not.toHaveBeenCalled();
|
expect(Kolide.users.confirmEmailChange).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('componentWillReceiveProps', () => {
|
describe("componentWillReceiveProps", () => {
|
||||||
it('calls the API when a user is received', () => {
|
it("calls the API when a user is received", () => {
|
||||||
const mockStore = reduxMockStore();
|
const mockStore = reduxMockStore();
|
||||||
const props = { dispatch: mockStore.dispatch, token };
|
const props = { dispatch: mockStore.dispatch, token };
|
||||||
const Component = mount(<EmailTokenRedirect {...props} />);
|
const Component = mount(<EmailTokenRedirect {...props} />);
|
||||||
@ -58,7 +70,10 @@ describe('EmailTokenRedirect - component', () => {
|
|||||||
|
|
||||||
Component.setProps({ user: userStub });
|
Component.setProps({ user: userStub });
|
||||||
|
|
||||||
expect(Kolide.users.confirmEmailChange).toHaveBeenCalledWith(userStub, token);
|
expect(Kolide.users.confirmEmailChange).toHaveBeenCalledWith(
|
||||||
|
userStub,
|
||||||
|
token
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import PATHS from 'router/paths';
|
import PATHS from "router/paths";
|
||||||
import { push } from 'react-router-redux';
|
import { push } from "react-router-redux";
|
||||||
import { renderFlash } from 'redux/nodes/notifications/actions';
|
import { renderFlash } from "redux/nodes/notifications/actions";
|
||||||
import userActions from 'redux/nodes/entities/users/actions';
|
import userActions from "redux/nodes/entities/users/actions";
|
||||||
|
|
||||||
const confirmEmailChange = (dispatch, token, user) => {
|
const confirmEmailChange = (dispatch, token, user) => {
|
||||||
if (user && token) {
|
if (user && token) {
|
||||||
return dispatch(userActions.confirmEmailChange(user, token))
|
return dispatch(userActions.confirmEmailChange(user, token))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
dispatch(push(PATHS.USER_SETTINGS));
|
dispatch(push(PATHS.USER_SETTINGS));
|
||||||
dispatch(renderFlash('success', 'Email updated successfully!'));
|
dispatch(renderFlash("success", "Email updated successfully!"));
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
})
|
})
|
||||||
|
@ -1,65 +1,68 @@
|
|||||||
import { reduxMockStore } from 'test/helpers';
|
import { reduxMockStore } from "test/helpers";
|
||||||
|
|
||||||
import helpers from 'components/EmailTokenRedirect/helpers';
|
import helpers from "components/EmailTokenRedirect/helpers";
|
||||||
import Kolide from 'kolide';
|
import Kolide from "kolide";
|
||||||
import { userStub } from 'test/stubs';
|
import { userStub } from "test/stubs";
|
||||||
|
|
||||||
describe('EmailTokenRedirect - helpers', () => {
|
describe("EmailTokenRedirect - helpers", () => {
|
||||||
describe('#confirmEmailChage', () => {
|
describe("#confirmEmailChage", () => {
|
||||||
const { confirmEmailChange } = helpers;
|
const { confirmEmailChange } = helpers;
|
||||||
const token = 'KFBR392';
|
const token = "KFBR392";
|
||||||
const authStore = {
|
const authStore = {
|
||||||
auth: {
|
auth: {
|
||||||
user: userStub,
|
user: userStub,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('successfully dispatching the confirmEmailChange action', () => {
|
describe("successfully dispatching the confirmEmailChange action", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.spyOn(Kolide.users, 'confirmEmailChange')
|
jest
|
||||||
.mockImplementation(() => Promise.resolve({ ...userStub, email: 'new@email.com' }));
|
.spyOn(Kolide.users, "confirmEmailChange")
|
||||||
|
.mockImplementation(() =>
|
||||||
|
Promise.resolve({ ...userStub, email: "new@email.com" })
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('pushes the user to the settings page', () => {
|
it("pushes the user to the settings page", () => {
|
||||||
const mockStore = reduxMockStore(authStore);
|
const mockStore = reduxMockStore(authStore);
|
||||||
const { dispatch } = mockStore;
|
const { dispatch } = mockStore;
|
||||||
|
|
||||||
return confirmEmailChange(dispatch, userStub, token)
|
return confirmEmailChange(dispatch, userStub, token).then(() => {
|
||||||
.then(() => {
|
const dispatchedActions = mockStore.getActions();
|
||||||
const dispatchedActions = mockStore.getActions();
|
|
||||||
|
|
||||||
expect(dispatchedActions).toContainEqual({
|
expect(dispatchedActions).toContainEqual({
|
||||||
type: '@@router/CALL_HISTORY_METHOD',
|
type: "@@router/CALL_HISTORY_METHOD",
|
||||||
payload: {
|
payload: {
|
||||||
method: 'push',
|
method: "push",
|
||||||
args: ['/profile'],
|
args: ["/profile"],
|
||||||
},
|
},
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('unsuccessfully dispatching the confirmEmailChange action', () => {
|
describe("unsuccessfully dispatching the confirmEmailChange action", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
const errors = [
|
const errors = [
|
||||||
{
|
{
|
||||||
name: 'base',
|
name: "base",
|
||||||
reason: 'Unable to confirm your email address',
|
reason: "Unable to confirm your email address",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const errorResponse = {
|
const errorResponse = {
|
||||||
status: 422,
|
status: 422,
|
||||||
message: {
|
message: {
|
||||||
message: 'Unable to confirm email address',
|
message: "Unable to confirm email address",
|
||||||
errors,
|
errors,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
jest.spyOn(Kolide.users, 'confirmEmailChange')
|
jest
|
||||||
|
.spyOn(Kolide.users, "confirmEmailChange")
|
||||||
.mockImplementation(() => Promise.reject(errorResponse));
|
.mockImplementation(() => Promise.reject(errorResponse));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('pushes the user to the login page', () => {
|
it("pushes the user to the login page", () => {
|
||||||
const mockStore = reduxMockStore(authStore);
|
const mockStore = reduxMockStore(authStore);
|
||||||
const { dispatch } = mockStore;
|
const { dispatch } = mockStore;
|
||||||
|
|
||||||
@ -69,18 +72,18 @@ describe('EmailTokenRedirect - helpers', () => {
|
|||||||
const dispatchedActions = mockStore.getActions();
|
const dispatchedActions = mockStore.getActions();
|
||||||
|
|
||||||
expect(dispatchedActions).toContainEqual({
|
expect(dispatchedActions).toContainEqual({
|
||||||
type: '@@router/CALL_HISTORY_METHOD',
|
type: "@@router/CALL_HISTORY_METHOD",
|
||||||
payload: {
|
payload: {
|
||||||
method: 'push',
|
method: "push",
|
||||||
args: ['/login'],
|
args: ["/login"],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the user or token are not present', () => {
|
describe("when the user or token are not present", () => {
|
||||||
it('does not dispatch any actions when the user is not present', (done) => {
|
it("does not dispatch any actions when the user is not present", (done) => {
|
||||||
const mockStore = reduxMockStore(authStore);
|
const mockStore = reduxMockStore(authStore);
|
||||||
const { dispatch } = mockStore;
|
const { dispatch } = mockStore;
|
||||||
|
|
||||||
@ -95,7 +98,7 @@ describe('EmailTokenRedirect - helpers', () => {
|
|||||||
.catch(done);
|
.catch(done);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not dispatch any actions when the token is not present', (done) => {
|
it("does not dispatch any actions when the token is not present", (done) => {
|
||||||
const mockStore = reduxMockStore(authStore);
|
const mockStore = reduxMockStore(authStore);
|
||||||
const { dispatch } = mockStore;
|
const { dispatch } = mockStore;
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './EmailTokenRedirect';
|
export { default } from "./EmailTokenRedirect";
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
import { connect } from 'react-redux';
|
import { connect } from "react-redux";
|
||||||
import { noop } from 'lodash';
|
import { noop } from "lodash";
|
||||||
import { push } from 'react-router-redux';
|
import { push } from "react-router-redux";
|
||||||
|
|
||||||
import paths from 'router/paths';
|
import paths from "router/paths";
|
||||||
import userInterface from 'interfaces/user';
|
import userInterface from "interfaces/user";
|
||||||
|
|
||||||
export default (WrappedComponent) => {
|
export default (WrappedComponent) => {
|
||||||
class EnsureUnauthenticated extends Component {
|
class EnsureUnauthenticated extends Component {
|
||||||
@ -19,7 +19,7 @@ export default (WrappedComponent) => {
|
|||||||
dispatch: noop,
|
dispatch: noop,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount() {
|
||||||
const { currentUser, dispatch } = this.props;
|
const { currentUser, dispatch } = this.props;
|
||||||
const { HOME } = paths;
|
const { HOME } = paths;
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ export default (WrappedComponent) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps (nextProps) {
|
componentWillReceiveProps(nextProps) {
|
||||||
const { currentUser, dispatch } = nextProps;
|
const { currentUser, dispatch } = nextProps;
|
||||||
const { HOME } = paths;
|
const { HOME } = paths;
|
||||||
|
|
||||||
@ -41,7 +41,7 @@ export default (WrappedComponent) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { currentUser, isLoadingUser } = this.props;
|
const { currentUser, isLoadingUser } = this.props;
|
||||||
|
|
||||||
if (isLoadingUser || currentUser) {
|
if (isLoadingUser || currentUser) {
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './EnsureUnauthenticated';
|
export { default } from "./EnsureUnauthenticated";
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import ReactTooltip from 'react-tooltip';
|
import ReactTooltip from "react-tooltip";
|
||||||
|
|
||||||
interface IIconToolTipProps {
|
interface IIconToolTipProps {
|
||||||
text: string;
|
text: string;
|
||||||
@ -12,13 +12,26 @@ const IconToolTip = (props: IIconToolTipProps): JSX.Element => {
|
|||||||
return (
|
return (
|
||||||
<div className="icon-tooltip">
|
<div className="icon-tooltip">
|
||||||
<span data-tip={text} data-html={isHtml}>
|
<span data-tip={text} data-html={isHtml}>
|
||||||
<svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="17"
|
||||||
|
viewBox="0 0 16 17"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
<circle cx="8" cy="8.59961" r="8" fill="#6A67FE" />
|
<circle cx="8" cy="8.59961" r="8" fill="#6A67FE" />
|
||||||
<path d="M7.49605 10.1893V9.70927C7.49605 9.33327 7.56405 8.98527 7.70005 8.66527C7.84405 8.34527 8.08405 7.99727 8.42005 7.62127C8.67605 7.34127 8.85205 7.10127 8.94805 6.90127C9.05205 6.70127 9.10405 6.48927 9.10405 6.26527C9.10405 6.00127 9.00805 5.79327 8.81605 5.64127C8.62405 5.48927 8.35205 5.41326 8.00005 5.41326C7.21605 5.41326 6.49205 5.70127 5.82805 6.27727L5.32405 5.12527C5.66005 4.82127 6.07605 4.57727 6.57205 4.39327C7.07605 4.20927 7.58405 4.11727 8.09605 4.11727C8.60005 4.11727 9.04005 4.20127 9.41605 4.36927C9.80005 4.53727 10.096 4.76927 10.304 5.06527C10.52 5.36127 10.628 5.70927 10.628 6.10927C10.628 6.47727 10.544 6.82127 10.376 7.14127C10.216 7.46127 9.92805 7.80927 9.51205 8.18527C9.13605 8.52927 8.87605 8.82927 8.73205 9.08527C8.58805 9.34127 8.49605 9.59727 8.45605 9.85327L8.40805 10.1893H7.49605ZM7.11205 12.6973V11.0293H8.79205V12.6973H7.11205Z" fill="white" />
|
<path
|
||||||
|
d="M7.49605 10.1893V9.70927C7.49605 9.33327 7.56405 8.98527 7.70005 8.66527C7.84405 8.34527 8.08405 7.99727 8.42005 7.62127C8.67605 7.34127 8.85205 7.10127 8.94805 6.90127C9.05205 6.70127 9.10405 6.48927 9.10405 6.26527C9.10405 6.00127 9.00805 5.79327 8.81605 5.64127C8.62405 5.48927 8.35205 5.41326 8.00005 5.41326C7.21605 5.41326 6.49205 5.70127 5.82805 6.27727L5.32405 5.12527C5.66005 4.82127 6.07605 4.57727 6.57205 4.39327C7.07605 4.20927 7.58405 4.11727 8.09605 4.11727C8.60005 4.11727 9.04005 4.20127 9.41605 4.36927C9.80005 4.53727 10.096 4.76927 10.304 5.06527C10.52 5.36127 10.628 5.70927 10.628 6.10927C10.628 6.47727 10.544 6.82127 10.376 7.14127C10.216 7.46127 9.92805 7.80927 9.51205 8.18527C9.13605 8.52927 8.87605 8.82927 8.73205 9.08527C8.58805 9.34127 8.49605 9.59727 8.45605 9.85327L8.40805 10.1893H7.49605ZM7.11205 12.6973V11.0293H8.79205V12.6973H7.11205Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
{/* same colour as $core-dark-blue-grey */}
|
{/* same colour as $core-dark-blue-grey */}
|
||||||
<ReactTooltip effect={'solid'} data-html={isHtml} backgroundColor={'#3e4771'} />
|
<ReactTooltip
|
||||||
|
effect={"solid"}
|
||||||
|
data-html={isHtml}
|
||||||
|
backgroundColor={"#3e4771"}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './IconToolTip';
|
export { default } from "./IconToolTip";
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
import AceEditor from 'react-ace';
|
import AceEditor from "react-ace";
|
||||||
import classnames from 'classnames';
|
import classnames from "classnames";
|
||||||
import 'brace/mode/sql';
|
import "brace/mode/sql";
|
||||||
import 'brace/ext/linking';
|
import "brace/ext/linking";
|
||||||
import 'brace/ext/language_tools';
|
import "brace/ext/language_tools";
|
||||||
|
|
||||||
import './mode';
|
import "./mode";
|
||||||
import './theme';
|
import "./theme";
|
||||||
|
|
||||||
const baseClass = 'kolide-ace';
|
const baseClass = "kolide-ace";
|
||||||
|
|
||||||
class KolideAce extends Component {
|
class KolideAce extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -30,7 +30,7 @@ class KolideAce extends Component {
|
|||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
name: 'query-editor',
|
name: "query-editor",
|
||||||
showGutter: true,
|
showGutter: true,
|
||||||
wrapEnabled: false,
|
wrapEnabled: false,
|
||||||
};
|
};
|
||||||
@ -42,10 +42,8 @@ class KolideAce extends Component {
|
|||||||
[`${baseClass}__label--error`]: error,
|
[`${baseClass}__label--error`]: error,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return <p className={labelClassName}>{error || label}</p>;
|
||||||
<p className={labelClassName}>{error || label}</p>
|
};
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderHint = () => {
|
renderHint = () => {
|
||||||
const { hint } = this.props;
|
const { hint } = this.props;
|
||||||
@ -55,9 +53,9 @@ class KolideAce extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const {
|
const {
|
||||||
error,
|
error,
|
||||||
fontSize,
|
fontSize,
|
||||||
@ -78,7 +76,7 @@ class KolideAce extends Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const fixHotkeys = (editor) => {
|
const fixHotkeys = (editor) => {
|
||||||
editor.commands.removeCommands(['gotoline', 'find']);
|
editor.commands.removeCommands(["gotoline", "find"]);
|
||||||
onLoad && onLoad(editor);
|
onLoad && onLoad(editor);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -104,11 +102,13 @@ class KolideAce extends Component {
|
|||||||
value={value}
|
value={value}
|
||||||
width="100%"
|
width="100%"
|
||||||
wrapEnabled={wrapEnabled}
|
wrapEnabled={wrapEnabled}
|
||||||
commands={[{
|
commands={[
|
||||||
name: 'commandName',
|
{
|
||||||
bindKey: { win: 'Ctrl-Enter', mac: 'Ctrl-Enter' },
|
name: "commandName",
|
||||||
exec: handleSubmit,
|
bindKey: { win: "Ctrl-Enter", mac: "Ctrl-Enter" },
|
||||||
}]}
|
exec: handleSubmit,
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
{renderHint()}
|
{renderHint()}
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,7 +29,7 @@
|
|||||||
color: $core-blue;
|
color: $core-blue;
|
||||||
background-color: $ui-light-grey;
|
background-color: $ui-light-grey;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
font-family: 'SourceCodePro', $monospace;
|
font-family: "SourceCodePro", $monospace;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './KolideAce';
|
export { default } from "./KolideAce";
|
||||||
|
@ -1,105 +1,138 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import { osqueryTableNames } from 'utilities/osquery_tables';
|
import { osqueryTableNames } from "utilities/osquery_tables";
|
||||||
|
|
||||||
ace.define("ace/mode/kolide_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/sql_highlight_rules"], function(acequire, exports, module) {
|
ace.define(
|
||||||
"use strict";
|
"ace/mode/kolide_highlight_rules",
|
||||||
|
[
|
||||||
|
"require",
|
||||||
|
"exports",
|
||||||
|
"module",
|
||||||
|
"ace/lib/oop",
|
||||||
|
"ace/mode/sql_highlight_rules",
|
||||||
|
],
|
||||||
|
function (acequire, exports, module) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
var oop = acequire("../lib/oop");
|
var oop = acequire("../lib/oop");
|
||||||
var SqlHighlightRules = acequire("./sql_highlight_rules").SqlHighlightRules;
|
var SqlHighlightRules = acequire("./sql_highlight_rules").SqlHighlightRules;
|
||||||
|
|
||||||
var KolideHighlightRules = function() {
|
var KolideHighlightRules = function () {
|
||||||
var keywords = (
|
var keywords =
|
||||||
"select|insert|update|delete|from|where|and|or|group|by|order|limit|offset|having|as|case|" +
|
"select|insert|update|delete|from|where|and|or|group|by|order|limit|offset|having|as|case|" +
|
||||||
"when|else|end|type|left|right|join|on|outer|desc|asc|union|create|table|primary|key|if|" +
|
"when|else|end|type|left|right|join|on|outer|desc|asc|union|create|table|primary|key|if|" +
|
||||||
"foreign|not|references|default|null|inner|cross|natural|database|drop|grant"
|
"foreign|not|references|default|null|inner|cross|natural|database|drop|grant";
|
||||||
);
|
|
||||||
|
|
||||||
var builtinConstants = (
|
var builtinConstants = "true|false";
|
||||||
"true|false"
|
|
||||||
);
|
|
||||||
|
|
||||||
var builtinFunctions = (
|
var builtinFunctions =
|
||||||
"avg|count|first|last|max|min|sum|ucase|lcase|mid|len|round|rank|now|format|" +
|
"avg|count|first|last|max|min|sum|ucase|lcase|mid|len|round|rank|now|format|" +
|
||||||
"coalesce|ifnull|isnull|nvl"
|
"coalesce|ifnull|isnull|nvl";
|
||||||
);
|
|
||||||
|
|
||||||
var dataTypes = (
|
var dataTypes =
|
||||||
"int|numeric|decimal|date|varchar|char|bigint|float|double|bit|binary|text|set|timestamp|" +
|
"int|numeric|decimal|date|varchar|char|bigint|float|double|bit|binary|text|set|timestamp|" +
|
||||||
"money|real|number|integer"
|
"money|real|number|integer";
|
||||||
);
|
|
||||||
|
|
||||||
var osqueryTables = osqueryTableNames.join('|');
|
var osqueryTables = osqueryTableNames.join("|");
|
||||||
|
|
||||||
var keywordMapper = this.createKeywordMapper({
|
var keywordMapper = this.createKeywordMapper(
|
||||||
"osquery-token": osqueryTables,
|
{
|
||||||
"support.function": builtinFunctions,
|
"osquery-token": osqueryTables,
|
||||||
"keyword": keywords,
|
"support.function": builtinFunctions,
|
||||||
"constant.language": builtinConstants,
|
keyword: keywords,
|
||||||
"storage.type": dataTypes,
|
"constant.language": builtinConstants,
|
||||||
}, "identifier", true);
|
"storage.type": dataTypes,
|
||||||
|
},
|
||||||
|
"identifier",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
this.$rules = {
|
this.$rules = {
|
||||||
"start" : [{
|
start: [
|
||||||
token : "comment",
|
{
|
||||||
regex : "--.*$"
|
token: "comment",
|
||||||
}, {
|
regex: "--.*$",
|
||||||
token : "comment",
|
},
|
||||||
start : "/\\*",
|
{
|
||||||
end : "\\*/"
|
token: "comment",
|
||||||
}, {
|
start: "/\\*",
|
||||||
token : "string", // " string
|
end: "\\*/",
|
||||||
regex : '".*?"'
|
},
|
||||||
}, {
|
{
|
||||||
token : "string", // ' string
|
token: "string", // " string
|
||||||
regex : "'.*?'"
|
regex: '".*?"',
|
||||||
}, {
|
},
|
||||||
token : "constant.numeric", // float
|
{
|
||||||
regex : "[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"
|
token: "string", // ' string
|
||||||
}, {
|
regex: "'.*?'",
|
||||||
token : keywordMapper,
|
},
|
||||||
regex : "[a-zA-Z_$][a-zA-Z0-9_$]*\\b"
|
{
|
||||||
}, {
|
token: "constant.numeric", // float
|
||||||
token : "keyword.operator",
|
regex: "[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b",
|
||||||
regex : "\\+|\\-|\\/|\\/\\/|%|<@>|@>|<@|&|\\^|~|<|>|<=|=>|==|!=|<>|="
|
},
|
||||||
}, {
|
{
|
||||||
token : "paren.lparen",
|
token: keywordMapper,
|
||||||
regex : "[\\(]"
|
regex: "[a-zA-Z_$][a-zA-Z0-9_$]*\\b",
|
||||||
}, {
|
},
|
||||||
token : "paren.rparen",
|
{
|
||||||
regex : "[\\)]"
|
token: "keyword.operator",
|
||||||
}, {
|
regex:
|
||||||
token : "text",
|
"\\+|\\-|\\/|\\/\\/|%|<@>|@>|<@|&|\\^|~|<|>|<=|=>|==|!=|<>|=",
|
||||||
regex : "\\s+"
|
},
|
||||||
}]
|
{
|
||||||
|
token: "paren.lparen",
|
||||||
|
regex: "[\\(]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "paren.rparen",
|
||||||
|
regex: "[\\)]",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
token: "text",
|
||||||
|
regex: "\\s+",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
this.normalizeRules();
|
||||||
};
|
};
|
||||||
|
|
||||||
this.normalizeRules();
|
oop.inherits(KolideHighlightRules, SqlHighlightRules);
|
||||||
};
|
|
||||||
|
|
||||||
oop.inherits(KolideHighlightRules, SqlHighlightRules);
|
exports.KolideHighlightRules = KolideHighlightRules;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
exports.KolideHighlightRules = KolideHighlightRules;
|
ace.define(
|
||||||
});
|
"ace/mode/kolide",
|
||||||
|
[
|
||||||
|
"require",
|
||||||
|
"exports",
|
||||||
|
"module",
|
||||||
|
"ace/lib/oop",
|
||||||
|
"ace/mode/sql",
|
||||||
|
"ace/mode/kolide_highlight_rules",
|
||||||
|
"ace/range",
|
||||||
|
],
|
||||||
|
function (acequire, exports, module) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
ace.define("ace/mode/kolide",["require","exports","module","ace/lib/oop","ace/mode/sql","ace/mode/kolide_highlight_rules","ace/range"], function(acequire, exports, module) {
|
var oop = acequire("../lib/oop");
|
||||||
"use strict";
|
var TextMode = acequire("./sql").Mode;
|
||||||
|
var KolideHighlightRules = acequire("./kolide_highlight_rules")
|
||||||
|
.KolideHighlightRules;
|
||||||
|
var Range = acequire("../range").Range;
|
||||||
|
|
||||||
var oop = acequire("../lib/oop");
|
var Mode = function () {
|
||||||
var TextMode = acequire("./sql").Mode;
|
this.HighlightRules = KolideHighlightRules;
|
||||||
var KolideHighlightRules = acequire("./kolide_highlight_rules").KolideHighlightRules;
|
};
|
||||||
var Range = acequire("../range").Range;
|
oop.inherits(Mode, TextMode);
|
||||||
|
|
||||||
var Mode = function() {
|
(function () {
|
||||||
this.HighlightRules = KolideHighlightRules;
|
this.lineCommentStart = "--";
|
||||||
};
|
|
||||||
oop.inherits(Mode, TextMode);
|
|
||||||
|
|
||||||
(function() {
|
this.$id = "ace/mode/kolide";
|
||||||
|
}.call(Mode.prototype));
|
||||||
|
|
||||||
this.lineCommentStart = "--";
|
exports.Mode = Mode;
|
||||||
|
}
|
||||||
this.$id = "ace/mode/kolide";
|
);
|
||||||
}).call(Mode.prototype);
|
|
||||||
|
|
||||||
exports.Mode = Mode;
|
|
||||||
});
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
.ace_editor.ace-kolide {
|
.ace_editor.ace-kolide {
|
||||||
font-family: 'SourceCodePro', monospace;
|
font-family: "SourceCodePro", monospace;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
background-color: #FAFAFA;
|
background-color: #fafafa;
|
||||||
color: #66696f;
|
color: #66696f;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: solid 1px #DBE3E5;
|
border: solid 1px #dbe3e5;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,7 +21,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide.ace_autocomplete .ace_content {
|
.ace-kolide.ace_autocomplete .ace_content {
|
||||||
padding-left: 0px;
|
padding-left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_content {
|
.ace-kolide .ace_content {
|
||||||
@ -33,7 +33,7 @@
|
|||||||
background: #fff;
|
background: #fff;
|
||||||
color: #c38dec;
|
color: #c38dec;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
border-right: solid 1px #E3E3E3;
|
border-right: solid 1px #e3e3e3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_gutter-active-line {
|
.ace-kolide .ace_gutter-active-line {
|
||||||
@ -55,12 +55,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_cursor {
|
.ace-kolide .ace_cursor {
|
||||||
color: #aeafad
|
color: #aeafad;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide cursor in read-only mode */
|
/* Hide cursor in read-only mode */
|
||||||
.ace-kolide .ace_hidden-cursors {
|
.ace-kolide .ace_hidden-cursors {
|
||||||
opacity:0
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_marker-layer .ace_selection {
|
.ace-kolide .ace_marker-layer .ace_selection {
|
||||||
@ -72,20 +72,20 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_marker-layer .ace_step {
|
.ace-kolide .ace_marker-layer .ace_step {
|
||||||
background: rgb(255, 255, 0)
|
background: rgb(255, 255, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_marker-layer .ace_bracket {
|
.ace-kolide .ace_marker-layer .ace_bracket {
|
||||||
margin: -1px 0 0 -1px;
|
margin: -1px 0 0 -1px;
|
||||||
border: 1px solid #d1d1d1
|
border: 1px solid #d1d1d1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_marker-layer .ace_selected-word {
|
.ace-kolide .ace_marker-layer .ace_selected-word {
|
||||||
border: 1px solid #d6d6d6
|
border: 1px solid #d6d6d6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_invisible {
|
.ace-kolide .ace_invisible {
|
||||||
color: #d1d1d1
|
color: #d1d1d1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_keyword {
|
.ace-kolide .ace_keyword {
|
||||||
@ -93,7 +93,7 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_osquery-token{
|
.ace-kolide .ace_osquery-token {
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background-color: #ae6ddf;
|
background-color: #ae6ddf;
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
@ -111,11 +111,11 @@
|
|||||||
.ace-kolide .ace_storage,
|
.ace-kolide .ace_storage,
|
||||||
.ace-kolide .ace_storage.ace_type,
|
.ace-kolide .ace_storage.ace_type,
|
||||||
.ace-kolide .ace_support.ace_type {
|
.ace-kolide .ace_support.ace_type {
|
||||||
color: #8959a8
|
color: #8959a8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_keyword.ace_operator {
|
.ace-kolide .ace_keyword.ace_operator {
|
||||||
color: #3e999f
|
color: #3e999f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_constant.ace_character,
|
.ace-kolide .ace_constant.ace_character,
|
||||||
@ -124,43 +124,43 @@
|
|||||||
.ace-kolide .ace_keyword.ace_other.ace_unit,
|
.ace-kolide .ace_keyword.ace_other.ace_unit,
|
||||||
.ace-kolide .ace_support.ace_constant,
|
.ace-kolide .ace_support.ace_constant,
|
||||||
.ace-kolide .ace_variable.ace_parameter {
|
.ace-kolide .ace_variable.ace_parameter {
|
||||||
color: #f5871f
|
color: #f5871f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_constant.ace_other {
|
.ace-kolide .ace_constant.ace_other {
|
||||||
color: #666969
|
color: #666969;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_invalid {
|
.ace-kolide .ace_invalid {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
background-color: #c82829
|
background-color: #c82829;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_invalid.ace_deprecated {
|
.ace-kolide .ace_invalid.ace_deprecated {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
background-color: #ae6ddf
|
background-color: #ae6ddf;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_fold {
|
.ace-kolide .ace_fold {
|
||||||
background-color: #4271ae;
|
background-color: #4271ae;
|
||||||
border-color: #4d4d4c
|
border-color: #4d4d4c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_entity.ace_name.ace_function,
|
.ace-kolide .ace_entity.ace_name.ace_function,
|
||||||
.ace-kolide .ace_support.ace_function,
|
.ace-kolide .ace_support.ace_function,
|
||||||
.ace-kolide .ace_variable {
|
.ace-kolide .ace_variable {
|
||||||
color: #4271ae
|
color: #4271ae;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_support.ace_class,
|
.ace-kolide .ace_support.ace_class,
|
||||||
.ace-kolide .ace_support.ace_type {
|
.ace-kolide .ace_support.ace_type {
|
||||||
color: #c99e00
|
color: #c99e00;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_heading,
|
.ace-kolide .ace_heading,
|
||||||
.ace-kolide .ace_markup.ace_heading,
|
.ace-kolide .ace_markup.ace_heading,
|
||||||
.ace-kolide .ace_string {
|
.ace-kolide .ace_string {
|
||||||
color: #4fd061
|
color: #4fd061;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_entity.ace_name.ace_tag,
|
.ace-kolide .ace_entity.ace_name.ace_tag,
|
||||||
@ -168,13 +168,14 @@
|
|||||||
.ace-kolide .ace_meta.ace_tag,
|
.ace-kolide .ace_meta.ace_tag,
|
||||||
.ace-kolide .ace_string.ace_regexp,
|
.ace-kolide .ace_string.ace_regexp,
|
||||||
.ace-kolide .ace_variable {
|
.ace-kolide .ace_variable {
|
||||||
color: #c82829
|
color: #c82829;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_comment {
|
.ace-kolide .ace_comment {
|
||||||
color: #8e908c
|
color: #8e908c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ace-kolide .ace_indent-guide {
|
.ace-kolide .ace_indent-guide {
|
||||||
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bdu3f/BwAlfgctduB85QAAAABJRU5ErkJggg==) right repeat-y
|
background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bdu3f/BwAlfgctduB85QAAAABJRU5ErkJggg==)
|
||||||
|
right repeat-y;
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
ace.define("ace/theme/kolide",["require","exports","module","ace/lib/dom"], function(acequire, exports, module) {
|
ace.define(
|
||||||
|
"ace/theme/kolide",
|
||||||
|
["require", "exports", "module", "ace/lib/dom"],
|
||||||
|
function (acequire, exports, module) {
|
||||||
|
exports.isDark = false;
|
||||||
|
exports.cssClass = "ace-kolide";
|
||||||
|
exports.cssText = require("./theme.css");
|
||||||
|
|
||||||
exports.isDark = false;
|
var dom = acequire("../lib/dom");
|
||||||
exports.cssClass = "ace-kolide";
|
dom.importCssString(exports.cssText, exports.cssClass);
|
||||||
exports.cssText = require('./theme.css');
|
}
|
||||||
|
);
|
||||||
var dom = acequire("../lib/dom");
|
|
||||||
dom.importCssString(exports.cssText, exports.cssClass);
|
|
||||||
});
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
import { connect } from 'react-redux';
|
import { connect } from "react-redux";
|
||||||
|
|
||||||
import { hideBackgroundImage } from 'redux/nodes/app/actions';
|
import { hideBackgroundImage } from "redux/nodes/app/actions";
|
||||||
import { ssoSettings } from 'redux/nodes/auth/actions';
|
import { ssoSettings } from "redux/nodes/auth/actions";
|
||||||
import LoginPage from 'pages/LoginPage';
|
import LoginPage from "pages/LoginPage";
|
||||||
|
|
||||||
export class LoginRoutes extends Component {
|
export class LoginRoutes extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -16,22 +16,21 @@ export class LoginRoutes extends Component {
|
|||||||
token: PropTypes.string,
|
token: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount() {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
dispatch(ssoSettings())
|
dispatch(ssoSettings()).catch(() => false);
|
||||||
.catch(() => false);
|
|
||||||
|
|
||||||
dispatch(hideBackgroundImage);
|
dispatch(hideBackgroundImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount() {
|
||||||
const { dispatch } = this.props;
|
const { dispatch } = this.props;
|
||||||
|
|
||||||
dispatch(hideBackgroundImage);
|
dispatch(hideBackgroundImage);
|
||||||
}
|
}
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
isResetPassPage,
|
isResetPassPage,
|
||||||
@ -42,24 +41,27 @@ export class LoginRoutes extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="login-routes">
|
<div className="login-routes">
|
||||||
{children ||
|
{children || (
|
||||||
<LoginPage
|
<LoginPage
|
||||||
pathname={pathname}
|
pathname={pathname}
|
||||||
token={token}
|
token={token}
|
||||||
isForgotPassPage={isForgotPassPage}
|
isForgotPassPage={isForgotPassPage}
|
||||||
isResetPassPage={isResetPassPage}
|
isResetPassPage={isResetPassPage}
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapStateToProps = (state, ownProps) => {
|
const mapStateToProps = (state, ownProps) => {
|
||||||
const { location: { pathname, query } } = ownProps;
|
const {
|
||||||
|
location: { pathname, query },
|
||||||
|
} = ownProps;
|
||||||
const { token } = query;
|
const { token } = query;
|
||||||
|
|
||||||
const isForgotPassPage = pathname.endsWith('/login/forgot');
|
const isForgotPassPage = pathname.endsWith("/login/forgot");
|
||||||
const isResetPassPage = pathname.endsWith('/login/reset');
|
const isResetPassPage = pathname.endsWith("/login/reset");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isForgotPassPage,
|
isForgotPassPage,
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './LoginRoutes';
|
export { default } from "./LoginRoutes";
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
const NumberPill = ({ number }) => {
|
const NumberPill = ({ number }) => {
|
||||||
return <span className="number-pill">{number}</span>;
|
return <span className="number-pill">{number}</span>;
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './NumberPill';
|
export { default } from "./NumberPill";
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
import Button from 'components/buttons/Button';
|
import Button from "components/buttons/Button";
|
||||||
import KolideIcon from 'components/icons/KolideIcon';
|
import KolideIcon from "components/icons/KolideIcon";
|
||||||
|
|
||||||
const baseClass = 'pagination';
|
const baseClass = "pagination";
|
||||||
|
|
||||||
class Pagination extends PureComponent {
|
class Pagination extends PureComponent {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -16,27 +16,34 @@ class Pagination extends PureComponent {
|
|||||||
|
|
||||||
disablePrev = () => {
|
disablePrev = () => {
|
||||||
return this.props.currentPage === 0;
|
return this.props.currentPage === 0;
|
||||||
}
|
};
|
||||||
|
|
||||||
disableNext = () => {
|
disableNext = () => {
|
||||||
// NOTE: not sure why resultsOnCurrentPage is getting assigned undefined.
|
// NOTE: not sure why resultsOnCurrentPage is getting assigned undefined.
|
||||||
// but this seems to work when there is no data in the table.
|
// but this seems to work when there is no data in the table.
|
||||||
return this.props.resultsOnCurrentPage === undefined ||
|
return (
|
||||||
this.props.resultsOnCurrentPage < this.props.resultsPerPage;
|
this.props.resultsOnCurrentPage === undefined ||
|
||||||
}
|
this.props.resultsOnCurrentPage < this.props.resultsPerPage
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const {
|
const { currentPage, onPaginationChange } = this.props;
|
||||||
currentPage,
|
|
||||||
onPaginationChange,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${baseClass}__pager-wrap`}>
|
<div className={`${baseClass}__pager-wrap`}>
|
||||||
<Button variant="unstyled" disabled={this.disablePrev()} onClick={() => onPaginationChange(currentPage - 1)}>
|
<Button
|
||||||
|
variant="unstyled"
|
||||||
|
disabled={this.disablePrev()}
|
||||||
|
onClick={() => onPaginationChange(currentPage - 1)}
|
||||||
|
>
|
||||||
<KolideIcon name="chevronleft" /> Prev
|
<KolideIcon name="chevronleft" /> Prev
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="unstyled" disabled={this.disableNext()} onClick={() => onPaginationChange(currentPage + 1)}>
|
<Button
|
||||||
|
variant="unstyled"
|
||||||
|
disabled={this.disableNext()}
|
||||||
|
onClick={() => onPaginationChange(currentPage + 1)}
|
||||||
|
>
|
||||||
Next <KolideIcon name="chevronright" />
|
Next <KolideIcon name="chevronright" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
.pagination {
|
.pagination {
|
||||||
|
|
||||||
&__pager-wrap {
|
&__pager-wrap {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './Pagination';
|
export { default } from "./Pagination";
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
import { Link } from 'react-router';
|
import { Link } from "react-router";
|
||||||
import classnames from 'classnames';
|
import classnames from "classnames";
|
||||||
|
|
||||||
import KolideIcon from 'components/icons/KolideIcon';
|
import KolideIcon from "components/icons/KolideIcon";
|
||||||
|
|
||||||
const baseClass = 'stacked-white-boxes';
|
const baseClass = "stacked-white-boxes";
|
||||||
|
|
||||||
class StackedWhiteBoxes extends Component {
|
class StackedWhiteBoxes extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -17,7 +17,7 @@ class StackedWhiteBoxes extends Component {
|
|||||||
previousLocation: PropTypes.string,
|
previousLocation: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor (props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
@ -27,13 +27,13 @@ class StackedWhiteBoxes extends Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount() {
|
||||||
this.setState({
|
this.setState({
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount() {
|
||||||
const { didLoad } = this;
|
const { didLoad } = this;
|
||||||
didLoad();
|
didLoad();
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ class StackedWhiteBoxes extends Component {
|
|||||||
isLoading: false,
|
isLoading: false,
|
||||||
isLoaded: true,
|
isLoaded: true,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
nowLeaving = (evt) => {
|
nowLeaving = (evt) => {
|
||||||
const { window } = global;
|
const { window } = global;
|
||||||
@ -59,14 +59,13 @@ class StackedWhiteBoxes extends Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (previousLocation) {
|
if (previousLocation) {
|
||||||
window.setTimeout(
|
window.setTimeout(() => {
|
||||||
() => { onLeave(previousLocation); },
|
onLeave(previousLocation);
|
||||||
300,
|
}, 300);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
renderBackButton = () => {
|
renderBackButton = () => {
|
||||||
const { previousLocation } = this.props;
|
const { previousLocation } = this.props;
|
||||||
@ -76,12 +75,16 @@ class StackedWhiteBoxes extends Component {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`${baseClass}__back`}>
|
<div className={`${baseClass}__back`}>
|
||||||
<Link to={previousLocation} className={`${baseClass}__back-link`} onClick={nowLeaving}>
|
<Link
|
||||||
|
to={previousLocation}
|
||||||
|
className={`${baseClass}__back-link`}
|
||||||
|
onClick={nowLeaving}
|
||||||
|
>
|
||||||
<KolideIcon name="x" />
|
<KolideIcon name="x" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
renderHeader = () => {
|
renderHeader = () => {
|
||||||
const { headerText } = this.props;
|
const { headerText } = this.props;
|
||||||
@ -91,26 +94,18 @@ class StackedWhiteBoxes extends Component {
|
|||||||
<p className={`${baseClass}__header-text`}>{headerText}</p>
|
<p className={`${baseClass}__header-text`}>{headerText}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { children, className, leadText } = this.props;
|
const { children, className, leadText } = this.props;
|
||||||
const {
|
const { isLoading, isLoaded, isLeaving } = this.state;
|
||||||
isLoading,
|
|
||||||
isLoaded,
|
|
||||||
isLeaving,
|
|
||||||
} = this.state;
|
|
||||||
const { renderBackButton, renderHeader } = this;
|
const { renderBackButton, renderHeader } = this;
|
||||||
|
|
||||||
const boxClass = classnames(
|
const boxClass = classnames(baseClass, className, {
|
||||||
baseClass,
|
[`${baseClass}--loading`]: isLoading,
|
||||||
className,
|
[`${baseClass}--loaded`]: isLoaded,
|
||||||
{
|
[`${baseClass}--leaving`]: isLeaving,
|
||||||
[`${baseClass}--loading`]: isLoading,
|
});
|
||||||
[`${baseClass}--loaded`]: isLoaded,
|
|
||||||
[`${baseClass}--leaving`]: isLeaving,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={boxClass}>
|
<div className={boxClass}>
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './StackedWhiteBoxes';
|
export { default } from "./StackedWhiteBoxes";
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
import classnames from 'classnames';
|
import classnames from "classnames";
|
||||||
|
|
||||||
import Dropdown from 'components/forms/fields/Dropdown';
|
import Dropdown from "components/forms/fields/Dropdown";
|
||||||
import EditUserForm from 'components/forms/admin/EditUserForm';
|
import EditUserForm from "components/forms/admin/EditUserForm";
|
||||||
import Modal from 'components/modals/Modal';
|
import Modal from "components/modals/Modal";
|
||||||
import helpers from 'components/UserRow/helpers';
|
import helpers from "components/UserRow/helpers";
|
||||||
import userInterface from 'interfaces/user';
|
import userInterface from "interfaces/user";
|
||||||
|
|
||||||
class UserRow extends Component {
|
class UserRow extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -30,28 +30,32 @@ class UserRow extends Component {
|
|||||||
const { onToggleEditUser, user } = this.props;
|
const { onToggleEditUser, user } = this.props;
|
||||||
|
|
||||||
return onToggleEditUser(user);
|
return onToggleEditUser(user);
|
||||||
}
|
};
|
||||||
|
|
||||||
onEditUser = (updatedUser) => {
|
onEditUser = (updatedUser) => {
|
||||||
const { onEditUser, user } = this.props;
|
const { onEditUser, user } = this.props;
|
||||||
|
|
||||||
return onEditUser(user, updatedUser);
|
return onEditUser(user, updatedUser);
|
||||||
}
|
};
|
||||||
|
|
||||||
onUserActionSelect = (action) => {
|
onUserActionSelect = (action) => {
|
||||||
const { onSelect, onToggleEditUser, user } = this.props;
|
const { onSelect, onToggleEditUser, user } = this.props;
|
||||||
|
|
||||||
if (action === 'modify_details') {
|
if (action === "modify_details") {
|
||||||
return onToggleEditUser(user);
|
return onToggleEditUser(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
return onSelect(user, action);
|
return onSelect(user, action);
|
||||||
}
|
};
|
||||||
|
|
||||||
renderCTAs = () => {
|
renderCTAs = () => {
|
||||||
const { isCurrentUser, isInvite, user } = this.props;
|
const { isCurrentUser, isInvite, user } = this.props;
|
||||||
const { onUserActionSelect } = this;
|
const { onUserActionSelect } = this;
|
||||||
const userActionOptions = helpers.userActionOptions(isCurrentUser, user, isInvite);
|
const userActionOptions = helpers.userActionOptions(
|
||||||
|
isCurrentUser,
|
||||||
|
user,
|
||||||
|
isInvite
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@ -59,10 +63,10 @@ class UserRow extends Component {
|
|||||||
options={userActionOptions}
|
options={userActionOptions}
|
||||||
placeholder="Actions..."
|
placeholder="Actions..."
|
||||||
onChange={onUserActionSelect}
|
onChange={onUserActionSelect}
|
||||||
className={isInvite ? 'revoke-invite' : ''}
|
className={isInvite ? "revoke-invite" : ""}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
renderEditUserModal = (isEditing) => {
|
renderEditUserModal = (isEditing) => {
|
||||||
const { userErrors, isCurrentUser, user } = this.props;
|
const { userErrors, isCurrentUser, user } = this.props;
|
||||||
@ -70,10 +74,7 @@ class UserRow extends Component {
|
|||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal title="Edit user" onExit={onToggleEditing}>
|
||||||
title="Edit user"
|
|
||||||
onExit={onToggleEditing}
|
|
||||||
>
|
|
||||||
<EditUserForm
|
<EditUserForm
|
||||||
isCurrentUser={isCurrentUser}
|
isCurrentUser={isCurrentUser}
|
||||||
onCancel={onToggleEditing}
|
onCancel={onToggleEditing}
|
||||||
@ -85,37 +86,25 @@ class UserRow extends Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { isInvite, user, isEditing } = this.props;
|
const { isInvite, user, isEditing } = this.props;
|
||||||
const {
|
const { admin, email, name, position, username } = user;
|
||||||
admin,
|
|
||||||
email,
|
|
||||||
name,
|
|
||||||
position,
|
|
||||||
username,
|
|
||||||
} = user;
|
|
||||||
const { renderCTAs, renderEditUserModal } = this;
|
const { renderCTAs, renderEditUserModal } = this;
|
||||||
const statusLabel = helpers.userStatusLabel(user, isInvite);
|
const statusLabel = helpers.userStatusLabel(user, isInvite);
|
||||||
const userLabel = admin ? 'Admin' : 'User';
|
const userLabel = admin ? "Admin" : "User";
|
||||||
|
|
||||||
const baseClass = 'user-row';
|
const baseClass = "user-row";
|
||||||
const statusClassName = classnames(
|
const statusClassName = classnames(
|
||||||
`${baseClass}__status`,
|
`${baseClass}__status`,
|
||||||
`${baseClass}__status--${statusLabel.toLowerCase()}`,
|
`${baseClass}__status--${statusLabel.toLowerCase()}`
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={`user-${user.id}-table`}>
|
<tr key={`user-${user.id}-table`}>
|
||||||
<td
|
<td className={`${baseClass}__username`}>{username}</td>
|
||||||
className={`${baseClass}__username`}
|
<td className={statusClassName}>{statusLabel}</td>
|
||||||
>
|
|
||||||
{username}
|
|
||||||
</td>
|
|
||||||
<td className={statusClassName}>
|
|
||||||
{statusLabel}
|
|
||||||
</td>
|
|
||||||
<td>{name}</td>
|
<td>{name}</td>
|
||||||
<td>{email}</td>
|
<td>{email}</td>
|
||||||
<td>{userLabel}</td>
|
<td>{userLabel}</td>
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { mount } from 'enzyme';
|
import { mount } from "enzyme";
|
||||||
import { noop } from 'lodash';
|
import { noop } from "lodash";
|
||||||
|
|
||||||
import UserRow from 'components/UserRow/UserRow';
|
import UserRow from "components/UserRow/UserRow";
|
||||||
import { fillInFormInput } from 'test/helpers';
|
import { fillInFormInput } from "test/helpers";
|
||||||
import { userStub } from 'test/stubs';
|
import { userStub } from "test/stubs";
|
||||||
|
|
||||||
describe('UserRow - component', () => {
|
describe("UserRow - component", () => {
|
||||||
const defaultInviteProps = {
|
const defaultInviteProps = {
|
||||||
isCurrentUser: false,
|
isCurrentUser: false,
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
@ -23,116 +23,97 @@ describe('UserRow - component', () => {
|
|||||||
user: userStub,
|
user: userStub,
|
||||||
};
|
};
|
||||||
|
|
||||||
it('renders a user row', () => {
|
it("renders a user row", () => {
|
||||||
const props = { ...defaultUserProps, user: userStub };
|
const props = { ...defaultUserProps, user: userStub };
|
||||||
const component = mount(<UserRow {...props} />);
|
const component = mount(<UserRow {...props} />);
|
||||||
|
|
||||||
expect(component.length).toEqual(1);
|
expect(component.length).toEqual(1);
|
||||||
expect(component.find('Dropdown').length).toEqual(1);
|
expect(component.find("Dropdown").length).toEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(
|
it("calls the onToggleEditUser prop with the user when Modify Details is selected", () => {
|
||||||
'calls the onToggleEditUser prop with the user when Modify Details is selected',
|
const spy = jest.fn();
|
||||||
() => {
|
const props = { ...defaultUserProps, onToggleEditUser: spy };
|
||||||
const spy = jest.fn();
|
const component = mount(<UserRow {...props} />);
|
||||||
const props = { ...defaultUserProps, onToggleEditUser: spy };
|
|
||||||
const component = mount(<UserRow {...props} />);
|
|
||||||
|
|
||||||
component.find('.Select-control').simulate('keyDown', { keyCode: 40 });
|
component.find(".Select-control").simulate("keyDown", { keyCode: 40 });
|
||||||
component.find('[aria-label="Modify Details"]').simulate('mousedown');
|
component.find('[aria-label="Modify Details"]').simulate("mousedown");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith(userStub);
|
expect(spy).toHaveBeenCalledWith(userStub);
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
it(
|
it("calls the onSelect prop with the user when Promote User is selected", () => {
|
||||||
'calls the onSelect prop with the user when Promote User is selected',
|
const spy = jest.fn();
|
||||||
() => {
|
const props = { ...defaultUserProps, onSelect: spy };
|
||||||
const spy = jest.fn();
|
const component = mount(<UserRow {...props} />);
|
||||||
const props = { ...defaultUserProps, onSelect: spy };
|
|
||||||
const component = mount(<UserRow {...props} />);
|
|
||||||
|
|
||||||
component.find('.Select-control').simulate('keyDown', { keyCode: 40 });
|
component.find(".Select-control").simulate("keyDown", { keyCode: 40 });
|
||||||
component.find('[aria-label="Promote User"]').simulate('mousedown');
|
component.find('[aria-label="Promote User"]').simulate("mousedown");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith(userStub, 'promote_user');
|
expect(spy).toHaveBeenCalledWith(userStub, "promote_user");
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
it(
|
it("calls the onSelect prop with the user when Demote User is selected", () => {
|
||||||
'calls the onSelect prop with the user when Demote User is selected',
|
const adminUser = { ...userStub, admin: true };
|
||||||
() => {
|
const spy = jest.fn();
|
||||||
const adminUser = { ...userStub, admin: true };
|
const props = { ...defaultUserProps, onSelect: spy, user: adminUser };
|
||||||
const spy = jest.fn();
|
const component = mount(<UserRow {...props} />);
|
||||||
const props = { ...defaultUserProps, onSelect: spy, user: adminUser };
|
|
||||||
const component = mount(<UserRow {...props} />);
|
|
||||||
|
|
||||||
component.find('.Select-control').simulate('keyDown', { keyCode: 40 });
|
component.find(".Select-control").simulate("keyDown", { keyCode: 40 });
|
||||||
component.find('[aria-label="Demote User"]').simulate('mousedown');
|
component.find('[aria-label="Demote User"]').simulate("mousedown");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith(adminUser, 'demote_user');
|
expect(spy).toHaveBeenCalledWith(adminUser, "demote_user");
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
it(
|
it("calls the onSelect prop with the user when Disable Account is selected", () => {
|
||||||
'calls the onSelect prop with the user when Disable Account is selected',
|
const spy = jest.fn();
|
||||||
() => {
|
const props = { ...defaultUserProps, onSelect: spy };
|
||||||
const spy = jest.fn();
|
const component = mount(<UserRow {...props} />);
|
||||||
const props = { ...defaultUserProps, onSelect: spy };
|
|
||||||
const component = mount(<UserRow {...props} />);
|
|
||||||
|
|
||||||
component.find('.Select-control').simulate('keyDown', { keyCode: 40 });
|
component.find(".Select-control").simulate("keyDown", { keyCode: 40 });
|
||||||
component.find('[aria-label="Disable Account"]').simulate('mousedown');
|
component.find('[aria-label="Disable Account"]').simulate("mousedown");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith(userStub, 'disable_account');
|
expect(spy).toHaveBeenCalledWith(userStub, "disable_account");
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
it(
|
it("calls the onSelect prop with the user when Enable Account is selected", () => {
|
||||||
'calls the onSelect prop with the user when Enable Account is selected',
|
const disabledUser = { ...userStub, enabled: false };
|
||||||
() => {
|
const spy = jest.fn();
|
||||||
const disabledUser = { ...userStub, enabled: false };
|
const props = { ...defaultUserProps, onSelect: spy, user: disabledUser };
|
||||||
const spy = jest.fn();
|
const component = mount(<UserRow {...props} />);
|
||||||
const props = { ...defaultUserProps, onSelect: spy, user: disabledUser };
|
|
||||||
const component = mount(<UserRow {...props} />);
|
|
||||||
|
|
||||||
component.find('.Select-control').simulate('keyDown', { keyCode: 40 });
|
component.find(".Select-control").simulate("keyDown", { keyCode: 40 });
|
||||||
component.find('[aria-label="Enable Account"]').simulate('mousedown');
|
component.find('[aria-label="Enable Account"]').simulate("mousedown");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith(disabledUser, 'enable_account');
|
expect(spy).toHaveBeenCalledWith(disabledUser, "enable_account");
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
it(
|
it("calls the onSelect prop with the user when Require Password Reset is selected", () => {
|
||||||
'calls the onSelect prop with the user when Require Password Reset is selected',
|
const spy = jest.fn();
|
||||||
() => {
|
const props = { ...defaultUserProps, onSelect: spy };
|
||||||
const spy = jest.fn();
|
const component = mount(<UserRow {...props} />);
|
||||||
const props = { ...defaultUserProps, onSelect: spy };
|
|
||||||
const component = mount(<UserRow {...props} />);
|
|
||||||
|
|
||||||
component.find('.Select-control').simulate('keyDown', { keyCode: 40 });
|
component.find(".Select-control").simulate("keyDown", { keyCode: 40 });
|
||||||
component.find('[aria-label="Require Password Reset"]').simulate('mousedown');
|
component
|
||||||
|
.find('[aria-label="Require Password Reset"]')
|
||||||
|
.simulate("mousedown");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith(userStub, 'reset_password');
|
expect(spy).toHaveBeenCalledWith(userStub, "reset_password");
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
it(
|
it("calls the onEditUser prop with the user and updated user when the edit form is submitted", () => {
|
||||||
'calls the onEditUser prop with the user and updated user when the edit form is submitted',
|
const spy = jest.fn();
|
||||||
() => {
|
const props = { ...defaultUserProps, isEditing: true, onEditUser: spy };
|
||||||
const spy = jest.fn();
|
const component = mount(<UserRow {...props} />);
|
||||||
const props = { ...defaultUserProps, isEditing: true, onEditUser: spy };
|
const form = component.find("EditUserForm");
|
||||||
const component = mount(<UserRow {...props} />);
|
|
||||||
const form = component.find('EditUserForm');
|
|
||||||
|
|
||||||
expect(form.length).toEqual(1);
|
expect(form.length).toEqual(1);
|
||||||
|
|
||||||
const nameInput = form.find({ name: 'name' }).find('input');
|
const nameInput = form.find({ name: "name" }).find("input");
|
||||||
|
|
||||||
fillInFormInput(nameInput, 'Foobar');
|
fillInFormInput(nameInput, "Foobar");
|
||||||
form.simulate('submit');
|
form.simulate("submit");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith(userStub, { ...userStub, name: 'Foobar' });
|
expect(spy).toHaveBeenCalledWith(userStub, { ...userStub, name: "Foobar" });
|
||||||
},
|
});
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
.user-row {
|
.user-row {
|
||||||
|
|
||||||
&__actions {
|
&__actions {
|
||||||
.form-field--dropdown {
|
.form-field--dropdown {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
@ -34,7 +33,7 @@
|
|||||||
&:before {
|
&:before {
|
||||||
background-color: $success;
|
background-color: $success;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
content: ' ';
|
content: " ";
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
@ -46,7 +45,7 @@
|
|||||||
&:before {
|
&:before {
|
||||||
background-color: $warning;
|
background-color: $warning;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
content: ' ';
|
content: " ";
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
@ -58,7 +57,7 @@
|
|||||||
&:before {
|
&:before {
|
||||||
background-color: $core-red;
|
background-color: $core-red;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
content: ' ';
|
content: " ";
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
|
@ -1,33 +1,43 @@
|
|||||||
const userActionOptions = (isCurrentUser, user, invite) => {
|
const userActionOptions = (isCurrentUser, user, invite) => {
|
||||||
const inviteActions = [
|
const inviteActions = [
|
||||||
{ disabled: false, label: 'Revoke Invitation', value: 'revert_invitation' },
|
{ disabled: false, label: "Revoke Invitation", value: "revert_invitation" },
|
||||||
];
|
];
|
||||||
const userEnableAction = user.enabled
|
const userEnableAction = user.enabled
|
||||||
? { disabled: isCurrentUser, label: 'Disable Account', value: 'disable_account' }
|
? {
|
||||||
: { disabled: false, label: 'Enable Account', value: 'enable_account' };
|
disabled: isCurrentUser,
|
||||||
|
label: "Disable Account",
|
||||||
|
value: "disable_account",
|
||||||
|
}
|
||||||
|
: { disabled: false, label: "Enable Account", value: "enable_account" };
|
||||||
const userPromotionAction = user.admin
|
const userPromotionAction = user.admin
|
||||||
? { disabled: isCurrentUser, label: 'Demote User', value: 'demote_user' }
|
? { disabled: isCurrentUser, label: "Demote User", value: "demote_user" }
|
||||||
: { disabled: false, label: 'Promote User', value: 'promote_user' };
|
: { disabled: false, label: "Promote User", value: "promote_user" };
|
||||||
|
|
||||||
if (invite) return inviteActions;
|
if (invite) return inviteActions;
|
||||||
|
|
||||||
const result = [
|
const result = [userEnableAction, userPromotionAction];
|
||||||
userEnableAction,
|
|
||||||
userPromotionAction,
|
|
||||||
];
|
|
||||||
if (!user.sso_enabled) {
|
if (!user.sso_enabled) {
|
||||||
result.push({ disabled: false, label: 'Require Password Reset', value: 'reset_password', helpText: 'This will revoke all active Fleet API tokens for this user.' });
|
result.push({
|
||||||
|
disabled: false,
|
||||||
|
label: "Require Password Reset",
|
||||||
|
value: "reset_password",
|
||||||
|
helpText: "This will revoke all active Fleet API tokens for this user.",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
result.push({ disabled: false, label: 'Modify Details', value: 'modify_details' });
|
result.push({
|
||||||
|
disabled: false,
|
||||||
|
label: "Modify Details",
|
||||||
|
value: "modify_details",
|
||||||
|
});
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const userStatusLabel = (user, invite) => {
|
const userStatusLabel = (user, invite) => {
|
||||||
if (invite) {
|
if (invite) {
|
||||||
return 'Invited';
|
return "Invited";
|
||||||
}
|
}
|
||||||
|
|
||||||
return user.enabled ? 'Active' : 'Disabled';
|
return user.enabled ? "Active" : "Disabled";
|
||||||
};
|
};
|
||||||
|
|
||||||
export default { userActionOptions, userStatusLabel };
|
export default { userActionOptions, userStatusLabel };
|
||||||
|
@ -1,74 +1,102 @@
|
|||||||
import helpers from 'components/UserRow/helpers';
|
import helpers from "components/UserRow/helpers";
|
||||||
import { userStub } from 'test/stubs';
|
import { userStub } from "test/stubs";
|
||||||
|
|
||||||
describe('UserRow - helpers', () => {
|
describe("UserRow - helpers", () => {
|
||||||
describe('#userActionOptions', () => {
|
describe("#userActionOptions", () => {
|
||||||
const { userActionOptions } = helpers;
|
const { userActionOptions } = helpers;
|
||||||
|
|
||||||
it('returns the correct options for invites', () => {
|
it("returns the correct options for invites", () => {
|
||||||
expect(userActionOptions(false, userStub, true)).toEqual([
|
expect(userActionOptions(false, userStub, true)).toEqual([
|
||||||
{ disabled: false, label: 'Revoke Invitation', value: 'revert_invitation' },
|
{
|
||||||
|
disabled: false,
|
||||||
|
label: "Revoke Invitation",
|
||||||
|
value: "revert_invitation",
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the correct options for an enabled user', () => {
|
it("returns the correct options for an enabled user", () => {
|
||||||
expect(userActionOptions(false, userStub, false)).toEqual([
|
expect(userActionOptions(false, userStub, false)).toEqual([
|
||||||
{ disabled: false, label: 'Disable Account', value: 'disable_account' },
|
{ disabled: false, label: "Disable Account", value: "disable_account" },
|
||||||
{ disabled: false, label: 'Promote User', value: 'promote_user' },
|
{ disabled: false, label: "Promote User", value: "promote_user" },
|
||||||
{ disabled: false, label: 'Require Password Reset', value: 'reset_password', helpText: 'This will revoke all active Fleet API tokens for this user.' },
|
{
|
||||||
{ disabled: false, label: 'Modify Details', value: 'modify_details' },
|
disabled: false,
|
||||||
|
label: "Require Password Reset",
|
||||||
|
value: "reset_password",
|
||||||
|
helpText:
|
||||||
|
"This will revoke all active Fleet API tokens for this user.",
|
||||||
|
},
|
||||||
|
{ disabled: false, label: "Modify Details", value: "modify_details" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the correct options for a disabled user', () => {
|
it("returns the correct options for a disabled user", () => {
|
||||||
const disabledUser = { ...userStub, enabled: false };
|
const disabledUser = { ...userStub, enabled: false };
|
||||||
|
|
||||||
expect(userActionOptions(false, disabledUser, false)).toEqual([
|
expect(userActionOptions(false, disabledUser, false)).toEqual([
|
||||||
{ disabled: false, label: 'Enable Account', value: 'enable_account' },
|
{ disabled: false, label: "Enable Account", value: "enable_account" },
|
||||||
{ disabled: false, label: 'Promote User', value: 'promote_user' },
|
{ disabled: false, label: "Promote User", value: "promote_user" },
|
||||||
{ disabled: false, label: 'Require Password Reset', value: 'reset_password', helpText: 'This will revoke all active Fleet API tokens for this user.' },
|
{
|
||||||
{ disabled: false, label: 'Modify Details', value: 'modify_details' },
|
disabled: false,
|
||||||
|
label: "Require Password Reset",
|
||||||
|
value: "reset_password",
|
||||||
|
helpText:
|
||||||
|
"This will revoke all active Fleet API tokens for this user.",
|
||||||
|
},
|
||||||
|
{ disabled: false, label: "Modify Details", value: "modify_details" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the correct options for an admin', () => {
|
it("returns the correct options for an admin", () => {
|
||||||
const adminUser = { ...userStub, admin: true };
|
const adminUser = { ...userStub, admin: true };
|
||||||
|
|
||||||
expect(userActionOptions(false, adminUser, false)).toEqual([
|
expect(userActionOptions(false, adminUser, false)).toEqual([
|
||||||
{ disabled: false, label: 'Disable Account', value: 'disable_account' },
|
{ disabled: false, label: "Disable Account", value: "disable_account" },
|
||||||
{ disabled: false, label: 'Demote User', value: 'demote_user' },
|
{ disabled: false, label: "Demote User", value: "demote_user" },
|
||||||
{ disabled: false, label: 'Require Password Reset', value: 'reset_password', helpText: 'This will revoke all active Fleet API tokens for this user.' },
|
{
|
||||||
{ disabled: false, label: 'Modify Details', value: 'modify_details' },
|
disabled: false,
|
||||||
|
label: "Require Password Reset",
|
||||||
|
value: "reset_password",
|
||||||
|
helpText:
|
||||||
|
"This will revoke all active Fleet API tokens for this user.",
|
||||||
|
},
|
||||||
|
{ disabled: false, label: "Modify Details", value: "modify_details" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the correct options for the current user', () => {
|
it("returns the correct options for the current user", () => {
|
||||||
const adminUser = { ...userStub, admin: true };
|
const adminUser = { ...userStub, admin: true };
|
||||||
|
|
||||||
expect(userActionOptions(true, adminUser, false)).toEqual([
|
expect(userActionOptions(true, adminUser, false)).toEqual([
|
||||||
{ disabled: true, label: 'Disable Account', value: 'disable_account' },
|
{ disabled: true, label: "Disable Account", value: "disable_account" },
|
||||||
{ disabled: true, label: 'Demote User', value: 'demote_user' },
|
{ disabled: true, label: "Demote User", value: "demote_user" },
|
||||||
{ disabled: false, label: 'Require Password Reset', value: 'reset_password', helpText: 'This will revoke all active Fleet API tokens for this user.' },
|
{
|
||||||
{ disabled: false, label: 'Modify Details', value: 'modify_details' },
|
disabled: false,
|
||||||
|
label: "Require Password Reset",
|
||||||
|
value: "reset_password",
|
||||||
|
helpText:
|
||||||
|
"This will revoke all active Fleet API tokens for this user.",
|
||||||
|
},
|
||||||
|
{ disabled: false, label: "Modify Details", value: "modify_details" },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#userStatusLabel', () => {
|
describe("#userStatusLabel", () => {
|
||||||
const { userStatusLabel } = helpers;
|
const { userStatusLabel } = helpers;
|
||||||
|
|
||||||
it('returns the correct options for an invite', () => {
|
it("returns the correct options for an invite", () => {
|
||||||
expect(userStatusLabel(userStub, true)).toEqual('Invited');
|
expect(userStatusLabel(userStub, true)).toEqual("Invited");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the correct options for an enabled user', () => {
|
it("returns the correct options for an enabled user", () => {
|
||||||
expect(userStatusLabel(userStub, false)).toEqual('Active');
|
expect(userStatusLabel(userStub, false)).toEqual("Active");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns the correct options for a disabled user', () => {
|
it("returns the correct options for a disabled user", () => {
|
||||||
const disabledUser = { ...userStub, enabled: false };
|
const disabledUser = { ...userStub, enabled: false };
|
||||||
|
|
||||||
expect(userStatusLabel(disabledUser, false)).toEqual('Disabled');
|
expect(userStatusLabel(disabledUser, false)).toEqual("Disabled");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './UserRow';
|
export { default } from "./UserRow";
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
import classnames from 'classnames';
|
import classnames from "classnames";
|
||||||
|
|
||||||
const baseClass = 'warning-banner';
|
const baseClass = "warning-banner";
|
||||||
|
|
||||||
const WarningBanner = ({ children, className, shouldShowWarning }) => {
|
const WarningBanner = ({ children, className, shouldShowWarning }) => {
|
||||||
if (!shouldShowWarning) {
|
if (!shouldShowWarning) {
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from "enzyme";
|
||||||
|
|
||||||
import WarningBanner from 'components/WarningBanner/WarningBanner';
|
import WarningBanner from "components/WarningBanner/WarningBanner";
|
||||||
|
|
||||||
describe('WarningBanner - component', () => {
|
describe("WarningBanner - component", () => {
|
||||||
it('renders empty when disabled', () => {
|
it("renders empty when disabled", () => {
|
||||||
const props = { shouldShowWarning: false, message: 'message' };
|
const props = { shouldShowWarning: false, message: "message" };
|
||||||
const component = shallow(<WarningBanner {...props} />);
|
const component = shallow(<WarningBanner {...props} />);
|
||||||
expect(component.html()).toBe(null);
|
expect(component.html()).toBe(null);
|
||||||
});
|
});
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './WarningBanner';
|
export { default } from "./WarningBanner";
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
import AceEditor from 'react-ace';
|
import AceEditor from "react-ace";
|
||||||
import classnames from 'classnames';
|
import classnames from "classnames";
|
||||||
|
|
||||||
import 'ace-builds/src-noconflict/mode-yaml';
|
import "ace-builds/src-noconflict/mode-yaml";
|
||||||
|
|
||||||
const baseClass = 'yaml-ace';
|
const baseClass = "yaml-ace";
|
||||||
|
|
||||||
class YamlAce extends Component {
|
class YamlAce extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -15,20 +15,17 @@ class YamlAce extends Component {
|
|||||||
onChange: PropTypes.func.isRequired,
|
onChange: PropTypes.func.isRequired,
|
||||||
value: PropTypes.string,
|
value: PropTypes.string,
|
||||||
wrapperClassName: PropTypes.string,
|
wrapperClassName: PropTypes.string,
|
||||||
}
|
};
|
||||||
|
|
||||||
renderLabel = () => {
|
renderLabel = () => {
|
||||||
const { error, label } = this.props;
|
const { error, label } = this.props;
|
||||||
|
|
||||||
const labelClassName = classnames(
|
const labelClassName = classnames(`${baseClass}__label`, {
|
||||||
`${baseClass}__label`,
|
[`${baseClass}__label--error`]: error,
|
||||||
{ [`${baseClass}__label--error`]: error },
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return <p className={labelClassName}>{error || label}</p>;
|
||||||
<p className={labelClassName}>{error || label}</p>
|
};
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './YamlAce';
|
export { default } from "./YamlAce";
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import classnames from 'classnames';
|
import classnames from "classnames";
|
||||||
|
|
||||||
const baseClass = 'button';
|
const baseClass = "button";
|
||||||
|
|
||||||
interface IButtonProps {
|
interface IButtonProps {
|
||||||
autofocus?: boolean;
|
autofocus?: boolean;
|
||||||
@ -12,7 +12,7 @@ interface IButtonProps {
|
|||||||
onClick: (evt: React.MouseEvent<HTMLButtonElement>) => void;
|
onClick: (evt: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
size?: string;
|
size?: string;
|
||||||
tabIndex?: number;
|
tabIndex?: number;
|
||||||
type?: 'button' | 'submit' | 'reset';
|
type?: "button" | "submit" | "reset";
|
||||||
title?: string;
|
title?: string;
|
||||||
variant?: string;
|
variant?: string;
|
||||||
}
|
}
|
||||||
@ -27,14 +27,16 @@ interface Inputs {
|
|||||||
class Button extends React.Component<IButtonProps, IButtonState> {
|
class Button extends React.Component<IButtonProps, IButtonState> {
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
block: false,
|
block: false,
|
||||||
size: '',
|
size: "",
|
||||||
type: 'button',
|
type: "button",
|
||||||
variant: 'default',
|
variant: "default",
|
||||||
};
|
};
|
||||||
|
|
||||||
componentDidMount(): void {
|
componentDidMount(): void {
|
||||||
const { autofocus } = this.props;
|
const { autofocus } = this.props;
|
||||||
const { inputs: { button } } = this;
|
const {
|
||||||
|
inputs: { button },
|
||||||
|
} = this;
|
||||||
|
|
||||||
if (autofocus && button) {
|
if (autofocus && button) {
|
||||||
button.focus();
|
button.focus();
|
||||||
@ -45,7 +47,7 @@ class Button extends React.Component<IButtonProps, IButtonState> {
|
|||||||
this.inputs.button = button;
|
this.inputs.button = button;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
inputs: Inputs = {};
|
inputs: Inputs = {};
|
||||||
|
|
||||||
@ -61,16 +63,31 @@ class Button extends React.Component<IButtonProps, IButtonState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
render(): JSX.Element {
|
render(): JSX.Element {
|
||||||
const { handleClick, setRef } = this;
|
const { handleClick, setRef } = this;
|
||||||
const { block, children, className, disabled, size, tabIndex, type, title, variant } = this.props;
|
const {
|
||||||
const fullClassName = classnames(baseClass, `${baseClass}--${variant}`, className, {
|
block,
|
||||||
[`${baseClass}--block`]: block,
|
children,
|
||||||
[`${baseClass}--disabled`]: disabled,
|
className,
|
||||||
[`${baseClass}--${size}`]: size !== undefined,
|
disabled,
|
||||||
});
|
size,
|
||||||
|
tabIndex,
|
||||||
|
type,
|
||||||
|
title,
|
||||||
|
variant,
|
||||||
|
} = this.props;
|
||||||
|
const fullClassName = classnames(
|
||||||
|
baseClass,
|
||||||
|
`${baseClass}--${variant}`,
|
||||||
|
className,
|
||||||
|
{
|
||||||
|
[`${baseClass}--block`]: block,
|
||||||
|
[`${baseClass}--disabled`]: disabled,
|
||||||
|
[`${baseClass}--${size}`]: size !== undefined,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
$base-class: 'button';
|
$base-class: "button";
|
||||||
|
|
||||||
@mixin button-variant($color, $hover: null, $active: null, $inverse: null) {
|
@mixin button-variant($color, $hover: null, $active: null, $inverse: null) {
|
||||||
|
|
||||||
background-color: $color;
|
background-color: $color;
|
||||||
|
|
||||||
@if $inverse {
|
@if $inverse {
|
||||||
@ -14,8 +13,7 @@ $base-class: 'button';
|
|||||||
border: 2px solid $active;
|
border: 2px solid $active;
|
||||||
color: $active;
|
color: $active;
|
||||||
}
|
}
|
||||||
} @else {
|
} @else {
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: $hover;
|
background-color: $hover;
|
||||||
}
|
}
|
||||||
@ -28,7 +26,8 @@ $base-class: 'button';
|
|||||||
|
|
||||||
.#{$base-class} {
|
.#{$base-class} {
|
||||||
@include button-variant($core-blue);
|
@include button-variant($core-blue);
|
||||||
transition: color 150ms ease-in-out, background 150ms ease-in-out, top 50ms ease-in-out, box-shadow 50ms ease-in-out, border 50ms ease-in-out;
|
transition: color 150ms ease-in-out, background 150ms ease-in-out,
|
||||||
|
top 50ms ease-in-out, box-shadow 50ms ease-in-out, border 50ms ease-in-out;
|
||||||
position: relative;
|
position: relative;
|
||||||
color: $white;
|
color: $white;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
@ -38,7 +37,7 @@ $base-class: 'button';
|
|||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: $small;
|
font-size: $small;
|
||||||
font-family: 'Nunito Sans', sans-serif;
|
font-family: "Nunito Sans", sans-serif;
|
||||||
font-weight: $bold;
|
font-weight: $bold;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
height: 38px;
|
height: 38px;
|
||||||
@ -63,11 +62,15 @@ $base-class: 'button';
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--blue-green {
|
&--blue-green {
|
||||||
@include button-variant($core-blue-green)
|
@include button-variant($core-blue-green);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--grey {
|
&--grey {
|
||||||
@include button-variant($core-medium-blue-grey, $core-dark-blue-grey-over, $core-dark-blue-grey-down);
|
@include button-variant(
|
||||||
|
$core-medium-blue-grey,
|
||||||
|
$core-dark-blue-grey-over,
|
||||||
|
$core-dark-blue-grey-down
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
&--warning {
|
&--warning {
|
||||||
@ -79,7 +82,12 @@ $base-class: 'button';
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--label {
|
&--label {
|
||||||
@include button-variant($core-light-blue-grey, $core-blue-over, null, $inverse: true);
|
@include button-variant(
|
||||||
|
$core-light-blue-grey,
|
||||||
|
$core-blue-over,
|
||||||
|
null,
|
||||||
|
$inverse: true
|
||||||
|
);
|
||||||
color: $core-blue;
|
color: $core-blue;
|
||||||
border: 1px solid $core-blue;
|
border: 1px solid $core-blue;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@ -127,14 +135,24 @@ $base-class: 'button';
|
|||||||
}
|
}
|
||||||
|
|
||||||
&--inverse {
|
&--inverse {
|
||||||
@include button-variant($white, $core-blue-over, $core-blue-down, $inverse: true);
|
@include button-variant(
|
||||||
|
$white,
|
||||||
|
$core-blue-over,
|
||||||
|
$core-blue-down,
|
||||||
|
$inverse: true
|
||||||
|
);
|
||||||
color: $core-blue;
|
color: $core-blue;
|
||||||
border: 2px solid $core-blue;
|
border: 2px solid $core-blue;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--inverse-alert {
|
&--inverse-alert {
|
||||||
@include button-variant($white, $core-red-over, $core-red-down, $inverse: true);
|
@include button-variant(
|
||||||
|
$white,
|
||||||
|
$core-red-over,
|
||||||
|
$core-red-down,
|
||||||
|
$inverse: true
|
||||||
|
);
|
||||||
color: $alert;
|
color: $alert;
|
||||||
border: 2px solid $alert;
|
border: 2px solid $alert;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './Button.tsx';
|
export { default } from "./Button.tsx";
|
||||||
|
@ -1,13 +1,13 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
import { noop } from 'lodash';
|
import { noop } from "lodash";
|
||||||
import classnames from 'classnames';
|
import classnames from "classnames";
|
||||||
|
|
||||||
import ClickOutside from 'components/ClickOutside';
|
import ClickOutside from "components/ClickOutside";
|
||||||
import KolideIcon from 'components/icons/KolideIcon';
|
import KolideIcon from "components/icons/KolideIcon";
|
||||||
import Button from 'components/buttons/Button';
|
import Button from "components/buttons/Button";
|
||||||
|
|
||||||
const baseClass = 'dropdown-button';
|
const baseClass = "dropdown-button";
|
||||||
|
|
||||||
export class DropdownButton extends Component {
|
export class DropdownButton extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -19,7 +19,7 @@ export class DropdownButton extends Component {
|
|||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
onClick: PropTypes.func,
|
onClick: PropTypes.func,
|
||||||
}),
|
})
|
||||||
).isRequired,
|
).isRequired,
|
||||||
size: PropTypes.string,
|
size: PropTypes.string,
|
||||||
tabIndex: PropTypes.number,
|
tabIndex: PropTypes.number,
|
||||||
@ -31,7 +31,7 @@ export class DropdownButton extends Component {
|
|||||||
onChange: noop,
|
onChange: noop,
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor (props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = { isOpen: false };
|
this.state = { isOpen: false };
|
||||||
@ -39,7 +39,7 @@ export class DropdownButton extends Component {
|
|||||||
|
|
||||||
setDOMNode = (DOMNode) => {
|
setDOMNode = (DOMNode) => {
|
||||||
this.DOMNode = DOMNode;
|
this.DOMNode = DOMNode;
|
||||||
}
|
};
|
||||||
|
|
||||||
toggleDropdown = () => {
|
toggleDropdown = () => {
|
||||||
const { isOpen } = this.state;
|
const { isOpen } = this.state;
|
||||||
@ -56,13 +56,22 @@ export class DropdownButton extends Component {
|
|||||||
const { disabled, label, onClick } = opt;
|
const { disabled, label, onClick } = opt;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className={`${baseClass}__option`} key={`dropdown-button-option-${idx}`}>
|
<li
|
||||||
<Button variant="unstyled" onClick={evt => optionClick(evt, onClick)} disabled={disabled}>{label}</Button>
|
className={`${baseClass}__option`}
|
||||||
|
key={`dropdown-button-option-${idx}`}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
variant="unstyled"
|
||||||
|
onClick={(evt) => optionClick(evt, onClick)}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const {
|
const {
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
@ -92,7 +101,8 @@ export class DropdownButton extends Component {
|
|||||||
type={type}
|
type={type}
|
||||||
variant={variant}
|
variant={variant}
|
||||||
>
|
>
|
||||||
{children} <KolideIcon name="downcarat" className={`${baseClass}__carat`} />
|
{children}{" "}
|
||||||
|
<KolideIcon name="downcarat" className={`${baseClass}__carat`} />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<ul className={optionsClass}>
|
<ul className={optionsClass}>
|
||||||
|
@ -1,23 +1,28 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { mount } from 'enzyme';
|
import { mount } from "enzyme";
|
||||||
import { noop } from 'lodash';
|
import { noop } from "lodash";
|
||||||
|
|
||||||
import { DropdownButton } from './DropdownButton';
|
import { DropdownButton } from "./DropdownButton";
|
||||||
|
|
||||||
describe('DropdownButton - component', () => {
|
describe("DropdownButton - component", () => {
|
||||||
it("calls the clicked item's onClick attribute", () => {
|
it("calls the clicked item's onClick attribute", () => {
|
||||||
const optionSpy = jest.fn();
|
const optionSpy = jest.fn();
|
||||||
const dropdownOptions = [{ label: 'btn1', onClick: noop }, { label: 'btn2', onClick: optionSpy }];
|
const dropdownOptions = [
|
||||||
|
{ label: "btn1", onClick: noop },
|
||||||
|
{ label: "btn2", onClick: optionSpy },
|
||||||
|
];
|
||||||
const component = mount(
|
const component = mount(
|
||||||
<DropdownButton options={dropdownOptions}>
|
<DropdownButton options={dropdownOptions}>New Button</DropdownButton>
|
||||||
New Button
|
|
||||||
</DropdownButton>,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
component.find('button.dropdown-button').simulate('click');
|
component.find("button.dropdown-button").simulate("click");
|
||||||
expect(component.state().isOpen).toEqual(true);
|
expect(component.state().isOpen).toEqual(true);
|
||||||
|
|
||||||
component.find('li.dropdown-button__option').last().find('Button').simulate('click');
|
component
|
||||||
|
.find("li.dropdown-button__option")
|
||||||
|
.last()
|
||||||
|
.find("Button")
|
||||||
|
.simulate("click");
|
||||||
expect(optionSpy).toHaveBeenCalled();
|
expect(optionSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './DropdownButton';
|
export { default } from "./DropdownButton";
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
import { calculateTooltipDirection } from './helpers';
|
import { calculateTooltipDirection } from "./helpers";
|
||||||
import ClickOutside from '../../ClickOutside';
|
import ClickOutside from "../../ClickOutside";
|
||||||
|
|
||||||
const baseClass = 'ellipsis-menu';
|
const baseClass = "ellipsis-menu";
|
||||||
|
|
||||||
export class EllipsisMenu extends Component {
|
export class EllipsisMenu extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -12,7 +12,7 @@ export class EllipsisMenu extends Component {
|
|||||||
positionStyles: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
positionStyles: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor (props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
@ -20,18 +20,18 @@ export class EllipsisMenu extends Component {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount () {
|
componentDidMount() {
|
||||||
const { setTooltipDirection } = this;
|
const { setTooltipDirection } = this;
|
||||||
|
|
||||||
global.window.addEventListener('resize', setTooltipDirection);
|
global.window.addEventListener("resize", setTooltipDirection);
|
||||||
|
|
||||||
return setTooltipDirection();
|
return setTooltipDirection();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount () {
|
componentWillUnmount() {
|
||||||
const { setTooltipDirection } = this;
|
const { setTooltipDirection } = this;
|
||||||
|
|
||||||
global.window.removeEventListener('resize', setTooltipDirection);
|
global.window.removeEventListener("resize", setTooltipDirection);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -42,11 +42,11 @@ export class EllipsisMenu extends Component {
|
|||||||
this.setState({ showChildren: !showChildren });
|
this.setState({ showChildren: !showChildren });
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
setDOMNode = (DOMNode) => {
|
setDOMNode = (DOMNode) => {
|
||||||
this.DOMNode = DOMNode;
|
this.DOMNode = DOMNode;
|
||||||
}
|
};
|
||||||
|
|
||||||
setTooltipDirection = () => {
|
setTooltipDirection = () => {
|
||||||
if (this.DOMNode) {
|
if (this.DOMNode) {
|
||||||
@ -56,12 +56,12 @@ export class EllipsisMenu extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
renderChildren = () => {
|
renderChildren = () => {
|
||||||
const { children } = this.props;
|
const { children } = this.props;
|
||||||
const { showChildren, tooltipDirection } = this.state;
|
const { showChildren, tooltipDirection } = this.state;
|
||||||
const triangleDirection = tooltipDirection === 'left' ? 'right' : 'left';
|
const triangleDirection = tooltipDirection === "left" ? "right" : "left";
|
||||||
|
|
||||||
if (!showChildren) {
|
if (!showChildren) {
|
||||||
return false;
|
return false;
|
||||||
@ -74,18 +74,14 @@ export class EllipsisMenu extends Component {
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { onToggleChildren, renderChildren, setDOMNode } = this;
|
const { onToggleChildren, renderChildren, setDOMNode } = this;
|
||||||
const { positionStyles } = this.props;
|
const { positionStyles } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div ref={setDOMNode} className={baseClass} style={positionStyles}>
|
||||||
ref={setDOMNode}
|
|
||||||
className={baseClass}
|
|
||||||
style={positionStyles}
|
|
||||||
>
|
|
||||||
<button
|
<button
|
||||||
onClick={onToggleChildren}
|
onClick={onToggleChildren}
|
||||||
className={`${baseClass}__btn button button--unstyled`}
|
className={`${baseClass}__btn button button--unstyled`}
|
||||||
|
@ -1,22 +1,22 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { mount } from 'enzyme';
|
import { mount } from "enzyme";
|
||||||
|
|
||||||
import { EllipsisMenu } from './EllipsisMenu';
|
import { EllipsisMenu } from "./EllipsisMenu";
|
||||||
|
|
||||||
describe('EllipsisMenu - component', () => {
|
describe("EllipsisMenu - component", () => {
|
||||||
it('Displays children on click', () => {
|
it("Displays children on click", () => {
|
||||||
const component = mount(
|
const component = mount(
|
||||||
<EllipsisMenu>
|
<EllipsisMenu>
|
||||||
<span>EllipsisMenu Children</span>
|
<span>EllipsisMenu Children</span>
|
||||||
</EllipsisMenu>,
|
</EllipsisMenu>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(component.state().showChildren).toEqual(false);
|
expect(component.state().showChildren).toEqual(false);
|
||||||
expect(component.text()).not.toContainEqual('EllipsisMenu Children');
|
expect(component.text()).not.toContainEqual("EllipsisMenu Children");
|
||||||
|
|
||||||
component.find('button').simulate('click');
|
component.find("button").simulate("click");
|
||||||
|
|
||||||
expect(component.state().showChildren).toEqual(true);
|
expect(component.state().showChildren).toEqual(true);
|
||||||
expect(component.text()).toContain('EllipsisMenu Children');
|
expect(component.text()).toContain("EllipsisMenu Children");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -7,9 +7,11 @@ const calculateElementDistanceToBrowserRight = (el) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const calculateTooltipDirection = (el) => {
|
export const calculateTooltipDirection = (el) => {
|
||||||
const elementDistanceToBrowserRight = calculateElementDistanceToBrowserRight(el);
|
const elementDistanceToBrowserRight = calculateElementDistanceToBrowserRight(
|
||||||
|
el
|
||||||
|
);
|
||||||
|
|
||||||
return elementDistanceToBrowserRight < TOOLTIP_WIDTH ? 'left' : 'right';
|
return elementDistanceToBrowserRight < TOOLTIP_WIDTH ? "left" : "right";
|
||||||
};
|
};
|
||||||
|
|
||||||
export default { calculateTooltipDirection };
|
export default { calculateTooltipDirection };
|
||||||
|
@ -1,22 +1,19 @@
|
|||||||
import { calculateTooltipDirection } from './helpers';
|
import { calculateTooltipDirection } from "./helpers";
|
||||||
|
|
||||||
describe('EllipsisMenu - helpers', () => {
|
describe("EllipsisMenu - helpers", () => {
|
||||||
describe('#calculateTooltipDirection', () => {
|
describe("#calculateTooltipDirection", () => {
|
||||||
it(
|
it('returns "left" if the element does not fit to the right in the browser', () => {
|
||||||
'returns "left" if the element does not fit to the right in the browser',
|
const el = {
|
||||||
() => {
|
getBoundingClientRect: () => {
|
||||||
const el = {
|
return {
|
||||||
getBoundingClientRect: () => {
|
// test DOM window.innerWidth is 1024px
|
||||||
return {
|
right: 725,
|
||||||
// test DOM window.innerWidth is 1024px
|
};
|
||||||
right: 725,
|
},
|
||||||
};
|
};
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(calculateTooltipDirection(el)).toEqual('left');
|
expect(calculateTooltipDirection(el)).toEqual("left");
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
it('returns "right" if the element fits to the right in the browser', () => {
|
it('returns "right" if the element fits to the right in the browser', () => {
|
||||||
const el = {
|
const el = {
|
||||||
@ -28,7 +25,7 @@ describe('EllipsisMenu - helpers', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(calculateTooltipDirection(el)).toEqual('right');
|
expect(calculateTooltipDirection(el)).toEqual("right");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './EllipsisMenu';
|
export { default } from "./EllipsisMenu";
|
||||||
|
@ -1,26 +1,26 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
import FileSaver from 'file-saver';
|
import FileSaver from "file-saver";
|
||||||
|
|
||||||
import Button from 'components/buttons/Button';
|
import Button from "components/buttons/Button";
|
||||||
import enrollSecretInterface from 'interfaces/enroll_secret';
|
import enrollSecretInterface from "interfaces/enroll_secret";
|
||||||
import InputField from 'components/forms/fields/InputField';
|
import InputField from "components/forms/fields/InputField";
|
||||||
import KolideIcon from 'components/icons/KolideIcon';
|
import KolideIcon from "components/icons/KolideIcon";
|
||||||
import { stringToClipboard } from 'utilities/copy_text';
|
import { stringToClipboard } from "utilities/copy_text";
|
||||||
import EyeIcon from '../../../../assets/images/icon-eye-16x16@2x.png';
|
import EyeIcon from "../../../../assets/images/icon-eye-16x16@2x.png";
|
||||||
import DownloadIcon from '../../../../assets/images/icon-download-12x12@2x.png';
|
import DownloadIcon from "../../../../assets/images/icon-download-12x12@2x.png";
|
||||||
|
|
||||||
const baseClass = 'enroll-secrets';
|
const baseClass = "enroll-secrets";
|
||||||
|
|
||||||
class EnrollSecretRow extends Component {
|
class EnrollSecretRow extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
secret: PropTypes.string.isRequired,
|
secret: PropTypes.string.isRequired,
|
||||||
}
|
};
|
||||||
|
|
||||||
constructor (props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = { showSecret: false, copyMessage: '' };
|
this.state = { showSecret: false, copyMessage: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
onCopySecret = (evt) => {
|
onCopySecret = (evt) => {
|
||||||
@ -29,28 +29,27 @@ class EnrollSecretRow extends Component {
|
|||||||
const { secret } = this.props;
|
const { secret } = this.props;
|
||||||
|
|
||||||
stringToClipboard(secret)
|
stringToClipboard(secret)
|
||||||
.then(() => this.setState({ copyMessage: '(copied)' }))
|
.then(() => this.setState({ copyMessage: "(copied)" }))
|
||||||
.catch(() => this.setState({ copyMessage: '(copy failed)' }));
|
.catch(() => this.setState({ copyMessage: "(copy failed)" }));
|
||||||
|
|
||||||
// Clear message after 1 second
|
// Clear message after 1 second
|
||||||
setTimeout(() => this.setState({ copyMessage: '' }), 1000);
|
setTimeout(() => this.setState({ copyMessage: "" }), 1000);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
onDownloadSecret = (evt) => {
|
onDownloadSecret = (evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
|
|
||||||
const { secret } = this.props;
|
const { secret } = this.props;
|
||||||
|
|
||||||
const filename = 'secret.txt';
|
const filename = "secret.txt";
|
||||||
const file = new global.window.File([secret], filename);
|
const file = new global.window.File([secret], filename);
|
||||||
|
|
||||||
FileSaver.saveAs(file);
|
FileSaver.saveAs(file);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
onToggleSecret = (evt) => {
|
onToggleSecret = (evt) => {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
@ -88,9 +87,9 @@ class EnrollSecretRow extends Component {
|
|||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { secret } = this.props;
|
const { secret } = this.props;
|
||||||
const { showSecret } = this.state;
|
const { showSecret } = this.state;
|
||||||
const { renderLabel, onDownloadSecret } = this;
|
const { renderLabel, onDownloadSecret } = this;
|
||||||
@ -102,7 +101,7 @@ class EnrollSecretRow extends Component {
|
|||||||
inputWrapperClass={`${baseClass}__secret-input`}
|
inputWrapperClass={`${baseClass}__secret-input`}
|
||||||
name="osqueryd-secret"
|
name="osqueryd-secret"
|
||||||
label={renderLabel()}
|
label={renderLabel()}
|
||||||
type={showSecret ? 'text' : 'password'}
|
type={showSecret ? "text" : "password"}
|
||||||
value={secret}
|
value={secret}
|
||||||
/>
|
/>
|
||||||
<a
|
<a
|
||||||
@ -122,22 +121,27 @@ class EnrollSecretRow extends Component {
|
|||||||
class EnrollSecretTable extends Component {
|
class EnrollSecretTable extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
secrets: enrollSecretInterface.isRequired,
|
secrets: enrollSecretInterface.isRequired,
|
||||||
}
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { secrets } = this.props;
|
const { secrets } = this.props;
|
||||||
const activeSecrets = secrets.filter(s => s.active);
|
const activeSecrets = secrets.filter((s) => s.active);
|
||||||
|
|
||||||
let enrollSecrectsClass = baseClass;
|
let enrollSecrectsClass = baseClass;
|
||||||
if (activeSecrets.length === 0) {
|
if (activeSecrets.length === 0) {
|
||||||
return (<div className={baseClass}><em>No active enroll secrets.</em></div>);
|
return (
|
||||||
} else if (activeSecrets.length > 1) enrollSecrectsClass += ` ${baseClass}--multiple-secrets`;
|
<div className={baseClass}>
|
||||||
|
<em>No active enroll secrets.</em>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
} else if (activeSecrets.length > 1)
|
||||||
|
enrollSecrectsClass += ` ${baseClass}--multiple-secrets`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={enrollSecrectsClass}>
|
<div className={enrollSecrectsClass}>
|
||||||
{activeSecrets.map(({ name, secret }) =>
|
{activeSecrets.map(({ name, secret }) => (
|
||||||
<EnrollSecretRow key={name} name={name} secret={secret} />,
|
<EnrollSecretRow key={name} name={name} secret={secret} />
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,74 +1,80 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { shallow, mount } from 'enzyme';
|
import { shallow, mount } from "enzyme";
|
||||||
|
|
||||||
import * as copy from 'utilities/copy_text';
|
import * as copy from "utilities/copy_text";
|
||||||
import EnrollSecretTable, { EnrollSecretRow } from 'components/config/EnrollSecretTable/EnrollSecretTable';
|
import EnrollSecretTable, {
|
||||||
|
EnrollSecretRow,
|
||||||
|
} from "components/config/EnrollSecretTable/EnrollSecretTable";
|
||||||
|
|
||||||
describe('EnrollSecretTable', () => {
|
describe("EnrollSecretTable", () => {
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
secrets: [
|
secrets: [
|
||||||
{ name: 'foo', secret: 'foo_secret', active: true },
|
{ name: "foo", secret: "foo_secret", active: true },
|
||||||
{ name: 'bar', secret: 'bar_secret', active: true },
|
{ name: "bar", secret: "bar_secret", active: true },
|
||||||
{ name: 'inactive', secret: 'inactive', active: false },
|
{ name: "inactive", secret: "inactive", active: false },
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
it('renders properly filtered rows', () => {
|
it("renders properly filtered rows", () => {
|
||||||
const table = shallow(<EnrollSecretTable {...defaultProps} />);
|
const table = shallow(<EnrollSecretTable {...defaultProps} />);
|
||||||
expect(table.find('EnrollSecretRow').length).toEqual(2);
|
expect(table.find("EnrollSecretRow").length).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders text when empty', () => {
|
it("renders text when empty", () => {
|
||||||
const table = shallow(<EnrollSecretTable secrets={[]} />);
|
const table = shallow(<EnrollSecretTable secrets={[]} />);
|
||||||
expect(table.find('EnrollSecretRow').length).toEqual(0);
|
expect(table.find("EnrollSecretRow").length).toEqual(0);
|
||||||
expect(table.find('div').text()).toEqual('No active enroll secrets.');
|
expect(table.find("div").text()).toEqual("No active enroll secrets.");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('EnrollSecretRow', () => {
|
describe("EnrollSecretRow", () => {
|
||||||
const defaultProps = { name: 'foo', secret: 'bar' };
|
const defaultProps = { name: "foo", secret: "bar" };
|
||||||
it('should hide secret by default', () => {
|
it("should hide secret by default", () => {
|
||||||
const row = mount(<EnrollSecretRow {...defaultProps} />);
|
const row = mount(<EnrollSecretRow {...defaultProps} />);
|
||||||
const inputField = row.find('InputField').find('input');
|
const inputField = row.find("InputField").find("input");
|
||||||
expect(inputField.prop('type')).toEqual('password');
|
expect(inputField.prop("type")).toEqual("password");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show secret when enabled', () => {
|
it("should show secret when enabled", () => {
|
||||||
const row = mount(<EnrollSecretRow {...defaultProps} />);
|
const row = mount(<EnrollSecretRow {...defaultProps} />);
|
||||||
row.setState({ showSecret: true });
|
row.setState({ showSecret: true });
|
||||||
const inputField = row.find('InputField').find('input');
|
const inputField = row.find("InputField").find("input");
|
||||||
expect(inputField.prop('type')).toEqual('text');
|
expect(inputField.prop("type")).toEqual("text");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should change input type when show/hide is clicked', () => {
|
it("should change input type when show/hide is clicked", () => {
|
||||||
const row = mount(<EnrollSecretRow {...defaultProps} />);
|
const row = mount(<EnrollSecretRow {...defaultProps} />);
|
||||||
|
|
||||||
let inputField = row.find('InputField').find('input');
|
let inputField = row.find("InputField").find("input");
|
||||||
expect(inputField.prop('type')).toEqual('password');
|
expect(inputField.prop("type")).toEqual("password");
|
||||||
|
|
||||||
const showLink = row.find('.enroll-secrets__show-secret');
|
const showLink = row.find(".enroll-secrets__show-secret");
|
||||||
expect(showLink.find('img').prop('alt')).toEqual('show/hide');
|
expect(showLink.find("img").prop("alt")).toEqual("show/hide");
|
||||||
|
|
||||||
showLink.simulate('click');
|
showLink.simulate("click");
|
||||||
|
|
||||||
inputField = row.find('InputField').find('input');
|
inputField = row.find("InputField").find("input");
|
||||||
expect(inputField.prop('type')).toEqual('text');
|
expect(inputField.prop("type")).toEqual("text");
|
||||||
|
|
||||||
const hideLink = row.find('.enroll-secrets__show-secret');
|
const hideLink = row.find(".enroll-secrets__show-secret");
|
||||||
expect(showLink.find('img').prop('alt')).toEqual('show/hide');
|
expect(showLink.find("img").prop("alt")).toEqual("show/hide");
|
||||||
|
|
||||||
hideLink.simulate('click');
|
hideLink.simulate("click");
|
||||||
|
|
||||||
inputField = row.find('InputField').find('input');
|
inputField = row.find("InputField").find("input");
|
||||||
expect(inputField.prop('type')).toEqual('password');
|
expect(inputField.prop("type")).toEqual("password");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call copy when button is clicked', () => {
|
it("should call copy when button is clicked", () => {
|
||||||
const row = mount(<EnrollSecretRow {...defaultProps} />);
|
const row = mount(<EnrollSecretRow {...defaultProps} />);
|
||||||
const spy = jest.spyOn(copy, 'stringToClipboard').mockImplementation(() => Promise.resolve());
|
const spy = jest
|
||||||
|
.spyOn(copy, "stringToClipboard")
|
||||||
|
.mockImplementation(() => Promise.resolve());
|
||||||
|
|
||||||
const copyLink = row.find('.enroll-secrets__secret-copy-icon').find('Button');
|
const copyLink = row
|
||||||
copyLink.simulate('click');
|
.find(".enroll-secrets__secret-copy-icon")
|
||||||
|
.find("Button");
|
||||||
|
copyLink.simulate("click");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith(defaultProps.secret);
|
expect(spy).toHaveBeenCalledWith(defaultProps.secret);
|
||||||
});
|
});
|
||||||
|
@ -69,7 +69,7 @@
|
|||||||
|
|
||||||
&--multiple-secrets {
|
&--multiple-secrets {
|
||||||
&:before {
|
&:before {
|
||||||
content: '';
|
content: "";
|
||||||
position: sticky;
|
position: sticky;
|
||||||
display: block;
|
display: block;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@ -78,11 +78,15 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 17px;
|
height: 17px;
|
||||||
// We explicityly use rgba(255,255,255,0) because it's equivalent to transparent for most broswers except Safari.
|
// We explicityly use rgba(255,255,255,0) because it's equivalent to transparent for most broswers except Safari.
|
||||||
background-image: linear-gradient(to bottom, $white, rgba(255, 255, 255, 0));
|
background-image: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
$white,
|
||||||
|
rgba(255, 255, 255, 0)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:after {
|
&:after {
|
||||||
content: '';
|
content: "";
|
||||||
position: sticky;
|
position: sticky;
|
||||||
display: block;
|
display: block;
|
||||||
bottom: -1px;
|
bottom: -1px;
|
||||||
@ -90,7 +94,11 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 17px;
|
height: 17px;
|
||||||
// We explicityly use rgba(255,255,255,0) because it's equivalent to transparent for most broswers except Safari.
|
// We explicityly use rgba(255,255,255,0) because it's equivalent to transparent for most broswers except Safari.
|
||||||
background-image: linear-gradient(to bottom, rgba(255, 255, 255, 0), $white);
|
background-image: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
rgba(255, 255, 255, 0),
|
||||||
|
$white
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field__label {
|
.form-field__label {
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './EnrollSecretTable';
|
export { default } from "./EnrollSecretTable";
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
import classnames from 'classnames';
|
import classnames from "classnames";
|
||||||
|
|
||||||
import notificationInterface from 'interfaces/notification';
|
import notificationInterface from "interfaces/notification";
|
||||||
import KolideIcon from 'components/icons/KolideIcon';
|
import KolideIcon from "components/icons/KolideIcon";
|
||||||
import Button from 'components/buttons/Button';
|
import Button from "components/buttons/Button";
|
||||||
|
|
||||||
const baseClass = 'flash-message';
|
const baseClass = "flash-message";
|
||||||
|
|
||||||
const FlashMessage = ({ fullWidth, notification, onRemoveFlash, onUndoActionClick }) => {
|
const FlashMessage = ({
|
||||||
|
fullWidth,
|
||||||
|
notification,
|
||||||
|
onRemoveFlash,
|
||||||
|
onUndoActionClick,
|
||||||
|
}) => {
|
||||||
const { alertType, isVisible, message, undoAction } = notification;
|
const { alertType, isVisible, message, undoAction } = notification;
|
||||||
const klass = classnames(baseClass, `${baseClass}--${alertType}`, {
|
const klass = classnames(baseClass, `${baseClass}--${alertType}`, {
|
||||||
[`${baseClass}--full-width`]: fullWidth,
|
[`${baseClass}--full-width`]: fullWidth,
|
||||||
@ -18,14 +23,14 @@ const FlashMessage = ({ fullWidth, notification, onRemoveFlash, onUndoActionClic
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const alertIcon = alertType === 'success' ? 'success-check' : 'warning-filled';
|
const alertIcon =
|
||||||
|
alertType === "success" ? "success-check" : "warning-filled";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={klass}>
|
<div className={klass}>
|
||||||
<div className={`${baseClass}__content`}>
|
<div className={`${baseClass}__content`}>
|
||||||
<KolideIcon name={alertIcon} /> <span>{message}</span>
|
<KolideIcon name={alertIcon} /> <span>{message}</span>
|
||||||
|
{undoAction && (
|
||||||
{undoAction &&
|
|
||||||
<Button
|
<Button
|
||||||
className={`${baseClass}__undo`}
|
className={`${baseClass}__undo`}
|
||||||
variant="unstyled"
|
variant="unstyled"
|
||||||
@ -33,7 +38,7 @@ const FlashMessage = ({ fullWidth, notification, onRemoveFlash, onUndoActionClic
|
|||||||
>
|
>
|
||||||
Undo
|
Undo
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={`${baseClass}__action`}>
|
<div className={`${baseClass}__action`}>
|
||||||
<Button
|
<Button
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './FlashMessage';
|
export { default } from "./FlashMessage";
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
import classnames from 'classnames';
|
import classnames from "classnames";
|
||||||
|
|
||||||
import KolideIcon from 'components/icons/KolideIcon';
|
import KolideIcon from "components/icons/KolideIcon";
|
||||||
|
|
||||||
const baseClass = 'persistent-flash';
|
const baseClass = "persistent-flash";
|
||||||
|
|
||||||
const PersistentFlash = ({ message }) => {
|
const PersistentFlash = ({ message }) => {
|
||||||
const klass = classnames(baseClass, `${baseClass}--error`);
|
const klass = classnames(baseClass, `${baseClass}--error`);
|
||||||
@ -23,4 +23,3 @@ PersistentFlash.propTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default PersistentFlash;
|
export default PersistentFlash;
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './PersistentFlash';
|
export { default } from "./PersistentFlash";
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
import Button from 'components/buttons/Button';
|
import Button from "components/buttons/Button";
|
||||||
import Form from 'components/forms/Form';
|
import Form from "components/forms/Form";
|
||||||
import formFieldInterface from 'interfaces/form_field';
|
import formFieldInterface from "interfaces/form_field";
|
||||||
import InputField from 'components/forms/fields/InputField';
|
import InputField from "components/forms/fields/InputField";
|
||||||
|
|
||||||
const baseClass = 'change-email-form';
|
const baseClass = "change-email-form";
|
||||||
|
|
||||||
class ChangeEmailForm extends Component {
|
class ChangeEmailForm extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -17,7 +17,7 @@ class ChangeEmailForm extends Component {
|
|||||||
onCancel: PropTypes.func.isRequired,
|
onCancel: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { fields, handleSubmit, onCancel } = this.props;
|
const { fields, handleSubmit, onCancel } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -32,7 +32,13 @@ class ChangeEmailForm extends Component {
|
|||||||
<Button className={`${baseClass}__btn`} type="submit" variant="brand">
|
<Button className={`${baseClass}__btn`} type="submit" variant="brand">
|
||||||
Submit
|
Submit
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={onCancel} variant="inverse" className={`${baseClass}__btn`}>Cancel</Button>
|
<Button
|
||||||
|
onClick={onCancel}
|
||||||
|
variant="inverse"
|
||||||
|
className={`${baseClass}__btn`}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
@ -40,12 +46,12 @@ class ChangeEmailForm extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default Form(ChangeEmailForm, {
|
export default Form(ChangeEmailForm, {
|
||||||
fields: ['password'],
|
fields: ["password"],
|
||||||
validate: (formData) => {
|
validate: (formData) => {
|
||||||
if (!formData.password) {
|
if (!formData.password) {
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
errors: { password: 'Password must be present' },
|
errors: { password: "Password must be present" },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './ChangeEmailForm';
|
export { default } from "./ChangeEmailForm";
|
||||||
|
@ -1,14 +1,18 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
|
|
||||||
import Button from 'components/buttons/Button';
|
import Button from "components/buttons/Button";
|
||||||
import Form from 'components/forms/Form';
|
import Form from "components/forms/Form";
|
||||||
import formFieldInterface from 'interfaces/form_field';
|
import formFieldInterface from "interfaces/form_field";
|
||||||
import InputField from 'components/forms/fields/InputField';
|
import InputField from "components/forms/fields/InputField";
|
||||||
import validate from 'components/forms/ChangePasswordForm/validate';
|
import validate from "components/forms/ChangePasswordForm/validate";
|
||||||
|
|
||||||
const formFields = ['old_password', 'new_password', 'new_password_confirmation'];
|
const formFields = [
|
||||||
const baseClass = 'change-password-form';
|
"old_password",
|
||||||
|
"new_password",
|
||||||
|
"new_password_confirmation",
|
||||||
|
];
|
||||||
|
const baseClass = "change-password-form";
|
||||||
|
|
||||||
class ChangePasswordForm extends Component {
|
class ChangePasswordForm extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
@ -21,7 +25,7 @@ class ChangePasswordForm extends Component {
|
|||||||
onCancel: PropTypes.func.isRequired,
|
onCancel: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { fields, handleSubmit, onCancel } = this.props;
|
const { fields, handleSubmit, onCancel } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -43,8 +47,16 @@ class ChangePasswordForm extends Component {
|
|||||||
type="password"
|
type="password"
|
||||||
/>
|
/>
|
||||||
<div className={`${baseClass}__btn-wrap`}>
|
<div className={`${baseClass}__btn-wrap`}>
|
||||||
<Button type="submit" variant="brand" className={`${baseClass}__btn`}>Change password</Button>
|
<Button type="submit" variant="brand" className={`${baseClass}__btn`}>
|
||||||
<Button onClick={onCancel} variant="inverse" className={`${baseClass}__btn`}>Cancel</Button>
|
Change password
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={onCancel}
|
||||||
|
variant="inverse"
|
||||||
|
className={`${baseClass}__btn`}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
@ -52,4 +64,3 @@ class ChangePasswordForm extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default Form(ChangePasswordForm, { fields: formFields, validate });
|
export default Form(ChangePasswordForm, { fields: formFields, validate });
|
||||||
|
|
||||||
|
@ -1,78 +1,112 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { mount } from 'enzyme';
|
import { mount } from "enzyme";
|
||||||
import { noop } from 'lodash';
|
import { noop } from "lodash";
|
||||||
|
|
||||||
import ChangePasswordForm from 'components/forms/ChangePasswordForm';
|
import ChangePasswordForm from "components/forms/ChangePasswordForm";
|
||||||
import helpers from 'test/helpers';
|
import helpers from "test/helpers";
|
||||||
|
|
||||||
const { fillInFormInput, itBehavesLikeAFormInputElement } = helpers;
|
const { fillInFormInput, itBehavesLikeAFormInputElement } = helpers;
|
||||||
|
|
||||||
describe('ChangePasswordForm - component', () => {
|
describe("ChangePasswordForm - component", () => {
|
||||||
it('has the correct fields', () => {
|
it("has the correct fields", () => {
|
||||||
const form = mount(<ChangePasswordForm handleSubmit={noop} onCancel={noop} />);
|
const form = mount(
|
||||||
|
<ChangePasswordForm handleSubmit={noop} onCancel={noop} />
|
||||||
|
);
|
||||||
|
|
||||||
itBehavesLikeAFormInputElement(form, 'old_password');
|
itBehavesLikeAFormInputElement(form, "old_password");
|
||||||
itBehavesLikeAFormInputElement(form, 'new_password');
|
itBehavesLikeAFormInputElement(form, "new_password");
|
||||||
itBehavesLikeAFormInputElement(form, 'new_password_confirmation');
|
itBehavesLikeAFormInputElement(form, "new_password_confirmation");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the password fields as HTML password fields', () => {
|
it("renders the password fields as HTML password fields", () => {
|
||||||
const form = mount(<ChangePasswordForm handleSubmit={noop} onCancel={noop} />);
|
const form = mount(
|
||||||
|
<ChangePasswordForm handleSubmit={noop} onCancel={noop} />
|
||||||
|
);
|
||||||
const passwordField = form.find('input[name="old_password"]');
|
const passwordField = form.find('input[name="old_password"]');
|
||||||
const newPasswordField = form.find('input[name="new_password"]');
|
const newPasswordField = form.find('input[name="new_password"]');
|
||||||
const newPasswordConfirmationField = form.find('input[name="new_password_confirmation"]');
|
const newPasswordConfirmationField = form.find(
|
||||||
|
'input[name="new_password_confirmation"]'
|
||||||
|
);
|
||||||
|
|
||||||
expect(passwordField.prop('type')).toEqual('password');
|
expect(passwordField.prop("type")).toEqual("password");
|
||||||
expect(newPasswordField.prop('type')).toEqual('password');
|
expect(newPasswordField.prop("type")).toEqual("password");
|
||||||
expect(newPasswordConfirmationField.prop('type')).toEqual('password');
|
expect(newPasswordConfirmationField.prop("type")).toEqual("password");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls the handleSubmit props with form data', () => {
|
it("calls the handleSubmit props with form data", () => {
|
||||||
const handleSubmitSpy = jest.fn();
|
const handleSubmitSpy = jest.fn();
|
||||||
const form = mount(<ChangePasswordForm handleSubmit={handleSubmitSpy} onCancel={noop} />).find('form');
|
const form = mount(
|
||||||
const expectedFormData = { old_password: 'p@ssw0rd', new_password: 'p@ssw0rd1', new_password_confirmation: 'p@ssw0rd1' };
|
<ChangePasswordForm handleSubmit={handleSubmitSpy} onCancel={noop} />
|
||||||
const passwordInput = form.find({ name: 'old_password' }).find('input');
|
).find("form");
|
||||||
const newPasswordInput = form.find({ name: 'new_password' }).find('input');
|
const expectedFormData = {
|
||||||
const newPasswordConfirmationInput = form.find({ name: 'new_password_confirmation' }).find('input');
|
old_password: "p@ssw0rd",
|
||||||
|
new_password: "p@ssw0rd1",
|
||||||
|
new_password_confirmation: "p@ssw0rd1",
|
||||||
|
};
|
||||||
|
const passwordInput = form.find({ name: "old_password" }).find("input");
|
||||||
|
const newPasswordInput = form.find({ name: "new_password" }).find("input");
|
||||||
|
const newPasswordConfirmationInput = form
|
||||||
|
.find({ name: "new_password_confirmation" })
|
||||||
|
.find("input");
|
||||||
|
|
||||||
fillInFormInput(passwordInput, expectedFormData.old_password);
|
fillInFormInput(passwordInput, expectedFormData.old_password);
|
||||||
fillInFormInput(newPasswordInput, expectedFormData.new_password);
|
fillInFormInput(newPasswordInput, expectedFormData.new_password);
|
||||||
fillInFormInput(newPasswordConfirmationInput, expectedFormData.new_password_confirmation);
|
fillInFormInput(
|
||||||
|
newPasswordConfirmationInput,
|
||||||
|
expectedFormData.new_password_confirmation
|
||||||
|
);
|
||||||
|
|
||||||
form.simulate('submit');
|
form.simulate("submit");
|
||||||
|
|
||||||
expect(handleSubmitSpy).toHaveBeenCalledWith(expectedFormData);
|
expect(handleSubmitSpy).toHaveBeenCalledWith(expectedFormData);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls the onCancel prop when CANCEL is clicked', () => {
|
it("calls the onCancel prop when CANCEL is clicked", () => {
|
||||||
const onCancelSpy = jest.fn();
|
const onCancelSpy = jest.fn();
|
||||||
const form = mount(<ChangePasswordForm handleSubmit={noop} onCancel={onCancelSpy} />).find('form');
|
const form = mount(
|
||||||
const cancelBtn = form.find('Button').findWhere(n => n.prop('children') === 'Cancel').find('button');
|
<ChangePasswordForm handleSubmit={noop} onCancel={onCancelSpy} />
|
||||||
|
).find("form");
|
||||||
|
const cancelBtn = form
|
||||||
|
.find("Button")
|
||||||
|
.findWhere((n) => n.prop("children") === "Cancel")
|
||||||
|
.find("button");
|
||||||
|
|
||||||
cancelBtn.simulate('click');
|
cancelBtn.simulate("click");
|
||||||
|
|
||||||
expect(onCancelSpy).toHaveBeenCalled();
|
expect(onCancelSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not submit when the new password is invalid', () => {
|
it("does not submit when the new password is invalid", () => {
|
||||||
const handleSubmitSpy = jest.fn();
|
const handleSubmitSpy = jest.fn();
|
||||||
const component = mount(<ChangePasswordForm handleSubmit={handleSubmitSpy} onCancel={noop} />);
|
const component = mount(
|
||||||
const form = component.find('form');
|
<ChangePasswordForm handleSubmit={handleSubmitSpy} onCancel={noop} />
|
||||||
const expectedFormData = { old_password: 'p@ssw0rd', new_password: 'new_password', new_password_confirmation: 'new_password' };
|
);
|
||||||
const passwordInput = form.find({ name: 'old_password' }).find('input');
|
const form = component.find("form");
|
||||||
const newPasswordInput = form.find({ name: 'new_password' }).find('input');
|
const expectedFormData = {
|
||||||
const newPasswordConfirmationInput = form.find({ name: 'new_password_confirmation' }).find('input');
|
old_password: "p@ssw0rd",
|
||||||
|
new_password: "new_password",
|
||||||
|
new_password_confirmation: "new_password",
|
||||||
|
};
|
||||||
|
const passwordInput = form.find({ name: "old_password" }).find("input");
|
||||||
|
const newPasswordInput = form.find({ name: "new_password" }).find("input");
|
||||||
|
const newPasswordConfirmationInput = form
|
||||||
|
.find({ name: "new_password_confirmation" })
|
||||||
|
.find("input");
|
||||||
|
|
||||||
fillInFormInput(passwordInput, expectedFormData.old_password);
|
fillInFormInput(passwordInput, expectedFormData.old_password);
|
||||||
fillInFormInput(newPasswordInput, expectedFormData.new_password);
|
fillInFormInput(newPasswordInput, expectedFormData.new_password);
|
||||||
fillInFormInput(newPasswordConfirmationInput, expectedFormData.new_password_confirmation);
|
fillInFormInput(
|
||||||
|
newPasswordConfirmationInput,
|
||||||
|
expectedFormData.new_password_confirmation
|
||||||
|
);
|
||||||
|
|
||||||
form.simulate('submit');
|
form.simulate("submit");
|
||||||
|
|
||||||
expect(handleSubmitSpy).not.toHaveBeenCalled();
|
expect(handleSubmitSpy).not.toHaveBeenCalled();
|
||||||
|
|
||||||
expect(component.state('errors')).toMatchObject({
|
expect(component.state("errors")).toMatchObject({
|
||||||
new_password: 'Password must be at least 7 characters and contain at least 1 letter, 1 number, and 1 symbol',
|
new_password:
|
||||||
|
"Password must be at least 7 characters and contain at least 1 letter, 1 number, and 1 symbol",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './ChangePasswordForm';
|
export { default } from "./ChangePasswordForm";
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { size } from 'lodash';
|
import { size } from "lodash";
|
||||||
import validateEquality from 'components/forms/validators/validate_equality';
|
import validateEquality from "components/forms/validators/validate_equality";
|
||||||
import validPassword from 'components/forms/validators/valid_password';
|
import validPassword from "components/forms/validators/valid_password";
|
||||||
|
|
||||||
export default (formData) => {
|
export default (formData) => {
|
||||||
const errors = {};
|
const errors = {};
|
||||||
@ -11,24 +11,30 @@ export default (formData) => {
|
|||||||
} = formData;
|
} = formData;
|
||||||
|
|
||||||
if (newPassword && newPasswordConfirmation && !validPassword(newPassword)) {
|
if (newPassword && newPasswordConfirmation && !validPassword(newPassword)) {
|
||||||
errors.new_password = 'Password must be at least 7 characters and contain at least 1 letter, 1 number, and 1 symbol';
|
errors.new_password =
|
||||||
|
"Password must be at least 7 characters and contain at least 1 letter, 1 number, and 1 symbol";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!oldPassword) {
|
if (!oldPassword) {
|
||||||
errors.old_password = 'Password must be present';
|
errors.old_password = "Password must be present";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newPassword) {
|
if (!newPassword) {
|
||||||
errors.new_password = 'New password must be present';
|
errors.new_password = "New password must be present";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!newPasswordConfirmation) {
|
if (!newPasswordConfirmation) {
|
||||||
errors.new_password_confirmation = 'New password confirmation must be present';
|
errors.new_password_confirmation =
|
||||||
|
"New password confirmation must be present";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newPassword && newPasswordConfirmation &&
|
if (
|
||||||
!validateEquality(newPassword, newPasswordConfirmation)) {
|
newPassword &&
|
||||||
errors.new_password_confirmation = 'New password confirmation does not match new password';
|
newPasswordConfirmation &&
|
||||||
|
!validateEquality(newPassword, newPasswordConfirmation)
|
||||||
|
) {
|
||||||
|
errors.new_password_confirmation =
|
||||||
|
"New password confirmation does not match new password";
|
||||||
}
|
}
|
||||||
|
|
||||||
const valid = !size(errors);
|
const valid = !size(errors);
|
||||||
|
@ -1,54 +1,64 @@
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from "prop-types";
|
||||||
import { pull } from 'lodash';
|
import { pull } from "lodash";
|
||||||
|
|
||||||
import KolideIcon from 'components/icons/KolideIcon';
|
import KolideIcon from "components/icons/KolideIcon";
|
||||||
import Button from 'components/buttons/Button';
|
import Button from "components/buttons/Button";
|
||||||
import Dropdown from 'components/forms/fields/Dropdown';
|
import Dropdown from "components/forms/fields/Dropdown";
|
||||||
import Form from 'components/forms/Form';
|
import Form from "components/forms/Form";
|
||||||
import formFieldInterface from 'interfaces/form_field';
|
import formFieldInterface from "interfaces/form_field";
|
||||||
import InputField from 'components/forms/fields/InputField';
|
import InputField from "components/forms/fields/InputField";
|
||||||
import validate from 'components/forms/ConfigurePackQueryForm/validate';
|
import validate from "components/forms/ConfigurePackQueryForm/validate";
|
||||||
|
|
||||||
const baseClass = 'configure-pack-query-form';
|
const baseClass = "configure-pack-query-form";
|
||||||
const fieldNames = ['query_id', 'interval', 'logging_type', 'platform', 'shard', 'version'];
|
const fieldNames = [
|
||||||
|
"query_id",
|
||||||
|
"interval",
|
||||||
|
"logging_type",
|
||||||
|
"platform",
|
||||||
|
"shard",
|
||||||
|
"version",
|
||||||
|
];
|
||||||
const platformOptions = [
|
const platformOptions = [
|
||||||
{ label: 'All', value: '' },
|
{ label: "All", value: "" },
|
||||||
{ label: 'Windows', value: 'windows' },
|
{ label: "Windows", value: "windows" },
|
||||||
{ label: 'Linux', value: 'linux' },
|
{ label: "Linux", value: "linux" },
|
||||||
{ label: 'macOS', value: 'darwin' },
|
{ label: "macOS", value: "darwin" },
|
||||||
];
|
];
|
||||||
const loggingTypeOptions = [
|
const loggingTypeOptions = [
|
||||||
{ label: 'Differential', value: 'differential' },
|
{ label: "Differential", value: "differential" },
|
||||||
{ label: 'Differential (Ignore Removals)', value: 'differential_ignore_removals' },
|
{
|
||||||
{ label: 'Snapshot', value: 'snapshot' },
|
label: "Differential (Ignore Removals)",
|
||||||
|
value: "differential_ignore_removals",
|
||||||
|
},
|
||||||
|
{ label: "Snapshot", value: "snapshot" },
|
||||||
];
|
];
|
||||||
const minOsqueryVersionOptions = [
|
const minOsqueryVersionOptions = [
|
||||||
{ label: 'All', value: '' },
|
{ label: "All", value: "" },
|
||||||
{ label: '4.7.0 +', value: '4.7.0' },
|
{ label: "4.7.0 +", value: "4.7.0" },
|
||||||
{ label: '4.6.0 +', value: '4.6.0' },
|
{ label: "4.6.0 +", value: "4.6.0" },
|
||||||
{ label: '4.5.1 +', value: '4.5.1' },
|
{ label: "4.5.1 +", value: "4.5.1" },
|
||||||
{ label: '4.5.0 +', value: '4.5.0' },
|
{ label: "4.5.0 +", value: "4.5.0" },
|
||||||
{ label: '4.4.0 +', value: '4.4.0' },
|
{ label: "4.4.0 +", value: "4.4.0" },
|
||||||
{ label: '4.3.0 +', value: '4.3.0' },
|
{ label: "4.3.0 +", value: "4.3.0" },
|
||||||
{ label: '4.2.0 +', value: '4.2.0' },
|
{ label: "4.2.0 +", value: "4.2.0" },
|
||||||
{ label: '4.1.2 +', value: '4.1.2' },
|
{ label: "4.1.2 +", value: "4.1.2" },
|
||||||
{ label: '4.1.1 +', value: '4.1.1' },
|
{ label: "4.1.1 +", value: "4.1.1" },
|
||||||
{ label: '4.1.0 +', value: '4.1.0' },
|
{ label: "4.1.0 +", value: "4.1.0" },
|
||||||
{ label: '4.0.2 +', value: '4.0.2' },
|
{ label: "4.0.2 +", value: "4.0.2" },
|
||||||
{ label: '4.0.1 +', value: '4.0.1' },
|
{ label: "4.0.1 +", value: "4.0.1" },
|
||||||
{ label: '4.0.0 +', value: '4.0.0' },
|
{ label: "4.0.0 +", value: "4.0.0" },
|
||||||
{ label: '3.4.0 +', value: '3.4.0' },
|
{ label: "3.4.0 +", value: "3.4.0" },
|
||||||
{ label: '3.3.2 +', value: '3.3.2' },
|
{ label: "3.3.2 +", value: "3.3.2" },
|
||||||
{ label: '3.3.1 +', value: '3.3.1' },
|
{ label: "3.3.1 +", value: "3.3.1" },
|
||||||
{ label: '3.2.6 +', value: '3.2.6' },
|
{ label: "3.2.6 +", value: "3.2.6" },
|
||||||
{ label: '2.2.1 +', value: '2.2.1' },
|
{ label: "2.2.1 +", value: "2.2.1" },
|
||||||
{ label: '2.2.0 +', value: '2.2.0' },
|
{ label: "2.2.0 +", value: "2.2.0" },
|
||||||
{ label: '2.1.2 +', value: '2.1.2' },
|
{ label: "2.1.2 +", value: "2.1.2" },
|
||||||
{ label: '2.1.1 +', value: '2.1.1' },
|
{ label: "2.1.1 +", value: "2.1.1" },
|
||||||
{ label: '2.0.0 +', value: '2.0.0' },
|
{ label: "2.0.0 +", value: "2.0.0" },
|
||||||
{ label: '1.8.2 +', value: '1.8.2' },
|
{ label: "1.8.2 +", value: "1.8.2" },
|
||||||
{ label: '1.8.1 +', value: '1.8.1' },
|
{ label: "1.8.1 +", value: "1.8.1" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export class ConfigurePackQueryForm extends Component {
|
export class ConfigurePackQueryForm extends Component {
|
||||||
@ -67,11 +77,11 @@ export class ConfigurePackQueryForm extends Component {
|
|||||||
onCancel: PropTypes.func,
|
onCancel: PropTypes.func,
|
||||||
};
|
};
|
||||||
|
|
||||||
componentWillMount () {
|
componentWillMount() {
|
||||||
const { fields } = this.props;
|
const { fields } = this.props;
|
||||||
|
|
||||||
if (fields && fields.shard && !fields.shard.value) {
|
if (fields && fields.shard && !fields.shard.value) {
|
||||||
fields.shard.value = '';
|
fields.shard.value = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -81,24 +91,26 @@ export class ConfigurePackQueryForm extends Component {
|
|||||||
const { formData, onCancel: handleCancel } = this.props;
|
const { formData, onCancel: handleCancel } = this.props;
|
||||||
|
|
||||||
return handleCancel(formData);
|
return handleCancel(formData);
|
||||||
}
|
};
|
||||||
|
|
||||||
handlePlatformChoice = (value) => {
|
handlePlatformChoice = (value) => {
|
||||||
const { fields: { platform } } = this.props;
|
const {
|
||||||
const valArray = value.split(',');
|
fields: { platform },
|
||||||
|
} = this.props;
|
||||||
|
const valArray = value.split(",");
|
||||||
|
|
||||||
// Remove All if another OS is chosen
|
// Remove All if another OS is chosen
|
||||||
if (valArray.indexOf('') === 0 && valArray.length > 1) {
|
if (valArray.indexOf("") === 0 && valArray.length > 1) {
|
||||||
return platform.onChange(pull(valArray, '').join(','));
|
return platform.onChange(pull(valArray, "").join(","));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove OS if All is chosen
|
// Remove OS if All is chosen
|
||||||
if (valArray.length > 1 && valArray.indexOf('') > -1) {
|
if (valArray.length > 1 && valArray.indexOf("") > -1) {
|
||||||
return platform.onChange('');
|
return platform.onChange("");
|
||||||
}
|
}
|
||||||
|
|
||||||
return platform.onChange(value);
|
return platform.onChange(value);
|
||||||
}
|
};
|
||||||
|
|
||||||
renderCancelButton = () => {
|
renderCancelButton = () => {
|
||||||
const { formData } = this.props;
|
const { formData } = this.props;
|
||||||
@ -109,13 +121,17 @@ export class ConfigurePackQueryForm extends Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button className={`${baseClass}__cancel-btn`} onClick={onCancel} variant="inverse">
|
<Button
|
||||||
|
className={`${baseClass}__cancel-btn`}
|
||||||
|
onClick={onCancel}
|
||||||
|
variant="inverse"
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
render () {
|
render() {
|
||||||
const { fields, handleSubmit } = this.props;
|
const { fields, handleSubmit } = this.props;
|
||||||
const { handlePlatformChoice, renderCancelButton } = this;
|
const { handlePlatformChoice, renderCancelButton } = this;
|
||||||
|
|
||||||
@ -144,7 +160,11 @@ export class ConfigurePackQueryForm extends Component {
|
|||||||
{...fields.version}
|
{...fields.version}
|
||||||
options={minOsqueryVersionOptions}
|
options={minOsqueryVersionOptions}
|
||||||
placeholder="- - -"
|
placeholder="- - -"
|
||||||
label={['minimum ', <KolideIcon name="osquery" key="min-osquery-vers" />, ' version']}
|
label={[
|
||||||
|
"minimum ",
|
||||||
|
<KolideIcon name="osquery" key="min-osquery-vers" />,
|
||||||
|
" version",
|
||||||
|
]}
|
||||||
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--osquer-vers`}
|
wrapperClassName={`${baseClass}__form-field ${baseClass}__form-field--osquer-vers`}
|
||||||
/>
|
/>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
@ -163,7 +183,11 @@ export class ConfigurePackQueryForm extends Component {
|
|||||||
/>
|
/>
|
||||||
<div className={`${baseClass}__btn-wrapper`}>
|
<div className={`${baseClass}__btn-wrapper`}>
|
||||||
{renderCancelButton()}
|
{renderCancelButton()}
|
||||||
<Button className={`${baseClass}__submit-btn`} type="submit" variant="brand">
|
<Button
|
||||||
|
className={`${baseClass}__submit-btn`}
|
||||||
|
type="submit"
|
||||||
|
variant="brand"
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,29 +1,30 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { mount } from 'enzyme';
|
import { mount } from "enzyme";
|
||||||
import { noop } from 'lodash';
|
import { noop } from "lodash";
|
||||||
|
|
||||||
import DefaultConfigurePackQueryForm, { ConfigurePackQueryForm } from 'components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm';
|
import DefaultConfigurePackQueryForm, {
|
||||||
import { itBehavesLikeAFormDropdownElement, itBehavesLikeAFormInputElement } from 'test/helpers';
|
ConfigurePackQueryForm,
|
||||||
import { scheduledQueryStub } from 'test/stubs';
|
} from "components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm";
|
||||||
|
import {
|
||||||
|
itBehavesLikeAFormDropdownElement,
|
||||||
|
itBehavesLikeAFormInputElement,
|
||||||
|
} from "test/helpers";
|
||||||
|
import { scheduledQueryStub } from "test/stubs";
|
||||||
|
|
||||||
describe('ConfigurePackQueryForm - component', () => {
|
describe("ConfigurePackQueryForm - component", () => {
|
||||||
describe('form fields', () => {
|
describe("form fields", () => {
|
||||||
const form = mount(
|
const form = mount(<DefaultConfigurePackQueryForm handleSubmit={noop} />);
|
||||||
<DefaultConfigurePackQueryForm
|
|
||||||
handleSubmit={noop}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
it('updates form state', () => {
|
it("updates form state", () => {
|
||||||
itBehavesLikeAFormInputElement(form, 'interval');
|
itBehavesLikeAFormInputElement(form, "interval");
|
||||||
itBehavesLikeAFormDropdownElement(form, 'logging_type');
|
itBehavesLikeAFormDropdownElement(form, "logging_type");
|
||||||
itBehavesLikeAFormDropdownElement(form, 'platform');
|
itBehavesLikeAFormDropdownElement(form, "platform");
|
||||||
itBehavesLikeAFormDropdownElement(form, 'version');
|
itBehavesLikeAFormDropdownElement(form, "version");
|
||||||
itBehavesLikeAFormInputElement(form, 'shard');
|
itBehavesLikeAFormInputElement(form, "shard");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('platform options', () => {
|
describe("platform options", () => {
|
||||||
const onChangeSpy = jest.fn();
|
const onChangeSpy = jest.fn();
|
||||||
const fieldsObj = {
|
const fieldsObj = {
|
||||||
platform: {
|
platform: {
|
||||||
@ -38,85 +39,86 @@ describe('ConfigurePackQueryForm - component', () => {
|
|||||||
fields={fieldsObj}
|
fields={fieldsObj}
|
||||||
handleSubmit={noop}
|
handleSubmit={noop}
|
||||||
formData={{ query_id: 1 }}
|
formData={{ query_id: 1 }}
|
||||||
/>,
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
it("doesn't allow All when other options are chosen", () => {
|
it("doesn't allow All when other options are chosen", () => {
|
||||||
form.instance().handlePlatformChoice(',windows');
|
form.instance().handlePlatformChoice(",windows");
|
||||||
|
|
||||||
expect(onChangeSpy).toHaveBeenCalledWith('windows');
|
expect(onChangeSpy).toHaveBeenCalledWith("windows");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("doesn't allow other options when All is chosen", () => {
|
it("doesn't allow other options when All is chosen", () => {
|
||||||
form.instance().handlePlatformChoice('darwin,linux,');
|
form.instance().handlePlatformChoice("darwin,linux,");
|
||||||
|
|
||||||
expect(onChangeSpy).toHaveBeenCalledWith('');
|
expect(onChangeSpy).toHaveBeenCalledWith("");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('submitting the form', () => {
|
describe("submitting the form", () => {
|
||||||
const spy = jest.fn();
|
const spy = jest.fn();
|
||||||
const form = mount(
|
const form = mount(
|
||||||
<DefaultConfigurePackQueryForm
|
<DefaultConfigurePackQueryForm
|
||||||
handleSubmit={spy}
|
handleSubmit={spy}
|
||||||
formData={{ query_id: 1 }}
|
formData={{ query_id: 1 }}
|
||||||
/>,
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
it('submits the form with the form data', () => {
|
it("submits the form with the form data", () => {
|
||||||
itBehavesLikeAFormInputElement(form, 'interval', 'InputField', 123);
|
itBehavesLikeAFormInputElement(form, "interval", "InputField", 123);
|
||||||
itBehavesLikeAFormDropdownElement(form, 'logging_type');
|
itBehavesLikeAFormDropdownElement(form, "logging_type");
|
||||||
itBehavesLikeAFormDropdownElement(form, 'platform');
|
itBehavesLikeAFormDropdownElement(form, "platform");
|
||||||
itBehavesLikeAFormDropdownElement(form, 'version');
|
itBehavesLikeAFormDropdownElement(form, "version");
|
||||||
itBehavesLikeAFormInputElement(form, 'shard', 'InputField', 12);
|
itBehavesLikeAFormInputElement(form, "shard", "InputField", 12);
|
||||||
|
|
||||||
form.find('form').simulate('submit');
|
form.find("form").simulate("submit");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith({
|
expect(spy).toHaveBeenCalledWith({
|
||||||
interval: 123,
|
interval: 123,
|
||||||
logging_type: 'differential',
|
logging_type: "differential",
|
||||||
platform: '',
|
platform: "",
|
||||||
query_id: 1,
|
query_id: 1,
|
||||||
version: '',
|
version: "",
|
||||||
shard: 12,
|
shard: 12,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('cancelling the form', () => {
|
describe("cancelling the form", () => {
|
||||||
const CancelButton = form => form.find('.configure-pack-query-form__cancel-btn');
|
const CancelButton = (form) =>
|
||||||
|
form.find(".configure-pack-query-form__cancel-btn");
|
||||||
|
|
||||||
it('displays a cancel Button when updating a scheduled query', () => {
|
it("displays a cancel Button when updating a scheduled query", () => {
|
||||||
const NewScheduledQueryForm = mount(
|
const NewScheduledQueryForm = mount(
|
||||||
<DefaultConfigurePackQueryForm
|
<DefaultConfigurePackQueryForm
|
||||||
formData={{ query_id: 1 }}
|
formData={{ query_id: 1 }}
|
||||||
handleSubmit={noop}
|
handleSubmit={noop}
|
||||||
onCancel={noop}
|
onCancel={noop}
|
||||||
/>,
|
/>
|
||||||
);
|
);
|
||||||
const UpdateScheduledQueryForm = mount(
|
const UpdateScheduledQueryForm = mount(
|
||||||
<DefaultConfigurePackQueryForm
|
<DefaultConfigurePackQueryForm
|
||||||
formData={scheduledQueryStub}
|
formData={scheduledQueryStub}
|
||||||
handleSubmit={noop}
|
handleSubmit={noop}
|
||||||
onCancel={noop}
|
onCancel={noop}
|
||||||
/>,
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(CancelButton(NewScheduledQueryForm).length).toEqual(0);
|
expect(CancelButton(NewScheduledQueryForm).length).toEqual(0);
|
||||||
expect(CancelButton(UpdateScheduledQueryForm).length).toBeGreaterThan(0);
|
expect(CancelButton(UpdateScheduledQueryForm).length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls the onCancel prop when the cancel Button is clicked', () => {
|
it("calls the onCancel prop when the cancel Button is clicked", () => {
|
||||||
const spy = jest.fn();
|
const spy = jest.fn();
|
||||||
const UpdateScheduledQueryForm = mount(
|
const UpdateScheduledQueryForm = mount(
|
||||||
<DefaultConfigurePackQueryForm
|
<DefaultConfigurePackQueryForm
|
||||||
formData={scheduledQueryStub}
|
formData={scheduledQueryStub}
|
||||||
handleSubmit={noop}
|
handleSubmit={noop}
|
||||||
onCancel={spy}
|
onCancel={spy}
|
||||||
/>,
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
CancelButton(UpdateScheduledQueryForm).hostNodes().simulate('click');
|
CancelButton(UpdateScheduledQueryForm).hostNodes().simulate("click");
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledWith(scheduledQueryStub);
|
expect(spy).toHaveBeenCalledWith(scheduledQueryStub);
|
||||||
});
|
});
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
.configure-pack-query-form {
|
.configure-pack-query-form {
|
||||||
|
|
||||||
&__form-field {
|
&__form-field {
|
||||||
&--interval {
|
&--interval {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -1 +1 @@
|
|||||||
export { default } from './ConfigurePackQueryForm';
|
export { default } from "./ConfigurePackQueryForm";
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user