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:
Gabe Hernandez 2021-04-12 14:32:25 +01:00 committed by GitHub
parent df5fa7f515
commit efb35b537a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
631 changed files with 31182 additions and 29945 deletions

View File

@ -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"),
}, },
}, },
}, },

View File

@ -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
View 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
View File

@ -0,0 +1 @@
{}

View File

@ -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

View File

@ -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);
});
}); });
}); });

View File

@ -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");
}); });
}); });

View File

@ -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");
}); });
}); });

View File

@ -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");
}); });
}); });

View File

@ -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$/);
}); });
}); });

View File

@ -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");
}); });
}); });

View File

@ -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);
}); });
}); });

View File

@ -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,
}, },

View File

@ -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')

View File

@ -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"
]
} }

View File

@ -1,3 +1,3 @@
// __mocks__/fileMock.js // __mocks__/fileMock.js
module.exports = 'test-file-stub'; module.exports = "test-file-stub";

View File

@ -1,4 +1,4 @@
export default { export default {
FAKE_PASSWORD: '********', FAKE_PASSWORD: "********",
DEFAULT_SMTP_PORT: '587', DEFAULT_SMTP_PORT: "587",
}; };

View File

@ -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,

View File

@ -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>
);
} }
} }

View File

@ -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);

View File

@ -1 +1 @@
export { default } from './App'; export { default } from "./App";

View File

@ -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}
</>
);
} }
} }

View File

@ -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);
}); });

View File

@ -1 +1 @@
export { default } from './AuthenticatedAdminRoutes'; export { default } from "./AuthenticatedAdminRoutes";

View File

@ -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>
);
} }
} }

View File

@ -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);

View File

@ -1 +1 @@
export { default } from './AuthenticatedRoutes'; export { default } from "./AuthenticatedRoutes";

View File

@ -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 (

View File

@ -1 +1 @@
export { default } from './AuthenticationFormWrapper'; export { default } from "./AuthenticationFormWrapper";

View File

@ -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}
/>
); );
} }
} }

View File

@ -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%;

View File

@ -1 +1 @@
export { default } from './Avatar.tsx'; export { default } from "./Avatar.tsx";

View File

@ -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} />;
} }

View File

@ -1 +1 @@
export { default } from './ClickOutside'; export { default } from "./ClickOutside";

View File

@ -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 */
}; };

View File

@ -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();
}); });
}); });

View File

@ -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 />;
} }
} }

View File

@ -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
);
}); });
}); });
}); });

View File

@ -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;
}) })

View File

@ -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;

View File

@ -1 +1 @@
export { default } from './EmailTokenRedirect'; export { default } from "./EmailTokenRedirect";

View File

@ -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) {

View File

@ -1 +1 @@
export { default } from './EnsureUnauthenticated'; export { default } from "./EnsureUnauthenticated";

View File

@ -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>
); );
}; };

View File

@ -1 +1 @@
export { default } from './IconToolTip'; export { default } from "./IconToolTip";

View File

@ -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>

View File

@ -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;
} }
} }
} }

View File

@ -1 +1 @@
export { default } from './KolideAce'; export { default } from "./KolideAce";

View File

@ -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;
});

View File

@ -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;
} }

View File

@ -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);
});

View File

@ -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,

View File

@ -1 +1 @@
export { default } from './LoginRoutes'; export { default } from "./LoginRoutes";

View File

@ -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>;

View File

@ -1 +1 @@
export { default } from './NumberPill'; export { default } from "./NumberPill";

View File

@ -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>

View File

@ -1,5 +1,4 @@
.pagination { .pagination {
&__pager-wrap { &__pager-wrap {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;

View File

@ -1 +1 @@
export { default } from './Pagination'; export { default } from "./Pagination";

View File

@ -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}>

View File

@ -1 +1 @@
export { default } from './StackedWhiteBoxes'; export { default } from "./StackedWhiteBoxes";

View File

@ -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>

View File

@ -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" });
}, });
);
}); });

View File

@ -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;

View File

@ -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 };

View File

@ -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");
}); });
}); });
}); });

View File

@ -1 +1 @@
export { default } from './UserRow'; export { default } from "./UserRow";

View File

@ -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) {

View File

@ -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);
}); });

View File

@ -1 +1 @@
export { default } from './WarningBanner'; export { default } from "./WarningBanner";

View File

@ -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 {

View File

@ -1 +1 @@
export { default } from './YamlAce'; export { default } from "./YamlAce";

View File

@ -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

View File

@ -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;

View File

@ -1 +1 @@
export { default } from './Button.tsx'; export { default } from "./Button.tsx";

View File

@ -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}>

View File

@ -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();
}); });
}); });

View File

@ -1 +1 @@
export { default } from './DropdownButton'; export { default } from "./DropdownButton";

View File

@ -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`}

View File

@ -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");
}); });
}); });

View File

@ -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 };

View File

@ -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");
}); });
}); });
}); });

View File

@ -1 +1 @@
export { default } from './EllipsisMenu'; export { default } from "./EllipsisMenu";

View File

@ -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>
); );
} }

View File

@ -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);
}); });

View File

@ -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 {

View File

@ -1 +1 @@
export { default } from './EnrollSecretTable'; export { default } from "./EnrollSecretTable";

View File

@ -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

View File

@ -1 +1 @@
export { default } from './FlashMessage'; export { default } from "./FlashMessage";

View File

@ -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;

View File

@ -1 +1 @@
export { default } from './PersistentFlash'; export { default } from "./PersistentFlash";

View File

@ -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" },
}; };
} }

View File

@ -1 +1 @@
export { default } from './ChangeEmailForm'; export { default } from "./ChangeEmailForm";

View File

@ -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 });

View File

@ -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",
}); });
}); });
}); });

View File

@ -1 +1 @@
export { default } from './ChangePasswordForm'; export { default } from "./ChangePasswordForm";

View File

@ -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);

View File

@ -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>

View File

@ -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);
}); });

View File

@ -1,5 +1,4 @@
.configure-pack-query-form { .configure-pack-query-form {
&__form-field { &__form-field {
&--interval { &--interval {
position: relative; position: relative;

View File

@ -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