mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Merge master into teams
This commit is contained in:
commit
04712c0426
755 changed files with 44994 additions and 31320 deletions
88
.eslintrc.js
88
.eslintrc.js
|
|
@ -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"),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
3
.github/ISSUE_TEMPLATE/bug-report.md
vendored
3
.github/ISSUE_TEMPLATE/bug-report.md
vendored
|
|
@ -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
43
.github/workflows/test-website.yml
vendored
Normal 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
|
||||
43
.github/workflows/test.yml
vendored
43
.github/workflows/test.yml
vendored
|
|
@ -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
34
.prettierignore
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# markdown
|
||||
*.md
|
||||
|
||||
# output directories
|
||||
build
|
||||
vendor
|
||||
node_modules
|
||||
|
||||
# generated artifacts
|
||||
assets/bundle*.*
|
||||
assets/*@*.svg
|
||||
assets/*@*.png
|
||||
assets/*@*.eot
|
||||
assets/*@*.woff
|
||||
assets/*@*.woff2
|
||||
assets/*@*.ttf
|
||||
frontend/templates/react.tmpl
|
||||
bindata.go
|
||||
server/bindata/generated.go
|
||||
*.cover
|
||||
*.test
|
||||
*.log
|
||||
|
||||
# typescript generated test files
|
||||
tmp/
|
||||
|
||||
# editors
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# Cypress e2e testing
|
||||
cypress/screenshots
|
||||
cypress/videos
|
||||
cypress/downloads
|
||||
1
.prettierrc.json
Normal file
1
.prettierrc.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{}
|
||||
120
.sass-lint.yml
120
.sass-lint.yml
|
|
@ -1,120 +0,0 @@
|
|||
# Linter ReadMe: https://github.com/sasstools/sass-lint/tree/master/docs/rules
|
||||
files:
|
||||
include: 'frontend/**/*.s+(a|c)ss'
|
||||
ignore:
|
||||
- 'frontend/styles/global/_fonts.scss'
|
||||
- 'frontend/styles/global/_icons.scss'
|
||||
options:
|
||||
formatter: stylish
|
||||
merge-default-rules: false
|
||||
rules:
|
||||
bem-depth:
|
||||
- 1
|
||||
- max-depth: 1
|
||||
border-zero:
|
||||
- 1
|
||||
- convention: '0'
|
||||
brace-style:
|
||||
- 1
|
||||
- allow-single-line: false
|
||||
class-name-format:
|
||||
- 1
|
||||
- convention: ^(?!js-).*
|
||||
convention-explanation: should not be written in the form js-*
|
||||
clean-import-paths:
|
||||
- 1
|
||||
- filename-extension: true
|
||||
leading-underscore: false
|
||||
empty-line-between-blocks:
|
||||
- 1
|
||||
- ignore-single-line-rulesets: false
|
||||
extends-before-declarations: 1
|
||||
extends-before-mixins: 1
|
||||
final-newline:
|
||||
- 1
|
||||
- include: true
|
||||
force-attribute-nesting: 1
|
||||
force-element-nesting: 1
|
||||
force-pseudo-nesting: 1
|
||||
function-name-format:
|
||||
- 1
|
||||
- convention: '^[\-_a-z]+$'
|
||||
convention-explanation: 'Variables must contain only lowercase letters, hyphens, and underscores'
|
||||
hex-length:
|
||||
- 1
|
||||
- style: short
|
||||
hex-notation:
|
||||
- 1
|
||||
- style: lowercase
|
||||
id-name-format:
|
||||
- 1
|
||||
- convention: hyphenatedbem
|
||||
indentation:
|
||||
- 1
|
||||
- size: 2
|
||||
leading-zero:
|
||||
- 0
|
||||
mixin-name-format:
|
||||
- 1
|
||||
- convention: '^[\-_a-z]+$'
|
||||
convention-explanation: 'Variables must contain only lowercase letters, hyphens, and underscores'
|
||||
mixins-before-declarations:
|
||||
- 1
|
||||
- exclude: ['breakpoint', 'media', 'placeholder']
|
||||
nesting-depth:
|
||||
- 1
|
||||
- max-depth: 4
|
||||
no-color-keywords: 1
|
||||
no-color-literals: 0
|
||||
no-css-comments: 1
|
||||
no-duplicate-properties: 1
|
||||
no-empty-rulesets: 1
|
||||
no-extends: 0
|
||||
no-ids: 1
|
||||
no-important: 1
|
||||
no-invalid-hex: 1
|
||||
no-mergeable-selectors: 1
|
||||
no-misspelled-properties:
|
||||
- 1
|
||||
- extra-properties: []
|
||||
no-qualifying-elements:
|
||||
- 1
|
||||
- allow-element-with-attribute: true
|
||||
allow-element-with-class: false
|
||||
allow-element-with-id: false
|
||||
no-trailing-zero: 1
|
||||
no-transition-all: 1
|
||||
no-url-protocols: 1
|
||||
placeholder-name-format:
|
||||
- 1
|
||||
- convention: hyphenatedbem
|
||||
property-sort-order: 0
|
||||
quotes:
|
||||
- 1
|
||||
- style: single
|
||||
shorthand-values: 1
|
||||
single-line-per-selector: 1
|
||||
space-after-bang:
|
||||
- 1
|
||||
- include: false
|
||||
space-after-colon:
|
||||
- 1
|
||||
- include: true
|
||||
space-after-comma: 1
|
||||
space-before-bang:
|
||||
- 1
|
||||
- include: true
|
||||
space-before-brace:
|
||||
- 1
|
||||
- include: true
|
||||
space-before-colon: 1
|
||||
space-between-parens:
|
||||
- 1
|
||||
- include: false
|
||||
trailing-semicolon: 1
|
||||
url-quotes: 1
|
||||
variable-name-format:
|
||||
- 1
|
||||
- convention: '^[\-_a-z]+$'
|
||||
convention-explanation: 'Variables must contain only lowercase letters, hyphens, and underscores'
|
||||
zero-unit: 1
|
||||
|
|
@ -1,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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||

