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 = {
extends: [
'airbnb',
'plugin:jest/recommended',
'plugin:react-hooks/recommended',
'plugin:@typescript-eslint/recommended',
'plugin:cypress/recommended',
],
parser: '@typescript-eslint/parser',
plugins: [
'jest',
'react',
'@typescript-eslint',
"airbnb",
"plugin:jest/recommended",
"plugin:react-hooks/recommended",
"plugin:@typescript-eslint/recommended",
"plugin:cypress/recommended",
"plugin:prettier/recommended",
],
parser: "@typescript-eslint/parser",
plugins: ["jest", "react", "@typescript-eslint"],
env: {
node: true,
mocha: true,
browser: true,
'jest/globals': true,
"jest/globals": true,
},
globals: {
expect: false,
describe: false,
},
rules: {
camelcase: 'off',
'consistent-return': 1,
'arrow-body-style': 0,
'max-len': 0,
'no-unused-expressions': 0,
'no-console': 0,
'space-before-function-paren': 0,
'react/prefer-stateless-function': 0,
'react/no-multi-comp': 0,
'react/no-unused-prop-types': [1, { customValidators: [], skipShapeProps: true }],
'react/require-default-props': 0, // TODO set default props and enable this check
'react/jsx-filename-extension': [1, { extensions: ['.jsx', '.tsx'] }],
'no-param-reassign': 0,
'new-cap': 0,
'import/no-unresolved': [2, { caseSensitive: false }],
'linebreak-style': 0,
'import/no-named-as-default': 'off',
'import/no-named-as-default-member': 'off',
'import/extensions': 0,
'import/no-extraneous-dependencies': 0,
'no-underscore-dangle': 0,
'jsx-a11y/no-static-element-interactions': 'off',
camelcase: "off",
"consistent-return": 1,
"arrow-body-style": 0,
"max-len": 0,
"no-unused-expressions": 0,
"no-console": 0,
"space-before-function-paren": 0,
"react/prefer-stateless-function": 0,
"react/no-multi-comp": 0,
"react/no-unused-prop-types": [
1,
{ customValidators: [], skipShapeProps: true },
],
"react/require-default-props": 0, // TODO set default props and enable this check
"react/jsx-filename-extension": [1, { extensions: [".jsx", ".tsx"] }],
"no-param-reassign": 0,
"new-cap": 0,
"import/no-unresolved": [2, { caseSensitive: false }],
"linebreak-style": 0,
"import/no-named-as-default": "off",
"import/no-named-as-default-member": "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:
// https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-use-before-define.md
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': ['error'],
"no-use-before-define": "off",
"@typescript-eslint/no-use-before-define": ["error"],
// 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
'@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)
// 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.
'jsx-a11y/heading-has-content': 'off',
'jsx-a11y/anchor-has-content': 'off',
"jsx-a11y/heading-has-content": "off",
"jsx-a11y/anchor-has-content": "off",
},
overrides: [
{
files: ['*.ts', '*.tsx'],
files: ["*.ts", "*.tsx"],
rules: {
// Set to warn for now at the beginning to make migration easier
// 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: {
'import/resolver': {
"import/resolver": {
webpack: {
config: path.join(__dirname, 'webpack.config.js'),
config: path.join(__dirname, "webpack.config.js"),
},
},
},

View File

@ -107,6 +107,32 @@ jobs:
run: |
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:
strategy:
@ -158,4 +184,3 @@ jobs:
- name: Run Go Linting
run: |
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(() => {
cy.setup();
cy.login();
});
it('Add new host', () => {
cy.visit('/');
it("Add new host", () => {
cy.visit("/");
cy.contains('button', /add new host/i)
.click();
cy.contains("button", /add new host/i).click();
cy.contains('a', /download/i).first()
cy.contains("a", /download/i)
.first()
.click();
cy.get('a[href*="showSecret"]').click();
// Assert enroll secret downloaded matches the one displayed
cy.readFile(path.join(Cypress.config('downloadsFolder'), 'secret.txt'), { timeout: 3000 })
.then((contents) => {
cy.get('input[disabled]').should('have.value', contents);
cy.readFile(path.join(Cypress.config("downloadsFolder"), "secret.txt"), {
timeout: 3000,
}).then((contents) => {
cy.get("input[disabled]").should("have.value", contents);
});
});
});

View File

@ -1,59 +1,59 @@
describe('Label flow', () => {
describe("Label flow", () => {
beforeEach(() => {
cy.setup();
cy.login();
});
it('Create, edit, and delete a label successfully', () => {
cy.visit('/hosts/manage');
it("Create, edit, and delete a label successfully", () => {
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
cy.get('.ace_content')
cy.get(".ace_content")
.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)
.click()
.type('Select all users across platforms.');
.type("Select all users across platforms.");
// Cannot call cy.select on div disguised as a dropdown
cy.findByText(/select one/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.contains('button', /edit/i).click();
cy.contains("button", /edit/i).click();
// Label SQL not editable to test
cy.findByLabelText(/name/i)
.click()
.type('{selectall}{backspace}Show all usernames');
.type("{selectall}{backspace}Show all usernames");
cy.findByLabelText(/description/i)
.click()
.type('{selectall}{backspace}Select all usernames on Mac.');
.type("{selectall}{backspace}Select all usernames on Mac.");
cy.findByText(/select one/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 use findByText because delete button under modal
cy.get('.manage-hosts__modal-buttons > .button--alert')
.contains('button', /delete/i)
cy.get(".manage-hosts__modal-buttons > .button--alert")
.contains("button", /delete/i)
.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(() => {
cy.setup();
cy.login();
});
it('Create, edit, and delete a pack successfully', () => {
cy.visit('/packs/manage');
it("Create, edit, and delete a pack successfully", () => {
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)
.click()
.type('Errors and crashes');
.type("Errors and crashes");
cy.findByLabelText(/query pack description/i)
.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();
@ -27,28 +27,28 @@ describe('Pack flow', () => {
cy.findByLabelText(/query pack title/i)
.click()
.type('{selectall}{backspace}Server errors');
.type("{selectall}{backspace}Server errors");
cy.findByLabelText(/query pack description/i)
.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 use findByText because delete button under modal
cy.get('.all-packs-page__modal-btn-wrap > .button--alert')
.contains('button', /delete/i)
cy.get(".all-packs-page__modal-btn-wrap > .button--alert")
.contains("button", /delete/i)
.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(() => {
cy.setup();
cy.login();
});
it('Create, check, edit, and delete a query successfully', () => {
cy.visit('/queries/manage');
it("Create, check, edit, and delete a query successfully", () => {
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
cy.get('.ace_content')
cy.get(".ace_content")
.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
cy.visit('/queries/manage');
cy.visit("/queries/manage");
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()
.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
cy.get('#query-checkbox-1')
.check({ force: true });
cy.get("#query-checkbox-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 use findByText because delete button under modal
cy.get('.manage-queries-page__modal-btn-wrap > .button--alert')
.contains('button', /delete/i)
cy.get(".manage-queries-page__modal-btn-wrap > .button--alert")
.contains("button", /delete/i)
.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
// so sharing some state should be okay and saves a bit of runtime.
before(() => {
cy.setup();
});
it('Logs in and out successfully', () => {
cy.visit('/');
it("Logs in and out successfully", () => {
cy.visit("/");
cy.contains(/forgot password/i);
// Log in
cy.get('input').first()
.type('test@fleetdm.com');
cy.get('input').last()
.type('admin123#');
cy.get('button')
.click();
cy.get("input").first().type("test@fleetdm.com");
cy.get("input").last().type("admin123#");
cy.get("button").click();
// Verify dashboard
cy.url().should('include', '/hosts/manage');
cy.contains('All Hosts');
cy.url().should("include", "/hosts/manage");
cy.contains("All Hosts");
// Log out
cy.findByAltText(/user avatar/i)
.click();
cy.contains('button', 'Sign out')
.click();
cy.findByAltText(/user avatar/i).click();
cy.contains("button", "Sign out").click();
cy.url().should('match', /\/login$/);
cy.url().should("match", /\/login$/);
});
it('Fails login with invalid password', () => {
cy.visit('/');
cy.get('input').first()
.type('test@fleetdm.com');
cy.get('input').last()
.type('bad_password');
cy.get('.button')
.click();
it("Fails login with invalid password", () => {
cy.visit("/");
cy.get("input").first().type("test@fleetdm.com");
cy.get("input").last().type("bad_password");
cy.get(".button").click();
cy.url().should('match', /\/login$/);
cy.contains('Authentication failed');
cy.url().should("match", /\/login$/);
cy.contains("Authentication failed");
});
it('Fails to access authenticated resource', () => {
cy.visit('/hosts/manage');
it("Fails to access authenticated resource", () => {
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(() => {
cy.setup();
});
it('Can login with username/password', () => {
it("Can login with username/password", () => {
cy.login();
cy.setupSSO(enable_idp_login = true);
cy.setupSSO((enable_idp_login = true));
cy.logout();
cy.visit('/');
cy.visit("/");
cy.contains(/forgot password/i);
// Log in
cy.get('input').first()
.type('test@fleetdm.com');
cy.get('input').last()
.type('admin123#');
cy.contains('button', 'Login')
.click();
cy.get("input").first().type("test@fleetdm.com");
cy.get("input").last().type("admin123#");
cy.contains("button", "Login").click();
// Verify dashboard
cy.url().should('include', '/hosts/manage');
cy.contains('All Hosts');
cy.url().should("include", "/hosts/manage");
cy.contains("All Hosts");
// Log out
cy.findByAltText(/user avatar/i)
.click();
cy.contains('button', 'Sign out')
.click();
cy.findByAltText(/user avatar/i).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.setupSSO(enable_idp_login = true);
cy.setupSSO((enable_idp_login = true));
cy.logout();
cy.visit('/');
cy.visit("/");
// Log in
cy.contains('button', 'Sign On With SimpleSAML');
cy.contains("button", "Sign On With SimpleSAML");
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.setupSSO();
cy.logout();
cy.visit('/');
cy.visit("/");
cy.contains('button', 'Sign On With SimpleSAML');
cy.contains("button", "Sign On With SimpleSAML");
cy.loginSSO();
// 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.
beforeEach(() => {
cy.exec('make e2e-reset-db', { timeout: 5000 });
cy.exec("make e2e-reset-db", { timeout: 5000 });
});
it('Completes setup', () => {
cy.visit('/');
cy.url().should('match', /\/setup$/);
it("Completes setup", () => {
cy.visit("/");
cy.url().should("match", /\/setup$/);
cy.contains(/setup/i);
// Page 1
cy.findByPlaceholderText(/username/i)
.type('test');
cy.findByPlaceholderText(/username/i).type("test");
cy.findByPlaceholderText(/^password/i).first()
.type('admin123#');
cy.findByPlaceholderText(/^password/i)
.first()
.type("admin123#");
cy.findByPlaceholderText(/confirm password/i).last()
.type('admin123#');
cy.findByPlaceholderText(/confirm password/i)
.last()
.type("admin123#");
cy.findByPlaceholderText(/email/i)
.type('test@fleetdm.com');
cy.findByPlaceholderText(/email/i).type("test@fleetdm.com");
cy.contains('button:enabled', /next/i)
.click();
cy.contains("button:enabled", /next/i).click();
// Page 2
cy.findByPlaceholderText(/organization name/i)
.type('Fleet Test');
cy.findByPlaceholderText(/organization name/i).type("Fleet Test");
cy.contains('button:enabled', /next/i)
.click();
cy.contains("button:enabled", /next/i).click();
// Page 3
cy.contains('button:enabled', /submit/i)
.click();
cy.contains("button:enabled", /submit/i).click();
// Page 4
cy.contains('button:enabled', /finish/i)
.click();
cy.contains("button:enabled", /finish/i).click();
cy.url().should('match', /\/hosts\/manage$/i);
cy.url().should("match", /\/hosts\/manage$/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
@ -26,85 +26,89 @@ import '@testing-library/cypress/add-commands';
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
Cypress.Commands.add('setup', () => {
cy.exec('make e2e-reset-db e2e-setup', { timeout: 20000 });
Cypress.Commands.add("setup", () => {
cy.exec("make e2e-reset-db e2e-setup", { timeout: 20000 });
});
Cypress.Commands.add('login', (username, password) => {
username ||= 'test';
password ||= 'admin123#';
cy.request('POST', '/api/v1/fleet/login', { username, password })
.then((resp) => {
window.localStorage.setItem('KOLIDE::auth_token', resp.body.token);
});
Cypress.Commands.add("login", (username, password) => {
username ||= "test";
password ||= "admin123#";
cy.request("POST", "/api/v1/fleet/login", { username, password }).then(
(resp) => {
window.localStorage.setItem("KOLIDE::auth_token", resp.body.token);
}
);
});
Cypress.Commands.add('logout', () => {
Cypress.Commands.add("logout", () => {
cy.request({
url: '/api/v1/fleet/logout',
method: 'POST',
url: "/api/v1/fleet/logout",
method: "POST",
body: {},
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 = {
sso_settings: {
enable_sso: true,
enable_sso_idp_login: enable_idp_login,
entity_id: 'https://localhost:8080',
idp_name: 'SimpleSAML',
issuer_uri: 'http://localhost:8080/simplesaml/saml2/idp/SSOService.php',
metadata_url: 'http://localhost:9080/simplesaml/saml2/idp/metadata.php',
entity_id: "https://localhost:8080",
idp_name: "SimpleSAML",
issuer_uri: "http://localhost:8080/simplesaml/saml2/idp/SSOService.php",
metadata_url: "http://localhost:9080/simplesaml/saml2/idp/metadata.php",
},
};
cy.request({
url: '/api/v1/fleet/config',
method: 'PATCH',
url: "/api/v1/fleet/config",
method: "PATCH",
body,
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
// work properly. This is handled automatically by the browser.
cy.request({
method: 'GET',
url: 'http://localhost:9080/simplesaml/saml2/idp/SSOService.php?spentityid=https://localhost:8080',
method: "GET",
url:
"http://localhost:9080/simplesaml/saml2/idp/SSOService.php?spentityid=https://localhost:8080",
followRedirect: false,
}).then((firstResponse) => {
const redirect = firstResponse.headers.location;
cy.request({
method: 'GET',
method: "GET",
url: redirect,
followRedirect: false,
}).then((secondResponse) => {
const el = document.createElement('html');
const el = document.createElement("html");
el.innerHTML = secondResponse.body;
const authState = el.getElementsByTagName('input').namedItem('AuthState').defaultValue;
const authState = el.getElementsByTagName("input").namedItem("AuthState")
.defaultValue;
cy.request({
method: 'POST',
method: "POST",
url: redirect,
body: `username=user1&password=user1pass&AuthState=${authState}`,
form: true,
followRedirect: false,
}).then((finalResponse) => {
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
cy.visit({
url: '/api/v1/fleet/sso/callback',
method: 'POST',
url: "/api/v1/fleet/sso/callback",
method: "POST",
body: {
SAMLResponse: saml,
},

View File

@ -14,7 +14,7 @@
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';
import "./commands";
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

@ -4,7 +4,5 @@
"lib": ["es5", "dom"],
"types": ["cypress", "@testing-library/cypress", "node"]
},
"include": [
"**/*.ts"
]
"include": ["**/*.ts"]
}

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import APP_SETTINGS from 'app_constants/APP_SETTINGS';
import HTTP_STATUS from 'app_constants/HTTP_STATUS';
import PATHS from 'router/paths';
import APP_SETTINGS from "app_constants/APP_SETTINGS";
import HTTP_STATUS from "app_constants/HTTP_STATUS";
import PATHS from "router/paths";
export default {
APP_SETTINGS,

View File

@ -1,13 +1,13 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { noop } from 'lodash';
import classnames from 'classnames';
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { noop } from "lodash";
import classnames from "classnames";
import { authToken } from 'utilities/local';
import { fetchCurrentUser } from 'redux/nodes/auth/actions';
import { getConfig, getEnrollSecret } from 'redux/nodes/app/actions';
import userInterface from 'interfaces/user';
import { authToken } from "utilities/local";
import { fetchCurrentUser } from "redux/nodes/auth/actions";
import { getConfig, getEnrollSecret } from "redux/nodes/app/actions";
import userInterface from "interfaces/user";
export class App extends Component {
static propTypes = {
@ -20,47 +20,36 @@ export class App extends Component {
dispatch: noop,
};
componentWillMount () {
componentWillMount() {
const { dispatch, user } = this.props;
if (!user && authToken()) {
dispatch(fetchCurrentUser())
.catch(() => false);
dispatch(fetchCurrentUser()).catch(() => false);
}
if (user) {
dispatch(getConfig())
.catch(() => false);
dispatch(getEnrollSecret())
.catch(() => false);
dispatch(getConfig()).catch(() => false);
dispatch(getEnrollSecret()).catch(() => false);
}
return false;
}
componentWillReceiveProps (nextProps) {
componentWillReceiveProps(nextProps) {
const { dispatch, user } = nextProps;
if (user && this.props.user !== user) {
dispatch(getConfig())
.catch(() => false);
dispatch(getEnrollSecret())
.catch(() => false);
dispatch(getConfig()).catch(() => false);
dispatch(getEnrollSecret()).catch(() => false);
}
}
render () {
render() {
const { children } = this.props;
const wrapperStyles = classnames(
'wrapper',
);
const wrapperStyles = classnames("wrapper");
return (
<div className={wrapperStyles}>
{children}
</div>
);
return <div className={wrapperStyles}>{children}</div>;
}
}

View File

@ -1,36 +1,33 @@
import { mount } from 'enzyme';
import { mount } from "enzyme";
import ConnectedApp from './App';
import * as authActions from '../../redux/nodes/auth/actions';
import helpers from '../../test/helpers';
import local from '../../utilities/local';
import ConnectedApp from "./App";
import * as authActions from "../../redux/nodes/auth/actions";
import helpers from "../../test/helpers";
import local from "../../utilities/local";
const {
connectedComponent,
reduxMockStore,
} = helpers;
const { connectedComponent, reduxMockStore } = helpers;
describe('App - component', () => {
describe("App - component", () => {
const store = { app: {}, auth: {}, notifications: {} };
const mockStore = reduxMockStore(store);
const component = mount(
connectedComponent(ConnectedApp, { mockStore }),
);
const component = mount(connectedComponent(ConnectedApp, { mockStore }));
afterEach(() => {
local.setItem('auth_token', null);
local.setItem("auth_token", null);
});
it('renders', () => {
it("renders", () => {
expect(component).toBeTruthy();
});
it('loads the current user if there is an auth token but no user', () => {
local.setItem('auth_token', 'ABC123');
it("loads the current user if there is an auth token but no user", () => {
local.setItem("auth_token", "ABC123");
const spy = jest.spyOn(authActions, 'fetchCurrentUser').mockImplementation(() => {
const spy = jest
.spyOn(authActions, "fetchCurrentUser")
.mockImplementation(() => {
return (dispatch) => {
dispatch({ type: 'LOAD_USER_ACTION' });
dispatch({ type: "LOAD_USER_ACTION" });
return Promise.resolve();
};
});
@ -40,34 +37,40 @@ describe('App - component', () => {
expect(spy).toHaveBeenCalled();
});
it('does not load the current user if is it already loaded', () => {
local.setItem('auth_token', 'ABC123');
it("does not load the current user if is it already loaded", () => {
local.setItem("auth_token", "ABC123");
const spy = jest.spyOn(authActions, 'fetchCurrentUser').mockImplementation(() => {
return { type: 'LOAD_USER_ACTION' };
const spy = jest
.spyOn(authActions, "fetchCurrentUser")
.mockImplementation(() => {
return { type: "LOAD_USER_ACTION" };
});
const storeWithUser = {
app: {},
auth: {
user: {
id: 1,
email: 'hi@thegnar.co',
email: "hi@thegnar.co",
},
},
notifications: {},
};
const mockStoreWithUser = reduxMockStore(storeWithUser);
const application = connectedComponent(ConnectedApp, { mockStore: mockStoreWithUser });
const application = connectedComponent(ConnectedApp, {
mockStore: mockStoreWithUser,
});
mount(application);
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();
const spy = jest.spyOn(authActions, 'fetchCurrentUser').mockImplementation(() => {
throw new Error('should not have been called');
const spy = jest
.spyOn(authActions, "fetchCurrentUser")
.mockImplementation(() => {
throw new Error("should not have been called");
});
const application = connectedComponent(ConnectedApp, { mockStore });

View File

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

View File

@ -1,10 +1,10 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { push } from 'react-router-redux';
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { push } from "react-router-redux";
import paths from '../../router/paths';
import userInterface from '../../interfaces/user';
import paths from "../../router/paths";
import userInterface from "../../interfaces/user";
export class AuthenticatedAdminRoutes extends Component {
static propTypes = {
@ -13,8 +13,11 @@ export class AuthenticatedAdminRoutes extends Component {
user: userInterface,
};
componentWillMount () {
const { dispatch, user: { admin } } = this.props;
componentWillMount() {
const {
dispatch,
user: { admin },
} = this.props;
const { HOME } = paths;
if (!admin) {
@ -24,18 +27,14 @@ export class AuthenticatedAdminRoutes extends Component {
return false;
}
render () {
render() {
const { children, user } = this.props;
if (!user) {
return false;
}
return (
<>
{children}
</>
);
return <>{children}</>;
}
}

View File

@ -1,35 +1,31 @@
import { mount } from 'enzyme';
import { mount } from "enzyme";
import ConnectedAdminRoutes from './AuthenticatedAdminRoutes';
import { connectedComponent, reduxMockStore } from '../../test/helpers';
import ConnectedAdminRoutes from "./AuthenticatedAdminRoutes";
import { connectedComponent, reduxMockStore } from "../../test/helpers";
describe('AuthenticatedAdminRoutes - layout', () => {
describe("AuthenticatedAdminRoutes - layout", () => {
const redirectToHomeAction = {
type: '@@router/CALL_HISTORY_METHOD',
type: "@@router/CALL_HISTORY_METHOD",
payload: {
method: 'push',
args: ['/'],
method: "push",
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 storeWithoutAdminUser = { auth: { user } };
const mockStore = reduxMockStore(storeWithoutAdminUser);
mount(
connectedComponent(ConnectedAdminRoutes, { mockStore }),
);
mount(connectedComponent(ConnectedAdminRoutes, { mockStore }));
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 storeWithAdminUser = { auth: { user } };
const mockStore = reduxMockStore(storeWithAdminUser);
mount(
connectedComponent(ConnectedAdminRoutes, { mockStore }),
);
mount(connectedComponent(ConnectedAdminRoutes, { mockStore }));
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 PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { isEqual } from 'lodash';
import { push } from 'react-router-redux';
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { isEqual } from "lodash";
import { push } from "react-router-redux";
import paths from 'router/paths';
import redirectLocationInterface from 'interfaces/redirect_location';
import { setRedirectLocation } from 'redux/nodes/redirectLocation/actions';
import userInterface from 'interfaces/user';
import paths from "router/paths";
import redirectLocationInterface from "interfaces/redirect_location";
import { setRedirectLocation } from "redux/nodes/redirectLocation/actions";
import userInterface from "interfaces/user";
export class AuthenticatedRoutes extends Component {
static propTypes = {
@ -18,7 +18,7 @@ export class AuthenticatedRoutes extends Component {
user: userInterface,
};
componentWillMount () {
componentWillMount() {
const { loading, user } = this.props;
const { redirectToLogin, redirectToPasswordReset } = this;
@ -33,7 +33,7 @@ export class AuthenticatedRoutes extends Component {
return false;
}
componentWillReceiveProps (nextProps) {
componentWillReceiveProps(nextProps) {
if (isEqual(this.props, nextProps)) return false;
const { loading, user } = nextProps;
@ -56,27 +56,23 @@ export class AuthenticatedRoutes extends Component {
dispatch(setRedirectLocation(locationBeforeTransitions));
return dispatch(push(LOGIN));
}
};
redirectToPasswordReset = () => {
const { dispatch } = this.props;
const { RESET_PASSWORD } = paths;
return dispatch(push(RESET_PASSWORD));
}
};
render () {
render() {
const { children, user } = this.props;
if (!user) {
return false;
}
return (
<div>
{children}
</div>
);
return <div>{children}</div>;
}
}

View File

@ -1,32 +1,32 @@
import React from 'react';
import { mount } from 'enzyme';
import { Provider } from 'react-redux';
import React from "react";
import { mount } from "enzyme";
import { Provider } from "react-redux";
import AuthenticatedRoutes from './index';
import helpers from '../../test/helpers';
import AuthenticatedRoutes from "./index";
import helpers from "../../test/helpers";
describe('AuthenticatedRoutes - component', () => {
describe("AuthenticatedRoutes - component", () => {
const redirectToLoginAction = {
type: '@@router/CALL_HISTORY_METHOD',
type: "@@router/CALL_HISTORY_METHOD",
payload: {
method: 'push',
args: ['/login'],
method: "push",
args: ["/login"],
},
};
const redirectToPasswordResetAction = {
type: '@@router/CALL_HISTORY_METHOD',
type: "@@router/CALL_HISTORY_METHOD",
payload: {
method: 'push',
args: ['/login/reset'],
method: "push",
args: ["/login/reset"],
},
};
const renderedText = 'This text was rendered';
const renderedText = "This text was rendered";
const storeWithUser = {
auth: {
loading: false,
user: {
id: 1,
email: 'hi@thegnar.co',
email: "hi@thegnar.co",
force_password_reset: false,
},
},
@ -39,7 +39,7 @@ describe('AuthenticatedRoutes - component', () => {
loading: false,
user: {
id: 1,
email: 'hi@thegnar.co',
email: "hi@thegnar.co",
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 mockStore = reduxMockStore(storeWithUser);
const component = mount(
@ -74,13 +74,13 @@ describe('AuthenticatedRoutes - component', () => {
<AuthenticatedRoutes>
<div>{renderedText}</div>
</AuthenticatedRoutes>
</Provider>,
</Provider>
);
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 mockStore = reduxMockStore(storeWithUserRequiringPwReset);
mount(
@ -88,13 +88,15 @@ describe('AuthenticatedRoutes - component', () => {
<AuthenticatedRoutes>
<div>{renderedText}</div>
</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 mockStore = reduxMockStore(storeWithoutUser);
const component = mount(
@ -102,14 +104,14 @@ describe('AuthenticatedRoutes - component', () => {
<AuthenticatedRoutes>
<div>{renderedText}</div>
</AuthenticatedRoutes>
</Provider>,
</Provider>
);
expect(mockStore.getActions()).toContainEqual(redirectToLoginAction);
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 mockStore = reduxMockStore(storeLoadingUser);
const component = mount(
@ -117,7 +119,7 @@ describe('AuthenticatedRoutes - component', () => {
<AuthenticatedRoutes>
<div>{renderedText}</div>
</AuthenticatedRoutes>
</Provider>,
</Provider>
);
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 PropTypes from 'prop-types';
import React, { Component } from "react";
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 {
static propTypes = {
children: PropTypes.node,
};
render () {
render() {
const { children } = this.props;
return (

View File

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

View File

@ -1,5 +1,5 @@
import React from 'react';
import classnames from 'classnames';
import React from "react";
import classnames from "classnames";
interface IAvatarUserInterface {
gravatarURL: string;
@ -11,23 +11,19 @@ interface IAvatarInterface {
user: IAvatarUserInterface;
}
const baseClass = 'avatar';
const baseClass = "avatar";
class Avatar extends React.Component<IAvatarInterface, null> {
render(): JSX.Element {
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, {
[`${baseClass}--${size}`]: isSmall,
});
const { gravatarURL } = user;
return (
<img
alt="User Avatar"
className={avatarClasses}
src={gravatarURL}
/>
<img alt="User Avatar" className={avatarClasses} src={gravatarURL} />
);
}
}

View File

@ -1,5 +1,6 @@
.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;
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 { noop } from 'lodash';
import React, { Component } from "react";
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 {
componentDidMount () {
componentDidMount() {
const { componentInstance } = this;
const clickHandler = onOutsideClick(componentInstance);
const componentNode = getDOMNode(componentInstance);
this.handleAction = handleClickOutside(clickHandler, componentNode);
global.document.addEventListener('mousedown', this.handleAction);
global.document.addEventListener('touchStart', this.handleAction);
global.document.addEventListener("mousedown", this.handleAction);
global.document.addEventListener("touchStart", this.handleAction);
}
componentWillUnmount () {
global.document.removeEventListener('mousedown', this.handleAction);
global.document.removeEventListener('touchStart', this.handleAction);
componentWillUnmount() {
global.document.removeEventListener("mousedown", this.handleAction);
global.document.removeEventListener("touchStart", this.handleAction);
}
setInstance = (instance) => {
this.componentInstance = instance;
}
};
render () {
render() {
const { setInstance } = this;
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 PropTypes from 'prop-types';
import React from "react";
import PropTypes from "prop-types";
const ClickableTableRow = ({ children, className, onClick, onDoubleClick }) => {
/* 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 */
};

View File

@ -1,7 +1,7 @@
import React from 'react';
import { mount } from 'enzyme';
import React from "react";
import { mount } from "enzyme";
import ClickableTableRow from './index';
import ClickableTableRow from "./index";
const clickSpy = jest.fn();
const dblClickSpy = jest.fn();
@ -11,16 +11,16 @@ const props = {
onDoubleClick: dblClickSpy,
};
describe('ClickableTableRow - component', () => {
it('calls onDblClick when row is double clicked', () => {
describe("ClickableTableRow - component", () => {
it("calls onDblClick when row is double clicked", () => {
const queryRow = mount(<ClickableTableRow {...props} />);
queryRow.find('tr').simulate('doubleclick');
queryRow.find("tr").simulate("doubleclick");
expect(dblClickSpy).toHaveBeenCalled();
});
it('calls onSelect when row is clicked', () => {
it("calls onSelect when row is clicked", () => {
const queryRow = mount(<ClickableTableRow {...props} />);
queryRow.find('tr').simulate('click');
queryRow.find("tr").simulate("click");
expect(clickSpy).toHaveBeenCalled();
});
});

View File

@ -1,9 +1,9 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import helpers from 'components/EmailTokenRedirect/helpers';
import userInterface from 'interfaces/user';
import helpers from "components/EmailTokenRedirect/helpers";
import userInterface from "interfaces/user";
export class EmailTokenRedirect extends Component {
static propTypes = {
@ -12,13 +12,13 @@ export class EmailTokenRedirect extends Component {
user: userInterface,
};
componentWillMount () {
componentWillMount() {
const { dispatch, token, user } = this.props;
return helpers.confirmEmailChange(dispatch, token, user);
}
componentWillReceiveProps (nextProps) {
componentWillReceiveProps(nextProps) {
const { dispatch, token: newToken, user: newUser } = nextProps;
const { token: oldToken, user: oldUser } = this.props;
@ -31,7 +31,7 @@ export class EmailTokenRedirect extends Component {
return false;
}
render () {
render() {
return <div />;
}
}

View File

@ -1,15 +1,20 @@
import React from 'react';
import { mount } from 'enzyme';
import React from "react";
import { mount } from "enzyme";
import { connectedComponent, reduxMockStore } from 'test/helpers';
import ConnectedEmailTokenRedirect, { EmailTokenRedirect } from 'components/EmailTokenRedirect/EmailTokenRedirect';
import Kolide from 'kolide';
import { userStub } from 'test/stubs';
import { connectedComponent, reduxMockStore } from "test/helpers";
import ConnectedEmailTokenRedirect, {
EmailTokenRedirect,
} from "components/EmailTokenRedirect/EmailTokenRedirect";
import Kolide from "kolide";
import { userStub } from "test/stubs";
describe('EmailTokenRedirect - component', () => {
describe("EmailTokenRedirect - component", () => {
beforeEach(() => {
jest.spyOn(Kolide.users, 'confirmEmailChange')
.mockImplementation(() => Promise.resolve({ ...userStub, email: 'new@email.com' }));
jest
.spyOn(Kolide.users, "confirmEmailChange")
.mockImplementation(() =>
Promise.resolve({ ...userStub, email: "new@email.com" })
);
});
const authStore = {
@ -17,39 +22,46 @@ describe('EmailTokenRedirect - component', () => {
user: userStub,
},
};
const token = 'KFBR392';
const token = "KFBR392";
const defaultProps = {
params: {
token,
},
};
describe('componentWillMount', () => {
it('calls the API when a token and user are present', () => {
describe("componentWillMount", () => {
it("calls the API when a token and user are present", () => {
const mockStore = reduxMockStore(authStore);
mount(connectedComponent(ConnectedEmailTokenRedirect, {
mount(
connectedComponent(ConnectedEmailTokenRedirect, {
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: {} });
mount(connectedComponent(ConnectedEmailTokenRedirect, {
mount(
connectedComponent(ConnectedEmailTokenRedirect, {
mockStore,
props: defaultProps,
}));
})
);
expect(Kolide.users.confirmEmailChange).not.toHaveBeenCalled();
});
});
describe('componentWillReceiveProps', () => {
it('calls the API when a user is received', () => {
describe("componentWillReceiveProps", () => {
it("calls the API when a user is received", () => {
const mockStore = reduxMockStore();
const props = { dispatch: mockStore.dispatch, token };
const Component = mount(<EmailTokenRedirect {...props} />);
@ -58,7 +70,10 @@ describe('EmailTokenRedirect - component', () => {
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 { push } from 'react-router-redux';
import { renderFlash } from 'redux/nodes/notifications/actions';
import userActions from 'redux/nodes/entities/users/actions';
import PATHS from "router/paths";
import { push } from "react-router-redux";
import { renderFlash } from "redux/nodes/notifications/actions";
import userActions from "redux/nodes/entities/users/actions";
const confirmEmailChange = (dispatch, token, user) => {
if (user && token) {
return dispatch(userActions.confirmEmailChange(user, token))
.then(() => {
dispatch(push(PATHS.USER_SETTINGS));
dispatch(renderFlash('success', 'Email updated successfully!'));
dispatch(renderFlash("success", "Email updated successfully!"));
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 Kolide from 'kolide';
import { userStub } from 'test/stubs';
import helpers from "components/EmailTokenRedirect/helpers";
import Kolide from "kolide";
import { userStub } from "test/stubs";
describe('EmailTokenRedirect - helpers', () => {
describe('#confirmEmailChage', () => {
describe("EmailTokenRedirect - helpers", () => {
describe("#confirmEmailChage", () => {
const { confirmEmailChange } = helpers;
const token = 'KFBR392';
const token = "KFBR392";
const authStore = {
auth: {
user: userStub,
},
};
describe('successfully dispatching the confirmEmailChange action', () => {
describe("successfully dispatching the confirmEmailChange action", () => {
beforeEach(() => {
jest.spyOn(Kolide.users, 'confirmEmailChange')
.mockImplementation(() => Promise.resolve({ ...userStub, email: 'new@email.com' }));
jest
.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 { dispatch } = mockStore;
return confirmEmailChange(dispatch, userStub, token)
.then(() => {
return confirmEmailChange(dispatch, userStub, token).then(() => {
const dispatchedActions = mockStore.getActions();
expect(dispatchedActions).toContainEqual({
type: '@@router/CALL_HISTORY_METHOD',
type: "@@router/CALL_HISTORY_METHOD",
payload: {
method: 'push',
args: ['/profile'],
method: "push",
args: ["/profile"],
},
});
});
});
});
describe('unsuccessfully dispatching the confirmEmailChange action', () => {
describe("unsuccessfully dispatching the confirmEmailChange action", () => {
beforeEach(() => {
const errors = [
{
name: 'base',
reason: 'Unable to confirm your email address',
name: "base",
reason: "Unable to confirm your email address",
},
];
const errorResponse = {
status: 422,
message: {
message: 'Unable to confirm email address',
message: "Unable to confirm email address",
errors,
},
};
jest.spyOn(Kolide.users, 'confirmEmailChange')
jest
.spyOn(Kolide.users, "confirmEmailChange")
.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 { dispatch } = mockStore;
@ -69,18 +72,18 @@ describe('EmailTokenRedirect - helpers', () => {
const dispatchedActions = mockStore.getActions();
expect(dispatchedActions).toContainEqual({
type: '@@router/CALL_HISTORY_METHOD',
type: "@@router/CALL_HISTORY_METHOD",
payload: {
method: 'push',
args: ['/login'],
method: "push",
args: ["/login"],
},
});
});
});
});
describe('when the user or token are not present', () => {
it('does not dispatch any actions when the user is not present', (done) => {
describe("when the user or token are not present", () => {
it("does not dispatch any actions when the user is not present", (done) => {
const mockStore = reduxMockStore(authStore);
const { dispatch } = mockStore;
@ -95,7 +98,7 @@ describe('EmailTokenRedirect - helpers', () => {
.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 { 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 PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { noop } from 'lodash';
import { push } from 'react-router-redux';
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { noop } from "lodash";
import { push } from "react-router-redux";
import paths from 'router/paths';
import userInterface from 'interfaces/user';
import paths from "router/paths";
import userInterface from "interfaces/user";
export default (WrappedComponent) => {
class EnsureUnauthenticated extends Component {
@ -19,7 +19,7 @@ export default (WrappedComponent) => {
dispatch: noop,
};
componentWillMount () {
componentWillMount() {
const { currentUser, dispatch } = this.props;
const { HOME } = paths;
@ -30,7 +30,7 @@ export default (WrappedComponent) => {
return false;
}
componentWillReceiveProps (nextProps) {
componentWillReceiveProps(nextProps) {
const { currentUser, dispatch } = nextProps;
const { HOME } = paths;
@ -41,7 +41,7 @@ export default (WrappedComponent) => {
return false;
}
render () {
render() {
const { currentUser, isLoadingUser } = this.props;
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 ReactTooltip from 'react-tooltip';
import React from "react";
import ReactTooltip from "react-tooltip";
interface IIconToolTipProps {
text: string;
@ -12,13 +12,26 @@ const IconToolTip = (props: IIconToolTipProps): JSX.Element => {
return (
<div className="icon-tooltip">
<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" />
<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>
</span>
{/* same colour as $core-dark-blue-grey */}
<ReactTooltip effect={'solid'} data-html={isHtml} backgroundColor={'#3e4771'} />
<ReactTooltip
effect={"solid"}
data-html={isHtml}
backgroundColor={"#3e4771"}
/>
</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 PropTypes from 'prop-types';
import AceEditor from 'react-ace';
import classnames from 'classnames';
import 'brace/mode/sql';
import 'brace/ext/linking';
import 'brace/ext/language_tools';
import React, { Component } from "react";
import PropTypes from "prop-types";
import AceEditor from "react-ace";
import classnames from "classnames";
import "brace/mode/sql";
import "brace/ext/linking";
import "brace/ext/language_tools";
import './mode';
import './theme';
import "./mode";
import "./theme";
const baseClass = 'kolide-ace';
const baseClass = "kolide-ace";
class KolideAce extends Component {
static propTypes = {
@ -30,7 +30,7 @@ class KolideAce extends Component {
static defaultProps = {
fontSize: 14,
name: 'query-editor',
name: "query-editor",
showGutter: true,
wrapEnabled: false,
};
@ -42,10 +42,8 @@ class KolideAce extends Component {
[`${baseClass}__label--error`]: error,
});
return (
<p className={labelClassName}>{error || label}</p>
);
}
return <p className={labelClassName}>{error || label}</p>;
};
renderHint = () => {
const { hint } = this.props;
@ -55,9 +53,9 @@ class KolideAce extends Component {
}
return false;
}
};
render () {
render() {
const {
error,
fontSize,
@ -78,7 +76,7 @@ class KolideAce extends Component {
});
const fixHotkeys = (editor) => {
editor.commands.removeCommands(['gotoline', 'find']);
editor.commands.removeCommands(["gotoline", "find"]);
onLoad && onLoad(editor);
};
@ -104,11 +102,13 @@ class KolideAce extends Component {
value={value}
width="100%"
wrapEnabled={wrapEnabled}
commands={[{
name: 'commandName',
bindKey: { win: 'Ctrl-Enter', mac: 'Ctrl-Enter' },
commands={[
{
name: "commandName",
bindKey: { win: "Ctrl-Enter", mac: "Ctrl-Enter" },
exec: handleSubmit,
}]}
},
]}
/>
{renderHint()}
</div>

View File

@ -29,7 +29,7 @@
color: $core-blue;
background-color: $ui-light-grey;
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,76 +1,96 @@
/* 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(
"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 SqlHighlightRules = acequire("./sql_highlight_rules").SqlHighlightRules;
var KolideHighlightRules = function() {
var keywords = (
var KolideHighlightRules = function () {
var keywords =
"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|" +
"foreign|not|references|default|null|inner|cross|natural|database|drop|grant"
);
"foreign|not|references|default|null|inner|cross|natural|database|drop|grant";
var builtinConstants = (
"true|false"
);
var builtinConstants = "true|false";
var builtinFunctions = (
var builtinFunctions =
"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|" +
"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,
"keyword": keywords,
keyword: keywords,
"constant.language": builtinConstants,
"storage.type": dataTypes,
}, "identifier", true);
},
"identifier",
true
);
this.$rules = {
"start" : [{
token : "comment",
regex : "--.*$"
}, {
token : "comment",
start : "/\\*",
end : "\\*/"
}, {
token : "string", // " string
regex : '".*?"'
}, {
token : "string", // ' string
regex : "'.*?'"
}, {
token : "constant.numeric", // float
regex : "[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b"
}, {
token : keywordMapper,
regex : "[a-zA-Z_$][a-zA-Z0-9_$]*\\b"
}, {
token : "keyword.operator",
regex : "\\+|\\-|\\/|\\/\\/|%|<@>|@>|<@|&|\\^|~|<|>|<=|=>|==|!=|<>|="
}, {
token : "paren.lparen",
regex : "[\\(]"
}, {
token : "paren.rparen",
regex : "[\\)]"
}, {
token : "text",
regex : "\\s+"
}]
start: [
{
token: "comment",
regex: "--.*$",
},
{
token: "comment",
start: "/\\*",
end: "\\*/",
},
{
token: "string", // " string
regex: '".*?"',
},
{
token: "string", // ' string
regex: "'.*?'",
},
{
token: "constant.numeric", // float
regex: "[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b",
},
{
token: keywordMapper,
regex: "[a-zA-Z_$][a-zA-Z0-9_$]*\\b",
},
{
token: "keyword.operator",
regex:
"\\+|\\-|\\/|\\/\\/|%|<@>|@>|<@|&|\\^|~|<|>|<=|=>|==|!=|<>|=",
},
{
token: "paren.lparen",
regex: "[\\(]",
},
{
token: "paren.rparen",
regex: "[\\)]",
},
{
token: "text",
regex: "\\s+",
},
],
};
this.normalizeRules();
@ -79,27 +99,40 @@ ace.define("ace/mode/kolide_highlight_rules",["require","exports","module","ace/
oop.inherits(KolideHighlightRules, SqlHighlightRules);
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) {
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";
var oop = acequire("../lib/oop");
var TextMode = acequire("./sql").Mode;
var KolideHighlightRules = acequire("./kolide_highlight_rules").KolideHighlightRules;
var KolideHighlightRules = acequire("./kolide_highlight_rules")
.KolideHighlightRules;
var Range = acequire("../range").Range;
var Mode = function() {
var Mode = function () {
this.HighlightRules = KolideHighlightRules;
};
oop.inherits(Mode, TextMode);
(function() {
(function () {
this.lineCommentStart = "--";
this.$id = "ace/mode/kolide";
}).call(Mode.prototype);
}.call(Mode.prototype));
exports.Mode = Mode;
});
}
);

View File

@ -1,10 +1,10 @@
.ace_editor.ace-kolide {
font-family: 'SourceCodePro', monospace;
font-family: "SourceCodePro", monospace;
font-size: 14px;
background-color: #FAFAFA;
background-color: #fafafa;
color: #66696f;
border-radius: 4px;
border: solid 1px #DBE3E5;
border: solid 1px #dbe3e5;
line-height: 24px;
}
@ -33,7 +33,7 @@
background: #fff;
color: #c38dec;
z-index: 1;
border-right: solid 1px #E3E3E3;
border-right: solid 1px #e3e3e3;
}
.ace-kolide .ace_gutter-active-line {
@ -55,12 +55,12 @@
}
.ace-kolide .ace_cursor {
color: #aeafad
color: #aeafad;
}
/* Hide cursor in read-only mode */
.ace-kolide .ace_hidden-cursors {
opacity:0
opacity: 0;
}
.ace-kolide .ace_marker-layer .ace_selection {
@ -72,20 +72,20 @@
}
.ace-kolide .ace_marker-layer .ace_step {
background: rgb(255, 255, 0)
background: rgb(255, 255, 0);
}
.ace-kolide .ace_marker-layer .ace_bracket {
margin: -1px 0 0 -1px;
border: 1px solid #d1d1d1
border: 1px solid #d1d1d1;
}
.ace-kolide .ace_marker-layer .ace_selected-word {
border: 1px solid #d6d6d6
border: 1px solid #d6d6d6;
}
.ace-kolide .ace_invisible {
color: #d1d1d1
color: #d1d1d1;
}
.ace-kolide .ace_keyword {
@ -93,7 +93,7 @@
font-weight: 600;
}
.ace-kolide .ace_osquery-token{
.ace-kolide .ace_osquery-token {
border-radius: 3px;
background-color: #ae6ddf;
color: #ffffff;
@ -111,11 +111,11 @@
.ace-kolide .ace_storage,
.ace-kolide .ace_storage.ace_type,
.ace-kolide .ace_support.ace_type {
color: #8959a8
color: #8959a8;
}
.ace-kolide .ace_keyword.ace_operator {
color: #3e999f
color: #3e999f;
}
.ace-kolide .ace_constant.ace_character,
@ -124,43 +124,43 @@
.ace-kolide .ace_keyword.ace_other.ace_unit,
.ace-kolide .ace_support.ace_constant,
.ace-kolide .ace_variable.ace_parameter {
color: #f5871f
color: #f5871f;
}
.ace-kolide .ace_constant.ace_other {
color: #666969
color: #666969;
}
.ace-kolide .ace_invalid {
color: #ffffff;
background-color: #c82829
background-color: #c82829;
}
.ace-kolide .ace_invalid.ace_deprecated {
color: #ffffff;
background-color: #ae6ddf
background-color: #ae6ddf;
}
.ace-kolide .ace_fold {
background-color: #4271ae;
border-color: #4d4d4c
border-color: #4d4d4c;
}
.ace-kolide .ace_entity.ace_name.ace_function,
.ace-kolide .ace_support.ace_function,
.ace-kolide .ace_variable {
color: #4271ae
color: #4271ae;
}
.ace-kolide .ace_support.ace_class,
.ace-kolide .ace_support.ace_type {
color: #c99e00
color: #c99e00;
}
.ace-kolide .ace_heading,
.ace-kolide .ace_markup.ace_heading,
.ace-kolide .ace_string {
color: #4fd061
color: #4fd061;
}
.ace-kolide .ace_entity.ace_name.ace_tag,
@ -168,13 +168,14 @@
.ace-kolide .ace_meta.ace_tag,
.ace-kolide .ace_string.ace_regexp,
.ace-kolide .ace_variable {
color: #c82829
color: #c82829;
}
.ace-kolide .ace_comment {
color: #8e908c
color: #8e908c;
}
.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 */
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.cssText = require("./theme.css");
var dom = acequire("../lib/dom");
dom.importCssString(exports.cssText, exports.cssClass);
});
var dom = acequire("../lib/dom");
dom.importCssString(exports.cssText, exports.cssClass);
}
);

View File

@ -1,10 +1,10 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import React, { Component } from "react";
import PropTypes from "prop-types";
import { connect } from "react-redux";
import { hideBackgroundImage } from 'redux/nodes/app/actions';
import { ssoSettings } from 'redux/nodes/auth/actions';
import LoginPage from 'pages/LoginPage';
import { hideBackgroundImage } from "redux/nodes/app/actions";
import { ssoSettings } from "redux/nodes/auth/actions";
import LoginPage from "pages/LoginPage";
export class LoginRoutes extends Component {
static propTypes = {
@ -16,22 +16,21 @@ export class LoginRoutes extends Component {
token: PropTypes.string,
};
componentWillMount () {
componentWillMount() {
const { dispatch } = this.props;
dispatch(ssoSettings())
.catch(() => false);
dispatch(ssoSettings()).catch(() => false);
dispatch(hideBackgroundImage);
}
componentWillUnmount () {
componentWillUnmount() {
const { dispatch } = this.props;
dispatch(hideBackgroundImage);
}
render () {
render() {
const {
children,
isResetPassPage,
@ -42,24 +41,27 @@ export class LoginRoutes extends Component {
return (
<div className="login-routes">
{children ||
{children || (
<LoginPage
pathname={pathname}
token={token}
isForgotPassPage={isForgotPassPage}
isResetPassPage={isResetPassPage}
/>}
/>
)}
</div>
);
}
}
const mapStateToProps = (state, ownProps) => {
const { location: { pathname, query } } = ownProps;
const {
location: { pathname, query },
} = ownProps;
const { token } = query;
const isForgotPassPage = pathname.endsWith('/login/forgot');
const isResetPassPage = pathname.endsWith('/login/reset');
const isForgotPassPage = pathname.endsWith("/login/forgot");
const isResetPassPage = pathname.endsWith("/login/reset");
return {
isForgotPassPage,

View File

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

View File

@ -1,5 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import React from "react";
import PropTypes from "prop-types";
const NumberPill = ({ number }) => {
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 PropTypes from 'prop-types';
import React, { PureComponent } from "react";
import PropTypes from "prop-types";
import Button from 'components/buttons/Button';
import KolideIcon from 'components/icons/KolideIcon';
import Button from "components/buttons/Button";
import KolideIcon from "components/icons/KolideIcon";
const baseClass = 'pagination';
const baseClass = "pagination";
class Pagination extends PureComponent {
static propTypes = {
@ -16,27 +16,34 @@ class Pagination extends PureComponent {
disablePrev = () => {
return this.props.currentPage === 0;
}
};
disableNext = () => {
// NOTE: not sure why resultsOnCurrentPage is getting assigned undefined.
// but this seems to work when there is no data in the table.
return this.props.resultsOnCurrentPage === undefined ||
this.props.resultsOnCurrentPage < this.props.resultsPerPage;
}
return (
this.props.resultsOnCurrentPage === undefined ||
this.props.resultsOnCurrentPage < this.props.resultsPerPage
);
};
render () {
const {
currentPage,
onPaginationChange,
} = this.props;
render() {
const { currentPage, onPaginationChange } = this.props;
return (
<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
</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" />
</Button>
</div>

View File

@ -1,5 +1,4 @@
.pagination {
&__pager-wrap {
display: flex;
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 PropTypes from 'prop-types';
import { Link } from 'react-router';
import classnames from 'classnames';
import React, { Component } from "react";
import PropTypes from "prop-types";
import { Link } from "react-router";
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 {
static propTypes = {
@ -17,7 +17,7 @@ class StackedWhiteBoxes extends Component {
previousLocation: PropTypes.string,
};
constructor (props) {
constructor(props) {
super(props);
this.state = {
@ -27,13 +27,13 @@ class StackedWhiteBoxes extends Component {
};
}
componentWillMount () {
componentWillMount() {
this.setState({
isLoading: true,
});
}
componentDidMount () {
componentDidMount() {
const { didLoad } = this;
didLoad();
@ -45,7 +45,7 @@ class StackedWhiteBoxes extends Component {
isLoading: false,
isLoaded: true,
});
}
};
nowLeaving = (evt) => {
const { window } = global;
@ -59,14 +59,13 @@ class StackedWhiteBoxes extends Component {
});
if (previousLocation) {
window.setTimeout(
() => { onLeave(previousLocation); },
300,
);
window.setTimeout(() => {
onLeave(previousLocation);
}, 300);
}
return false;
}
};
renderBackButton = () => {
const { previousLocation } = this.props;
@ -76,12 +75,16 @@ class StackedWhiteBoxes extends Component {
return (
<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" />
</Link>
</div>
);
}
};
renderHeader = () => {
const { headerText } = this.props;
@ -91,26 +94,18 @@ class StackedWhiteBoxes extends Component {
<p className={`${baseClass}__header-text`}>{headerText}</p>
</div>
);
}
};
render () {
render() {
const { children, className, leadText } = this.props;
const {
isLoading,
isLoaded,
isLeaving,
} = this.state;
const { isLoading, isLoaded, isLeaving } = this.state;
const { renderBackButton, renderHeader } = this;
const boxClass = classnames(
baseClass,
className,
{
const boxClass = classnames(baseClass, className, {
[`${baseClass}--loading`]: isLoading,
[`${baseClass}--loaded`]: isLoaded,
[`${baseClass}--leaving`]: isLeaving,
},
);
});
return (
<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 PropTypes from 'prop-types';
import classnames from 'classnames';
import React, { Component } from "react";
import PropTypes from "prop-types";
import classnames from "classnames";
import Dropdown from 'components/forms/fields/Dropdown';
import EditUserForm from 'components/forms/admin/EditUserForm';
import Modal from 'components/modals/Modal';
import helpers from 'components/UserRow/helpers';
import userInterface from 'interfaces/user';
import Dropdown from "components/forms/fields/Dropdown";
import EditUserForm from "components/forms/admin/EditUserForm";
import Modal from "components/modals/Modal";
import helpers from "components/UserRow/helpers";
import userInterface from "interfaces/user";
class UserRow extends Component {
static propTypes = {
@ -30,28 +30,32 @@ class UserRow extends Component {
const { onToggleEditUser, user } = this.props;
return onToggleEditUser(user);
}
};
onEditUser = (updatedUser) => {
const { onEditUser, user } = this.props;
return onEditUser(user, updatedUser);
}
};
onUserActionSelect = (action) => {
const { onSelect, onToggleEditUser, user } = this.props;
if (action === 'modify_details') {
if (action === "modify_details") {
return onToggleEditUser(user);
}
return onSelect(user, action);
}
};
renderCTAs = () => {
const { isCurrentUser, isInvite, user } = this.props;
const { onUserActionSelect } = this;
const userActionOptions = helpers.userActionOptions(isCurrentUser, user, isInvite);
const userActionOptions = helpers.userActionOptions(
isCurrentUser,
user,
isInvite
);
return (
<Dropdown
@ -59,10 +63,10 @@ class UserRow extends Component {
options={userActionOptions}
placeholder="Actions..."
onChange={onUserActionSelect}
className={isInvite ? 'revoke-invite' : ''}
className={isInvite ? "revoke-invite" : ""}
/>
);
}
};
renderEditUserModal = (isEditing) => {
const { userErrors, isCurrentUser, user } = this.props;
@ -70,10 +74,7 @@ class UserRow extends Component {
if (isEditing) {
return (
<Modal
title="Edit user"
onExit={onToggleEditing}
>
<Modal title="Edit user" onExit={onToggleEditing}>
<EditUserForm
isCurrentUser={isCurrentUser}
onCancel={onToggleEditing}
@ -85,37 +86,25 @@ class UserRow extends Component {
);
}
return false;
}
};
render () {
render() {
const { isInvite, user, isEditing } = this.props;
const {
admin,
email,
name,
position,
username,
} = user;
const { admin, email, name, position, username } = user;
const { renderCTAs, renderEditUserModal } = this;
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(
`${baseClass}__status`,
`${baseClass}__status--${statusLabel.toLowerCase()}`,
`${baseClass}__status--${statusLabel.toLowerCase()}`
);
return (
<tr key={`user-${user.id}-table`}>
<td
className={`${baseClass}__username`}
>
{username}
</td>
<td className={statusClassName}>
{statusLabel}
</td>
<td className={`${baseClass}__username`}>{username}</td>
<td className={statusClassName}>{statusLabel}</td>
<td>{name}</td>
<td>{email}</td>
<td>{userLabel}</td>

View File

@ -1,12 +1,12 @@
import React from 'react';
import { mount } from 'enzyme';
import { noop } from 'lodash';
import React from "react";
import { mount } from "enzyme";
import { noop } from "lodash";
import UserRow from 'components/UserRow/UserRow';
import { fillInFormInput } from 'test/helpers';
import { userStub } from 'test/stubs';
import UserRow from "components/UserRow/UserRow";
import { fillInFormInput } from "test/helpers";
import { userStub } from "test/stubs";
describe('UserRow - component', () => {
describe("UserRow - component", () => {
const defaultInviteProps = {
isCurrentUser: false,
isEditing: false,
@ -23,116 +23,97 @@ describe('UserRow - component', () => {
user: userStub,
};
it('renders a user row', () => {
it("renders a user row", () => {
const props = { ...defaultUserProps, user: userStub };
const component = mount(<UserRow {...props} />);
expect(component.length).toEqual(1);
expect(component.find('Dropdown').length).toEqual(1);
expect(component.find("Dropdown").length).toEqual(1);
});
it(
'calls the onToggleEditUser prop with the user when Modify Details is selected',
() => {
it("calls the onToggleEditUser prop with the user when Modify Details is selected", () => {
const spy = jest.fn();
const props = { ...defaultUserProps, onToggleEditUser: spy };
const component = mount(<UserRow {...props} />);
component.find('.Select-control').simulate('keyDown', { keyCode: 40 });
component.find('[aria-label="Modify Details"]').simulate('mousedown');
component.find(".Select-control").simulate("keyDown", { keyCode: 40 });
component.find('[aria-label="Modify Details"]').simulate("mousedown");
expect(spy).toHaveBeenCalledWith(userStub);
},
);
});
it(
'calls the onSelect prop with the user when Promote User is selected',
() => {
it("calls the onSelect prop with the user when Promote User is selected", () => {
const spy = jest.fn();
const props = { ...defaultUserProps, onSelect: spy };
const component = mount(<UserRow {...props} />);
component.find('.Select-control').simulate('keyDown', { keyCode: 40 });
component.find('[aria-label="Promote User"]').simulate('mousedown');
component.find(".Select-control").simulate("keyDown", { keyCode: 40 });
component.find('[aria-label="Promote User"]').simulate("mousedown");
expect(spy).toHaveBeenCalledWith(userStub, 'promote_user');
},
);
expect(spy).toHaveBeenCalledWith(userStub, "promote_user");
});
it(
'calls the onSelect prop with the user when Demote User is selected',
() => {
it("calls the onSelect prop with the user when Demote User is selected", () => {
const adminUser = { ...userStub, admin: true };
const spy = jest.fn();
const props = { ...defaultUserProps, onSelect: spy, user: adminUser };
const component = mount(<UserRow {...props} />);
component.find('.Select-control').simulate('keyDown', { keyCode: 40 });
component.find('[aria-label="Demote User"]').simulate('mousedown');
component.find(".Select-control").simulate("keyDown", { keyCode: 40 });
component.find('[aria-label="Demote User"]').simulate("mousedown");
expect(spy).toHaveBeenCalledWith(adminUser, 'demote_user');
},
);
expect(spy).toHaveBeenCalledWith(adminUser, "demote_user");
});
it(
'calls the onSelect prop with the user when Disable Account is selected',
() => {
it("calls the onSelect prop with the user when Disable Account is selected", () => {
const spy = jest.fn();
const props = { ...defaultUserProps, onSelect: spy };
const component = mount(<UserRow {...props} />);
component.find('.Select-control').simulate('keyDown', { keyCode: 40 });
component.find('[aria-label="Disable Account"]').simulate('mousedown');
component.find(".Select-control").simulate("keyDown", { keyCode: 40 });
component.find('[aria-label="Disable Account"]').simulate("mousedown");
expect(spy).toHaveBeenCalledWith(userStub, 'disable_account');
},
);
expect(spy).toHaveBeenCalledWith(userStub, "disable_account");
});
it(
'calls the onSelect prop with the user when Enable Account is selected',
() => {
it("calls the onSelect prop with the user when Enable Account is selected", () => {
const disabledUser = { ...userStub, enabled: false };
const spy = jest.fn();
const props = { ...defaultUserProps, onSelect: spy, user: disabledUser };
const component = mount(<UserRow {...props} />);
component.find('.Select-control').simulate('keyDown', { keyCode: 40 });
component.find('[aria-label="Enable Account"]').simulate('mousedown');
component.find(".Select-control").simulate("keyDown", { keyCode: 40 });
component.find('[aria-label="Enable Account"]').simulate("mousedown");
expect(spy).toHaveBeenCalledWith(disabledUser, 'enable_account');
},
);
expect(spy).toHaveBeenCalledWith(disabledUser, "enable_account");
});
it(
'calls the onSelect prop with the user when Require Password Reset is selected',
() => {
it("calls the onSelect prop with the user when Require Password Reset is selected", () => {
const spy = jest.fn();
const props = { ...defaultUserProps, onSelect: spy };
const component = mount(<UserRow {...props} />);
component.find('.Select-control').simulate('keyDown', { keyCode: 40 });
component.find('[aria-label="Require Password Reset"]').simulate('mousedown');
component.find(".Select-control").simulate("keyDown", { keyCode: 40 });
component
.find('[aria-label="Require Password Reset"]')
.simulate("mousedown");
expect(spy).toHaveBeenCalledWith(userStub, 'reset_password');
},
);
expect(spy).toHaveBeenCalledWith(userStub, "reset_password");
});
it(
'calls the onEditUser prop with the user and updated user when the edit form is submitted',
() => {
it("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 component = mount(<UserRow {...props} />);
const form = component.find('EditUserForm');
const form = component.find("EditUserForm");
expect(form.length).toEqual(1);
const nameInput = form.find({ name: 'name' }).find('input');
const nameInput = form.find({ name: "name" }).find("input");
fillInFormInput(nameInput, 'Foobar');
form.simulate('submit');
fillInFormInput(nameInput, "Foobar");
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 {
&__actions {
.form-field--dropdown {
margin-bottom: 0;
@ -34,7 +33,7 @@
&:before {
background-color: $success;
border-radius: 100%;
content: ' ';
content: " ";
display: inline-block;
height: 8px;
margin-right: 8px;
@ -46,7 +45,7 @@
&:before {
background-color: $warning;
border-radius: 100%;
content: ' ';
content: " ";
display: inline-block;
height: 8px;
margin-right: 8px;
@ -58,7 +57,7 @@
&:before {
background-color: $core-red;
border-radius: 100%;
content: ' ';
content: " ";
display: inline-block;
height: 8px;
margin-right: 8px;

View File

@ -1,33 +1,43 @@
const userActionOptions = (isCurrentUser, user, invite) => {
const inviteActions = [
{ disabled: false, label: 'Revoke Invitation', value: 'revert_invitation' },
{ disabled: false, label: "Revoke Invitation", value: "revert_invitation" },
];
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
? { disabled: isCurrentUser, label: 'Demote User', value: 'demote_user' }
: { disabled: false, label: 'Promote User', value: 'promote_user' };
? { disabled: isCurrentUser, label: "Demote User", value: "demote_user" }
: { disabled: false, label: "Promote User", value: "promote_user" };
if (invite) return inviteActions;
const result = [
userEnableAction,
userPromotionAction,
];
const result = [userEnableAction, userPromotionAction];
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;
};
const userStatusLabel = (user, invite) => {
if (invite) {
return 'Invited';
return "Invited";
}
return user.enabled ? 'Active' : 'Disabled';
return user.enabled ? "Active" : "Disabled";
};
export default { userActionOptions, userStatusLabel };

View File

@ -1,74 +1,102 @@
import helpers from 'components/UserRow/helpers';
import { userStub } from 'test/stubs';
import helpers from "components/UserRow/helpers";
import { userStub } from "test/stubs";
describe('UserRow - helpers', () => {
describe('#userActionOptions', () => {
describe("UserRow - helpers", () => {
describe("#userActionOptions", () => {
const { userActionOptions } = helpers;
it('returns the correct options for invites', () => {
it("returns the correct options for invites", () => {
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([
{ disabled: false, label: 'Disable Account', value: 'disable_account' },
{ 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: "Disable Account", value: "disable_account" },
{ 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" },
]);
});
it('returns the correct options for a disabled user', () => {
it("returns the correct options for a disabled user", () => {
const disabledUser = { ...userStub, enabled: false };
expect(userActionOptions(false, disabledUser, false)).toEqual([
{ disabled: false, label: 'Enable Account', value: 'enable_account' },
{ 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: "Enable Account", value: "enable_account" },
{ 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" },
]);
});
it('returns the correct options for an admin', () => {
it("returns the correct options for an admin", () => {
const adminUser = { ...userStub, admin: true };
expect(userActionOptions(false, adminUser, false)).toEqual([
{ disabled: false, label: 'Disable Account', value: 'disable_account' },
{ 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: "Disable Account", value: "disable_account" },
{ 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" },
]);
});
it('returns the correct options for the current user', () => {
it("returns the correct options for the current user", () => {
const adminUser = { ...userStub, admin: true };
expect(userActionOptions(true, adminUser, false)).toEqual([
{ disabled: true, label: 'Disable Account', value: 'disable_account' },
{ 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: true, label: "Disable Account", value: "disable_account" },
{ 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" },
]);
});
});
describe('#userStatusLabel', () => {
describe("#userStatusLabel", () => {
const { userStatusLabel } = helpers;
it('returns the correct options for an invite', () => {
expect(userStatusLabel(userStub, true)).toEqual('Invited');
it("returns the correct options for an invite", () => {
expect(userStatusLabel(userStub, true)).toEqual("Invited");
});
it('returns the correct options for an enabled user', () => {
expect(userStatusLabel(userStub, false)).toEqual('Active');
it("returns the correct options for an enabled user", () => {
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 };
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 PropTypes from 'prop-types';
import classnames from 'classnames';
import React from "react";
import PropTypes from "prop-types";
import classnames from "classnames";
const baseClass = 'warning-banner';
const baseClass = "warning-banner";
const WarningBanner = ({ children, className, shouldShowWarning }) => {
if (!shouldShowWarning) {

View File

@ -1,11 +1,11 @@
import React from 'react';
import { shallow } from 'enzyme';
import React from "react";
import { shallow } from "enzyme";
import WarningBanner from 'components/WarningBanner/WarningBanner';
import WarningBanner from "components/WarningBanner/WarningBanner";
describe('WarningBanner - component', () => {
it('renders empty when disabled', () => {
const props = { shouldShowWarning: false, message: 'message' };
describe("WarningBanner - component", () => {
it("renders empty when disabled", () => {
const props = { shouldShowWarning: false, message: "message" };
const component = shallow(<WarningBanner {...props} />);
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 PropTypes from 'prop-types';
import AceEditor from 'react-ace';
import classnames from 'classnames';
import React, { Component } from "react";
import PropTypes from "prop-types";
import AceEditor from "react-ace";
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 {
static propTypes = {
@ -15,20 +15,17 @@ class YamlAce extends Component {
onChange: PropTypes.func.isRequired,
value: PropTypes.string,
wrapperClassName: PropTypes.string,
}
};
renderLabel = () => {
const { error, label } = this.props;
const labelClassName = classnames(
`${baseClass}__label`,
{ [`${baseClass}__label--error`]: error },
);
const labelClassName = classnames(`${baseClass}__label`, {
[`${baseClass}__label--error`]: error,
});
return (
<p className={labelClassName}>{error || label}</p>
);
}
return <p className={labelClassName}>{error || label}</p>;
};
render() {
const {

View File

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

View File

@ -1,7 +1,7 @@
import React from 'react';
import classnames from 'classnames';
import React from "react";
import classnames from "classnames";
const baseClass = 'button';
const baseClass = "button";
interface IButtonProps {
autofocus?: boolean;
@ -12,7 +12,7 @@ interface IButtonProps {
onClick: (evt: React.MouseEvent<HTMLButtonElement>) => void;
size?: string;
tabIndex?: number;
type?: 'button' | 'submit' | 'reset';
type?: "button" | "submit" | "reset";
title?: string;
variant?: string;
}
@ -27,14 +27,16 @@ interface Inputs {
class Button extends React.Component<IButtonProps, IButtonState> {
static defaultProps = {
block: false,
size: '',
type: 'button',
variant: 'default',
size: "",
type: "button",
variant: "default",
};
componentDidMount(): void {
const { autofocus } = this.props;
const { inputs: { button } } = this;
const {
inputs: { button },
} = this;
if (autofocus && button) {
button.focus();
@ -45,7 +47,7 @@ class Button extends React.Component<IButtonProps, IButtonState> {
this.inputs.button = button;
return false;
}
};
inputs: Inputs = {};
@ -61,16 +63,31 @@ class Button extends React.Component<IButtonProps, IButtonState> {
}
return false;
}
};
render(): JSX.Element {
const { handleClick, setRef } = this;
const { block, children, className, disabled, size, tabIndex, type, title, variant } = this.props;
const fullClassName = classnames(baseClass, `${baseClass}--${variant}`, className, {
const {
block,
children,
className,
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 (
<button

View File

@ -1,7 +1,6 @@
$base-class: 'button';
$base-class: "button";
@mixin button-variant($color, $hover: null, $active: null, $inverse: null) {
background-color: $color;
@if $inverse {
@ -15,7 +14,6 @@ $base-class: 'button';
color: $active;
}
} @else {
&:hover {
background-color: $hover;
}
@ -28,7 +26,8 @@ $base-class: 'button';
.#{$base-class} {
@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;
color: $white;
text-decoration: none;
@ -38,7 +37,7 @@ $base-class: 'button';
padding: 8px 16px;
border-radius: 4px;
font-size: $small;
font-family: 'Nunito Sans', sans-serif;
font-family: "Nunito Sans", sans-serif;
font-weight: $bold;
display: inline-flex;
height: 38px;
@ -63,11 +62,15 @@ $base-class: 'button';
}
&--blue-green {
@include button-variant($core-blue-green)
@include button-variant($core-blue-green);
}
&--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 {
@ -79,7 +82,12 @@ $base-class: 'button';
}
&--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;
border: 1px solid $core-blue;
box-sizing: border-box;
@ -127,14 +135,24 @@ $base-class: 'button';
}
&--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;
border: 2px solid $core-blue;
box-sizing: border-box;
}
&--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;
border: 2px solid $alert;
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 PropTypes from 'prop-types';
import { noop } from 'lodash';
import classnames from 'classnames';
import React, { Component } from "react";
import PropTypes from "prop-types";
import { noop } from "lodash";
import classnames from "classnames";
import ClickOutside from 'components/ClickOutside';
import KolideIcon from 'components/icons/KolideIcon';
import Button from 'components/buttons/Button';
import ClickOutside from "components/ClickOutside";
import KolideIcon from "components/icons/KolideIcon";
import Button from "components/buttons/Button";
const baseClass = 'dropdown-button';
const baseClass = "dropdown-button";
export class DropdownButton extends Component {
static propTypes = {
@ -19,7 +19,7 @@ export class DropdownButton extends Component {
disabled: PropTypes.bool,
label: PropTypes.string,
onClick: PropTypes.func,
}),
})
).isRequired,
size: PropTypes.string,
tabIndex: PropTypes.number,
@ -31,7 +31,7 @@ export class DropdownButton extends Component {
onChange: noop,
};
constructor (props) {
constructor(props) {
super(props);
this.state = { isOpen: false };
@ -39,7 +39,7 @@ export class DropdownButton extends Component {
setDOMNode = (DOMNode) => {
this.DOMNode = DOMNode;
}
};
toggleDropdown = () => {
const { isOpen } = this.state;
@ -56,13 +56,22 @@ export class DropdownButton extends Component {
const { disabled, label, onClick } = opt;
return (
<li className={`${baseClass}__option`} key={`dropdown-button-option-${idx}`}>
<Button variant="unstyled" onClick={evt => optionClick(evt, onClick)} disabled={disabled}>{label}</Button>
<li
className={`${baseClass}__option`}
key={`dropdown-button-option-${idx}`}
>
<Button
variant="unstyled"
onClick={(evt) => optionClick(evt, onClick)}
disabled={disabled}
>
{label}
</Button>
</li>
);
};
render () {
render() {
const {
children,
className,
@ -92,7 +101,8 @@ export class DropdownButton extends Component {
type={type}
variant={variant}
>
{children} <KolideIcon name="downcarat" className={`${baseClass}__carat`} />
{children}{" "}
<KolideIcon name="downcarat" className={`${baseClass}__carat`} />
</Button>
<ul className={optionsClass}>

View File

@ -1,23 +1,28 @@
import React from 'react';
import { mount } from 'enzyme';
import { noop } from 'lodash';
import React from "react";
import { mount } from "enzyme";
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", () => {
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(
<DropdownButton options={dropdownOptions}>
New Button
</DropdownButton>,
<DropdownButton options={dropdownOptions}>New Button</DropdownButton>
);
component.find('button.dropdown-button').simulate('click');
component.find("button.dropdown-button").simulate("click");
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();
});
});

View File

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

View File

@ -1,10 +1,10 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import React, { Component } from "react";
import PropTypes from "prop-types";
import { calculateTooltipDirection } from './helpers';
import ClickOutside from '../../ClickOutside';
import { calculateTooltipDirection } from "./helpers";
import ClickOutside from "../../ClickOutside";
const baseClass = 'ellipsis-menu';
const baseClass = "ellipsis-menu";
export class EllipsisMenu extends Component {
static propTypes = {
@ -12,7 +12,7 @@ export class EllipsisMenu extends Component {
positionStyles: PropTypes.object, // eslint-disable-line react/forbid-prop-types
};
constructor (props) {
constructor(props) {
super(props);
this.state = {
@ -20,18 +20,18 @@ export class EllipsisMenu extends Component {
};
}
componentDidMount () {
componentDidMount() {
const { setTooltipDirection } = this;
global.window.addEventListener('resize', setTooltipDirection);
global.window.addEventListener("resize", setTooltipDirection);
return setTooltipDirection();
}
componentWillUnmount () {
componentWillUnmount() {
const { setTooltipDirection } = this;
global.window.removeEventListener('resize', setTooltipDirection);
global.window.removeEventListener("resize", setTooltipDirection);
return false;
}
@ -42,11 +42,11 @@ export class EllipsisMenu extends Component {
this.setState({ showChildren: !showChildren });
return false;
}
};
setDOMNode = (DOMNode) => {
this.DOMNode = DOMNode;
}
};
setTooltipDirection = () => {
if (this.DOMNode) {
@ -56,12 +56,12 @@ export class EllipsisMenu extends Component {
}
return false;
}
};
renderChildren = () => {
const { children } = this.props;
const { showChildren, tooltipDirection } = this.state;
const triangleDirection = tooltipDirection === 'left' ? 'right' : 'left';
const triangleDirection = tooltipDirection === "left" ? "right" : "left";
if (!showChildren) {
return false;
@ -74,18 +74,14 @@ export class EllipsisMenu extends Component {
{children}
</div>
);
}
};
render () {
render() {
const { onToggleChildren, renderChildren, setDOMNode } = this;
const { positionStyles } = this.props;
return (
<div
ref={setDOMNode}
className={baseClass}
style={positionStyles}
>
<div ref={setDOMNode} className={baseClass} style={positionStyles}>
<button
onClick={onToggleChildren}
className={`${baseClass}__btn button button--unstyled`}

View File

@ -1,22 +1,22 @@
import React from 'react';
import { mount } from 'enzyme';
import React from "react";
import { mount } from "enzyme";
import { EllipsisMenu } from './EllipsisMenu';
import { EllipsisMenu } from "./EllipsisMenu";
describe('EllipsisMenu - component', () => {
it('Displays children on click', () => {
describe("EllipsisMenu - component", () => {
it("Displays children on click", () => {
const component = mount(
<EllipsisMenu>
<span>EllipsisMenu Children</span>
</EllipsisMenu>,
</EllipsisMenu>
);
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.text()).toContain('EllipsisMenu Children');
expect(component.text()).toContain("EllipsisMenu Children");
});
});

View File

@ -7,9 +7,11 @@ const calculateElementDistanceToBrowserRight = (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 };

View File

@ -1,10 +1,8 @@
import { calculateTooltipDirection } from './helpers';
import { calculateTooltipDirection } from "./helpers";
describe('EllipsisMenu - helpers', () => {
describe('#calculateTooltipDirection', () => {
it(
'returns "left" if the element does not fit to the right in the browser',
() => {
describe("EllipsisMenu - helpers", () => {
describe("#calculateTooltipDirection", () => {
it('returns "left" if the element does not fit to the right in the browser', () => {
const el = {
getBoundingClientRect: () => {
return {
@ -14,9 +12,8 @@ describe('EllipsisMenu - helpers', () => {
},
};
expect(calculateTooltipDirection(el)).toEqual('left');
},
);
expect(calculateTooltipDirection(el)).toEqual("left");
});
it('returns "right" if the element fits to the right in the browser', () => {
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 PropTypes from 'prop-types';
import FileSaver from 'file-saver';
import React, { Component } from "react";
import PropTypes from "prop-types";
import FileSaver from "file-saver";
import Button from 'components/buttons/Button';
import enrollSecretInterface from 'interfaces/enroll_secret';
import InputField from 'components/forms/fields/InputField';
import KolideIcon from 'components/icons/KolideIcon';
import { stringToClipboard } from 'utilities/copy_text';
import EyeIcon from '../../../../assets/images/icon-eye-16x16@2x.png';
import DownloadIcon from '../../../../assets/images/icon-download-12x12@2x.png';
import Button from "components/buttons/Button";
import enrollSecretInterface from "interfaces/enroll_secret";
import InputField from "components/forms/fields/InputField";
import KolideIcon from "components/icons/KolideIcon";
import { stringToClipboard } from "utilities/copy_text";
import EyeIcon from "../../../../assets/images/icon-eye-16x16@2x.png";
import DownloadIcon from "../../../../assets/images/icon-download-12x12@2x.png";
const baseClass = 'enroll-secrets';
const baseClass = "enroll-secrets";
class EnrollSecretRow extends Component {
static propTypes = {
name: PropTypes.string.isRequired,
secret: PropTypes.string.isRequired,
}
};
constructor (props) {
constructor(props) {
super(props);
this.state = { showSecret: false, copyMessage: '' };
this.state = { showSecret: false, copyMessage: "" };
}
onCopySecret = (evt) => {
@ -29,28 +29,27 @@ class EnrollSecretRow extends Component {
const { secret } = this.props;
stringToClipboard(secret)
.then(() => this.setState({ copyMessage: '(copied)' }))
.catch(() => this.setState({ copyMessage: '(copy failed)' }));
.then(() => this.setState({ copyMessage: "(copied)" }))
.catch(() => this.setState({ copyMessage: "(copy failed)" }));
// Clear message after 1 second
setTimeout(() => this.setState({ copyMessage: '' }), 1000);
setTimeout(() => this.setState({ copyMessage: "" }), 1000);
return false;
}
};
onDownloadSecret = (evt) => {
evt.preventDefault();
const { secret } = this.props;
const filename = 'secret.txt';
const filename = "secret.txt";
const file = new global.window.File([secret], filename);
FileSaver.saveAs(file);
return false;
}
};
onToggleSecret = (evt) => {
evt.preventDefault();
@ -88,9 +87,9 @@ class EnrollSecretRow extends Component {
</span>
</span>
);
}
};
render () {
render() {
const { secret } = this.props;
const { showSecret } = this.state;
const { renderLabel, onDownloadSecret } = this;
@ -102,7 +101,7 @@ class EnrollSecretRow extends Component {
inputWrapperClass={`${baseClass}__secret-input`}
name="osqueryd-secret"
label={renderLabel()}
type={showSecret ? 'text' : 'password'}
type={showSecret ? "text" : "password"}
value={secret}
/>
<a
@ -122,22 +121,27 @@ class EnrollSecretRow extends Component {
class EnrollSecretTable extends Component {
static propTypes = {
secrets: enrollSecretInterface.isRequired,
}
};
render() {
const { secrets } = this.props;
const activeSecrets = secrets.filter(s => s.active);
const activeSecrets = secrets.filter((s) => s.active);
let enrollSecrectsClass = baseClass;
if (activeSecrets.length === 0) {
return (<div className={baseClass}><em>No active enroll secrets.</em></div>);
} else if (activeSecrets.length > 1) enrollSecrectsClass += ` ${baseClass}--multiple-secrets`;
return (
<div className={baseClass}>
<em>No active enroll secrets.</em>
</div>
);
} else if (activeSecrets.length > 1)
enrollSecrectsClass += ` ${baseClass}--multiple-secrets`;
return (
<div className={enrollSecrectsClass}>
{activeSecrets.map(({ name, secret }) =>
<EnrollSecretRow key={name} name={name} secret={secret} />,
)}
{activeSecrets.map(({ name, secret }) => (
<EnrollSecretRow key={name} name={name} secret={secret} />
))}
</div>
);
}

View File

@ -1,74 +1,80 @@
import React from 'react';
import { shallow, mount } from 'enzyme';
import React from "react";
import { shallow, mount } from "enzyme";
import * as copy from 'utilities/copy_text';
import EnrollSecretTable, { EnrollSecretRow } from 'components/config/EnrollSecretTable/EnrollSecretTable';
import * as copy from "utilities/copy_text";
import EnrollSecretTable, {
EnrollSecretRow,
} from "components/config/EnrollSecretTable/EnrollSecretTable";
describe('EnrollSecretTable', () => {
describe("EnrollSecretTable", () => {
const defaultProps = {
secrets: [
{ name: 'foo', secret: 'foo_secret', active: true },
{ name: 'bar', secret: 'bar_secret', active: true },
{ name: 'inactive', secret: 'inactive', active: false },
{ name: "foo", secret: "foo_secret", active: true },
{ name: "bar", secret: "bar_secret", active: true },
{ name: "inactive", secret: "inactive", active: false },
],
};
it('renders properly filtered rows', () => {
it("renders properly filtered rows", () => {
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={[]} />);
expect(table.find('EnrollSecretRow').length).toEqual(0);
expect(table.find('div').text()).toEqual('No active enroll secrets.');
expect(table.find("EnrollSecretRow").length).toEqual(0);
expect(table.find("div").text()).toEqual("No active enroll secrets.");
});
});
describe('EnrollSecretRow', () => {
const defaultProps = { name: 'foo', secret: 'bar' };
it('should hide secret by default', () => {
describe("EnrollSecretRow", () => {
const defaultProps = { name: "foo", secret: "bar" };
it("should hide secret by default", () => {
const row = mount(<EnrollSecretRow {...defaultProps} />);
const inputField = row.find('InputField').find('input');
expect(inputField.prop('type')).toEqual('password');
const inputField = row.find("InputField").find("input");
expect(inputField.prop("type")).toEqual("password");
});
it('should show secret when enabled', () => {
it("should show secret when enabled", () => {
const row = mount(<EnrollSecretRow {...defaultProps} />);
row.setState({ showSecret: true });
const inputField = row.find('InputField').find('input');
expect(inputField.prop('type')).toEqual('text');
const inputField = row.find("InputField").find("input");
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} />);
let inputField = row.find('InputField').find('input');
expect(inputField.prop('type')).toEqual('password');
let inputField = row.find("InputField").find("input");
expect(inputField.prop("type")).toEqual("password");
const showLink = row.find('.enroll-secrets__show-secret');
expect(showLink.find('img').prop('alt')).toEqual('show/hide');
const showLink = row.find(".enroll-secrets__show-secret");
expect(showLink.find("img").prop("alt")).toEqual("show/hide");
showLink.simulate('click');
showLink.simulate("click");
inputField = row.find('InputField').find('input');
expect(inputField.prop('type')).toEqual('text');
inputField = row.find("InputField").find("input");
expect(inputField.prop("type")).toEqual("text");
const hideLink = row.find('.enroll-secrets__show-secret');
expect(showLink.find('img').prop('alt')).toEqual('show/hide');
const hideLink = row.find(".enroll-secrets__show-secret");
expect(showLink.find("img").prop("alt")).toEqual("show/hide");
hideLink.simulate('click');
hideLink.simulate("click");
inputField = row.find('InputField').find('input');
expect(inputField.prop('type')).toEqual('password');
inputField = row.find("InputField").find("input");
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 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');
copyLink.simulate('click');
const copyLink = row
.find(".enroll-secrets__secret-copy-icon")
.find("Button");
copyLink.simulate("click");
expect(spy).toHaveBeenCalledWith(defaultProps.secret);
});

View File

@ -69,7 +69,7 @@
&--multiple-secrets {
&:before {
content: '';
content: "";
position: sticky;
display: block;
z-index: 1;
@ -78,11 +78,15 @@
width: 100%;
height: 17px;
// 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 {
content: '';
content: "";
position: sticky;
display: block;
bottom: -1px;
@ -90,7 +94,11 @@
width: 100%;
height: 17px;
// 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 {

View File

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

View File

@ -1,14 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import React from "react";
import PropTypes from "prop-types";
import classnames from "classnames";
import notificationInterface from 'interfaces/notification';
import KolideIcon from 'components/icons/KolideIcon';
import Button from 'components/buttons/Button';
import notificationInterface from "interfaces/notification";
import KolideIcon from "components/icons/KolideIcon";
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 klass = classnames(baseClass, `${baseClass}--${alertType}`, {
[`${baseClass}--full-width`]: fullWidth,
@ -18,14 +23,14 @@ const FlashMessage = ({ fullWidth, notification, onRemoveFlash, onUndoActionClic
return false;
}
const alertIcon = alertType === 'success' ? 'success-check' : 'warning-filled';
const alertIcon =
alertType === "success" ? "success-check" : "warning-filled";
return (
<div className={klass}>
<div className={`${baseClass}__content`}>
<KolideIcon name={alertIcon} /> <span>{message}</span>
{undoAction &&
{undoAction && (
<Button
className={`${baseClass}__undo`}
variant="unstyled"
@ -33,7 +38,7 @@ const FlashMessage = ({ fullWidth, notification, onRemoveFlash, onUndoActionClic
>
Undo
</Button>
}
)}
</div>
<div className={`${baseClass}__action`}>
<Button

View File

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

View File

@ -1,10 +1,10 @@
import React from 'react';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import React from "react";
import PropTypes from "prop-types";
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 klass = classnames(baseClass, `${baseClass}--error`);
@ -23,4 +23,3 @@ PersistentFlash.propTypes = {
};
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 PropTypes from 'prop-types';
import React, { Component } from "react";
import PropTypes from "prop-types";
import Button from 'components/buttons/Button';
import Form from 'components/forms/Form';
import formFieldInterface from 'interfaces/form_field';
import InputField from 'components/forms/fields/InputField';
import Button from "components/buttons/Button";
import Form from "components/forms/Form";
import formFieldInterface from "interfaces/form_field";
import InputField from "components/forms/fields/InputField";
const baseClass = 'change-email-form';
const baseClass = "change-email-form";
class ChangeEmailForm extends Component {
static propTypes = {
@ -17,7 +17,7 @@ class ChangeEmailForm extends Component {
onCancel: PropTypes.func.isRequired,
};
render () {
render() {
const { fields, handleSubmit, onCancel } = this.props;
return (
@ -32,7 +32,13 @@ class ChangeEmailForm extends Component {
<Button className={`${baseClass}__btn`} type="submit" variant="brand">
Submit
</Button>
<Button onClick={onCancel} variant="inverse" className={`${baseClass}__btn`}>Cancel</Button>
<Button
onClick={onCancel}
variant="inverse"
className={`${baseClass}__btn`}
>
Cancel
</Button>
</div>
</form>
);
@ -40,12 +46,12 @@ class ChangeEmailForm extends Component {
}
export default Form(ChangeEmailForm, {
fields: ['password'],
fields: ["password"],
validate: (formData) => {
if (!formData.password) {
return {
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 PropTypes from 'prop-types';
import React, { Component } from "react";
import PropTypes from "prop-types";
import Button from 'components/buttons/Button';
import Form from 'components/forms/Form';
import formFieldInterface from 'interfaces/form_field';
import InputField from 'components/forms/fields/InputField';
import validate from 'components/forms/ChangePasswordForm/validate';
import Button from "components/buttons/Button";
import Form from "components/forms/Form";
import formFieldInterface from "interfaces/form_field";
import InputField from "components/forms/fields/InputField";
import validate from "components/forms/ChangePasswordForm/validate";
const formFields = ['old_password', 'new_password', 'new_password_confirmation'];
const baseClass = 'change-password-form';
const formFields = [
"old_password",
"new_password",
"new_password_confirmation",
];
const baseClass = "change-password-form";
class ChangePasswordForm extends Component {
static propTypes = {
@ -21,7 +25,7 @@ class ChangePasswordForm extends Component {
onCancel: PropTypes.func.isRequired,
};
render () {
render() {
const { fields, handleSubmit, onCancel } = this.props;
return (
@ -43,8 +47,16 @@ class ChangePasswordForm extends Component {
type="password"
/>
<div className={`${baseClass}__btn-wrap`}>
<Button type="submit" variant="brand" className={`${baseClass}__btn`}>Change password</Button>
<Button onClick={onCancel} variant="inverse" className={`${baseClass}__btn`}>Cancel</Button>
<Button type="submit" variant="brand" className={`${baseClass}__btn`}>
Change password
</Button>
<Button
onClick={onCancel}
variant="inverse"
className={`${baseClass}__btn`}
>
Cancel
</Button>
</div>
</form>
);
@ -52,4 +64,3 @@ class ChangePasswordForm extends Component {
}
export default Form(ChangePasswordForm, { fields: formFields, validate });

View File

@ -1,78 +1,112 @@
import React from 'react';
import { mount } from 'enzyme';
import { noop } from 'lodash';
import React from "react";
import { mount } from "enzyme";
import { noop } from "lodash";
import ChangePasswordForm from 'components/forms/ChangePasswordForm';
import helpers from 'test/helpers';
import ChangePasswordForm from "components/forms/ChangePasswordForm";
import helpers from "test/helpers";
const { fillInFormInput, itBehavesLikeAFormInputElement } = helpers;
describe('ChangePasswordForm - component', () => {
it('has the correct fields', () => {
const form = mount(<ChangePasswordForm handleSubmit={noop} onCancel={noop} />);
describe("ChangePasswordForm - component", () => {
it("has the correct fields", () => {
const form = mount(
<ChangePasswordForm handleSubmit={noop} onCancel={noop} />
);
itBehavesLikeAFormInputElement(form, 'old_password');
itBehavesLikeAFormInputElement(form, 'new_password');
itBehavesLikeAFormInputElement(form, 'new_password_confirmation');
itBehavesLikeAFormInputElement(form, "old_password");
itBehavesLikeAFormInputElement(form, "new_password");
itBehavesLikeAFormInputElement(form, "new_password_confirmation");
});
it('renders the password fields as HTML password fields', () => {
const form = mount(<ChangePasswordForm handleSubmit={noop} onCancel={noop} />);
it("renders the password fields as HTML password fields", () => {
const form = mount(
<ChangePasswordForm handleSubmit={noop} onCancel={noop} />
);
const passwordField = form.find('input[name="old_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(newPasswordField.prop('type')).toEqual('password');
expect(newPasswordConfirmationField.prop('type')).toEqual('password');
expect(passwordField.prop("type")).toEqual("password");
expect(newPasswordField.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 form = mount(<ChangePasswordForm handleSubmit={handleSubmitSpy} onCancel={noop} />).find('form');
const expectedFormData = { 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');
const form = mount(
<ChangePasswordForm handleSubmit={handleSubmitSpy} onCancel={noop} />
).find("form");
const expectedFormData = {
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(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);
});
it('calls the onCancel prop when CANCEL is clicked', () => {
it("calls the onCancel prop when CANCEL is clicked", () => {
const onCancelSpy = jest.fn();
const form = mount(<ChangePasswordForm handleSubmit={noop} onCancel={onCancelSpy} />).find('form');
const cancelBtn = form.find('Button').findWhere(n => n.prop('children') === 'Cancel').find('button');
const form = mount(
<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();
});
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 component = mount(<ChangePasswordForm handleSubmit={handleSubmitSpy} onCancel={noop} />);
const form = component.find('form');
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 newPasswordInput = form.find({ name: 'new_password' }).find('input');
const newPasswordConfirmationInput = form.find({ name: 'new_password_confirmation' }).find('input');
const component = mount(
<ChangePasswordForm handleSubmit={handleSubmitSpy} onCancel={noop} />
);
const form = component.find("form");
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 newPasswordInput = form.find({ name: "new_password" }).find("input");
const newPasswordConfirmationInput = form
.find({ name: "new_password_confirmation" })
.find("input");
fillInFormInput(passwordInput, expectedFormData.old_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(component.state('errors')).toMatchObject({
new_password: 'Password must be at least 7 characters and contain at least 1 letter, 1 number, and 1 symbol',
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",
});
});
});

View File

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

View File

@ -1,6 +1,6 @@
import { size } from 'lodash';
import validateEquality from 'components/forms/validators/validate_equality';
import validPassword from 'components/forms/validators/valid_password';
import { size } from "lodash";
import validateEquality from "components/forms/validators/validate_equality";
import validPassword from "components/forms/validators/valid_password";
export default (formData) => {
const errors = {};
@ -11,24 +11,30 @@ export default (formData) => {
} = formData;
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) {
errors.old_password = 'Password must be present';
errors.old_password = "Password must be present";
}
if (!newPassword) {
errors.new_password = 'New password must be present';
errors.new_password = "New password must be present";
}
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 &&
!validateEquality(newPassword, newPasswordConfirmation)) {
errors.new_password_confirmation = 'New password confirmation does not match new password';
if (
newPassword &&
newPasswordConfirmation &&
!validateEquality(newPassword, newPasswordConfirmation)
) {
errors.new_password_confirmation =
"New password confirmation does not match new password";
}
const valid = !size(errors);

View File

@ -1,54 +1,64 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { pull } from 'lodash';
import React, { Component } from "react";
import PropTypes from "prop-types";
import { pull } from "lodash";
import KolideIcon from 'components/icons/KolideIcon';
import Button from 'components/buttons/Button';
import Dropdown from 'components/forms/fields/Dropdown';
import Form from 'components/forms/Form';
import formFieldInterface from 'interfaces/form_field';
import InputField from 'components/forms/fields/InputField';
import validate from 'components/forms/ConfigurePackQueryForm/validate';
import KolideIcon from "components/icons/KolideIcon";
import Button from "components/buttons/Button";
import Dropdown from "components/forms/fields/Dropdown";
import Form from "components/forms/Form";
import formFieldInterface from "interfaces/form_field";
import InputField from "components/forms/fields/InputField";
import validate from "components/forms/ConfigurePackQueryForm/validate";
const baseClass = 'configure-pack-query-form';
const fieldNames = ['query_id', 'interval', 'logging_type', 'platform', 'shard', 'version'];
const baseClass = "configure-pack-query-form";
const fieldNames = [
"query_id",
"interval",
"logging_type",
"platform",
"shard",
"version",
];
const platformOptions = [
{ label: 'All', value: '' },
{ label: 'Windows', value: 'windows' },
{ label: 'Linux', value: 'linux' },
{ label: 'macOS', value: 'darwin' },
{ label: "All", value: "" },
{ label: "Windows", value: "windows" },
{ label: "Linux", value: "linux" },
{ label: "macOS", value: "darwin" },
];
const loggingTypeOptions = [
{ label: 'Differential', value: 'differential' },
{ label: 'Differential (Ignore Removals)', value: 'differential_ignore_removals' },
{ label: 'Snapshot', value: 'snapshot' },
{ label: "Differential", value: "differential" },
{
label: "Differential (Ignore Removals)",
value: "differential_ignore_removals",
},
{ label: "Snapshot", value: "snapshot" },
];
const minOsqueryVersionOptions = [
{ label: 'All', value: '' },
{ label: '4.7.0 +', value: '4.7.0' },
{ label: '4.6.0 +', value: '4.6.0' },
{ label: '4.5.1 +', value: '4.5.1' },
{ label: '4.5.0 +', value: '4.5.0' },
{ label: '4.4.0 +', value: '4.4.0' },
{ label: '4.3.0 +', value: '4.3.0' },
{ label: '4.2.0 +', value: '4.2.0' },
{ label: '4.1.2 +', value: '4.1.2' },
{ label: '4.1.1 +', value: '4.1.1' },
{ label: '4.1.0 +', value: '4.1.0' },
{ label: '4.0.2 +', value: '4.0.2' },
{ label: '4.0.1 +', value: '4.0.1' },
{ label: '4.0.0 +', value: '4.0.0' },
{ label: '3.4.0 +', value: '3.4.0' },
{ label: '3.3.2 +', value: '3.3.2' },
{ label: '3.3.1 +', value: '3.3.1' },
{ label: '3.2.6 +', value: '3.2.6' },
{ label: '2.2.1 +', value: '2.2.1' },
{ label: '2.2.0 +', value: '2.2.0' },
{ label: '2.1.2 +', value: '2.1.2' },
{ label: '2.1.1 +', value: '2.1.1' },
{ label: '2.0.0 +', value: '2.0.0' },
{ label: '1.8.2 +', value: '1.8.2' },
{ label: '1.8.1 +', value: '1.8.1' },
{ label: "All", value: "" },
{ label: "4.7.0 +", value: "4.7.0" },
{ label: "4.6.0 +", value: "4.6.0" },
{ label: "4.5.1 +", value: "4.5.1" },
{ label: "4.5.0 +", value: "4.5.0" },
{ label: "4.4.0 +", value: "4.4.0" },
{ label: "4.3.0 +", value: "4.3.0" },
{ label: "4.2.0 +", value: "4.2.0" },
{ label: "4.1.2 +", value: "4.1.2" },
{ label: "4.1.1 +", value: "4.1.1" },
{ label: "4.1.0 +", value: "4.1.0" },
{ label: "4.0.2 +", value: "4.0.2" },
{ label: "4.0.1 +", value: "4.0.1" },
{ label: "4.0.0 +", value: "4.0.0" },
{ label: "3.4.0 +", value: "3.4.0" },
{ label: "3.3.2 +", value: "3.3.2" },
{ label: "3.3.1 +", value: "3.3.1" },
{ label: "3.2.6 +", value: "3.2.6" },
{ label: "2.2.1 +", value: "2.2.1" },
{ label: "2.2.0 +", value: "2.2.0" },
{ label: "2.1.2 +", value: "2.1.2" },
{ label: "2.1.1 +", value: "2.1.1" },
{ label: "2.0.0 +", value: "2.0.0" },
{ label: "1.8.2 +", value: "1.8.2" },
{ label: "1.8.1 +", value: "1.8.1" },
];
export class ConfigurePackQueryForm extends Component {
@ -67,11 +77,11 @@ export class ConfigurePackQueryForm extends Component {
onCancel: PropTypes.func,
};
componentWillMount () {
componentWillMount() {
const { fields } = this.props;
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;
return handleCancel(formData);
}
};
handlePlatformChoice = (value) => {
const { fields: { platform } } = this.props;
const valArray = value.split(',');
const {
fields: { platform },
} = this.props;
const valArray = value.split(",");
// Remove All if another OS is chosen
if (valArray.indexOf('') === 0 && valArray.length > 1) {
return platform.onChange(pull(valArray, '').join(','));
if (valArray.indexOf("") === 0 && valArray.length > 1) {
return platform.onChange(pull(valArray, "").join(","));
}
// Remove OS if All is chosen
if (valArray.length > 1 && valArray.indexOf('') > -1) {
return platform.onChange('');
if (valArray.length > 1 && valArray.indexOf("") > -1) {
return platform.onChange("");
}
return platform.onChange(value);
}
};
renderCancelButton = () => {
const { formData } = this.props;
@ -109,13 +121,17 @@ export class ConfigurePackQueryForm extends Component {
}
return (
<Button className={`${baseClass}__cancel-btn`} onClick={onCancel} variant="inverse">
<Button
className={`${baseClass}__cancel-btn`}
onClick={onCancel}
variant="inverse"
>
Cancel
</Button>
);
}
};
render () {
render() {
const { fields, handleSubmit } = this.props;
const { handlePlatformChoice, renderCancelButton } = this;
@ -144,7 +160,11 @@ export class ConfigurePackQueryForm extends Component {
{...fields.version}
options={minOsqueryVersionOptions}
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`}
/>
<Dropdown
@ -163,7 +183,11 @@ export class ConfigurePackQueryForm extends Component {
/>
<div className={`${baseClass}__btn-wrapper`}>
{renderCancelButton()}
<Button className={`${baseClass}__submit-btn`} type="submit" variant="brand">
<Button
className={`${baseClass}__submit-btn`}
type="submit"
variant="brand"
>
Save
</Button>
</div>

View File

@ -1,29 +1,30 @@
import React from 'react';
import { mount } from 'enzyme';
import { noop } from 'lodash';
import React from "react";
import { mount } from "enzyme";
import { noop } from "lodash";
import DefaultConfigurePackQueryForm, { ConfigurePackQueryForm } from 'components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm';
import { itBehavesLikeAFormDropdownElement, itBehavesLikeAFormInputElement } from 'test/helpers';
import { scheduledQueryStub } from 'test/stubs';
import DefaultConfigurePackQueryForm, {
ConfigurePackQueryForm,
} from "components/forms/ConfigurePackQueryForm/ConfigurePackQueryForm";
import {
itBehavesLikeAFormDropdownElement,
itBehavesLikeAFormInputElement,
} from "test/helpers";
import { scheduledQueryStub } from "test/stubs";
describe('ConfigurePackQueryForm - component', () => {
describe('form fields', () => {
const form = mount(
<DefaultConfigurePackQueryForm
handleSubmit={noop}
/>,
);
describe("ConfigurePackQueryForm - component", () => {
describe("form fields", () => {
const form = mount(<DefaultConfigurePackQueryForm handleSubmit={noop} />);
it('updates form state', () => {
itBehavesLikeAFormInputElement(form, 'interval');
itBehavesLikeAFormDropdownElement(form, 'logging_type');
itBehavesLikeAFormDropdownElement(form, 'platform');
itBehavesLikeAFormDropdownElement(form, 'version');
itBehavesLikeAFormInputElement(form, 'shard');
it("updates form state", () => {
itBehavesLikeAFormInputElement(form, "interval");
itBehavesLikeAFormDropdownElement(form, "logging_type");
itBehavesLikeAFormDropdownElement(form, "platform");
itBehavesLikeAFormDropdownElement(form, "version");
itBehavesLikeAFormInputElement(form, "shard");
});
});
describe('platform options', () => {
describe("platform options", () => {
const onChangeSpy = jest.fn();
const fieldsObj = {
platform: {
@ -38,85 +39,86 @@ describe('ConfigurePackQueryForm - component', () => {
fields={fieldsObj}
handleSubmit={noop}
formData={{ query_id: 1 }}
/>,
/>
);
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", () => {
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 form = mount(
<DefaultConfigurePackQueryForm
handleSubmit={spy}
formData={{ query_id: 1 }}
/>,
/>
);
it('submits the form with the form data', () => {
itBehavesLikeAFormInputElement(form, 'interval', 'InputField', 123);
itBehavesLikeAFormDropdownElement(form, 'logging_type');
itBehavesLikeAFormDropdownElement(form, 'platform');
itBehavesLikeAFormDropdownElement(form, 'version');
itBehavesLikeAFormInputElement(form, 'shard', 'InputField', 12);
it("submits the form with the form data", () => {
itBehavesLikeAFormInputElement(form, "interval", "InputField", 123);
itBehavesLikeAFormDropdownElement(form, "logging_type");
itBehavesLikeAFormDropdownElement(form, "platform");
itBehavesLikeAFormDropdownElement(form, "version");
itBehavesLikeAFormInputElement(form, "shard", "InputField", 12);
form.find('form').simulate('submit');
form.find("form").simulate("submit");
expect(spy).toHaveBeenCalledWith({
interval: 123,
logging_type: 'differential',
platform: '',
logging_type: "differential",
platform: "",
query_id: 1,
version: '',
version: "",
shard: 12,
});
});
});
describe('cancelling the form', () => {
const CancelButton = form => form.find('.configure-pack-query-form__cancel-btn');
describe("cancelling the form", () => {
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(
<DefaultConfigurePackQueryForm
formData={{ query_id: 1 }}
handleSubmit={noop}
onCancel={noop}
/>,
/>
);
const UpdateScheduledQueryForm = mount(
<DefaultConfigurePackQueryForm
formData={scheduledQueryStub}
handleSubmit={noop}
onCancel={noop}
/>,
/>
);
expect(CancelButton(NewScheduledQueryForm).length).toEqual(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 UpdateScheduledQueryForm = mount(
<DefaultConfigurePackQueryForm
formData={scheduledQueryStub}
handleSubmit={noop}
onCancel={spy}
/>,
/>
);
CancelButton(UpdateScheduledQueryForm).hostNodes().simulate('click');
CancelButton(UpdateScheduledQueryForm).hostNodes().simulate("click");
expect(spy).toHaveBeenCalledWith(scheduledQueryStub);
});

View File

@ -1,5 +1,4 @@
.configure-pack-query-form {
&__form-field {
&--interval {
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