Merge master into teams

This commit is contained in:
Gabriel Hernandez 2021-04-14 17:52:15 +01:00
commit 04712c0426
755 changed files with 44994 additions and 31320 deletions

View file

@ -1,61 +1,61 @@
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",
// Most of the js modules written by us need to be rewritten into TS. Until then
// we use ts-ignore comment to ignore the error TS gives us from not having those modules
@ -68,23 +68,23 @@ module.exports = {
// 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

@ -7,7 +7,7 @@ assignees: ''
---
**Fleet version** (`fleetctl version`):
**Fleet version** (Head to the "My account" page in the Fleet UI or run `fleetctl version`):
**Operating system** _(e.g. macOS 11.2.3)_:
**Web browser** _(e.g. Chrome 88.0.4324)_:
<hr/>
@ -23,5 +23,6 @@ assignees: ''
### More info
<!-- Any ideas? -->
<!-- If this is an issue with the Fleet UI: Please also [answer this question](https://github.com/fleetdm/fleet/blob/master/CONTRIBUTING.md#6-is-this-an-issue-with-the-fleet-ui). -->
<!-- If this is a performance issue: Please [follow these steps](https://github.com/fleetdm/fleet/blob/master/docs/1-Using-Fleet/5-Monitoring-Fleet.md#debugging-performance-issues) to generate and attach a debug archive. -->

43
.github/workflows/test-website.yml vendored Normal file
View file

@ -0,0 +1,43 @@
name: Test Fleet website
on:
pull_request:
paths:
- 'website/**'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.x]
steps:
- uses: actions/checkout@v2
# Set the Node.js version
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:
node-version: ${{ matrix.node-version }}
# Now start building!
# > …but first, get a little crazy for a sec and delete the top-level package.json file
# > i.e. the one used by the Fleet server. This is because require() in node will go
# > hunting in ancestral directories for missing dependencies, and since some of the
# > bundled transpiler tasks sniff for package availability using require(), this trips
# > up when it encounters another Node universe in the parent directory.
- run: rm -rf package.json package-lock.json node_modules/
# > Turns out there's a similar issue with how eslint plugins are looked up, so we
# > delete the top level .eslintrc file too.
- run: rm -f .eslintrc.js
# Get dependencies (including dev deps)
- run: cd website/ && npm install
# Run sanity checks
- run: cd website/ && npm test
# Compile assets
- run: cd website/ && npm run build-for-prod

View file

@ -25,18 +25,18 @@ jobs:
${{ runner.os }}-modules-
# It seems faster not to cache Go dependencies
- name: Install JS Dependencies
run: make deps-js
- name: Install Go Dependencies
run: make deps-go
# Pre-starting dependencies here means they are ready to go when we need them.
# Pre-starting dependencies here means they are ready to go when we need them.
- name: Start Infra Dependencies
# Use & to background this
run: docker-compose up -d mysql_test redis mailhog saml_idp &
- name: Build Fleet
run: |
export PATH=$PATH:~/go/bin
@ -51,7 +51,7 @@ jobs:
make e2e-setup
yarn cypress run --config video=false
test-js:
strategy:
matrix:
@ -106,7 +106,33 @@ jobs:
- name: Run JS Linting
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:
@ -122,15 +148,15 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v2
# Pre-starting dependencies here means they are ready to go when we need them.
# Pre-starting dependencies here means they are ready to go when we need them.
- name: Start Infra Dependencies
# Use & to background this
run: docker-compose up -d mysql_test redis &
# It seems faster not to cache Go dependencies
- name: Install Go Dependencies
run: make deps-go
- name: Generate static files
run: |
export PATH=$PATH:~/go/bin
@ -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,3 +1,7 @@
## Fleet 3.10.1 (Apr 6, 2021)
* Fix a frontend bug that prevented the "Pack" page and "Edit pack" page from rendering in the Fleet UI. This issue occurred when the `platform` key, in the requested pack's configuration, was set to any value other than `darwin`, `linux`, `windows`, or `all`.
## Fleet 3.10.0 (Mar 31, 2021)
* Add `fleetctl` agent auto-updates beta which introduces the ability to self-manage an agent update server. Available for Fleet Basic customers.

View file

@ -1,13 +1,35 @@
## Filing issues
### Bug report
When filing an issue, make sure to answer these five questions:
1. What version of Fleet are you using (`fleet version --full`)?
2. What operating system are you using?
3. What did you do?
4. What did you expect to happen?
5. What happened instead?
#### 1. What version of Fleet are you using?
Copy the version from the "My account" page in the Fleet UI (located below the "Get API token" button) or by run the `fleetctl version --full` command.
![My account page](./docs/images/my-account-page.png)
#### 2. What operating system are you using?
#### 3. What did you do?
#### 4. What did you expect to happen?
#### 5. What happened instead?
#### 6. Is this an issue with the Fleet UI?
If the answer is no, you can leave this blank.
If yes, please provide a summary or screenshots of your browser's JavaScript console and your browser's network requests. Why? The information revealed in the browser's console and network requests kickstarts the debugging efforts and thus helps us resolve your issue faster!
To open your browser's **Javascript console**, press Control Shift J (Windows, Linux, ChromeOS) or Command Option J (macOS).
To open your browser's **network requests**, press Control Shift J (Windows, Linux, ChromeOS) or Command Option J (macOS). Then select the "Network" tab.
### Report a security vulnerability
Sensitive security-related issues should be reported to
[security@fleetdm.com](mailto:security@fleetdm.com) before a public issue is made.

View file

@ -42,10 +42,6 @@ ifdef CIRCLE_TAG
DOCKER_IMAGE_TAG = ${CIRCLE_TAG}
endif
ifndef MYSQL_PORT_3306_TCP_ADDR
MYSQL_PORT_3306_TCP_ADDR = 127.0.0.1
endif
KIT_VERSION = "\
-X github.com/kolide/kit/version.appName=${APP_NAME} \
-X github.com/kolide/kit/version.version=${VERSION} \
@ -123,7 +119,7 @@ lint-go:
lint: lint-go lint-js
test-go:
go test -tags full ./...
go test -tags full -parallel 8 ./...
analyze-go:
go test -tags full -race -cover ./...

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 MiB

View file

@ -4,8 +4,8 @@ name: fleet
keywords:
- fleet
- osquery
version: 3.10.0
version: 3.10.1
home: https://github.com/fleetdm/fleet
sources:
- https://github.com/fleetdm/fleet.git
appVersion: 3.10.0
appVersion: 3.10.1

View file

@ -2,7 +2,7 @@
# All settings related to how Fleet is deployed in Kubernetes
hostName: fleet.localhost
replicas: 3 # The number of Fleet instances to deploy
imageTag: 3.10.0 # Version of Fleet to deploy
imageTag: 3.10.1 # Version of Fleet to deploy
createIngress: true # Whether or not to automatically create an Ingress
ingressAnnotations: {} # Additional annotation to add to the Ingress
podAnnotations: {} # Additional annotations to add to the Fleet pod

View file

@ -235,6 +235,20 @@ the way that the Fleet server works.
}
}()
// Flush seen hosts every second
go func() {
ticker := time.NewTicker(1 * time.Second)
for {
if err := svc.FlushSeenHosts(context.Background()); err != nil {
level.Info(logger).Log(
"err", err,
"msg", "failed to update host seen times",
)
}
<-ticker.C
}
}()
fieldKeys := []string{"method", "error"}
requestCount := kitprometheus.NewCounterFrom(prometheus.CounterOpts{
Namespace: "api",

View file

@ -0,0 +1,5 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

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

@ -0,0 +1,59 @@
describe("Label flow", () => {
beforeEach(() => {
cy.setup();
cy.login();
});
it("Create, edit, and delete a label successfully", () => {
cy.visit("/hosts/manage");
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")
.click()
.type("{selectall}{backspace}SELECT * FROM users;");
cy.findByLabelText(/name/i).click().type("Show all users");
cy.findByLabelText(/description/i)
.click()
.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.findByText(/show all users/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");
cy.findByLabelText(/description/i)
.click()
.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: /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)
.click();
cy.findByText(/show all users/i).should("not.exist");
});
});

View file

@ -1,39 +1,32 @@
describe('Manage Users', () => {
describe("Manage Users", () => {
beforeEach(() => {
cy.setup();
cy.login();
cy.setupSMTP();
});
it('Searching for a user', () => {
it("Searching for a user", () => {
cy.intercept({
method: 'GET',
url: '/api/v1/fleet/users',
}).as('getUsers');
method: "GET",
url: "/api/v1/fleet/users",
}).as("getUsers");
cy.visit('/settings/users');
cy.url().should('match', /\/settings\/users$/i);
cy.visit("/settings/users");
cy.url().should("match", /\/settings\/users$/i);
cy.wait('@getUsers');
cy.wait("@getUsers");
cy.findByText('test@fleetdm.com')
.should('exist');
cy.findByText('test+1@fleetdm.com')
.should('exist');
cy.findByText('test+2@fleetdm.com')
.should('exist');
cy.findByText("test@fleetdm.com").should("exist");
cy.findByText("test+1@fleetdm.com").should("exist");
cy.findByText("test+2@fleetdm.com").should("exist");
cy.findByPlaceholderText('Search')
.type('test@fleetdm.com');
cy.findByPlaceholderText("Search").type("test@fleetdm.com");
cy.wait('@getUsers');
cy.wait("@getUsers");
cy.findByText('test@fleetdm.com')
.should('exist');
cy.findByText('test+1@fleetdm.com')
.should('not.exist');
cy.findByText('test+2@fleetdm.com')
.should('not.exist');
cy.findByText("test@fleetdm.com").should("exist");
cy.findByText("test+1@fleetdm.com").should("not.exist");
cy.findByText("test+2@fleetdm.com").should("not.exist");
});
// it('Creating a user', () => {

View file

@ -0,0 +1,54 @@
describe("Pack flow", () => {
beforeEach(() => {
cy.setup();
cy.login();
});
it("Create, edit, and delete a pack successfully", () => {
cy.visit("/packs/manage");
cy.findByRole("button", { name: /create new pack/i }).click();
cy.findByLabelText(/query pack title/i)
.click()
.type("Errors and crashes");
cy.findByLabelText(/query pack description/i)
.click()
.type("See all user errors and window crashes.");
cy.findByRole("button", { name: /save query pack/i }).click();
cy.visit("/packs/manage");
cy.findByText(/errors and crashes/i).click();
cy.findByText(/edit pack/i).click();
cy.findByLabelText(/query pack title/i)
.click()
.type("{selectall}{backspace}Server errors");
cy.findByLabelText(/query pack description/i)
.click()
.type("{selectall}{backspace}See all server errors.");
cy.findByRole("button", { name: /save/i }).click();
cy.visit("/packs/manage");
cy.get("#select-pack-1").check({ force: true });
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)
.click();
cy.findByText(/successfully deleted/i).should("be.visible");
cy.findByText(/server errors/i).should("not.exist");
});
});

View file

@ -0,0 +1,66 @@
describe("Query flow", () => {
beforeEach(() => {
cy.setup();
cy.login();
});
it("Create, check, edit, and delete a query successfully", () => {
cy.visit("/queries/manage");
cy.findByRole("button", { name: /create new query/i }).click();
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")
.click()
.type("{selectall}{backspace}SELECT * FROM windows_crashes;");
cy.findByLabelText(/description/i)
.click()
.type("See all window crashes");
cy.findByRole("button", { name: /save/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.findByText(/query all/i).click();
cy.findByRole("button", { name: /edit or run query/i }).click();
cy.get(".ace_content")
.click()
.type(
"{selectall}{backspace}SELECT datetime, username FROM windows_crashes;"
);
cy.findByRole("button", { name: /save/i }).click();
cy.findByRole("button", { name: /save changes/i }).click();
cy.findByText(/query updated/i).should("be.visible");
cy.visit("/queries/manage");
// This element has no label, text, or role
cy.get("#query-checkbox-1").check({ force: true });
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)
.click();
cy.findByText(/successfully deleted/i).should("be.visible");
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,106 +26,110 @@ 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: 10000 });
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('setupSMTP', () => {
Cypress.Commands.add("setupSMTP", () => {
const body = {
smtp_settings: {
authentication_type: 'authtype_none',
authentication_type: "authtype_none",
enable_smtp: true,
port: 1025,
sender_address: 'gabriel+dev@fleetdm.com',
server: 'localhost',
sender_address: "gabriel+dev@fleetdm.com",
server: "localhost",
},
};
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('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,4 +14,4 @@
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';
import "./commands";

View file

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

View file

@ -5,7 +5,7 @@ services:
image: mysql:5.7
volumes:
- mysql-persistent-volume:/tmp
command: mysqld --datadir=/tmp/mysqldata --slow_query_log=1 --log_output=TABLE --log-queries-not-using-indexes --event-scheduler=ON
command: mysqld --datadir=/tmp/mysqldata --event-scheduler=ON
environment: &mysql-default-environment
MYSQL_ROOT_PASSWORD: toor
MYSQL_DATABASE: fleet
@ -16,7 +16,8 @@ services:
mysql_test:
image: mysql:5.7
command: mysqld --datadir=/tmpfs --slow_query_log=1 --log_output=TABLE --log-queries-not-using-indexes --event-scheduler=ON
# innodb-file-per-table=OFF gives ~20% speedup for test runs.
command: mysqld --datadir=/tmpfs --slow_query_log=1 --log_output=TABLE --log-queries-not-using-indexes --event-scheduler=ON --innodb-file-per-table=OFF
tmpfs: /tmpfs
environment: *mysql-default-environment
ports:

View file

@ -632,13 +632,13 @@ Contents of carves are returned as .tar archives, and compressed if that option
To download the contents of a carve with ID 3, use
```
fleetctl get carve 3 --outfile carve.tar
fleetctl get carve --outfile carve.tar 3
```
It can also be useful to pipe the results directly into the tar command for unarchiving:
```
fleetctl get carve 3 --stdout | tar -x
fleetctl get carve --stdout 3 | tar -x
```
#### Expiration

View file

@ -1668,10 +1668,10 @@ Promotes or demotes the selected user's level of access as an admin in Fleet. Ad
#### Parameters
| Name | Type | In | Description |
| ---- | ------- | ----- | ---------------------------- |
| id | integer | path | **Required**. The user's id. |
| enabled | boolean | body | **Required**. Whether or not the user can access Fleet. |
| Name | Type | In | Description |
| ----- | ------- | ----- | ---------------------------- |
| id | integer | path | **Required**. The user's id. |
| admin | boolean | body | **Required**. Whether or not the user is an admin. |
#### Example

View file

@ -3,6 +3,7 @@
- [End-to-end tests](#end-to-end-tests)
- [Email](#email)
- [Database backup/restore](#database-backuprestore)
- [MySQL shell](#mysql-shell)
- [Testing SSO](#testing-sso)
## Test suite
@ -169,6 +170,16 @@ Restore:
Note that a "restore" will replace the state of the development database with the state from the backup.
## MySQL shell
Connect to the MySQL shell to view and interact directly with the contents of the development database.
To connect via Docker:
```
docker-compose exec mysql mysql -uroot -ptoor -Dfleet
```
## Testing SSO
Fleet's `docker-compose` file includes a SAML identity provider (IdP) for testing SAML-based SSO locally.

View file

@ -9,7 +9,7 @@ Includes documentation about Fleet's full test suite and integration tests
### [Migrations](./3-Migrations.md)
Information about creating and updating database migrations
### [Commiting Changes](./4-Commiting-Changes.md)
### [Committing Changes](./4-Committing-Changes.md)
Contains information about how to merge changes into the codebase
### [Releasing Fleet](./5-Releasing-Fleet.md)

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

View file

@ -192,7 +192,7 @@ func updatesAddCommand() *cli.Command {
Required: true,
Usage: "Platform name of target (required)",
},
&cli.StringSliceFlag{
&cli.StringFlag{
Name: "version",
Required: true,
Usage: "Version of target (required)",
@ -225,15 +225,19 @@ func updatesAddFunc(c *cli.Context) error {
version := c.String("version")
platform := c.String("platform")
name := c.String("name")
target := c.String("target")
targetsPath := filepath.Join(c.String("path"), "staged", "targets")
var paths []string
for _, tag := range append([]string{version}, tags...) {
dstPath := filepath.Join(name, platform, tag, name)
if strings.HasSuffix(target, ".exe") {
dstPath += ".exe"
}
fullPath := filepath.Join(targetsPath, dstPath)
paths = append(paths, dstPath)
if err := copyTarget(c.String("target"), fullPath); err != nil {
if err := copyTarget(target, fullPath); err != nil {
return err
}
}

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,74 +1,77 @@
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(() => {
return (dispatch) => {
dispatch({ type: 'LOAD_USER_ACTION' });
return Promise.resolve();
};
});
const spy = jest
.spyOn(authActions, "fetchCurrentUser")
.mockImplementation(() => {
return (dispatch) => {
dispatch({ type: "LOAD_USER_ACTION" });
return Promise.resolve();
};
});
const application = connectedComponent(ConnectedApp, { mockStore });
mount(application);
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 });
mount(application);

View file

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

View file

@ -1,10 +1,10 @@
import React, { Component } from 'react';
import 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,29 +13,28 @@ export class AuthenticatedAdminRoutes extends Component {
user: userInterface,
};
componentWillMount () {
const { dispatch, user: { global_role } } = this.props;
componentWillMount() {
const {
dispatch,
user: { global_role },
} = this.props;
const { HOME } = paths;
if (global_role !== 'admin') {
if (global_role !== "admin") {
dispatch(push(HOME));
}
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 a global admin', () => {
const user = { id: 1, global_role: 'admin' };
it("does not redirect if the user is a global admin", () => {
const user = { id: 1, global_role: "admin" };
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: $core-white url('../assets/images/avatar-default.png') center 100% no-repeat;
background: $core-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, {
mockStore,
props: defaultProps,
}));
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, {
mockStore,
props: defaultProps,
}));
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(() => {
const dispatchedActions = mockStore.getActions();
return confirmEmailChange(dispatch, userStub, token).then(() => {
const dispatchedActions = mockStore.getActions();
expect(dispatchedActions).toContainEqual({
type: '@@router/CALL_HISTORY_METHOD',
payload: {
method: 'push',
args: ['/profile'],
},
});
expect(dispatchedActions).toContainEqual({
type: "@@router/CALL_HISTORY_METHOD",
payload: {
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-fleet-blue */}
<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,7 +1,7 @@
import React from 'react';
import classNames from 'classnames';
import React from "react";
import classNames from "classnames";
const baseClass = 'info-banner';
const baseClass = "info-banner";
interface IInfoBannerProps {
children: React.ReactNode;
@ -12,11 +12,7 @@ const InfoBanner = (props: IInfoBannerProps): JSX.Element => {
const { children, className } = props;
const wrapperClasses = classNames(baseClass, className);
return (
<div className={wrapperClasses}>
{children}
</div>
);
return <div className={wrapperClasses}>{children}</div>;
};
export default InfoBanner;

View file

@ -1,6 +1,6 @@
.info-banner {
padding: $pad-medium;
border-radius: $border-radius;
border: 1px solid #D9D9FE;
border: 1px solid #d9d9fe;
background-color: $ui-vibrant-blue-10;
}

View file

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

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,
@ -77,6 +75,11 @@ class KolideAce extends Component {
[`${baseClass}__wrapper--error`]: error,
});
const fixHotkeys = (editor) => {
editor.commands.removeCommands(["gotoline", "find"]);
onLoad && onLoad(editor);
};
return (
<div className={wrapperClass}>
{renderLabel()}
@ -90,7 +93,7 @@ class KolideAce extends Component {
maxLines={20}
name={name}
onChange={onChange}
onLoad={onLoad}
onLoad={fixHotkeys}
readOnly={readOnly}
setOptions={{ enableLinking: true }}
showGutter={showGutter}
@ -99,11 +102,13 @@ class KolideAce extends Component {
value={value}
width="100%"
wrapEnabled={wrapEnabled}
commands={[{
name: 'commandName',
bindKey: { win: 'Ctrl-Enter', mac: 'Ctrl-Enter' },
exec: handleSubmit,
}]}
commands={[
{
name: "commandName",
bindKey: { win: "Ctrl-Enter", mac: "Ctrl-Enter" },
exec: handleSubmit,
},
]}
/>
{renderHint()}
</div>

View file

@ -29,7 +29,7 @@
color: $core-vibrant-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,105 +1,138 @@
/* 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) {
"use strict";
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 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({
"osquery-token": osqueryTables,
"support.function": builtinFunctions,
"keyword": keywords,
"constant.language": builtinConstants,
"storage.type": dataTypes,
}, "identifier", true);
var keywordMapper = this.createKeywordMapper(
{
"osquery-token": osqueryTables,
"support.function": builtinFunctions,
keyword: keywords,
"constant.language": builtinConstants,
"storage.type": dataTypes,
},
"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+"
}]
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+",
},
],
};
this.normalizeRules();
};
this.normalizeRules();
};
oop.inherits(KolideHighlightRules, SqlHighlightRules);
oop.inherits(KolideHighlightRules, SqlHighlightRules);
exports.KolideHighlightRules = KolideHighlightRules;
}
);
exports.KolideHighlightRules = KolideHighlightRules;
});
ace.define(
"ace/mode/kolide",
[
"require",
"exports",
"module",
"ace/lib/oop",
"ace/mode/sql",
"ace/mode/kolide_highlight_rules",
"ace/range",
],
function (acequire, exports, module) {
"use strict";
ace.define("ace/mode/kolide",["require","exports","module","ace/lib/oop","ace/mode/sql","ace/mode/kolide_highlight_rules","ace/range"], function(acequire, exports, module) {
"use strict";
var oop = acequire("../lib/oop");
var TextMode = acequire("./sql").Mode;
var KolideHighlightRules = acequire("./kolide_highlight_rules")
.KolideHighlightRules;
var Range = acequire("../range").Range;
var oop = acequire("../lib/oop");
var TextMode = acequire("./sql").Mode;
var KolideHighlightRules = acequire("./kolide_highlight_rules").KolideHighlightRules;
var Range = acequire("../range").Range;
var Mode = function () {
this.HighlightRules = KolideHighlightRules;
};
oop.inherits(Mode, TextMode);
var Mode = function() {
this.HighlightRules = KolideHighlightRules;
};
oop.inherits(Mode, TextMode);
(function () {
this.lineCommentStart = "--";
(function() {
this.$id = "ace/mode/kolide";
}.call(Mode.prototype));
this.lineCommentStart = "--";
this.$id = "ace/mode/kolide";
}).call(Mode.prototype);
exports.Mode = Mode;
});
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;
}
@ -21,7 +21,7 @@
}
.ace-kolide.ace_autocomplete .ace_content {
padding-left: 0px;
padding-left: 0px;
}
.ace-kolide .ace_content {
@ -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.isDark = false;
exports.cssClass = "ace-kolide";
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,
{
[`${baseClass}--loading`]: isLoading,
[`${baseClass}--loaded`]: isLoaded,
[`${baseClass}--leaving`]: isLeaving,
},
);
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,10 +1,10 @@
import React, { useMemo, useEffect } from 'react';
import PropTypes from 'prop-types';
import { useTable, useSortBy } from 'react-table';
import React, { useMemo, useEffect } from "react";
import PropTypes from "prop-types";
import { useTable, useSortBy } from "react-table";
import Spinner from 'components/loaders/Spinner';
import Spinner from "components/loaders/Spinner";
const baseClass = 'data-table-container';
const baseClass = "data-table-container";
// This data table uses react-table for implementation. The relevant documentation of the library
// can be found here https://react-table.tanstack.com/docs/api/useTable
@ -27,23 +27,18 @@ const DataTable = (props) => {
return tableData;
}, [tableData]);
const {
headerGroups,
rows,
prepareRow,
state: tableState,
} = useTable(
const { headerGroups, rows, prepareRow, state: tableState } = useTable(
{
columns,
data,
initialState: {
sortBy: useMemo(() => {
return [{ id: sortHeader, desc: sortDirection === 'desc' }];
return [{ id: sortHeader, desc: sortDirection === "desc" }];
}, [sortHeader, sortDirection]),
},
disableMultiSort: true,
},
useSortBy,
useSortBy
);
const { sortBy } = tableState;
@ -51,7 +46,10 @@ const DataTable = (props) => {
useEffect(() => {
const column = sortBy[0];
if (column !== undefined) {
if (column.id !== sortHeader || column.desc !== (sortDirection === 'desc')) {
if (
column.id !== sortHeader ||
column.desc !== (sortDirection === "desc")
) {
onSort(column.id, column.desc);
}
} else {
@ -59,23 +57,22 @@ const DataTable = (props) => {
}
}, [sortBy, sortHeader, sortDirection]);
return (
<div className={baseClass}>
<div className={'data-table data-table__wrapper'}>
{isLoading &&
<div className={'loading-overlay'}>
<div className={"data-table data-table__wrapper"}>
{isLoading && (
<div className={"loading-overlay"}>
<Spinner />
</div>
}
)}
{/* TODO: can this be memoized? seems performance heavy */}
<table className={'data-table__table'}>
<table className={"data-table__table"}>
<thead>
{headerGroups.map(headerGroup => (
{headerGroups.map((headerGroup) => (
<tr {...headerGroup.getHeaderGroupProps()}>
{headerGroup.headers.map(column => (
{headerGroup.headers.map((column) => (
<th {...column.getHeaderProps(column.getSortByToggleProps())}>
{column.render('Header')}
{column.render("Header")}
</th>
))}
</tr>
@ -88,15 +85,12 @@ const DataTable = (props) => {
<tr {...row.getRowProps()}>
{row.cells.map((cell) => {
return (
<td {...cell.getCellProps()}>
{cell.render('Cell')}
</td>
<td {...cell.getCellProps()}>{cell.render("Cell")}</td>
);
})}
</tr>
);
})
}
})}
</tbody>
</table>
</div>

View file

@ -1,12 +1,12 @@
import React from 'react';
import React from "react";
// ignore TS error for now until these are rewritten in ts.
// @ts-ignore
import Dropdown from 'components/forms/fields/Dropdown';
import Dropdown from "components/forms/fields/Dropdown";
import { IDropdownOption } from 'interfaces/dropdownOption';
import { IDropdownOption } from "interfaces/dropdownOption";
const baseClass = 'dropdown-cell';
const baseClass = "dropdown-cell";
interface IDropdownCellProps {
options: IDropdownOption[];

View file

@ -1,5 +1,4 @@
.dropdown-cell {
.Select {
position: unset;
@ -20,17 +19,17 @@
}
.Select-menu-outer {
margin-top: $pad-xsmall;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
border-radius: $border-radius;
z-index: 6;
overflow: hidden;
border: 0;
width: auto;
right: 28px;
left: unset;
top: unset;
max-height: 220px;
}
margin-top: $pad-xsmall;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
border-radius: $border-radius;
z-index: 6;
overflow: hidden;
border: 0;
width: auto;
right: 28px;
left: unset;
top: unset;
max-height: 220px;
}
}
}

View file

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

View file

@ -1,4 +1,4 @@
import React from 'react';
import React from "react";
interface IHeaderCellProps {
value: string;
@ -6,18 +6,15 @@ interface IHeaderCellProps {
}
const HeaderCell = (props: IHeaderCellProps): JSX.Element => {
const {
value,
isSortedDesc,
} = props;
const { value, isSortedDesc } = props;
let sortArrowClass = '';
let sortArrowClass = "";
if (isSortedDesc === undefined) {
sortArrowClass = '';
sortArrowClass = "";
} else if (isSortedDesc) {
sortArrowClass = 'descending';
sortArrowClass = "descending";
} else {
sortArrowClass = 'ascending';
sortArrowClass = "ascending";
}
return (

View file

@ -12,29 +12,27 @@
}
.ascending-arrow {
width: 0;
height: 0;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 6px solid $ui-fleet-black-25;
}
.descending-arrow {
width: 0;
height: 0;
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid $ui-fleet-black-25;
}
&.ascending {
.ascending-arrow {
border-bottom-color: $core-vibrant-blue;
}
}
&.descending {
.descending-arrow {
border-top-color: $core-vibrant-blue;
}

View file

@ -1,11 +1,11 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import { push } from 'react-router-redux';
import React from "react";
import { useDispatch } from "react-redux";
import { push } from "react-router-redux";
import { IHost } from 'interfaces/host';
import helpers from 'kolide/helpers';
import PATHS from 'router/paths';
import Button from 'components/buttons/Button/Button';
import { IHost } from "interfaces/host";
import helpers from "kolide/helpers";
import PATHS from "router/paths";
import Button from "components/buttons/Button/Button";
interface ILinkCellProps {
value: string;
@ -24,11 +24,11 @@ const LinkCell = (props: ILinkCellProps): JSX.Element => {
const lastSeenTime = (status: string, seenTime: string): string => {
const { humanHostLastSeen } = helpers;
if (status !== 'online') {
if (status !== "online") {
return `Last Seen: ${humanHostLastSeen(seenTime)} UTC`;
}
return 'Online';
return "Online";
};
return (

View file

@ -1,27 +1,23 @@
import React from 'react';
import classnames from 'classnames';
import React from "react";
import classnames from "classnames";
interface IStatusCellProps {
value: string;
}
const generateClassTag = (rawValue: string): string => {
return rawValue.replace(' ', '-').toLowerCase();
return rawValue.replace(" ", "-").toLowerCase();
};
const StatusCell = (props: IStatusCellProps): JSX.Element => {
const { value } = props;
const statusClassName = classnames(
'data-table__status',
`data-table__status--${generateClassTag(value)}`,
"data-table__status",
`data-table__status--${generateClassTag(value)}`
);
return (
<span className={statusClassName}>
{value}
</span>
);
return <span className={statusClassName}>{value}</span>;
};
export default StatusCell;

View file

@ -1,4 +1,4 @@
import React from 'react';
import React from "react";
interface ITextCellProps {
value: string | number;
@ -8,14 +8,10 @@ interface ITextCellProps {
const TextCell = (props: ITextCellProps): JSX.Element => {
const {
value,
formatter = val => val, // identity function if no formatter is provided
formatter = (val) => val, // identity function if no formatter is provided
} = props;
return (
<span>
{formatter(value)}
</span>
);
return <span>{formatter(value)}</span>;
};
export default TextCell;

View file

@ -68,13 +68,12 @@
&:before {
border-radius: 100%;
content: ' ';
content: " ";
display: inline-block;
height: 8px;
margin-right: $pad-small;
width: 8px;
}
}
.loading-overlay {

View file

@ -1,23 +1,22 @@
import React, { useState, useEffect, useCallback, useRef } from 'react';
import classnames from 'classnames';
import { useAsyncDebounce } from 'react-table';
import React, { useState, useEffect, useCallback, useRef } from "react";
import classnames from "classnames";
import { useAsyncDebounce } from "react-table";
import Button from 'components/buttons/Button';
import Button from "components/buttons/Button";
// ignore TS error for now until these are rewritten in ts.
// @ts-ignore
import InputField from 'components/forms/fields/InputField';
import InputField from "components/forms/fields/InputField";
// @ts-ignore
import KolideIcon from 'components/icons/KolideIcon';
import KolideIcon from "components/icons/KolideIcon";
// @ts-ignore
import Pagination from 'components/Pagination';
import Pagination from "components/Pagination";
// @ts-ignore
import scrollToTop from 'utilities/scroll_to_top';
import scrollToTop from "utilities/scroll_to_top";
// @ts-ignore
import DataTable from './DataTable/DataTable';
import DataTable from "./DataTable/DataTable";
import TableContainerUtils from './TableContainerUtils';
import TableContainerUtils from "./TableContainerUtils";
interface ITableQueryData {
searchQuery: string;
@ -45,13 +44,15 @@ interface ITableContainerProps<T, U> {
className?: string;
}
const baseClass = 'table-container';
const baseClass = "table-container";
const DEFAULT_PAGE_SIZE = 100;
const DEFAULT_PAGE_INDEX = 0;
const DEBOUNCE_QUERY_DELAY = 300;
const TableContainer = <T, U>(props: ITableContainerProps<T, U>): JSX.Element => {
const TableContainer = <T, U>(
props: ITableContainerProps<T, U>
): JSX.Element => {
const {
columns,
data,
@ -69,9 +70,11 @@ const TableContainer = <T, U>(props: ITableContainerProps<T, U>): JSX.Element =>
actionButtonText,
} = props;
const [searchQuery, setSearchQuery] = useState('');
const [sortHeader, setSortHeader] = useState(defaultSortHeader || '');
const [sortDirection, setSortDirection] = useState(defaultSortDirection || '');
const [searchQuery, setSearchQuery] = useState("");
const [sortHeader, setSortHeader] = useState(defaultSortHeader || "");
const [sortDirection, setSortDirection] = useState(
defaultSortDirection || ""
);
const [pageSize] = useState(DEFAULT_PAGE_SIZE);
const [pageIndex, setPageIndex] = useState(DEFAULT_PAGE_INDEX);
@ -79,17 +82,19 @@ const TableContainer = <T, U>(props: ITableContainerProps<T, U>): JSX.Element =>
const EmptyComponent = emptyComponent;
const onSortChange = useCallback((id?:string, isDesc?: boolean) => {
if (id === undefined) {
setSortHeader('');
setSortDirection('');
} else {
setSortHeader(id);
const direction = isDesc ? 'desc' : 'asc';
setSortDirection(direction);
}
}, [setSortHeader, setSortDirection]);
const onSortChange = useCallback(
(id?: string, isDesc?: boolean) => {
if (id === undefined) {
setSortHeader("");
setSortDirection("");
} else {
setSortHeader(id);
const direction = isDesc ? "desc" : "asc";
setSortDirection(direction);
}
},
[setSortHeader, setSortDirection]
);
const onSearchQueryChange = (value: string) => {
setSearchQuery(value);
@ -106,9 +111,12 @@ const TableContainer = <T, U>(props: ITableContainerProps<T, U>): JSX.Element =>
// to later compare this the the current value and debounce a change handler.
const prevSearchQueryRef = useRef(searchQuery);
const prevSearchQuery = prevSearchQueryRef.current;
const debounceOnQueryChange = useAsyncDebounce((queryData: ITableQueryData) => {
onQueryChange(queryData);
}, DEBOUNCE_QUERY_DELAY);
const debounceOnQueryChange = useAsyncDebounce(
(queryData: ITableQueryData) => {
onQueryChange(queryData);
},
DEBOUNCE_QUERY_DELAY
);
// When any of our query params change, or if any additionalQueries change, we want to fire off
// the parent components handler function with this updated query data. There is logic in here to check
@ -140,17 +148,31 @@ const TableContainer = <T, U>(props: ITableContainerProps<T, U>): JSX.Element =>
}
hasPageIndexChangedRef.current = false;
}, [searchQuery, sortHeader, sortDirection, pageSize, pageIndex, additionalQueries, onQueryChange]);
}, [
searchQuery,
sortHeader,
sortDirection,
pageSize,
pageIndex,
additionalQueries,
onQueryChange,
]);
return (
<div className={wrapperClasses}>
<div className={`${baseClass}__header`}>
{ data && data.length ?
{data && data.length ? (
<p className={`${baseClass}__results-count`}>
{TableContainerUtils.generateResultsCountText(resultsTitle, pageIndex, pageSize, data.length)}
</p> :
{TableContainerUtils.generateResultsCountText(
resultsTitle,
pageIndex,
pageSize,
data.length
)}
</p>
) : (
<p />
}
)}
<div className={`${baseClass}__table-controls`}>
<Button
disabled={disableActionButton}
@ -174,8 +196,9 @@ const TableContainer = <T, U>(props: ITableContainerProps<T, U>): JSX.Element =>
</div>
<div className={`${baseClass}__data-table-container`}>
{/* No entities for this result. */}
{!isLoading && data.length === 0 ?
<EmptyComponent /> :
{!isLoading && data.length === 0 ? (
<EmptyComponent />
) : (
<>
<DataTable
isLoading={isLoading}
@ -192,8 +215,7 @@ const TableContainer = <T, U>(props: ITableContainerProps<T, U>): JSX.Element =>
onPaginationChange={onPaginationChange}
/>
</>
}
)}
</div>
</div>
);

View file

@ -1,10 +1,16 @@
const DEFAULT_RESULTS_NAME = 'results';
const DEFAULT_RESULTS_NAME = "results";
const generateResultsCountText = (name: string = DEFAULT_RESULTS_NAME, pageIndex: number, pageSize: number, resultsCount: number) => {
const generateResultsCountText = (
name: string = DEFAULT_RESULTS_NAME,
pageIndex: number,
pageSize: number,
resultsCount: number
) => {
if (resultsCount === 0) return `No ${name}`;
if (pageSize === resultsCount) return `${pageSize}+ ${name}`;
if (pageIndex !== 0 && (resultsCount <= pageSize)) return `${pageSize}+ ${name}`;
if (pageIndex !== 0 && resultsCount <= pageSize)
return `${pageSize}+ ${name}`;
return `${resultsCount} ${name}`;
};

View file

@ -1,5 +1,4 @@
.table-container {
&__header {
display: flex;
align-items: center;

View file

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

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

Some files were not shown because too many files have changed in this diff Show more