|
||||
|
||||
#### 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.
|
||||
|
||||
|
|
|
|||
6
Makefile
6
Makefile
|
|
@ -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 |
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
5
cypress/fixtures/example.json
Normal file
5
cypress/fixtures/example.json
Normal 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"
|
||||
}
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
59
cypress/integration/app/labelflow.spec.ts
Normal file
59
cypress/integration/app/labelflow.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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', () => {
|
||||
|
|
|
|||
54
cypress/integration/app/packflow.spec.ts
Normal file
54
cypress/integration/app/packflow.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
66
cypress/integration/app/queryflow.spec.ts
Normal file
66
cypress/integration/app/queryflow.spec.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
|
|
@ -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$/);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,4 +14,4 @@
|
|||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands';
|
||||
import "./commands";
|
||||
|
|
|
|||
|
|
@ -4,7 +4,5 @@
|
|||
"lib": ["es5", "dom"],
|
||||
"types": ["cypress", "@testing-library/cypress", "node"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
]
|
||||
"include": ["**/*.ts"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
BIN
docs/images/my-account-page.png
Normal file
BIN
docs/images/my-account-page.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 270 KiB |
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
// __mocks__/fileMock.js
|
||||
|
||||
module.exports = 'test-file-stub';
|
||||
module.exports = "test-file-stub";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export default {
|
||||
FAKE_PASSWORD: '********',
|
||||
DEFAULT_SMTP_PORT: '587',
|
||||
FAKE_PASSWORD: "********",
|
||||
DEFAULT_SMTP_PORT: "587",
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './App';
|
||||
export { default } from "./App";
|
||||
|
|
|
|||
|
|
@ -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}</>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './AuthenticatedAdminRoutes';
|
||||
export { default } from "./AuthenticatedAdminRoutes";
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './AuthenticatedRoutes';
|
||||
export { default } from "./AuthenticatedRoutes";
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './AuthenticationFormWrapper';
|
||||
export { default } from "./AuthenticationFormWrapper";
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './Avatar.tsx';
|
||||
export { default } from "./Avatar.tsx";
|
||||
|
|
|
|||
|
|
@ -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} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './ClickOutside';
|
||||
export { default } from "./ClickOutside";
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 />;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './EmailTokenRedirect';
|
||||
export { default } from "./EmailTokenRedirect";
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './EnsureUnauthenticated';
|
||||
export { default } from "./EnsureUnauthenticated";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './IconToolTip';
|
||||
export { default } from "./IconToolTip";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './InfoBanner';
|
||||
export { default } from "./InfoBanner";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@
|
|||
color: $core-vibrant-blue;
|
||||
background-color: $ui-light-grey;
|
||||
padding: 2px;
|
||||
font-family: 'SourceCodePro', $monospace;
|
||||
font-family: "SourceCodePro", $monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './KolideAce';
|
||||
export { default } from "./KolideAce";
|
||||
|
|
|
|||
|
|
@ -1,105 +1,138 @@
|
|||
/* eslint-disable */
|
||||
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;
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './LoginRoutes';
|
||||
export { default } from "./LoginRoutes";
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './NumberPill';
|
||||
export { default } from "./NumberPill";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
.pagination {
|
||||
|
||||
&__pager-wrap {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './Pagination';
|
||||
export { default } from "./Pagination";
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './StackedWhiteBoxes';
|
||||
export { default } from "./StackedWhiteBoxes";
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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[];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './DropdownCell';
|
||||
export { default } from "./DropdownCell";
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -68,13 +68,12 @@
|
|||
|
||||
&:before {
|
||||
border-radius: 100%;
|
||||
content: ' ';
|
||||
content: " ";
|
||||
display: inline-block;
|
||||
height: 8px;
|
||||
margin-right: $pad-small;
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
.table-container {
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { default } from './TableContainer';
|
||||
export { default } from "./TableContainer";
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue