From 6cfd1d48b3c7276d1719ca7c7e5cc65ae6528a72 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Fri, 17 Jan 2020 16:53:04 -0700 Subject: [PATCH 01/39] [SIEM][Detection Engine] Fixes critical regression on the backend with immutable and tags ## Summary Fixes regression with immutable caused from: https://github.com/elastic/kibana/pull/55004 * Updated types of Prepackaged * Updated unit tests * Fixed unit test for it Testing: ``` ./post_rule.sh { "created_at": "2020-01-17T19:11:31.813Z", "updated_at": "2020-01-17T19:11:31.813Z", "created_by": "elastic_kibana", "description": "Query with a rule_id that acts like an external id", "enabled": true, "false_positives": [], "from": "now-6m", "id": "41ef6309-ef98-4c9f-8d2d-90a070361fb7", "immutable": false, "interval": "5m", "rule_id": "query-rule-id", "language": "kuery", "output_index": ".siem-signals-frank-hassanabad-default", "max_signals": 100, "risk_score": 1, "name": "Query with a rule id", "query": "user.name: root or user.name: admin", "references": [], "severity": "high", "updated_by": "elastic_kibana", "tags": [], "to": "now", "type": "query", "threats": [], "version": 1 } ``` Then get the saved object using whatever the id is comes back from above. In this example it is 41ef6309-ef98-4c9f-8d2d-90a070361fb7, yours will be different ``` ./get_saved_objects.sh alert 41ef6309-ef98-4c9f-8d2d-90a070361fb7 { "id": "41ef6309-ef98-4c9f-8d2d-90a070361fb7", "type": "alert", "updated_at": "2020-01-17T19:11:32.844Z", "version": "WzY5NTQsMV0=", "attributes": { "name": "Query with a rule id", "tags": [ "__internal_rule_id:query-rule-id", "__internal_immutable:false" ], "alertTypeId": "siem.signals", "consumer": "siem", "params": { "createdAt": "2020-01-17T19:11:31.813Z", "description": "Query with a rule_id that acts like an external id", "ruleId": "query-rule-id", "index": null, "falsePositives": [], "from": "now-6m", "immutable": false, "query": "user.name: root or user.name: admin", "language": "kuery", "outputIndex": ".siem-signals-frank-hassanabad-default", "savedId": null, "timelineId": null, "timelineTitle": null, "meta": null, "filters": null, "maxSignals": 100, "riskScore": 1, "severity": "high", "threats": [], "to": "now", "type": "query", "updatedAt": "2020-01-17T19:11:31.813Z", "references": [], "version": 1 }, "schedule": { "interval": "5m" }, "enabled": true, "actions": [], "throttle": null, "apiKeyOwner": "elastic_kibana", "createdBy": "elastic_kibana", "updatedBy": "elastic_kibana", "createdAt": "2020-01-17T19:11:32.245Z", "muteAll": false, "mutedInstanceIds": [], "scheduledTaskId": "2c5cc340-395d-11ea-9276-d3c1c264ca9a" }, "references": [] } ``` Ensure you have the internal immutable of "__internal_immutable:false" In your tags Next test is to do a find filter of non-packaged rules: ``` ./find_rule_by_filter.sh "alert.attributes.tags:%20%22__internal_immutable:false%22" ``` You should get back the above rule any others you created. ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. ~~- [ ] This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~~ ~~- [ ] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md)~~ ~~- [ ] [Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~~ - [x] [Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios ~~- [ ] This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~~ ### For maintainers ~~- [ ] This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~~ - [x] This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process) --- .../routes/__mocks__/request_responses.ts | 6 +- .../routes/rules/create_rules_bulk_route.ts | 3 +- .../routes/rules/create_rules_route.ts | 3 +- .../routes/rules/update_rules_bulk_route.ts | 2 - .../routes/rules/update_rules_route.ts | 2 - .../add_prepackaged_rules_schema.test.ts | 127 +++++++++--------- .../schemas/create_rules_schema.test.ts | 3 - .../schemas/import_rules_schema.test.ts | 116 ++++++++-------- .../rules/get_prepackaged_rules.test.ts | 4 +- .../rules/get_prepackaged_rules.ts | 8 +- .../rules/get_rules_to_install.test.ts | 16 +-- .../rules/get_rules_to_install.ts | 6 +- .../rules/get_rules_to_update.test.ts | 16 +-- .../rules/get_rules_to_update.ts | 6 +- .../rules/install_prepacked_rules.ts | 4 +- .../rules/update_prepacked_rules.ts | 4 +- .../siem/server/lib/detection_engine/types.ts | 15 +++ 17 files changed, 170 insertions(+), 171 deletions(-) diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 30a8d9d935128..0c6ab1c82bcb8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -18,9 +18,9 @@ import { DETECTION_ENGINE_PREPACKAGED_URL, } from '../../../../../common/constants'; import { RuleAlertType, IRuleSavedAttributesSavedObjectAttributes } from '../../rules/types'; -import { RuleAlertParamsRest } from '../../types'; +import { RuleAlertParamsRest, PrepackagedRules } from '../../types'; -export const fullRuleAlertParamsRest = (): RuleAlertParamsRest => ({ +export const mockPrepackagedRule = (): PrepackagedRules => ({ rule_id: 'rule-1', description: 'Detecting root and admin users', index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], @@ -51,8 +51,6 @@ export const fullRuleAlertParamsRest = (): RuleAlertParamsRest => ({ false_positives: [], saved_id: 'some-id', max_signals: 100, - created_at: '2019-12-13T16:40:33.400Z', - updated_at: '2019-12-13T16:40:33.400Z', timeline_id: 'timeline-id', timeline_title: 'timeline-title', }); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index 9c18f9040008c..00a1d2eb980ec 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -55,7 +55,6 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou enabled, false_positives: falsePositives, from, - immutable, query, language, output_index: outputIndex, @@ -109,7 +108,7 @@ export const createCreateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou enabled, falsePositives, from, - immutable, + immutable: false, query, language, outputIndex: finalIndex, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index aa535d325f4b9..23acd12d341ed 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -39,7 +39,6 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = enabled, false_positives: falsePositives, from, - immutable, query, language, output_index: outputIndex, @@ -96,7 +95,7 @@ export const createCreateRulesRoute = (server: ServerFacade): Hapi.ServerRoute = enabled, falsePositives, from, - immutable, + immutable: false, query, language, outputIndex: finalIndex, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 180a75bdaaeea..b01108f0de21f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -44,7 +44,6 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou enabled, false_positives: falsePositives, from, - immutable, query, language, output_index: outputIndex, @@ -77,7 +76,6 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou enabled, falsePositives, from, - immutable, query, language, outputIndex, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index 147f3f9afa549..533fe9b724943 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -33,7 +33,6 @@ export const createUpdateRulesRoute: Hapi.ServerRoute = { enabled, false_positives: falsePositives, from, - immutable, query, language, output_index: outputIndex, @@ -75,7 +74,6 @@ export const createUpdateRulesRoute: Hapi.ServerRoute = { enabled, falsePositives, from, - immutable, query, language, outputIndex, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts index 1993948808ef4..abdd5a0c7b508 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/add_prepackaged_rules_schema.test.ts @@ -4,20 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UpdateRuleAlertParamsRest } from '../../rules/types'; -import { ThreatParams, RuleAlertParamsRest } from '../../types'; +import { ThreatParams, PrepackagedRules } from '../../types'; import { addPrepackagedRulesSchema } from './add_prepackaged_rules_schema'; describe('add prepackaged rules schema', () => { test('empty objects do not validate', () => { - expect( - addPrepackagedRulesSchema.validate>({}).error - ).toBeTruthy(); + expect(addPrepackagedRulesSchema.validate>({}).error).toBeTruthy(); }); test('made up values do not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -25,7 +22,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', }).error ).toBeTruthy(); @@ -33,7 +30,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', }).error @@ -42,7 +39,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -52,7 +49,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -63,7 +60,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, name] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -75,7 +72,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, name, severity] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -88,7 +85,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, name, severity, type] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -102,7 +99,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -117,7 +114,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -133,7 +130,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, name, severity, type, query, index, interval, version] does validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -152,7 +149,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -170,7 +167,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, version] does validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -190,7 +187,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does not validate because output_index is not allowed', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -211,7 +208,7 @@ describe('add prepackaged rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, version] does validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -229,7 +226,7 @@ describe('add prepackaged rules schema', () => { test('You can send in an empty array to threats', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -251,7 +248,7 @@ describe('add prepackaged rules schema', () => { }); test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, version, threats] does validate', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -286,7 +283,7 @@ describe('add prepackaged rules schema', () => { test('allows references to be sent as valid', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -307,7 +304,7 @@ describe('add prepackaged rules schema', () => { test('defaults references to an array', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -327,7 +324,7 @@ describe('add prepackaged rules schema', () => { test('defaults immutable to true', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -347,7 +344,7 @@ describe('add prepackaged rules schema', () => { test('immutable cannot be false', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -368,7 +365,7 @@ describe('add prepackaged rules schema', () => { test('immutable can be true', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -389,7 +386,7 @@ describe('add prepackaged rules schema', () => { test('defaults enabled to false', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -409,7 +406,7 @@ describe('add prepackaged rules schema', () => { test('rule_id is required', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ risk_score: 50, description: 'some description', from: 'now-5m', @@ -429,7 +426,7 @@ describe('add prepackaged rules schema', () => { test('references cannot be numbers', () => { expect( addPrepackagedRulesSchema.validate< - Partial> & { references: number[] } + Partial> & { references: number[] } >({ rule_id: 'rule-1', risk_score: 50, @@ -454,7 +451,7 @@ describe('add prepackaged rules schema', () => { test('indexes cannot be numbers', () => { expect( addPrepackagedRulesSchema.validate< - Partial> & { index: number[] } + Partial> & { index: number[] } >({ rule_id: 'rule-1', risk_score: 50, @@ -477,7 +474,7 @@ describe('add prepackaged rules schema', () => { test('defaults interval to 5 min', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -494,7 +491,7 @@ describe('add prepackaged rules schema', () => { test('defaults max signals to 100', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -512,7 +509,7 @@ describe('add prepackaged rules schema', () => { test('saved_id is required when type is saved_query and will not validate without out', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -530,7 +527,7 @@ describe('add prepackaged rules schema', () => { test('saved_id is required when type is saved_query and validates with it', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -549,7 +546,7 @@ describe('add prepackaged rules schema', () => { test('saved_query type can have filters with it', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -570,7 +567,7 @@ describe('add prepackaged rules schema', () => { test('filters cannot be a string', () => { expect( addPrepackagedRulesSchema.validate< - Partial & { filters: string }> + Partial & { filters: string }> >({ rule_id: 'rule-1', risk_score: 50, @@ -591,7 +588,7 @@ describe('add prepackaged rules schema', () => { test('language validates with kuery', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -612,7 +609,7 @@ describe('add prepackaged rules schema', () => { test('language validates with lucene', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -633,7 +630,7 @@ describe('add prepackaged rules schema', () => { test('language does not validate with something made up', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -654,7 +651,7 @@ describe('add prepackaged rules schema', () => { test('max_signals cannot be negative', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -676,7 +673,7 @@ describe('add prepackaged rules schema', () => { test('max_signals cannot be zero', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -698,7 +695,7 @@ describe('add prepackaged rules schema', () => { test('max_signals can be 1', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -720,7 +717,7 @@ describe('add prepackaged rules schema', () => { test('You can optionally send in an array of tags', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -744,7 +741,7 @@ describe('add prepackaged rules schema', () => { test('You cannot send in an array of tags that are numbers', () => { expect( addPrepackagedRulesSchema.validate< - Partial> & { tags: number[] } + Partial> & { tags: number[] } >({ rule_id: 'rule-1', risk_score: 50, @@ -771,7 +768,7 @@ describe('add prepackaged rules schema', () => { test('You cannot send in an array of threats that are missing "framework"', () => { expect( addPrepackagedRulesSchema.validate< - Partial> & { + Partial> & { threats: Array>>; } >({ @@ -815,7 +812,7 @@ describe('add prepackaged rules schema', () => { test('You cannot send in an array of threats that are missing "tactic"', () => { expect( addPrepackagedRulesSchema.validate< - Partial> & { + Partial> & { threats: Array>>; } >({ @@ -855,7 +852,7 @@ describe('add prepackaged rules schema', () => { test('You cannot send in an array of threats that are missing "techniques"', () => { expect( addPrepackagedRulesSchema.validate< - Partial> & { + Partial> & { threats: Array>>; } >({ @@ -892,7 +889,7 @@ describe('add prepackaged rules schema', () => { test('You can optionally send in an array of false positives', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -916,7 +913,7 @@ describe('add prepackaged rules schema', () => { test('You cannot send in an array of false positives that are numbers', () => { expect( addPrepackagedRulesSchema.validate< - Partial> & { false_positives: number[] } + Partial> & { false_positives: number[] } >({ rule_id: 'rule-1', risk_score: 50, @@ -942,7 +939,7 @@ describe('add prepackaged rules schema', () => { test('You can optionally set the immutable to be true', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -966,7 +963,7 @@ describe('add prepackaged rules schema', () => { test('You cannot set the immutable to be a number', () => { expect( addPrepackagedRulesSchema.validate< - Partial> & { immutable: number } + Partial> & { immutable: number } >({ rule_id: 'rule-1', risk_score: 50, @@ -990,7 +987,7 @@ describe('add prepackaged rules schema', () => { test('You cannot set the risk_score to 101', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 101, description: 'some description', @@ -1013,7 +1010,7 @@ describe('add prepackaged rules schema', () => { test('You cannot set the risk_score to -1', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: -1, description: 'some description', @@ -1036,7 +1033,7 @@ describe('add prepackaged rules schema', () => { test('You can set the risk_score to 0', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 0, description: 'some description', @@ -1059,7 +1056,7 @@ describe('add prepackaged rules schema', () => { test('You can set the risk_score to 100', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 100, description: 'some description', @@ -1082,7 +1079,7 @@ describe('add prepackaged rules schema', () => { test('You can set meta to any object you want', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -1109,7 +1106,7 @@ describe('add prepackaged rules schema', () => { test('You cannot create meta as a string', () => { expect( addPrepackagedRulesSchema.validate< - Partial & { meta: string }> + Partial & { meta: string }> >({ rule_id: 'rule-1', risk_score: 50, @@ -1134,7 +1131,7 @@ describe('add prepackaged rules schema', () => { test('You can omit the query string when filters are present', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -1157,7 +1154,7 @@ describe('add prepackaged rules schema', () => { test('validates with timeline_id and timeline_title', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -1180,7 +1177,7 @@ describe('add prepackaged rules schema', () => { test('You cannot omit timeline_title when timeline_id is present', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -1204,7 +1201,7 @@ describe('add prepackaged rules schema', () => { test('You cannot have a null value for timeline_title when timeline_id is present', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -1229,7 +1226,7 @@ describe('add prepackaged rules schema', () => { test('You cannot have empty string for timeline_title when timeline_id is present', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -1254,7 +1251,7 @@ describe('add prepackaged rules schema', () => { test('You cannot have timeline_title with an empty timeline_id', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -1279,7 +1276,7 @@ describe('add prepackaged rules schema', () => { test('You cannot have timeline_title without timeline_id', () => { expect( - addPrepackagedRulesSchema.validate>({ + addPrepackagedRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts index 15f4fa7f05648..c76071047434c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/create_rules_schema.test.ts @@ -884,7 +884,6 @@ describe('create rules schema', () => { description: 'some description', from: 'now-5m', to: 'now', - immutable: true, index: ['index-1'], name: 'some-name', severity: 'severity', @@ -907,7 +906,6 @@ describe('create rules schema', () => { description: 'some description', from: 'now-5m', to: 'now', - immutable: true, index: ['index-1'], name: 'some-name', severity: 'severity', @@ -999,7 +997,6 @@ describe('create rules schema', () => { description: 'some description', from: 'now-5m', to: 'now', - immutable: true, index: ['index-1'], name: 'some-name', severity: 'severity', diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts index bed64cc6e7a02..20f418c57b5db 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/schemas/import_rules_schema.test.ts @@ -9,18 +9,18 @@ import { importRulesQuerySchema, importRulesPayloadSchema, } from './import_rules_schema'; -import { ThreatParams, RuleAlertParamsRest, ImportRuleAlertRest } from '../../types'; +import { ThreatParams, ImportRuleAlertRest } from '../../types'; import { ImportRulesRequest } from '../../rules/types'; describe('import rules schema', () => { describe('importRulesSchema', () => { test('empty objects do not validate', () => { - expect(importRulesSchema.validate>({}).error).toBeTruthy(); + expect(importRulesSchema.validate>({}).error).toBeTruthy(); }); test('made up values do not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ madeUp: 'hi', }).error ).toBeTruthy(); @@ -28,7 +28,7 @@ describe('import rules schema', () => { test('[rule_id] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', }).error ).toBeTruthy(); @@ -36,7 +36,7 @@ describe('import rules schema', () => { test('[rule_id, description] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', }).error @@ -45,7 +45,7 @@ describe('import rules schema', () => { test('[rule_id, description, from] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -55,7 +55,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -66,7 +66,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, name] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -78,7 +78,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, name, severity] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -91,7 +91,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, name, severity, type] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -105,7 +105,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, name, severity, type, interval] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -120,7 +120,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, name, severity, type, interval, index] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -136,7 +136,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, name, severity, type, query, index, interval] does validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -154,7 +154,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language] does not validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -172,7 +172,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score] does validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, description: 'some description', @@ -191,7 +191,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, query, language, risk_score, output_index] does validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -211,7 +211,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score] does validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', description: 'some description', from: 'now-5m', @@ -228,7 +228,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index] does validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -246,7 +246,7 @@ describe('import rules schema', () => { test('You can send in an empty array to threats', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -269,7 +269,7 @@ describe('import rules schema', () => { test('[rule_id, description, from, to, index, name, severity, interval, type, filter, risk_score, output_index, threats] does validate', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -304,7 +304,7 @@ describe('import rules schema', () => { test('allows references to be sent as valid', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -325,7 +325,7 @@ describe('import rules schema', () => { test('defaults references to an array', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -346,7 +346,7 @@ describe('import rules schema', () => { test('references cannot be numbers', () => { expect( importRulesSchema.validate< - Partial> & { references: number[] } + Partial> & { references: number[] } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -371,7 +371,7 @@ describe('import rules schema', () => { test('indexes cannot be numbers', () => { expect( importRulesSchema.validate< - Partial> & { index: number[] } + Partial> & { index: number[] } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -394,7 +394,7 @@ describe('import rules schema', () => { test('defaults interval to 5 min', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -411,7 +411,7 @@ describe('import rules schema', () => { test('defaults max signals to 100', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -429,7 +429,7 @@ describe('import rules schema', () => { test('saved_id is required when type is saved_query and will not validate without out', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -447,7 +447,7 @@ describe('import rules schema', () => { test('saved_id is required when type is saved_query and validates with it', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, output_index: '.siem-signals', @@ -466,7 +466,7 @@ describe('import rules schema', () => { test('saved_query type can have filters with it', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -487,7 +487,7 @@ describe('import rules schema', () => { test('filters cannot be a string', () => { expect( importRulesSchema.validate< - Partial & { filters: string }> + Partial & { filters: string }> >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -508,7 +508,7 @@ describe('import rules schema', () => { test('language validates with kuery', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -529,7 +529,7 @@ describe('import rules schema', () => { test('language validates with lucene', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', risk_score: 50, output_index: '.siem-signals', @@ -550,7 +550,7 @@ describe('import rules schema', () => { test('language does not validate with something made up', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -571,7 +571,7 @@ describe('import rules schema', () => { test('max_signals cannot be negative', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -593,7 +593,7 @@ describe('import rules schema', () => { test('max_signals cannot be zero', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -615,7 +615,7 @@ describe('import rules schema', () => { test('max_signals can be 1', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -637,7 +637,7 @@ describe('import rules schema', () => { test('You can optionally send in an array of tags', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -660,7 +660,7 @@ describe('import rules schema', () => { test('You cannot send in an array of tags that are numbers', () => { expect( - importRulesSchema.validate> & { tags: number[] }>( + importRulesSchema.validate> & { tags: number[] }>( { rule_id: 'rule-1', output_index: '.siem-signals', @@ -688,7 +688,7 @@ describe('import rules schema', () => { test('You cannot send in an array of threats that are missing "framework"', () => { expect( importRulesSchema.validate< - Partial> & { + Partial> & { threats: Array>>; } >({ @@ -732,7 +732,7 @@ describe('import rules schema', () => { test('You cannot send in an array of threats that are missing "tactic"', () => { expect( importRulesSchema.validate< - Partial> & { + Partial> & { threats: Array>>; } >({ @@ -772,7 +772,7 @@ describe('import rules schema', () => { test('You cannot send in an array of threats that are missing "techniques"', () => { expect( importRulesSchema.validate< - Partial> & { + Partial> & { threats: Array>>; } >({ @@ -809,7 +809,7 @@ describe('import rules schema', () => { test('You can optionally send in an array of false positives', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -833,7 +833,7 @@ describe('import rules schema', () => { test('You cannot send in an array of false positives that are numbers', () => { expect( importRulesSchema.validate< - Partial> & { false_positives: number[] } + Partial> & { false_positives: number[] } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -859,7 +859,7 @@ describe('import rules schema', () => { test('You can optionally set the immutable to be true', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -883,7 +883,7 @@ describe('import rules schema', () => { test('You cannot set the immutable to be a number', () => { expect( importRulesSchema.validate< - Partial> & { immutable: number } + Partial> & { immutable: number } >({ rule_id: 'rule-1', output_index: '.siem-signals', @@ -907,7 +907,7 @@ describe('import rules schema', () => { test('You cannot set the risk_score to 101', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 101, @@ -930,7 +930,7 @@ describe('import rules schema', () => { test('You cannot set the risk_score to -1', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: -1, @@ -953,7 +953,7 @@ describe('import rules schema', () => { test('You can set the risk_score to 0', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 0, @@ -976,7 +976,7 @@ describe('import rules schema', () => { test('You can set the risk_score to 100', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 100, @@ -999,7 +999,7 @@ describe('import rules schema', () => { test('You can set meta to any object you want', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1025,7 +1025,7 @@ describe('import rules schema', () => { test('You cannot create meta as a string', () => { expect( - importRulesSchema.validate & { meta: string }>>({ + importRulesSchema.validate & { meta: string }>>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1049,7 +1049,7 @@ describe('import rules schema', () => { test('You can omit the query string when filters are present', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1072,7 +1072,7 @@ describe('import rules schema', () => { test('validates with timeline_id and timeline_title', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1095,7 +1095,7 @@ describe('import rules schema', () => { test('You cannot omit timeline_title when timeline_id is present', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1117,7 +1117,7 @@ describe('import rules schema', () => { test('You cannot have a null value for timeline_title when timeline_id is present', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1140,7 +1140,7 @@ describe('import rules schema', () => { test('You cannot have empty string for timeline_title when timeline_id is present', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1165,7 +1165,7 @@ describe('import rules schema', () => { test('You cannot have timeline_title with an empty timeline_id', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, @@ -1188,7 +1188,7 @@ describe('import rules schema', () => { test('You cannot have timeline_title without timeline_id', () => { expect( - importRulesSchema.validate>({ + importRulesSchema.validate>({ rule_id: 'rule-1', output_index: '.siem-signals', risk_score: 50, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.test.ts index 260147ed0506c..8ca07caef0c7f 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.test.ts @@ -5,7 +5,7 @@ */ import { getPrepackagedRules } from './get_prepackaged_rules'; -import { RuleAlertParamsRest } from '../types'; +import { PrepackagedRules } from '../types'; import { isEmpty } from 'lodash/fp'; describe('get_existing_prepackaged_rules', () => { @@ -15,7 +15,7 @@ describe('get_existing_prepackaged_rules', () => { test('no rule should have the same rule_id as another rule_id', () => { const prePacakgedRules = getPrepackagedRules(); - let existingRuleIds: RuleAlertParamsRest[] = []; + let existingRuleIds: PrepackagedRules[] = []; prePacakgedRules.forEach(rule => { const foundDuplicate = existingRuleIds.reduce((accum, existingRule) => { if (existingRule.rule_id === rule.rule_id) { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.ts index 855d0d73f6796..bcfe6ee203ecd 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_prepackaged_rules.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RuleAlertParamsRest } from '../types'; +import { PrepackagedRules } from '../types'; import { addPrepackagedRulesSchema } from '../routes/schemas/add_prepackaged_rules_schema'; import { rawRules } from './prepackaged_rules'; @@ -13,9 +13,7 @@ import { rawRules } from './prepackaged_rules'; * that they are adding incorrect schema rules. Also this will auto-flush in all the default * aspects such as default interval of 5 minutes, default arrays, etc... */ -export const validateAllPrepackagedRules = ( - rules: RuleAlertParamsRest[] -): RuleAlertParamsRest[] => { +export const validateAllPrepackagedRules = (rules: PrepackagedRules[]): PrepackagedRules[] => { return rules.map(rule => { const validatedRule = addPrepackagedRulesSchema.validate(rule); if (validatedRule.error != null) { @@ -35,6 +33,6 @@ export const validateAllPrepackagedRules = ( }); }; -export const getPrepackagedRules = (rules = rawRules): RuleAlertParamsRest[] => { +export const getPrepackagedRules = (rules = rawRules): PrepackagedRules[] => { return validateAllPrepackagedRules(rules); }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.test.ts index 1a2bd4a10ac2d..ee76bf2ef15b8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.test.ts @@ -5,7 +5,7 @@ */ import { getRulesToInstall } from './get_rules_to_install'; -import { getResult, fullRuleAlertParamsRest } from '../routes/__mocks__/request_responses'; +import { getResult, mockPrepackagedRule } from '../routes/__mocks__/request_responses'; describe('get_rules_to_install', () => { test('should return empty array if both rule sets are empty', () => { @@ -14,7 +14,7 @@ describe('get_rules_to_install', () => { }); test('should return empty array if the two rule ids match', () => { - const ruleFromFileSystem = fullRuleAlertParamsRest(); + const ruleFromFileSystem = mockPrepackagedRule(); ruleFromFileSystem.rule_id = 'rule-1'; const installedRule = getResult(); @@ -24,7 +24,7 @@ describe('get_rules_to_install', () => { }); test('should return the rule to install if the id of the two rules do not match', () => { - const ruleFromFileSystem = fullRuleAlertParamsRest(); + const ruleFromFileSystem = mockPrepackagedRule(); ruleFromFileSystem.rule_id = 'rule-1'; const installedRule = getResult(); @@ -34,10 +34,10 @@ describe('get_rules_to_install', () => { }); test('should return two rules to install if both the ids of the two rules do not match', () => { - const ruleFromFileSystem1 = fullRuleAlertParamsRest(); + const ruleFromFileSystem1 = mockPrepackagedRule(); ruleFromFileSystem1.rule_id = 'rule-1'; - const ruleFromFileSystem2 = fullRuleAlertParamsRest(); + const ruleFromFileSystem2 = mockPrepackagedRule(); ruleFromFileSystem2.rule_id = 'rule-2'; const installedRule = getResult(); @@ -47,13 +47,13 @@ describe('get_rules_to_install', () => { }); test('should return two rules of three to install if both the ids of the two rules do not match but the third does', () => { - const ruleFromFileSystem1 = fullRuleAlertParamsRest(); + const ruleFromFileSystem1 = mockPrepackagedRule(); ruleFromFileSystem1.rule_id = 'rule-1'; - const ruleFromFileSystem2 = fullRuleAlertParamsRest(); + const ruleFromFileSystem2 = mockPrepackagedRule(); ruleFromFileSystem2.rule_id = 'rule-2'; - const ruleFromFileSystem3 = fullRuleAlertParamsRest(); + const ruleFromFileSystem3 = mockPrepackagedRule(); ruleFromFileSystem3.rule_id = 'rule-3'; const installedRule = getResult(); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.ts index 1c795941cbb83..c44e4fb812c35 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_install.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RuleAlertParamsRest } from '../types'; +import { PrepackagedRules } from '../types'; import { RuleAlertType } from './types'; export const getRulesToInstall = ( - rulesFromFileSystem: RuleAlertParamsRest[], + rulesFromFileSystem: PrepackagedRules[], installedRules: RuleAlertType[] -): RuleAlertParamsRest[] => { +): PrepackagedRules[] => { return rulesFromFileSystem.filter( rule => !installedRules.some(installedRule => installedRule.params.ruleId === rule.rule_id) ); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.test.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.test.ts index 7f1b64d33cd9b..40e303bddac1a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.test.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.test.ts @@ -5,7 +5,7 @@ */ import { getRulesToUpdate } from './get_rules_to_update'; -import { getResult, fullRuleAlertParamsRest } from '../routes/__mocks__/request_responses'; +import { getResult, mockPrepackagedRule } from '../routes/__mocks__/request_responses'; describe('get_rules_to_update', () => { test('should return empty array if both rule sets are empty', () => { @@ -14,7 +14,7 @@ describe('get_rules_to_update', () => { }); test('should return empty array if the id of the two rules do not match', () => { - const ruleFromFileSystem = fullRuleAlertParamsRest(); + const ruleFromFileSystem = mockPrepackagedRule(); ruleFromFileSystem.rule_id = 'rule-1'; ruleFromFileSystem.version = 2; @@ -26,7 +26,7 @@ describe('get_rules_to_update', () => { }); test('should return empty array if the id of file system rule is less than the installed version', () => { - const ruleFromFileSystem = fullRuleAlertParamsRest(); + const ruleFromFileSystem = mockPrepackagedRule(); ruleFromFileSystem.rule_id = 'rule-1'; ruleFromFileSystem.version = 1; @@ -38,7 +38,7 @@ describe('get_rules_to_update', () => { }); test('should return empty array if the id of file system rule is the same as the installed version', () => { - const ruleFromFileSystem = fullRuleAlertParamsRest(); + const ruleFromFileSystem = mockPrepackagedRule(); ruleFromFileSystem.rule_id = 'rule-1'; ruleFromFileSystem.version = 1; @@ -50,7 +50,7 @@ describe('get_rules_to_update', () => { }); test('should return the rule to update if the id of file system rule is greater than the installed version', () => { - const ruleFromFileSystem = fullRuleAlertParamsRest(); + const ruleFromFileSystem = mockPrepackagedRule(); ruleFromFileSystem.rule_id = 'rule-1'; ruleFromFileSystem.version = 2; @@ -62,7 +62,7 @@ describe('get_rules_to_update', () => { }); test('should return 1 rule out of 2 to update if the id of file system rule is greater than the installed version of just one', () => { - const ruleFromFileSystem = fullRuleAlertParamsRest(); + const ruleFromFileSystem = mockPrepackagedRule(); ruleFromFileSystem.rule_id = 'rule-1'; ruleFromFileSystem.version = 2; @@ -79,11 +79,11 @@ describe('get_rules_to_update', () => { }); test('should return 2 rules out of 2 to update if the id of file system rule is greater than the installed version of both', () => { - const ruleFromFileSystem1 = fullRuleAlertParamsRest(); + const ruleFromFileSystem1 = mockPrepackagedRule(); ruleFromFileSystem1.rule_id = 'rule-1'; ruleFromFileSystem1.version = 2; - const ruleFromFileSystem2 = fullRuleAlertParamsRest(); + const ruleFromFileSystem2 = mockPrepackagedRule(); ruleFromFileSystem2.rule_id = 'rule-2'; ruleFromFileSystem2.version = 2; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.ts index 10b849493858a..31eff6a4ec87a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/get_rules_to_update.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RuleAlertParamsRest } from '../types'; +import { PrepackagedRules } from '../types'; import { RuleAlertType } from './types'; export const getRulesToUpdate = ( - rulesFromFileSystem: RuleAlertParamsRest[], + rulesFromFileSystem: PrepackagedRules[], installedRules: RuleAlertType[] -): RuleAlertParamsRest[] => { +): PrepackagedRules[] => { return rulesFromFileSystem.filter(rule => installedRules.some(installedRule => { return ( diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts index 9c3be64f71a0d..98c04f95387f4 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/install_prepacked_rules.ts @@ -7,12 +7,12 @@ import { ActionsClient } from '../../../../../actions'; import { AlertsClient } from '../../../../../alerting'; import { createRules } from './create_rules'; -import { RuleAlertParamsRest } from '../types'; +import { PrepackagedRules } from '../types'; export const installPrepackagedRules = async ( alertsClient: AlertsClient, actionsClient: ActionsClient, - rules: RuleAlertParamsRest[], + rules: PrepackagedRules[], outputIndex: string ): Promise => { await rules.forEach(async rule => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts index 3d2ca8f91281b..756634c8fa042 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -7,12 +7,12 @@ import { ActionsClient } from '../../../../../actions'; import { AlertsClient } from '../../../../../alerting'; import { updateRules } from './update_rules'; -import { RuleAlertParamsRest } from '../types'; +import { PrepackagedRules } from '../types'; export const updatePrepackagedRules = async ( alertsClient: AlertsClient, actionsClient: ActionsClient, - rules: RuleAlertParamsRest[], + rules: PrepackagedRules[], outputIndex: string ): Promise => { await rules.forEach(async rule => { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts index 8a9e050c039b4..c7bd92322360a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/types.ts @@ -58,6 +58,7 @@ export type RuleAlertParamsRest = Omit< RuleAlertParams, | 'ruleId' | 'falsePositives' + | 'immutable' | 'maxSignals' | 'savedId' | 'riskScore' @@ -99,11 +100,25 @@ export type OutputRuleAlertRest = RuleAlertParamsRest & { id: string; created_by: string | undefined | null; updated_by: string | undefined | null; + immutable: boolean; }; export type ImportRuleAlertRest = Omit & { id: string | undefined | null; rule_id: string; + immutable: boolean; }; +export type PrepackagedRules = Omit< + RuleAlertParamsRest, + | 'status' + | 'status_date' + | 'last_failure_at' + | 'last_success_at' + | 'last_failure_message' + | 'last_success_message' + | 'updated_at' + | 'created_at' +> & { rule_id: string; immutable: boolean }; + export type CallWithRequest = (endpoint: string, params: T, options?: U) => Promise; From c1ccb304b6e17475c1361e24c6557753c3eba963 Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Fri, 17 Jan 2020 17:57:24 -0600 Subject: [PATCH 02/39] Management API - redirect on disabled app path (#55136) * redirect on disabled management app path --- .../kibana/public/management/landing.html | 2 +- src/plugins/management/public/management_app.tsx | 7 ++++++- .../management_test_plugin/public/plugin.tsx | 16 ++++++++++++++++ .../test_suites/management/management_plugin.js | 8 ++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/management/landing.html b/src/legacy/core_plugins/kibana/public/management/landing.html index a69033e4131c9..39459b26f7415 100644 --- a/src/legacy/core_plugins/kibana/public/management/landing.html +++ b/src/legacy/core_plugins/kibana/public/management/landing.html @@ -1,3 +1,3 @@ -
+
diff --git a/src/plugins/management/public/management_app.tsx b/src/plugins/management/public/management_app.tsx index f7e8dba4f8210..705d98eaaf2ff 100644 --- a/src/plugins/management/public/management_app.tsx +++ b/src/plugins/management/public/management_app.tsx @@ -34,7 +34,7 @@ export class ManagementApp { readonly basePath: string; readonly order: number; readonly mount: ManagementSectionMount; - protected enabledStatus: boolean = true; + private enabledStatus = true; constructor( { id, title, basePath, order = 100, mount }: CreateManagementApp, @@ -54,6 +54,11 @@ export class ManagementApp { title, mount: async ({}, params) => { let appUnmount: Unmount; + if (!this.enabledStatus) { + const [coreStart] = await getStartServices(); + coreStart.application.navigateToApp('kibana#/management'); + return () => {}; + } async function setBreadcrumbs(crumbs: ChromeBreadcrumb[]) { const [coreStart] = await getStartServices(); coreStart.chrome.setBreadcrumbs([ diff --git a/test/plugin_functional/plugins/management_test_plugin/public/plugin.tsx b/test/plugin_functional/plugins/management_test_plugin/public/plugin.tsx index 8b7cdd653ed8c..f3b7a19f70ae3 100644 --- a/test/plugin_functional/plugins/management_test_plugin/public/plugin.tsx +++ b/test/plugin_functional/plugins/management_test_plugin/public/plugin.tsx @@ -62,6 +62,22 @@ export class ManagementTestPlugin }; }, }); + + testSection! + .registerApp({ + id: 'test-management-disabled', + title: 'Management Test Disabled', + mount(params) { + params.setBreadcrumbs([{ text: 'Management Test Disabled' }]); + ReactDOM.render(
This is a secret that should never be seen!
, params.element); + + return () => { + ReactDOM.unmountComponentAtNode(params.element); + }; + }, + }) + .disable(); + return {}; } diff --git a/test/plugin_functional/test_suites/management/management_plugin.js b/test/plugin_functional/test_suites/management/management_plugin.js index d65fb1dcd3a7e..0c185f4b385b5 100644 --- a/test/plugin_functional/test_suites/management/management_plugin.js +++ b/test/plugin_functional/test_suites/management/management_plugin.js @@ -36,5 +36,13 @@ export default function({ getService, getPageObjects }) { await testSubjects.click('test-management-link-basepath'); await testSubjects.existOrFail('test-management-link-one'); }); + + it('should redirect when app is disabled', async () => { + await PageObjects.common.navigateToActualUrl( + 'kibana', + 'management/test-section/test-management-disabled' + ); + await testSubjects.existOrFail('management-landing'); + }); }); } From 6760c3394d3573cef7c97ef56af0b5940cc2912e Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Fri, 17 Jan 2020 19:22:22 -0500 Subject: [PATCH 03/39] [SIEM] Detections add alert & signal tab (#55127) * add alert on detections * review I + fix unit test * review II * review III * review IV + bug fixes found during review * review VI --- .../public/components/alerts_viewer/index.tsx | 7 +- .../components/alerts_viewer/translations.ts | 6 +- .../public/components/link_to/link_to.tsx | 7 ++ .../link_to/redirect_to_detection_engine.tsx | 19 ++- .../components/matrix_histogram/index.tsx | 5 +- .../anomalies_query_tab_body/index.tsx | 17 ++- .../containers/matrix_histogram/index.tsx | 1 - .../containers/matrix_histogram/utils.ts | 12 +- .../components/signals/translations.ts | 2 +- .../signals_histogram_panel/config.ts | 21 ++-- .../signals_histogram_panel/index.tsx | 2 +- .../signals_histogram/index.tsx | 2 +- .../detection_engine/detection_engine.tsx | 116 +++++++++++++----- .../public/pages/detection_engine/index.tsx | 16 ++- .../detection_engine/rules/details/index.tsx | 47 ++++--- .../detection_engine/rules/translations.ts | 2 +- .../pages/detection_engine/translations.ts | 6 +- .../public/pages/detection_engine/types.ts | 10 ++ .../authentications_query_tab_body.tsx | 3 +- .../navigation/events_query_tab_body.tsx | 5 +- .../siem/public/pages/hosts/translations.ts | 21 ---- .../network/navigation/dns_query_tab_body.tsx | 3 +- .../siem/public/pages/network/translations.ts | 4 - .../overview/alerts_by_category/index.tsx | 31 ++--- .../overview/events_by_dataset/index.tsx | 12 +- .../public/pages/overview/translations.ts | 4 + 26 files changed, 240 insertions(+), 141 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/types.ts diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx index 87d83f7f2972c..0b99a8b059df7 100644 --- a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/index.tsx @@ -16,11 +16,11 @@ import { MatrixHistogramGqlQuery } from '../../containers/matrix_histogram/index const ID = 'alertsOverTimeQuery'; export const alertsStackByOptions: MatrixHistogramOption[] = [ { - text: i18n.CATEGORY, + text: 'event.category', value: 'event.category', }, { - text: i18n.MODULE, + text: 'event.module', value: 'event.module', }, ]; @@ -54,7 +54,6 @@ export const AlertsView = ({ <> diff --git a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/translations.ts b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/translations.ts index 8c6248e38c057..408c406a854be 100644 --- a/x-pack/legacy/plugins/siem/public/components/alerts_viewer/translations.ts +++ b/x-pack/legacy/plugins/siem/public/components/alerts_viewer/translations.ts @@ -14,10 +14,14 @@ export const TOTAL_COUNT_OF_ALERTS = i18n.translate('xpack.siem.alertsView.total defaultMessage: 'alerts match the search criteria', }); -export const ALERTS_TABLE_TITLE = i18n.translate('xpack.siem.alertsView.alertsDocumentType', { +export const ALERTS_TABLE_TITLE = i18n.translate('xpack.siem.alertsView.alertsTableTitle', { defaultMessage: 'Alerts', }); +export const ALERTS_GRAPH_TITLE = i18n.translate('xpack.siem.alertsView.alertsGraphTitle', { + defaultMessage: 'Alert detection frequency', +}); + export const ALERTS_STACK_BY_MODULE = i18n.translate( 'xpack.siem.alertsView.alertsStackByOptions.module', { diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx index 7c15af3fe642a..0519f5c7c956b 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx @@ -20,6 +20,7 @@ import { RedirectToHostsPage, RedirectToHostDetailsPage } from './redirect_to_ho import { RedirectToNetworkPage } from './redirect_to_network'; import { RedirectToOverviewPage } from './redirect_to_overview'; import { RedirectToTimelinesPage } from './redirect_to_timelines'; +import { DetectionEngineTab } from '../../pages/detection_engine/types'; interface LinkToPageProps { match: RouteMatch<{}>; @@ -63,6 +64,12 @@ export const LinkToPage = React.memo(({ match }) => ( path={`${match.url}/:pageName(${SiemPageName.detectionEngine})`} strict /> + ; export const DETECTION_ENGINE_PAGE_NAME = 'detection-engine'; export const RedirectToDetectionEnginePage = ({ + match: { + params: { tabName }, + }, location: { search }, -}: DetectionEngineComponentProps) => ( - -); +}: DetectionEngineComponentProps) => { + const defaultSelectedTab = DetectionEngineTab.signals; + const selectedTab = tabName ? tabName : defaultSelectedTab; + const to = `/${DETECTION_ENGINE_PAGE_NAME}/${selectedTab}${search}`; + + return ; +}; export const RedirectToRulesPage = ({ location: { search } }: DetectionEngineComponentProps) => { return ; @@ -28,7 +37,7 @@ export const RedirectToRulesPage = ({ location: { search } }: DetectionEngineCom export const RedirectToCreateRulePage = ({ location: { search }, }: DetectionEngineComponentProps) => { - return ; + return ; }; export const RedirectToRuleDetailsPage = ({ @@ -44,6 +53,8 @@ export const RedirectToEditRulePage = ({ location: { search } }: DetectionEngine }; export const getDetectionEngineUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}`; +export const getDetectionEngineAlertUrl = () => + `#/link-to/${DETECTION_ENGINE_PAGE_NAME}/${DetectionEngineTab.alerts}`; export const getRulesUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}/rules`; export const getCreateRuleUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}/rules/create-rule`; export const getRuleDetailsUrl = () => `#/link-to/${DETECTION_ENGINE_PAGE_NAME}/rules/rule-details`; diff --git a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx index 56ebbb06f3eb9..cdd62c430a50c 100644 --- a/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/matrix_histogram/index.tsx @@ -46,12 +46,12 @@ export const MatrixHistogramComponent: React.FC { + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, []); + const [, siemJobs] = useSiemJobs(true); const [anomalyScore] = useUiSetting$(DEFAULT_ANOMALY_SCORE); @@ -51,21 +59,12 @@ export const AnomaliesQueryTabBody = ({ ip ); - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - }, []); - return ( <> void; errorMessage: string; headerChildren?: React.ReactNode; hideHistogramIfEmpty?: boolean; diff --git a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/utils.ts b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/utils.ts index 9cda9d8f6115f..1df1aec76627c 100644 --- a/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/utils.ts +++ b/x-pack/legacy/plugins/siem/public/containers/matrix_histogram/utils.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { getOr } from 'lodash/fp'; -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { MatrixHistogramDataTypes, MatrixHistogramQueryProps, @@ -35,7 +35,7 @@ export const useQuery = ({ }: MatrixHistogramQueryProps) => { const [defaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); const [, dispatchToaster] = useStateToaster(); - const [refetch, setRefetch] = useState(); + const refetch = useRef(); const [loading, setLoading] = useState(false); const [data, setData] = useState(null); const [inspect, setInspect] = useState(null); @@ -71,7 +71,7 @@ export const useQuery = ({ return apolloClient .query({ query, - fetchPolicy: 'cache-first', + fetchPolicy: 'network-only', variables: matrixHistogramVariables, context: { fetchOptions: { @@ -103,9 +103,7 @@ export const useQuery = ({ } ); } - setRefetch(() => { - fetchData(); - }); + refetch.current = fetchData; fetchData(); return () => { isSubscribed = false; @@ -122,5 +120,5 @@ export const useQuery = ({ endDate, ]); - return { data, loading, inspect, totalCount, refetch }; + return { data, loading, inspect, totalCount, refetch: refetch.current }; }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts index d1ba946be41de..c262f907c9876 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals/translations.ts @@ -11,7 +11,7 @@ export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.pageTitle', }); export const SIGNALS_TABLE_TITLE = i18n.translate('xpack.siem.detectionEngine.signals.tableTitle', { - defaultMessage: 'All signals', + defaultMessage: 'Signals', }); export const SIGNALS_DOCUMENT_TYPE = i18n.translate( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts index f329780b075e3..d475fd155ea25 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts @@ -4,18 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import * as i18n from './translations'; import { SignalsHistogramOption } from './types'; export const signalsHistogramOptions: SignalsHistogramOption[] = [ - { text: i18n.STACK_BY_RISK_SCORES, value: 'signal.rule.risk_score' }, - { text: i18n.STACK_BY_SEVERITIES, value: 'signal.rule.severity' }, - { text: i18n.STACK_BY_DESTINATION_IPS, value: 'destination.ip' }, - { text: i18n.STACK_BY_ACTIONS, value: 'event.action' }, - { text: i18n.STACK_BY_CATEGORIES, value: 'event.category' }, - { text: i18n.STACK_BY_HOST_NAMES, value: 'host.name' }, - { text: i18n.STACK_BY_RULE_TYPES, value: 'signal.rule.type' }, - { text: i18n.STACK_BY_RULE_NAMES, value: 'signal.rule.name' }, - { text: i18n.STACK_BY_SOURCE_IPS, value: 'source.ip' }, - { text: i18n.STACK_BY_USERS, value: 'user.name' }, + { text: 'signal.rule.risk_score', value: 'signal.rule.risk_score' }, + { text: 'signal.rule.severity', value: 'signal.rule.severity' }, + { text: 'destination.ip', value: 'destination.ip' }, + { text: 'event.action', value: 'event.action' }, + { text: 'event.category', value: 'event.category' }, + { text: 'host.name', value: 'host.name' }, + { text: 'signal.rule.type', value: 'signal.rule.type' }, + { text: 'signal.rule.name', value: 'signal.rule.name' }, + { text: 'source.ip', value: 'source.ip' }, + { text: 'user.name', value: 'user.name' }, ]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx index fda40f5f9fa5d..64bc7ba24c689 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx @@ -46,7 +46,7 @@ export const SignalsHistogramPanel = memo( filters, query, from, - legendPosition = 'bottom', + legendPosition = 'right', loadingInitial = false, showLinkToSignals = false, showTotalSignalsCount = false, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/index.tsx index 218fcc3a70f79..d4db8cc7c37e8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram/index.tsx @@ -44,7 +44,7 @@ export const SignalsHistogram = React.memo( from, query, filters, - legendPosition = 'bottom', + legendPosition = 'right', loadingInitial, setTotalSignalsCount, stackByField, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index 388f667f47fe1..26a9ad128b1dc 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -4,27 +4,31 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiButton, EuiSpacer } from '@elastic/eui'; -import React, { useCallback } from 'react'; +import { EuiButton, EuiSpacer, EuiTab, EuiTabs } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { useParams } from 'react-router-dom'; import { StickyContainer } from 'react-sticky'; - import { connect } from 'react-redux'; import { ActionCreator } from 'typescript-fsa'; + +import { Query } from '../../../../../../../src/plugins/data/common/query'; +import { esFilters } from '../../../../../../../src/plugins/data/common/es_query'; + +import { GlobalTime } from '../../containers/global_time'; +import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; +import { AlertsTable } from '../../components/alerts_viewer/alerts_table'; import { FiltersGlobal } from '../../components/filters_global'; import { HeaderPage } from '../../components/header_page'; +import { DETECTION_ENGINE_PAGE_NAME } from '../../components/link_to/redirect_to_detection_engine'; import { SiemSearchBar } from '../../components/search_bar'; import { WrapperPage } from '../../components/wrapper_page'; -import { GlobalTime } from '../../containers/global_time'; -import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; -import { SpyRoute } from '../../utils/route/spy_routes'; - -import { Query } from '../../../../../../../src/plugins/data/common/query'; -import { esFilters } from '../../../../../../../src/plugins/data/common/es_query'; import { State } from '../../store'; import { inputsSelectors } from '../../store/inputs'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; +import { SpyRoute } from '../../utils/route/spy_routes'; import { InputsModelId } from '../../store/inputs/constants'; import { InputsRange } from '../../store/inputs/model'; +import { AlertsByCategory } from '../overview/alerts_by_category'; import { useSignalInfo } from './components/signals_info'; import { SignalsTable } from './components/signals'; import { NoWriteSignalsCallOut } from './components/no_write_signals_callout'; @@ -35,6 +39,7 @@ import { DetectionEngineEmptyPage } from './detection_engine_empty_page'; import { DetectionEngineNoIndex } from './detection_engine_no_signal_index'; import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unauthenticated'; import * as i18n from './translations'; +import { DetectionEngineTab } from './types'; interface ReduxProps { filters: esFilters.Filter[]; @@ -51,8 +56,22 @@ export interface DispatchProps { type DetectionEngineComponentProps = ReduxProps & DispatchProps; +const detectionsTabs = [ + { + id: DetectionEngineTab.signals, + name: i18n.SIGNAL, + disabled: false, + }, + { + id: DetectionEngineTab.alerts, + name: i18n.ALERT, + disabled: false, + }, +]; + const DetectionEngineComponent = React.memo( ({ filters, query, setAbsoluteRangeDatePicker }) => { + const { tabName = DetectionEngineTab.signals } = useParams(); const { loading, isSignalIndexExists, @@ -87,6 +106,25 @@ const DetectionEngineComponent = React.memo( ); } + + const tabs = useMemo( + () => ( + + {detectionsTabs.map(tab => ( + + {tab.name} + + ))} + + ), + [detectionsTabs, tabName] + ); + return ( <> {hasIndexWrite != null && !hasIndexWrite && } @@ -99,7 +137,6 @@ const DetectionEngineComponent = React.memo( @@ -117,26 +154,49 @@ const DetectionEngineComponent = React.memo( - {({ to, from }) => ( + {({ to, from, deleteQuery, setQuery }) => ( <> - + {tabs} - + {tabName === DetectionEngineTab.signals && ( + <> + + + + + )} + {tabName === DetectionEngineTab.alerts && ( + <> + + + + + )} )} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx index c4e83429aebdb..7a0b8df85416c 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx @@ -7,12 +7,13 @@ import React from 'react'; import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; +import { ManageUserInfo } from './components/user_info'; import { CreateRuleComponent } from './rules/create'; import { DetectionEngine } from './detection_engine'; import { EditRuleComponent } from './rules/edit'; import { RuleDetails } from './rules/details'; import { RulesComponent } from './rules'; -import { ManageUserInfo } from './components/user_info'; +import { DetectionEngineTab } from './types'; const detectionEnginePath = `/:pageName(detection-engine)`; @@ -21,7 +22,11 @@ type Props = Partial> & { url: string }; export const DetectionEngineContainer = React.memo(() => ( - + @@ -30,7 +35,7 @@ export const DetectionEngineContainer = React.memo(() => ( - + @@ -39,7 +44,10 @@ export const DetectionEngineContainer = React.memo(() => ( ( - + )} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index 099006a34920c..86d7178e73c60 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -12,6 +12,7 @@ import { EuiSpacer, EuiHealth, EuiTab, + EuiTabs, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { memo, useCallback, useMemo, useState } from 'react'; @@ -78,14 +79,19 @@ export interface DispatchProps { }>; } +enum RuleDetailTabs { + signals = 'signals', + failures = 'failures', +} + const ruleDetailTabs = [ { - id: 'signal', + id: RuleDetailTabs.signals, name: detectionI18n.SIGNAL, disabled: false, }, { - id: 'failure', + id: RuleDetailTabs.failures, name: i18n.FAILURE_HISTORY_TAB, disabled: false, }, @@ -106,7 +112,7 @@ const RuleDetailsComponent = memo( } = useUserInfo(); const { ruleId } = useParams(); const [isLoading, rule] = useRule(ruleId); - const [ruleDetailTab, setRuleDetailTab] = useState('signal'); + const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.signals); const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({ rule, detailsView: true, @@ -187,22 +193,27 @@ const RuleDetailsComponent = memo( : 'subdued'; const tabs = useMemo( - () => - ruleDetailTabs.map(tab => ( - setRuleDetailTab(tab.id)} - isSelected={tab.id === ruleDetailTab} - disabled={tab.disabled} - key={tab.name} - > - {tab.name} - - )), + () => ( + + {ruleDetailTabs.map(tab => ( + setRuleDetailTab(tab.id)} + isSelected={tab.id === ruleDetailTab} + disabled={tab.disabled} + key={tab.id} + > + {tab.name} + + ))} + + ), [ruleDetailTabs, ruleDetailTab, setRuleDetailTab] ); const ruleError = useMemo( () => - rule?.status === 'failed' && ruleDetailTab === 'signal' && rule?.last_failure_at != null ? ( + rule?.status === 'failed' && + ruleDetailTab === RuleDetailTabs.signals && + rule?.last_failure_at != null ? ( ( {ruleError} {tabs} - {ruleDetailTab === 'signal' && ( + {ruleDetailTab === RuleDetailTabs.signals && ( <> @@ -381,7 +392,9 @@ const RuleDetailsComponent = memo( )} )} - {ruleDetailTab === 'failure' && } + {ruleDetailTab === RuleDetailTabs.failures && ( + + )} )} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts index aeeef925d60e5..30b50c8cce209 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts @@ -22,7 +22,7 @@ export const ADD_NEW_RULE = i18n.translate('xpack.siem.detectionEngine.rules.add }); export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.pageTitle', { - defaultMessage: 'Rules', + defaultMessage: 'Signal detection rules', }); export const REFRESH = i18n.translate('xpack.siem.detectionEngine.rules.allRules.refreshTitle', { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts index e5f830d3a49b0..d1935b4fd581d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts @@ -22,8 +22,12 @@ export const SIGNAL = i18n.translate('xpack.siem.detectionEngine.signalTitle', { defaultMessage: 'Signals', }); +export const ALERT = i18n.translate('xpack.siem.detectionEngine.alertTitle', { + defaultMessage: 'Third-party alerts', +}); + export const BUTTON_MANAGE_RULES = i18n.translate('xpack.siem.detectionEngine.buttonManageRules', { - defaultMessage: 'Manage rules', + defaultMessage: 'Manage signal detection rules', }); export const PANEL_SUBTITLE_SHOWING = i18n.translate( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/types.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/types.ts new file mode 100644 index 0000000000000..d529d99ad3ad4 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/types.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export enum DetectionEngineTab { + signals = 'signals', + alerts = 'alerts', +} diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx index 0bb9563296316..0109eeef91463 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx @@ -25,7 +25,7 @@ const AuthenticationTableManage = manageQuery(AuthenticationTable); const ID = 'authenticationsOverTimeQuery'; const authStackByOptions: MatrixHistogramOption[] = [ { - text: i18n.NAVIGATION_AUTHENTICATIONS_STACK_BY_EVENT_TYPE, + text: 'event.type', value: 'event.type', }, ]; @@ -71,7 +71,6 @@ export const AuthenticationsQueryTabBody = ({ isAuthenticationsHistogram={true} dataKey="AuthenticationsHistogram" defaultStackByOption={authStackByOptions[0]} - deleteQuery={deleteQuery} endDate={endDate} errorMessage={i18n.ERROR_FETCHING_AUTHENTICATIONS_DATA} filterQuery={filterQuery} diff --git a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx index a07cbc8484a1b..85bca90cc8e04 100644 --- a/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx @@ -20,11 +20,11 @@ const EVENTS_HISTOGRAM_ID = 'eventsOverTimeQuery'; export const eventsStackByOptions: MatrixHistogramOption[] = [ { - text: i18n.NAVIGATION_EVENTS_STACK_BY_EVENT_ACTION, + text: 'event.action', value: 'event.action', }, { - text: i18n.NAVIGATION_EVENTS_STACK_BY_EVENT_DATASET, + text: 'event.dataset', value: 'event.dataset', }, ]; @@ -50,7 +50,6 @@ export const EventsQueryTabBody = ({ void; filters?: esFilters.Filter[]; from: number; + hideHeaderChildren?: boolean; indexPattern: IIndexPattern; query?: Query; setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; @@ -60,14 +60,24 @@ export const AlertsByCategory = React.memo( deleteQuery, filters = NO_FILTERS, from, + hideHeaderChildren = false, indexPattern, query = DEFAULT_QUERY, setAbsoluteRangeDatePicker, setQuery, to, }) => { + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, []); + const kibana = useKibana(); const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const updateDateRangeCallback = useCallback( (min: number, max: number) => { setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max }); @@ -76,17 +86,11 @@ export const AlertsByCategory = React.memo( ); const alertsCountViewAlertsButton = useMemo( () => ( - - {i18n.VIEW_ALERTS} - + {i18n.VIEW_ALERTS} ), [] ); - const getTitle = useCallback( - (option: MatrixHistogramOption) => i18n.ALERTS_COUNT_BY(option.text), - [] - ); const getSubtitle = useCallback( (totalCount: number) => `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, @@ -96,7 +100,6 @@ export const AlertsByCategory = React.memo( return ( ( queries: [query], filters, })} - headerChildren={alertsCountViewAlertsButton} + headerChildren={hideHeaderChildren ? null : alertsCountViewAlertsButton} id={ID} isAlertsHistogram={true} legendPosition={'right'} @@ -115,7 +118,7 @@ export const AlertsByCategory = React.memo( sourceId="default" stackByOptions={alertsStackByOptions} startDate={from} - title={getTitle} + title={i18n.ALERTS_GRAPH_TITLE} subtitle={getSubtitle} type={HostsType.page} updateDateRange={updateDateRangeCallback} diff --git a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx index 52084c4bfc280..191b4a2592695 100644 --- a/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/overview/events_by_dataset/index.tsx @@ -6,7 +6,7 @@ import { EuiButton } from '@elastic/eui'; import numeral from '@elastic/numeral'; -import React, { useCallback, useMemo } from 'react'; +import React, { useCallback, useEffect, useMemo } from 'react'; import { esFilters, IIndexPattern, Query } from 'src/plugins/data/public'; import styled from 'styled-components'; @@ -66,8 +66,17 @@ export const EventsByDataset = React.memo( setQuery, to, }) => { + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, []); + const kibana = useKibana(); const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const updateDateRangeCallback = useCallback( (min: number, max: number) => { setAbsoluteRangeDatePicker!({ id: 'global', from: min, to: max }); @@ -96,7 +105,6 @@ export const EventsByDataset = React.memo( return ( defaultMessage: 'Alerts count by {groupByField}', }); +export const ALERTS_GRAPH_TITLE = i18n.translate('xpack.siem.overview.alertsGraphTitle', { + defaultMessage: 'Alert detection frequency', +}); + export const EVENTS_COUNT_BY = (groupByField: string) => i18n.translate('xpack.siem.overview.eventsCountByTitle', { values: { groupByField }, From dfb35787934ca401bb39a17c22417fe4ec2c4c5b Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Fri, 17 Jan 2020 18:41:00 -0700 Subject: [PATCH 04/39] [SIEM] [Detection Engine] Fixes duplicate rule action (#55252) ## Summary This PR fixes the duplication of rules. The DE backend was updated to not allow `immutable` when creating a rule, so this broke the `Duplicate Rule` action as we were creating a new rule with `immutable: false`. This PR also switches rule duplication over to use the bulk `create` API introduced in https://github.com/elastic/kibana/pull/53543, so now we can duplicate multiple rules. And lastly, this PR removes the limitation of not being able to delete immutable rules. So long as you have the appropriate `write` permissions the delete action is now always available. ![duplicate_batch](https://user-images.githubusercontent.com/2946766/72652638-cee69a00-3944-11ea-9e15-cce3f2b8cefe.gif) ### Checklist Use ~~strikethroughs~~ to remove checklist items you don't feel are applicable to this PR. - [ ] ~This was checked for cross-browser compatibility, [including a check against IE11](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility)~ - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/master/packages/kbn-i18n/README.md) - [ ] ~[Documentation](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#writing-documentation) was added for features that require explanation or tutorials~ - [ ] ~[Unit or functional tests](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#cross-browser-compatibility) were updated or added to match the most common scenarios~ - [ ] ~This was checked for [keyboard-only and screenreader accessibility](https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Cross_browser_testing/Accessibility#Accessibility_testing_checklist)~ ### For maintainers - [ ] ~This was checked for breaking API changes and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~ - [ ] ~This includes a feature addition or change that requires a release note and was [labeled appropriately](https://github.com/elastic/kibana/blob/master/CONTRIBUTING.md#release-notes-process)~ --- .../containers/detection_engine/rules/api.ts | 50 +++++++++---------- .../detection_engine/rules/types.ts | 2 +- .../detection_engine/rules/all/actions.tsx | 22 +++++--- .../rules/all/batch_actions.tsx | 27 ++++++---- .../detection_engine/rules/all/columns.tsx | 5 +- .../rule_actions_overflow/index.tsx | 6 +-- .../detection_engine/rules/translations.ts | 6 +-- 7 files changed, 65 insertions(+), 53 deletions(-) diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts index 1e621363f5f29..8cd3e8f2d45c7 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -178,41 +178,41 @@ export const deleteRules = async ({ ids }: DeleteRulesProps): Promise => { - const requests = rules.map(rule => - fetch(`${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}`, { + const response = await fetch( + `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_URL}/_bulk_create`, + { method: 'POST', credentials: 'same-origin', headers: { 'content-type': 'application/json', 'kbn-xsrf': 'true', }, - body: JSON.stringify({ - ...rule, - name: `${rule.name} [${i18n.DUPLICATE}]`, - created_at: undefined, - created_by: undefined, - id: undefined, - rule_id: undefined, - updated_at: undefined, - updated_by: undefined, - enabled: rule.enabled, - immutable: false, - last_success_at: undefined, - last_success_message: undefined, - status: undefined, - status_date: undefined, - }), - }) + body: JSON.stringify( + rules.map(rule => ({ + ...rule, + name: `${rule.name} [${i18n.DUPLICATE}]`, + created_at: undefined, + created_by: undefined, + id: undefined, + rule_id: undefined, + updated_at: undefined, + updated_by: undefined, + enabled: rule.enabled, + immutable: undefined, + last_success_at: undefined, + last_success_message: undefined, + status: undefined, + status_date: undefined, + })) + ), + } ); - const responses = await Promise.all(requests); - await responses.map(response => throwIfNotOk(response)); - return Promise.all( - responses.map>(response => response.json()) - ); + await throwIfNotOk(response); + return response.json(); }; /** diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index feef888c0d47f..334daa8d1d028 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -146,7 +146,7 @@ export interface DeleteRulesProps { } export interface DuplicateRulesProps { - rules: Rules; + rules: Rule[]; } export interface BasicFetchProps { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx index f83a19445acd6..435edcab433b6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx @@ -29,17 +29,25 @@ export const editRuleAction = (rule: Rule, history: H.History) => { history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${rule.id}/edit`); }; -export const duplicateRuleAction = async ( - rule: Rule, +export const duplicateRulesAction = async ( + rules: Rule[], dispatch: React.Dispatch, dispatchToaster: Dispatch ) => { try { - dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: true }); - const duplicatedRule = await duplicateRules({ rules: [rule] }); - dispatch({ type: 'updateLoading', ids: [rule.id], isLoading: false }); - dispatch({ type: 'updateRules', rules: duplicatedRule, appendRuleId: rule.id }); - displaySuccessToast(i18n.SUCCESSFULLY_DUPLICATED_RULES(duplicatedRule.length), dispatchToaster); + const ruleIds = rules.map(r => r.id); + dispatch({ type: 'updateLoading', ids: ruleIds, isLoading: true }); + const duplicatedRules = await duplicateRules({ rules }); + dispatch({ type: 'updateLoading', ids: ruleIds, isLoading: false }); + dispatch({ + type: 'updateRules', + rules: duplicatedRules, + appendRuleId: rules[rules.length - 1].id, + }); + displaySuccessToast( + i18n.SUCCESSFULLY_DUPLICATED_RULES(duplicatedRules.length), + dispatchToaster + ); } catch (e) { displayErrorToast(i18n.DUPLICATE_RULE_ERROR, [e.message], dispatchToaster); } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx index 06d4c709a32bf..8a10d4f7100b9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx @@ -10,9 +10,13 @@ import * as H from 'history'; import * as i18n from '../translations'; import { TableData } from '../types'; import { Action } from './reducer'; -import { deleteRulesAction, enableRulesAction, exportRulesAction } from './actions'; +import { + deleteRulesAction, + duplicateRulesAction, + enableRulesAction, + exportRulesAction, +} from './actions'; import { ActionToaster } from '../../../../components/toasters'; -import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine'; export const getBatchItems = ( selectedState: TableData[], @@ -25,7 +29,6 @@ export const getBatchItems = ( const containsDisabled = selectedState.some(v => !v.activate); const containsLoading = selectedState.some(v => v.isLoading); const containsImmutable = selectedState.some(v => v.immutable); - const containsMultipleRules = Array.from(new Set(selectedState.map(v => v.rule_id))).length > 1; return [ , { closePopover(); - history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${selectedState[0].id}/edit`); + await duplicateRulesAction( + selectedState.map(s => s.sourceRule), + dispatch, + dispatchToaster + ); }} > - {i18n.BATCH_ACTION_EDIT_INDEX_PATTERNS} + {i18n.BATCH_ACTION_DUPLICATE_SELECTED} , { closePopover(); await deleteRulesAction( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index 91b018eb3078f..a73e656dddd96 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -18,7 +18,7 @@ import React, { Dispatch } from 'react'; import { getEmptyTagValue } from '../../../../components/empty_value'; import { deleteRulesAction, - duplicateRuleAction, + duplicateRulesAction, editRuleAction, exportRulesAction, } from './actions'; @@ -48,7 +48,7 @@ const getActions = ( icon: 'copy', name: i18n.DUPLICATE_RULE, onClick: (rowItem: TableData) => - duplicateRuleAction(rowItem.sourceRule, dispatch, dispatchToaster), + duplicateRulesAction([rowItem.sourceRule], dispatch, dispatchToaster), }, { description: i18n.EXPORT_RULE, @@ -62,7 +62,6 @@ const getActions = ( icon: 'trash', name: i18n.DELETE_RULE, onClick: (rowItem: TableData) => deleteRulesAction([rowItem.id], dispatch, dispatchToaster), - enabled: (rowItem: TableData) => !rowItem.immutable, }, ]; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx index 0a823ce545d72..b996bce8ab500 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx @@ -18,7 +18,7 @@ import { useHistory } from 'react-router-dom'; import { Rule } from '../../../../../containers/detection_engine/rules'; import * as i18n from './translations'; import * as i18nActions from '../../../rules/translations'; -import { deleteRulesAction, duplicateRuleAction } from '../../all/actions'; +import { deleteRulesAction, duplicateRulesAction } from '../../all/actions'; import { displaySuccessToast, useStateToaster } from '../../../../../components/toasters'; import { RuleDownloader } from '../rule_downloader'; import { DETECTION_ENGINE_PAGE_NAME } from '../../../../../components/link_to/redirect_to_detection_engine'; @@ -54,7 +54,7 @@ const RuleActionsOverflowComponent = ({ disabled={userHasNoPermissions} onClick={async () => { setIsPopoverOpen(false); - await duplicateRuleAction(rule, noop, dispatchToaster); + await duplicateRulesAction([rule], noop, dispatchToaster); }} > {i18nActions.DUPLICATE_RULE} @@ -73,7 +73,7 @@ const RuleActionsOverflowComponent = ({ { setIsPopoverOpen(false); await deleteRulesAction([rule.id], noop, dispatchToaster, onRuleDeletedCallback); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts index 30b50c8cce209..83479b819f81e 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts @@ -75,10 +75,10 @@ export const BATCH_ACTION_EXPORT_SELECTED = i18n.translate( } ); -export const BATCH_ACTION_EDIT_INDEX_PATTERNS = i18n.translate( - 'xpack.siem.detectionEngine.rules.allRules.batchActions.editIndexPatternsTitle', +export const BATCH_ACTION_DUPLICATE_SELECTED = i18n.translate( + 'xpack.siem.detectionEngine.rules.allRules.batchActions.duplicateSelectedTitle', { - defaultMessage: 'Edit selected index patterns…', + defaultMessage: 'Duplicate selected…', } ); From 2598d15ea1b45da641e6fca4f26d86e729429e23 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Fri, 17 Jan 2020 21:47:01 -0700 Subject: [PATCH 05/39] [Reporting/Migration] ReportingSetup, LegacySetup (#54198) * ReportingSetup, LegacySetup * fix ts --- x-pack/legacy/plugins/reporting/index.ts | 78 +++---------- .../legacy/plugins/reporting/server/plugin.ts | 107 ++++++++++++++++++ x-pack/legacy/plugins/reporting/types.d.ts | 6 +- 3 files changed, 123 insertions(+), 68 deletions(-) create mode 100644 x-pack/legacy/plugins/reporting/server/plugin.ts diff --git a/x-pack/legacy/plugins/reporting/index.ts b/x-pack/legacy/plugins/reporting/index.ts index 82d94422b70ce..52e26b3132007 100644 --- a/x-pack/legacy/plugins/reporting/index.ts +++ b/x-pack/legacy/plugins/reporting/index.ts @@ -7,47 +7,20 @@ import { resolve } from 'path'; import { i18n } from '@kbn/i18n'; import { Legacy } from 'kibana'; -import { IUiSettingsClient } from 'src/core/server'; -import { XPackMainPlugin } from '../xpack_main/server/xpack_main'; import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from './common/constants'; -// @ts-ignore untyped module defintition -import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; -import { registerRoutes } from './server/routes'; -import { - LevelLogger, - checkLicenseFactory, - getExportTypesRegistry, - runValidations, -} from './server/lib'; -import { createBrowserDriverFactory } from './server/browsers'; -import { registerReportingUsageCollector } from './server/usage'; import { ReportingConfigOptions, ReportingPluginSpecOptions } from './types.d'; import { config as reportingConfig } from './config'; -import { logConfiguration } from './log_configuration'; +import { + LegacySetup, + ReportingPlugin, + ReportingSetupDeps, + reportingPluginFactory, +} from './server/plugin'; const kbToBase64Length = (kb: number) => { return Math.floor((kb * 1024 * 8) / 6); }; -type LegacyPlugins = Legacy.Server['plugins']; - -export interface ServerFacade { - config: Legacy.Server['config']; - info: Legacy.Server['info']; - log: Legacy.Server['log']; - plugins: { - elasticsearch: LegacyPlugins['elasticsearch']; - security: LegacyPlugins['security']; - xpack_main: XPackMainPlugin & { - status?: any; - }; - }; - route: Legacy.Server['route']; - savedObjects: Legacy.Server['savedObjects']; - uiSettingsServiceFactory: Legacy.Server['uiSettingsServiceFactory']; - fieldFormatServiceFactory: (uiConfig: IUiSettingsClient) => unknown; -} - export const reporting = (kibana: any) => { return new kibana.Plugin({ id: PLUGIN_ID, @@ -93,7 +66,11 @@ export const reporting = (kibana: any) => { }, async init(server: Legacy.Server) { - const serverFacade: ServerFacade = { + const coreSetup = server.newPlatform.setup.core; + const pluginsSetup: ReportingSetupDeps = { + usageCollection: server.newPlatform.setup.plugins.usageCollection, + }; + const __LEGACY: LegacySetup = { config: server.config, info: server.info, route: server.route.bind(server), @@ -108,38 +85,9 @@ export const reporting = (kibana: any) => { fieldFormatServiceFactory: server.fieldFormatServiceFactory, log: server.log.bind(server), }; - const exportTypesRegistry = getExportTypesRegistry(); - - let isCollectorReady = false; - // Register a function with server to manage the collection of usage stats - const { usageCollection } = server.newPlatform.setup.plugins; - registerReportingUsageCollector( - usageCollection, - serverFacade, - () => isCollectorReady, - exportTypesRegistry - ); - - const logger = LevelLogger.createForServer(serverFacade, [PLUGIN_ID]); - const browserDriverFactory = await createBrowserDriverFactory(serverFacade); - - logConfiguration(serverFacade, logger); - runValidations(serverFacade, logger, browserDriverFactory); - - const { xpack_main: xpackMainPlugin } = serverFacade.plugins; - mirrorPluginStatus(xpackMainPlugin, this); - const checkLicense = checkLicenseFactory(exportTypesRegistry); - (xpackMainPlugin as any).status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info.feature(this.id).registerLicenseCheckResultsGenerator(checkLicense); - }); - - // Post initialization of the above code, the collector is now ready to fetch its data - isCollectorReady = true; - // Reporting routes - registerRoutes(serverFacade, exportTypesRegistry, browserDriverFactory, logger); + const plugin: ReportingPlugin = reportingPluginFactory(__LEGACY, this); + await plugin.setup(coreSetup, pluginsSetup); }, deprecations({ unused }: any) { diff --git a/x-pack/legacy/plugins/reporting/server/plugin.ts b/x-pack/legacy/plugins/reporting/server/plugin.ts new file mode 100644 index 0000000000000..934a3487209c4 --- /dev/null +++ b/x-pack/legacy/plugins/reporting/server/plugin.ts @@ -0,0 +1,107 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Legacy } from 'kibana'; +import { CoreSetup, CoreStart, Plugin } from 'src/core/server'; +import { IUiSettingsClient } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { XPackMainPlugin } from '../../xpack_main/server/xpack_main'; +// @ts-ignore +import { mirrorPluginStatus } from '../../../server/lib/mirror_plugin_status'; +import { PLUGIN_ID } from '../common/constants'; +import { ReportingPluginSpecOptions } from '../types.d'; +import { registerRoutes } from './routes'; +import { LevelLogger, checkLicenseFactory, getExportTypesRegistry, runValidations } from './lib'; +import { createBrowserDriverFactory } from './browsers'; +import { registerReportingUsageCollector } from './usage'; +import { logConfiguration } from '../log_configuration'; + +// For now there is no exposed functionality to other plugins +export type ReportingSetup = object; +export type ReportingStart = object; + +export interface ReportingSetupDeps { + usageCollection: UsageCollectionSetup; +} +export type ReportingStartDeps = object; + +type LegacyPlugins = Legacy.Server['plugins']; + +export interface LegacySetup { + config: Legacy.Server['config']; + info: Legacy.Server['info']; + log: Legacy.Server['log']; + plugins: { + elasticsearch: LegacyPlugins['elasticsearch']; + security: LegacyPlugins['security']; + xpack_main: XPackMainPlugin & { + status?: any; + }; + }; + route: Legacy.Server['route']; + savedObjects: Legacy.Server['savedObjects']; + uiSettingsServiceFactory: Legacy.Server['uiSettingsServiceFactory']; + fieldFormatServiceFactory: (uiConfig: IUiSettingsClient) => unknown; +} + +export type ReportingPlugin = Plugin< + ReportingSetup, + ReportingStart, + ReportingSetupDeps, + ReportingStartDeps +>; + +/* We need a factory that returns an instance of the class because the class + * implementation itself restricts against having Legacy dependencies passed + * into `setup`. The factory parameters take the legacy dependencies, and the + * `setup` method gets it from enclosure */ +export function reportingPluginFactory( + __LEGACY: LegacySetup, + legacyPlugin: ReportingPluginSpecOptions +) { + return new (class ReportingPlugin implements ReportingPlugin { + public async setup(core: CoreSetup, plugins: ReportingSetupDeps): Promise { + const exportTypesRegistry = getExportTypesRegistry(); + + let isCollectorReady = false; + // Register a function with server to manage the collection of usage stats + const { usageCollection } = plugins; + registerReportingUsageCollector( + usageCollection, + __LEGACY, + () => isCollectorReady, + exportTypesRegistry + ); + + const logger = LevelLogger.createForServer(__LEGACY, [PLUGIN_ID]); + const browserDriverFactory = await createBrowserDriverFactory(__LEGACY); + + logConfiguration(__LEGACY, logger); + runValidations(__LEGACY, logger, browserDriverFactory); + + const { xpack_main: xpackMainPlugin } = __LEGACY.plugins; + mirrorPluginStatus(xpackMainPlugin, legacyPlugin); + const checkLicense = checkLicenseFactory(exportTypesRegistry); + (xpackMainPlugin as any).status.once('green', () => { + // Register a function that is called whenever the xpack info changes, + // to re-compute the license check results for this plugin + xpackMainPlugin.info.feature(PLUGIN_ID).registerLicenseCheckResultsGenerator(checkLicense); + }); + + // Post initialization of the above code, the collector is now ready to fetch its data + isCollectorReady = true; + + // Reporting routes + registerRoutes(__LEGACY, exportTypesRegistry, browserDriverFactory, logger); + + return {}; + } + + public start(core: CoreStart, plugins: ReportingStartDeps): ReportingStart { + return {}; + } + })(); +} diff --git a/x-pack/legacy/plugins/reporting/types.d.ts b/x-pack/legacy/plugins/reporting/types.d.ts index f67f44860f14a..9fae60afee4e8 100644 --- a/x-pack/legacy/plugins/reporting/types.d.ts +++ b/x-pack/legacy/plugins/reporting/types.d.ts @@ -15,7 +15,7 @@ import { CancellationToken } from './common/cancellation_token'; import { LevelLogger } from './server/lib/level_logger'; import { HeadlessChromiumDriverFactory } from './server/browsers/chromium/driver_factory'; import { BrowserType } from './server/browsers/types'; -import { ServerFacade } from './index'; +import { LegacySetup } from './server/plugin'; export type ReportingPlugin = object; // For Plugin contract @@ -69,6 +69,8 @@ interface GenerateExportTypePayload { * Legacy System */ +export type ServerFacade = LegacySetup; + export type ReportingPluginSpecOptions = Legacy.PluginSpecOptions; export type EnqueueJobFn = ( @@ -353,5 +355,3 @@ export interface InterceptedRequest { frameId: string; resourceType: string; } - -export { ServerFacade }; From b98a636de2ce2cfb4e137d3e95ab40c3706be8a9 Mon Sep 17 00:00:00 2001 From: Matt Bargar Date: Sat, 18 Jan 2020 05:01:05 -0500 Subject: [PATCH 06/39] Remove nested root from index pattern (#54978) * Revert "Add label and icon to nested fields in the doc table (#54199)" This reverts commit f77b3620 * Apply label and icon to nested fields in the doc table * Add nested type to field_icon * Improve nested test and add comment * Fix tests * Always pass the field type --- .../np_ready/components/table/table.test.tsx | 72 +++++++++---------- .../np_ready/components/table/table.tsx | 47 ++++++++++++ .../np_ready/components/table/table_row.tsx | 4 +- .../field_caps_response.test.js | 12 ++-- .../field_capabilities/field_caps_response.ts | 2 +- .../public/field_icon/field_icon.tsx | 4 +- .../fields_for_wildcard_route/response.js | 16 ----- 7 files changed, 93 insertions(+), 64 deletions(-) diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table.test.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table.test.tsx index f816b33bcd0ae..386f405544a61 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table.test.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table.test.tsx @@ -26,50 +26,48 @@ import { IndexPattern, indexPatterns } from '../../../kibana_services'; jest.mock('ui/new_platform'); -// @ts-ignore const indexPattern = { - fields: { - getByName: (name: string) => { - const fields: { [name: string]: {} } = { - _index: { - name: '_index', - type: 'string', - scripted: false, - filterable: true, - }, - message: { - name: 'message', - type: 'string', - scripted: false, - filterable: false, - }, - extension: { - name: 'extension', - type: 'string', - scripted: false, - filterable: true, - }, - bytes: { - name: 'bytes', - type: 'number', - scripted: false, - filterable: true, - }, - scripted: { - name: 'scripted', - type: 'number', - scripted: true, - filterable: false, - }, - }; - return fields[name]; + fields: [ + { + name: '_index', + type: 'string', + scripted: false, + filterable: true, + }, + { + name: 'message', + type: 'string', + scripted: false, + filterable: false, + }, + { + name: 'extension', + type: 'string', + scripted: false, + filterable: true, }, - }, + { + name: 'bytes', + type: 'number', + scripted: false, + filterable: true, + }, + { + name: 'scripted', + type: 'number', + scripted: true, + filterable: false, + }, + ], metaFields: ['_index', '_score'], flattenHit: undefined, formatHit: jest.fn(hit => hit._source), } as IndexPattern; +indexPattern.fields.getByName = (name: string) => { + return indexPattern.fields.find(field => field.name === name); +}; + indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields); describe('DocViewTable at Discover', () => { diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table.tsx b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table.tsx index 4bb2f83016c22..85689768eb88e 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table.tsx +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/components/table/table.tsx @@ -17,6 +17,7 @@ * under the License. */ import React, { useState } from 'react'; +import { escapeRegExp } from 'lodash'; import { DocViewTableRow } from './table_row'; import { arrayContainsObjects, trimAngularSpan } from './table_helper'; import { DocViewRenderProps } from '../../doc_views/doc_views_types'; @@ -68,11 +69,57 @@ export function DocViewTable({ const displayNoMappingWarning = !mapping(field) && !displayUnderscoreWarning && !isArrayOfObjects; + // Discover doesn't flatten arrays of objects, so for documents with an `object` or `nested` field that + // contains an array, Discover will only detect the top level root field. We want to detect when those + // root fields are `nested` so that we can display the proper icon and label. However, those root + // `nested` fields are not a part of the index pattern. Their children are though, and contain nested path + // info. So to detect nested fields we look through the index pattern for nested children + // whose path begins with the current field. There are edge cases where + // this could incorrectly identify a plain `object` field as `nested`. Say we had the following document + // where `foo` is a plain object field and `bar` is a nested field. + // { + // "foo": [ + // { + // "bar": [ + // { + // "baz": "qux" + // } + // ] + // }, + // { + // "bar": [ + // { + // "baz": "qux" + // } + // ] + // } + // ] + // } + // + // The following code will search for `foo`, find it at the beginning of the path to the nested child field + // `foo.bar.baz` and incorrectly mark `foo` as nested. Any time we're searching for the name of a plain object + // field that happens to match a segment of a nested path, we'll get a false positive. + // We're aware of this issue and we'll have to live with + // it in the short term. The long term fix will be to add info about the `nested` and `object` root fields + // to the index pattern, but that has its own complications which you can read more about in the following + // issue: https://github.com/elastic/kibana/issues/54957 + const isNestedField = + !indexPattern.fields.find(patternField => patternField.name === field) && + !!indexPattern.fields.find(patternField => { + // We only want to match a full path segment + const nestedRootRegex = new RegExp(escapeRegExp(field) + '(\\.|$)'); + return nestedRootRegex.test(patternField.subType?.nested?.path ?? ''); + }); + const fieldType = isNestedField + ? 'nested' + : indexPattern.fields.find(patternField => patternField.name === field)?.type; + return ( )} - + {isCollapsible && ( diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js index 3ec903d5b18e4..0d787fa56b400 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js @@ -37,7 +37,7 @@ describe('index_patterns/field_capabilities/field_caps_response', () => { describe('conflicts', () => { it('returns a field for each in response, no filtering', () => { const fields = readFieldCapsResponse(esResponse); - expect(fields).toHaveLength(25); + expect(fields).toHaveLength(24); }); it( @@ -68,8 +68,8 @@ describe('index_patterns/field_capabilities/field_caps_response', () => { sandbox.spy(shouldReadFieldFromDocValuesNS, 'shouldReadFieldFromDocValues'); const fields = readFieldCapsResponse(esResponse); const conflictCount = fields.filter(f => f.type === 'conflict').length; - // +1 is for the object field which is filtered out of the final return value from readFieldCapsResponse - sinon.assert.callCount(shouldReadFieldFromDocValues, fields.length - conflictCount + 1); + // +2 is for the object and nested fields which get filtered out of the final return value from readFieldCapsResponse + sinon.assert.callCount(shouldReadFieldFromDocValues, fields.length - conflictCount + 2); }); it('converts es types to kibana types', () => { @@ -159,12 +159,10 @@ describe('index_patterns/field_capabilities/field_caps_response', () => { }); }); - it('returns the nested parent as not searchable or aggregatable', () => { + it('does not include the field actually mapped as nested itself', () => { const fields = readFieldCapsResponse(esResponse); const child = fields.find(f => f.name === 'nested_object_parent'); - expect(child.type).toBe('nested'); - expect(child.aggregatable).toBe(false); - expect(child.searchable).toBe(false); + expect(child).toBeUndefined(); }); it('should not confuse object children for multi or nested field children', () => { diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts index 0c8c2ce48fa84..2215bd8a95a1d 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts @@ -195,6 +195,6 @@ export function readFieldCapsResponse(fieldCapsResponse: FieldCapsResponse): Fie }); return kibanaFormattedCaps.filter(field => { - return !['object'].includes(field.type); + return !['object', 'nested'].includes(field.type); }); } diff --git a/src/plugins/kibana_react/public/field_icon/field_icon.tsx b/src/plugins/kibana_react/public/field_icon/field_icon.tsx index 7c44fe89d0e7f..2e199a7471a64 100644 --- a/src/plugins/kibana_react/public/field_icon/field_icon.tsx +++ b/src/plugins/kibana_react/public/field_icon/field_icon.tsx @@ -36,8 +36,8 @@ interface FieldIconProps { | 'number' | '_source' | 'string' - | 'nested' - | string; + | string + | 'nested'; label?: string; size?: IconSize; useColor?: boolean; diff --git a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js index 555056173ec62..25a533d39dd81 100644 --- a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js +++ b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js @@ -71,14 +71,6 @@ export default function({ getService }) { name: 'foo', readFromDocValues: true, }, - { - aggregatable: false, - esTypes: ['nested'], - name: 'nestedField', - readFromDocValues: false, - searchable: false, - type: 'nested', - }, { aggregatable: false, esTypes: ['keyword'], @@ -161,14 +153,6 @@ export default function({ getService }) { name: 'foo', readFromDocValues: true, }, - { - aggregatable: false, - esTypes: ['nested'], - name: 'nestedField', - readFromDocValues: false, - searchable: false, - type: 'nested', - }, { aggregatable: false, esTypes: ['keyword'], From 13c648b47aa2935148ad4803daf5ec4d16b55b67 Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Sat, 18 Jan 2020 07:49:31 -0500 Subject: [PATCH 07/39] [SIEM] Detection engine cleanup for rule details/creation/edit page (#55069) * update extra action on rule detail to match design * remove experimental label * allow pre-package to be deleted + do not allow wrong user to create pre-packages rules * Additional look back minimum value to 1 * fix flow with edit rule * add success toaster when rule is created or updated * Fix Timeline selector loading * review ben doc + change detectin engine to detection even in url * Succeeded text size consistency in rule details page * fix description of threats * fix test * fix type * fix internatinalization * Update x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/translations.ts Co-Authored-By: Garrett Spong * Update x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts Co-Authored-By: Garrett Spong * Update x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx Co-Authored-By: Garrett Spong * review I * fix type Co-authored-by: Garrett Spong --- .../public/components/header_global/index.tsx | 2 +- .../public/components/link_to/link_to.tsx | 12 +-- .../link_to/redirect_to_detection_engine.tsx | 2 +- .../components/navigation/index.test.tsx | 20 ++--- .../timeline/search_super_select/index.tsx | 18 ++++- .../public/components/url_state/constants.ts | 4 +- .../components/url_state/helpers.test.ts | 38 +-------- .../public/components/url_state/helpers.ts | 25 +----- .../siem/public/components/url_state/types.ts | 4 +- .../rules/use_create_packaged_rules.tsx | 81 +++++++++++++++++++ .../signals/use_signal_index.tsx | 2 - .../components/activity_monitor/index.tsx | 42 +++++----- .../signals_histogram_panel/translations.ts | 2 +- .../components/user_info/index.tsx | 9 +++ .../detection_engine/detection_engine.tsx | 2 +- .../public/pages/detection_engine/index.tsx | 9 +-- .../rules/all/__mocks__/mock.ts | 4 +- .../detection_engine/rules/all/columns.tsx | 4 +- .../detection_engine/rules/all/helpers.ts | 2 +- .../components/description_step/helpers.tsx | 4 +- .../__snapshots__/index.test.tsx.snap | 2 +- .../rule_actions_overflow/index.tsx | 43 +++++++--- .../components/schedule_item_form/index.tsx | 8 +- .../step_about_rule/default_value.ts | 3 +- .../components/step_about_rule/index.tsx | 14 +--- .../components/step_define_rule/index.tsx | 14 +--- .../components/step_schedule_rule/index.tsx | 14 +--- .../components/step_schedule_rule/schema.tsx | 10 +-- .../step_schedule_rule/translations.tsx | 4 +- .../detection_engine/rules/create/index.tsx | 25 ++++-- .../rules/create/translations.ts | 13 +++ .../detection_engine/rules/details/index.tsx | 4 +- .../rules/details/translations.ts | 2 +- .../detection_engine/rules/edit/index.tsx | 7 +- .../rules/edit/translations.ts | 6 ++ .../pages/detection_engine/rules/helpers.tsx | 15 +++- .../detection_engine/rules/translations.ts | 8 +- .../pages/detection_engine/translations.ts | 4 +- .../public/pages/home/home_navigations.tsx | 6 +- .../plugins/siem/public/pages/home/index.tsx | 2 +- .../siem/public/pages/home/translations.ts | 2 +- .../plugins/siem/public/pages/home/types.ts | 4 +- 42 files changed, 301 insertions(+), 195 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_create_packaged_rules.tsx diff --git a/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx b/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx index db6ff7cf55f92..5a286532fabfc 100644 --- a/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/header_global/index.tsx @@ -55,7 +55,7 @@ export const HeaderGlobal = React.memo(({ hideDetectionEngine display="condensed" navTabs={ hideDetectionEngine - ? pickBy((_, key) => key !== SiemPageName.detectionEngine, navTabs) + ? pickBy((_, key) => key !== SiemPageName.detections, navTabs) : navTabs } /> diff --git a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx index 0519f5c7c956b..3180fc955c690 100644 --- a/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/legacy/plugins/siem/public/components/link_to/link_to.tsx @@ -61,32 +61,32 @@ export const LinkToPage = React.memo(({ match }) => ( ; -export const DETECTION_ENGINE_PAGE_NAME = 'detection-engine'; +export const DETECTION_ENGINE_PAGE_NAME = 'detections'; export const RedirectToDetectionEnginePage = ({ match: { diff --git a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx index cae209a76fc1c..b6efc07ad8fe3 100644 --- a/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx +++ b/x-pack/legacy/plugins/siem/public/components/navigation/index.test.tsx @@ -64,12 +64,12 @@ describe('SIEM Navigation', () => { expect(setBreadcrumbs).toHaveBeenNthCalledWith(1, { detailName: undefined, navTabs: { - 'detection-engine': { + detections: { disabled: false, - href: '#/link-to/detection-engine', - id: 'detection-engine', - name: 'Detection engine', - urlKey: 'detection-engine', + href: '#/link-to/detections', + id: 'detections', + name: 'Detections', + urlKey: 'detections', }, hosts: { disabled: false, @@ -146,12 +146,12 @@ describe('SIEM Navigation', () => { detailName: undefined, filters: [], navTabs: { - 'detection-engine': { + detections: { disabled: false, - href: '#/link-to/detection-engine', - id: 'detection-engine', - name: 'Detection engine', - urlKey: 'detection-engine', + href: '#/link-to/detections', + id: 'detections', + name: 'Detections', + urlKey: 'detections', }, hosts: { disabled: false, diff --git a/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx index c7259edbdc593..009ab141e958e 100644 --- a/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx +++ b/x-pack/legacy/plugins/siem/public/components/timeline/search_super_select/index.tsx @@ -45,11 +45,25 @@ const MyEuiFlexItem = styled(EuiFlexItem)` white-space: nowrap; `; -const EuiSelectableContainer = styled.div` +const EuiSelectableContainer = styled.div<{ loading: boolean }>` .euiSelectable { .euiFormControlLayout__childrenWrapper { display: flex; } + ${({ loading }) => `${ + loading + ? ` + .euiFormControlLayoutIcons { + display: none; + } + .euiFormControlLayoutIcons.euiFormControlLayoutIcons--right { + display: block; + left: 12px; + top: 12px; + }` + : '' + } + `} } `; @@ -265,7 +279,7 @@ const SearchTimelineSuperSelectComponent: React.FC {({ timelines, loading, totalCount }) => ( - + { - describe('isKqlForRoute', () => { - test('host page and host page kuery', () => { - const result = isKqlForRoute(SiemPageName.hosts, undefined, CONSTANTS.hostsPage); - expect(result).toBeTruthy(); - }); - test('host page and host details kuery', () => { - const result = isKqlForRoute(SiemPageName.hosts, undefined, CONSTANTS.hostsDetails); - expect(result).toBeFalsy(); - }); - test('host details and host details kuery', () => { - const result = isKqlForRoute(SiemPageName.hosts, 'siem-kibana', CONSTANTS.hostsDetails); - expect(result).toBeTruthy(); - }); - test('host details and host page kuery', () => { - const result = isKqlForRoute(SiemPageName.hosts, 'siem-kibana', CONSTANTS.hostsPage); - expect(result).toBeFalsy(); - }); - test('network page and network page kuery', () => { - const result = isKqlForRoute(SiemPageName.network, undefined, CONSTANTS.networkPage); - expect(result).toBeTruthy(); - }); - test('network page and network details kuery', () => { - const result = isKqlForRoute(SiemPageName.network, undefined, CONSTANTS.networkDetails); - expect(result).toBeFalsy(); - }); - test('network details and network details kuery', () => { - const result = isKqlForRoute(SiemPageName.network, '10.100.7.198', CONSTANTS.networkDetails); - expect(result).toBeTruthy(); - }); - test('network details and network page kuery', () => { - const result = isKqlForRoute(SiemPageName.network, '123.234.34', CONSTANTS.networkPage); - expect(result).toBeFalsy(); - }); - }); describe('getTitle', () => { test('host page name', () => { const result = getTitle('hosts', undefined, navTabs); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts index aa340b54c1699..6ba5810f794b0 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/helpers.ts @@ -78,8 +78,8 @@ export const getUrlType = (pageName: string): UrlStateType => { return 'host'; } else if (pageName === SiemPageName.network) { return 'network'; - } else if (pageName === SiemPageName.detectionEngine) { - return 'detection-engine'; + } else if (pageName === SiemPageName.detections) { + return 'detections'; } else if (pageName === SiemPageName.timelines) { return 'timeline'; } @@ -111,31 +111,14 @@ export const getCurrentLocation = ( return CONSTANTS.networkDetails; } return CONSTANTS.networkPage; - } else if (pageName === SiemPageName.detectionEngine) { - return CONSTANTS.detectionEnginePage; + } else if (pageName === SiemPageName.detections) { + return CONSTANTS.detectionsPage; } else if (pageName === SiemPageName.timelines) { return CONSTANTS.timelinePage; } return CONSTANTS.unknown; }; -export const isKqlForRoute = ( - pageName: string, - detailName: string | undefined, - queryLocation: LocationTypes | null = null -): boolean => { - const currentLocation = getCurrentLocation(pageName, detailName); - if ( - (currentLocation === CONSTANTS.hostsPage && queryLocation === CONSTANTS.hostsPage) || - (currentLocation === CONSTANTS.networkPage && queryLocation === CONSTANTS.networkPage) || - (currentLocation === CONSTANTS.hostsDetails && queryLocation === CONSTANTS.hostsDetails) || - (currentLocation === CONSTANTS.networkDetails && queryLocation === CONSTANTS.networkDetails) - ) { - return true; - } - return false; -}; - export const makeMapStateToProps = () => { const getInputsSelector = inputsSelectors.inputsSelector(); const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); diff --git a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts index a48653a7ea6f4..be1ae1ad63bd4 100644 --- a/x-pack/legacy/plugins/siem/public/components/url_state/types.ts +++ b/x-pack/legacy/plugins/siem/public/components/url_state/types.ts @@ -24,7 +24,7 @@ export const ALL_URL_STATE_KEYS: KeyUrlState[] = [ ]; export const URL_STATE_KEYS: Record = { - 'detection-engine': [ + detections: [ CONSTANTS.appQuery, CONSTANTS.filters, CONSTANTS.savedQuery, @@ -56,7 +56,7 @@ export const URL_STATE_KEYS: Record = { }; export type LocationTypes = - | CONSTANTS.detectionEnginePage + | CONSTANTS.detectionsPage | CONSTANTS.hostsDetails | CONSTANTS.hostsPage | CONSTANTS.networkDetails diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_create_packaged_rules.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_create_packaged_rules.tsx new file mode 100644 index 0000000000000..592419f879011 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_create_packaged_rules.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useEffect, useState } from 'react'; + +import { createPrepackagedRules } from './api'; + +type Return = [boolean, boolean | null]; + +interface UseCreatePackagedRules { + canUserCRUD: boolean | null; + hasIndexManage: boolean | null; + hasManageApiKey: boolean | null; + isAuthenticated: boolean | null; + isSignalIndexExists: boolean | null; +} + +/** + * Hook for creating the packages rules + * + * @param canUserCRUD boolean + * @param hasIndexManage boolean + * @param hasManageApiKey boolean + * @param isAuthenticated boolean + * @param isSignalIndexExists boolean + * + * @returns [loading, hasCreatedPackageRules] + */ +export const useCreatePackagedRules = ({ + canUserCRUD, + hasIndexManage, + hasManageApiKey, + isAuthenticated, + isSignalIndexExists, +}: UseCreatePackagedRules): Return => { + const [hasCreatedPackageRules, setHasCreatedPackageRules] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + setLoading(true); + + async function createRules() { + try { + await createPrepackagedRules({ + signal: abortCtrl.signal, + }); + + if (isSubscribed) { + setHasCreatedPackageRules(true); + } + } catch (error) { + if (isSubscribed) { + setHasCreatedPackageRules(false); + } + } + if (isSubscribed) { + setLoading(false); + } + } + if ( + canUserCRUD && + hasIndexManage && + hasManageApiKey && + isAuthenticated && + isSignalIndexExists + ) { + createRules(); + } + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [canUserCRUD, hasIndexManage, hasManageApiKey, isAuthenticated, isSignalIndexExists]); + + return [loading, hasCreatedPackageRules]; +}; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx index 189d8a1bf3f75..c1ee5fd12b8c1 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx @@ -8,7 +8,6 @@ import { useEffect, useState, useRef } from 'react'; import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; import { useStateToaster } from '../../../components/toasters'; -import { createPrepackagedRules } from '../rules'; import { createSignalIndex, getSignalIndex } from './api'; import * as i18n from './translations'; import { PostSignalError, SignalIndexError } from './types'; @@ -41,7 +40,6 @@ export const useSignalIndex = (): Return => { if (isSubscribed && signal != null) { setSignalIndexName(signal.name); setSignalIndexExists(true); - createPrepackagedRules({ signal: abortCtrl.signal }); } } catch (error) { if (isSubscribed) { diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx index 8290da1ba3220..5f017a3a1f67f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx @@ -45,7 +45,7 @@ export const ActivityMonitor = React.memo(() => { { id: 1, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -55,7 +55,7 @@ export const ActivityMonitor = React.memo(() => { { id: 2, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -65,7 +65,7 @@ export const ActivityMonitor = React.memo(() => { { id: 3, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -76,7 +76,7 @@ export const ActivityMonitor = React.memo(() => { { id: 4, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -87,7 +87,7 @@ export const ActivityMonitor = React.memo(() => { { id: 5, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -98,7 +98,7 @@ export const ActivityMonitor = React.memo(() => { { id: 6, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -109,7 +109,7 @@ export const ActivityMonitor = React.memo(() => { { id: 7, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -120,7 +120,7 @@ export const ActivityMonitor = React.memo(() => { { id: 8, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -131,7 +131,7 @@ export const ActivityMonitor = React.memo(() => { { id: 9, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -142,7 +142,7 @@ export const ActivityMonitor = React.memo(() => { { id: 10, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -153,7 +153,7 @@ export const ActivityMonitor = React.memo(() => { { id: 11, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -164,7 +164,7 @@ export const ActivityMonitor = React.memo(() => { { id: 12, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -175,7 +175,7 @@ export const ActivityMonitor = React.memo(() => { { id: 13, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -186,7 +186,7 @@ export const ActivityMonitor = React.memo(() => { { id: 14, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -197,7 +197,7 @@ export const ActivityMonitor = React.memo(() => { { id: 15, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -208,7 +208,7 @@ export const ActivityMonitor = React.memo(() => { { id: 16, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -219,7 +219,7 @@ export const ActivityMonitor = React.memo(() => { { id: 17, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -230,7 +230,7 @@ export const ActivityMonitor = React.memo(() => { { id: 18, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -241,7 +241,7 @@ export const ActivityMonitor = React.memo(() => { { id: 19, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -252,7 +252,7 @@ export const ActivityMonitor = React.memo(() => { { id: 20, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', @@ -263,7 +263,7 @@ export const ActivityMonitor = React.memo(() => { { id: 21, rule: { - href: '#/detection-engine/rules/rule-details', + href: '#/detections/rules/rule-details', name: 'Automated exfiltration', }, ran: '2019-12-28 00:00:00.000-05:00', diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts index 0245b9968cc36..8c88fa4a5dae6 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts @@ -86,7 +86,7 @@ export const STACK_BY_USERS = i18n.translate( export const HISTOGRAM_HEADER = i18n.translate( 'xpack.siem.detectionEngine.signals.histogram.headerTitle', { - defaultMessage: 'Signal detection frequency', + defaultMessage: 'Signal count', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx index bbaccb7882484..24e14473d40e9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx @@ -10,6 +10,7 @@ import React, { useEffect, useReducer, Dispatch, createContext, useContext } fro import { usePrivilegeUser } from '../../../../containers/detection_engine/signals/use_privilege_user'; import { useSignalIndex } from '../../../../containers/detection_engine/signals/use_signal_index'; import { useKibana } from '../../../../lib/kibana'; +import { useCreatePackagedRules } from '../../../../containers/detection_engine/rules/use_create_packaged_rules'; export interface State { canUserCRUD: boolean | null; @@ -161,6 +162,14 @@ export const useUserInfo = (): State => { createSignalIndex, ] = useSignalIndex(); + useCreatePackagedRules({ + canUserCRUD, + hasIndexManage, + hasManageApiKey, + isAuthenticated, + isSignalIndexExists, + }); + const uiCapabilities = useKibana().services.application.capabilities; const capabilitiesCanUserCRUD: boolean = typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx index 26a9ad128b1dc..d9e0377b34060 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/detection_engine.tsx @@ -148,7 +148,7 @@ const DetectionEngineComponent = React.memo( } title={i18n.PAGE_TITLE} > - + {i18n.BUTTON_MANAGE_RULES} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx index 7a0b8df85416c..33186d2787d8a 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/index.tsx @@ -15,7 +15,7 @@ import { RuleDetails } from './rules/details'; import { RulesComponent } from './rules'; import { DetectionEngineTab } from './types'; -const detectionEnginePath = `/:pageName(detection-engine)`; +const detectionEnginePath = `/:pageName(detections)`; type Props = Partial> & { url: string }; @@ -42,12 +42,9 @@ export const DetectionEngineContainer = React.memo(() => ( ( - + )} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts index 757c1fabfc9cd..b79b3ed091f16 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -60,7 +60,7 @@ export const mockTableData: TableData[] = [ lastResponse: { type: '—' }, method: 'saved_query', rule: { - href: '#/detection-engine/rules/id/abe6c564-050d-45a5-aaf0-386c37dd1f61', + href: '#/detections/rules/id/abe6c564-050d-45a5-aaf0-386c37dd1f61', name: 'Home Grown!', status: 'Status Placeholder', }, @@ -112,7 +112,7 @@ export const mockTableData: TableData[] = [ lastResponse: { type: '—' }, method: 'saved_query', rule: { - href: '#/detection-engine/rules/id/63f06f34-c181-4b2d-af35-f2ace572a1ee', + href: '#/detections/rules/id/63f06f34-c181-4b2d-af35-f2ace572a1ee', name: 'Home Grown!', status: 'Status Placeholder', }, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index a73e656dddd96..f62343f77c499 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -86,7 +86,7 @@ export const getColumns = ( field: 'method', name: i18n.COLUMN_METHOD, truncateText: true, - width: '16%', + width: '14%', }, { field: 'severity', @@ -161,7 +161,7 @@ export const getColumns = ( /> ), sortable: true, - width: '85px', + width: '95px', }, ]; const actions: RulesColumns[] = [ diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts index 9666b7a5688cf..07a2f2f278987 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts @@ -24,7 +24,7 @@ export const formatRules = (rules: Rule[], selectedIds?: string[]): TableData[] immutable: rule.immutable, rule_id: rule.rule_id, rule: { - href: `#/detection-engine/rules/id/${encodeURIComponent(rule.id)}`, + href: `#/detections/rules/id/${encodeURIComponent(rule.id)}`, name: rule.name, status: 'Status Placeholder', }, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx index e8b6919165c8b..011c008c5b2d2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx @@ -125,7 +125,7 @@ export const buildThreatsDescription = ({ description: ( {threats.map((threat, index) => { - const tactic = tacticsOptions.find(t => t.name === threat.tactic.name); + const tactic = tacticsOptions.find(t => t.id === threat.tactic.id); return ( @@ -133,7 +133,7 @@ export const buildThreatsDescription = ({ {threat.techniques.map(technique => { - const myTechnique = techniquesOptions.find(t => t.name === technique.name); + const myTechnique = techniquesOptions.find(t => t.id === technique.id); return ( - theme.euiColorPrimary}; + width: 40px; + height: 40px; + } +`; + interface RuleActionsOverflowComponentProps { rule: Rule | null; userHasNoPermissions: boolean; @@ -86,20 +98,29 @@ const RuleActionsOverflowComponent = ({ [rule, userHasNoPermissions] ); + const handlePopoverOpen = useCallback(() => { + setIsPopoverOpen(!isPopoverOpen); + }, [setIsPopoverOpen, isPopoverOpen]); + + const button = useMemo( + () => ( + + + + ), + [handlePopoverOpen, userHasNoPermissions] + ); + return ( <> - setIsPopoverOpen(!isPopoverOpen)} - /> - - } + button={button} closePopover={() => setIsPopoverOpen(false)} id="ruleActionsOverflow" isOpen={isPopoverOpen} diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx index 0ef104e6891df..3bde2087f26b1 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx @@ -150,7 +150,13 @@ export const ScheduleItem = ({ /> } > - + ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts index 328c4a0f96066..92aca1cecf9f3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts @@ -5,6 +5,7 @@ */ import { AboutStepRule } from '../../types'; +import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/search_super_select/translations'; export const threatsDefault = [ { @@ -25,7 +26,7 @@ export const stepAboutDefaultValue: AboutStepRule = { tags: [], timeline: { id: null, - title: null, + title: DEFAULT_TIMELINE_TITLE, }, threats: threatsDefault, }; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx index 0e03a11776fb7..73c07673a82f4 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx @@ -5,10 +5,11 @@ */ import { EuiButton, EuiHorizontalRule, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { isEqual, get } from 'lodash/fp'; +import { isEqual } from 'lodash/fp'; import React, { FC, memo, useCallback, useEffect, useState } from 'react'; import styled from 'styled-components'; +import { setFieldValue } from '../../helpers'; import { RuleStepProps, RuleStep, AboutStepRule } from '../../types'; import * as RuleI18n from '../../translations'; import { AddItem } from '../add_item_form'; @@ -71,14 +72,7 @@ const StepAboutRuleComponent: FC = ({ isNew: false, }; setMyStepData(myDefaultValues); - if (!isReadOnlyView) { - Object.keys(schema).forEach(key => { - const val = get(key, myDefaultValues); - if (val != null) { - form.setFieldValue(key, val); - } - }); - } + setFieldValue(form, schema, myDefaultValues); } }, [defaultValues]); @@ -88,7 +82,7 @@ const StepAboutRuleComponent: FC = ({ } }, [form]); - return isReadOnlyView && myStepData != null ? ( + return isReadOnlyView && myStepData.name != null ? ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx index 6bdef4a69af1e..5409a5f161bba 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx @@ -11,7 +11,7 @@ import { EuiFlexItem, EuiButton, } from '@elastic/eui'; -import { isEmpty, isEqual, get } from 'lodash/fp'; +import { isEmpty, isEqual } from 'lodash/fp'; import React, { FC, memo, useCallback, useState, useEffect } from 'react'; import styled from 'styled-components'; @@ -19,6 +19,7 @@ import { IIndexPattern } from '../../../../../../../../../../src/plugins/data/pu import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules'; import { DEFAULT_INDEX_KEY } from '../../../../../../common/constants'; import { useUiSetting$ } from '../../../../../lib/kibana'; +import { setFieldValue } from '../../helpers'; import * as RuleI18n from '../../translations'; import { DefineStepRule, RuleStep, RuleStepProps } from '../../types'; import { StepRuleDescription } from '../description_step'; @@ -121,14 +122,7 @@ const StepDefineRuleComponent: FC = ({ if (!isEqual(myDefaultValues, myStepData)) { setMyStepData(myDefaultValues); setLocalUseIndicesConfig(isEqual(myDefaultValues.index, indicesConfig)); - if (!isReadOnlyView) { - Object.keys(schema).forEach(key => { - const val = get(key, myDefaultValues); - if (val != null) { - form.setFieldValue(key, val); - } - }); - } + setFieldValue(form, schema, myDefaultValues); } } }, [defaultValues, indicesConfig]); @@ -152,7 +146,7 @@ const StepDefineRuleComponent: FC = ({ setOpenTimelineSearch(false); }, []); - return isReadOnlyView && myStepData != null ? ( + return isReadOnlyView && myStepData?.queryBar != null ? ( = ({ @@ -67,14 +68,7 @@ const StepScheduleRuleComponent: FC = ({ isNew: false, }; setMyStepData(myDefaultValues); - if (!isReadOnlyView) { - Object.keys(schema).forEach(key => { - const val = get(key, myDefaultValues); - if (val != null) { - form.setFieldValue(key, val); - } - }); - } + setFieldValue(form, schema, myDefaultValues); } }, [defaultValues]); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx index 4da17b88b9ad0..a951c1fab7cc8 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx @@ -14,13 +14,14 @@ export const schema: FormSchema = { label: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldIntervalLabel', { - defaultMessage: 'Rule run interval & look-back', + defaultMessage: 'Runs every', } ), helpText: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldIntervalHelpText', { - defaultMessage: 'How often and how far back this rule will search specified indices.', + defaultMessage: + 'Rules run periodically and detect signals within the specified time frame.', } ), }, @@ -28,15 +29,14 @@ export const schema: FormSchema = { label: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackLabel', { - defaultMessage: 'Additional look-back', + defaultMessage: 'Additional look-back time', } ), labelAppend: OptionalFieldLabel, helpText: i18n.translate( 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackHelpText', { - defaultMessage: - 'Add more time to the look-back range in order to prevent potential gaps in signal reporting.', + defaultMessage: 'Adds time to the look-back period to prevent missed signals.', } ), }, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx index feaaf4e85b2af..67bcc1af8150b 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx @@ -9,13 +9,13 @@ import { i18n } from '@kbn/i18n'; export const COMPLETE_WITHOUT_ACTIVATING = i18n.translate( 'xpack.siem.detectionEngine.createRule. stepScheduleRule.completeWithoutActivatingTitle', { - defaultMessage: 'Complete rule without activating', + defaultMessage: 'Create rule without activating it', } ); export const COMPLETE_WITH_ACTIVATING = i18n.translate( 'xpack.siem.detectionEngine.createRule. stepScheduleRule.completeWithActivatingTitle', { - defaultMessage: 'Complete rule & activate', + defaultMessage: 'Create & activate rule', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx index e5656f5b081fb..cbc60015d9c87 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/create/index.tsx @@ -9,10 +9,11 @@ import React, { useCallback, useRef, useState } from 'react'; import { Redirect } from 'react-router-dom'; import styled from 'styled-components'; +import { usePersistRule } from '../../../../containers/detection_engine/rules'; import { HeaderPage } from '../../../../components/header_page'; import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine'; import { WrapperPage } from '../../../../components/wrapper_page'; -import { usePersistRule } from '../../../../containers/detection_engine/rules'; +import { displaySuccessToast, useStateToaster } from '../../../../components/toasters'; import { SpyRoute } from '../../../../utils/route/spy_routes'; import { useUserInfo } from '../../components/user_info'; import { AccordionTitle } from '../components/accordion_title'; @@ -55,6 +56,7 @@ export const CreateRuleComponent = React.memo(() => { canUserCRUD, hasManageApiKey, } = useUserInfo(); + const [, dispatchToaster] = useStateToaster(); const [openAccordionId, setOpenAccordionId] = useState(RuleStep.defineRule); const defineRuleRef = useRef(null); const aboutRuleRef = useRef(null); @@ -95,6 +97,7 @@ export const CreateRuleComponent = React.memo(() => { const stepRuleIdx = stepsRuleOrder.findIndex(item => step === item); if ([0, 1].includes(stepRuleIdx)) { if (isStepRuleInReadOnlyView[stepsRuleOrder[stepRuleIdx + 1]]) { + setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]); setIsStepRuleInEditView({ ...isStepRuleInReadOnlyView, [step]: true, @@ -203,12 +206,15 @@ export const CreateRuleComponent = React.memo(() => { async (id: RuleStep) => { const activeForm = await stepsForm.current[openAccordionId]?.submit(); if (activeForm != null && activeForm?.isValid) { + stepsData.current[openAccordionId] = { + ...stepsData.current[openAccordionId], + data: activeForm.data, + isValid: activeForm.isValid, + }; setOpenAccordionId(id); - openCloseAccordion(openAccordionId); - setIsStepRuleInEditView({ ...isStepRuleInReadOnlyView, - [openAccordionId]: openAccordionId === RuleStep.scheduleRule ? false : true, + [openAccordionId]: true, [id]: false, }); } @@ -217,6 +223,8 @@ export const CreateRuleComponent = React.memo(() => { ); if (isSaved) { + const ruleName = (stepsData.current[RuleStep.aboutRule].data as AboutStepRule).name; + displaySuccessToast(i18n.SUCCESSFULLY_CREATED_RULES(ruleName), dispatchToaster); return ; } @@ -224,7 +232,7 @@ export const CreateRuleComponent = React.memo(() => { <> { { { + i18n.translate('xpack.siem.detectionEngine.rules.create.successfullyCreatedRuleTitle', { + values: { ruleName }, + defaultMessage: '{ruleName} was created', + }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index 86d7178e73c60..32e40c7547400 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -12,6 +12,7 @@ import { EuiSpacer, EuiHealth, EuiTab, + EuiText, EuiTabs, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -249,7 +250,6 @@ const RuleDetailsComponent = memo( href: `#${DETECTION_ENGINE_PAGE_NAME}/rules`, text: i18n.BACK_TO_RULES, }} - badgeOptions={{ text: i18n.EXPERIMENTAL }} border subtitle={subTitle} subtitle2={[ @@ -273,7 +273,7 @@ const RuleDetailsComponent = memo( - {rule?.status ?? getEmptyTagValue()} + {rule?.status ?? getEmptyTagValue()} {rule?.status_date && ( diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts index 9976abc8412bf..7b349ec646ba9 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts @@ -13,7 +13,7 @@ export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.ruleDetails export const BACK_TO_RULES = i18n.translate( 'xpack.siem.detectionEngine.ruleDetails.backToRulesDescription', { - defaultMessage: 'Back to rules', + defaultMessage: 'Back to signal detection rules', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx index e583461f52439..9b7833afd7f4d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx @@ -17,11 +17,12 @@ import { FormattedMessage } from '@kbn/i18n/react'; import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Redirect, useParams } from 'react-router-dom'; +import { useRule, usePersistRule } from '../../../../containers/detection_engine/rules'; import { HeaderPage } from '../../../../components/header_page'; import { WrapperPage } from '../../../../components/wrapper_page'; -import { SpyRoute } from '../../../../utils/route/spy_routes'; import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine'; -import { useRule, usePersistRule } from '../../../../containers/detection_engine/rules'; +import { displaySuccessToast, useStateToaster } from '../../../../components/toasters'; +import { SpyRoute } from '../../../../utils/route/spy_routes'; import { useUserInfo } from '../../components/user_info'; import { FormHook, FormData } from '../components/shared_imports'; import { StepPanel } from '../components/step_panel'; @@ -48,6 +49,7 @@ interface ScheduleStepRuleForm extends StepRuleForm { } export const EditRuleComponent = memo(() => { + const [, dispatchToaster] = useStateToaster(); const { loading: initLoading, isSignalIndexExists, @@ -271,6 +273,7 @@ export const EditRuleComponent = memo(() => { }, []); if (isSaved || (rule != null && rule.immutable)) { + displaySuccessToast(i18n.SUCCESSFULLY_SAVED_RULE(rule?.name ?? ''), dispatchToaster); return ; } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts index b81ae58e565f0..f6e56dca19c21 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts @@ -28,3 +28,9 @@ export const SORRY_ERRORS = i18n.translate( export const BACK_TO = i18n.translate('xpack.siem.detectionEngine.editRule.backToDescription', { defaultMessage: 'Back to', }); + +export const SUCCESSFULLY_SAVED_RULE = (ruleName: string) => + i18n.translate('xpack.siem.detectionEngine.rules.update.successfullySavedRuleTitle', { + values: { ruleName }, + defaultMessage: '{ruleName} was saved', + }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx index cc0882dd7e426..cfe6cb8da1cb0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/helpers.tsx @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { pick } from 'lodash/fp'; +import { get, pick } from 'lodash/fp'; import { useLocation } from 'react-router-dom'; import { esFilters } from '../../../../../../../../src/plugins/data/public'; import { Rule } from '../../../containers/detection_engine/rules'; +import { FormData, FormHook, FormSchema } from './components/shared_imports'; import { AboutStepRule, DefineStepRule, IMitreEnterpriseAttack, ScheduleStepRule } from './types'; interface GetStepsData { @@ -67,3 +68,15 @@ export const getStepsData = ({ }; export const useQuery = () => new URLSearchParams(useLocation().search); + +export const setFieldValue = ( + form: FormHook, + schema: FormSchema, + defaultValues: unknown +) => + Object.keys(schema).forEach(key => { + const val = get(key, defaultValues); + if (val != null) { + form.setFieldValue(key, val); + } + }); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts index 83479b819f81e..e1257007d44a3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/translations.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; export const BACK_TO_DETECTION_ENGINE = i18n.translate( 'xpack.siem.detectionEngine.rules.backOptionsHeader', { - defaultMessage: 'Back to detection engine', + defaultMessage: 'Back to detections', } ); @@ -18,7 +18,7 @@ export const IMPORT_RULE = i18n.translate('xpack.siem.detectionEngine.rules.impo }); export const ADD_NEW_RULE = i18n.translate('xpack.siem.detectionEngine.rules.addNewRuleTitle', { - defaultMessage: 'Add new rule', + defaultMessage: 'Create new rule', }); export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.rules.pageTitle', { @@ -32,7 +32,7 @@ export const REFRESH = i18n.translate('xpack.siem.detectionEngine.rules.allRules export const BATCH_ACTIONS = i18n.translate( 'xpack.siem.detectionEngine.rules.allRules.batchActionsTitle', { - defaultMessage: 'Batch actions', + defaultMessage: 'Bulk actions', } ); @@ -243,7 +243,7 @@ export const COLUMN_TAGS = i18n.translate( export const COLUMN_ACTIVATE = i18n.translate( 'xpack.siem.detectionEngine.rules.allRules.columns.activateTitle', { - defaultMessage: 'Activate', + defaultMessage: 'Activated', } ); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts index d1935b4fd581d..ab785a8ad2c6d 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/translations.ts @@ -6,8 +6,8 @@ import { i18n } from '@kbn/i18n'; -export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.pageTitle', { - defaultMessage: 'Detection engine', +export const PAGE_TITLE = i18n.translate('xpack.siem.detectionEngine.detectionsPageTitle', { + defaultMessage: 'Detections', }); export const LAST_SIGNAL = i18n.translate('xpack.siem.detectionEngine.lastSignalTitle', { diff --git a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx index 220f8a958aa43..c0e959c5e97fa 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/home_navigations.tsx @@ -36,12 +36,12 @@ export const navTabs: SiemNavTab = { disabled: false, urlKey: 'network', }, - [SiemPageName.detectionEngine]: { - id: SiemPageName.detectionEngine, + [SiemPageName.detections]: { + id: SiemPageName.detections, name: i18n.DETECTION_ENGINE, href: getDetectionEngineUrl(), disabled: false, - urlKey: 'detection-engine', + urlKey: 'detections', }, [SiemPageName.timelines]: { id: SiemPageName.timelines, diff --git a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx index a545be447796d..b5bfdbde306ca 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/home/index.tsx @@ -105,7 +105,7 @@ export const HomePage: React.FC = () => ( )} /> ( )} diff --git a/x-pack/legacy/plugins/siem/public/pages/home/translations.ts b/x-pack/legacy/plugins/siem/public/pages/home/translations.ts index b87ea1c17a117..80800a3bd4198 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/home/translations.ts @@ -19,7 +19,7 @@ export const NETWORK = i18n.translate('xpack.siem.navigation.network', { }); export const DETECTION_ENGINE = i18n.translate('xpack.siem.navigation.detectionEngine', { - defaultMessage: 'Detection engine', + defaultMessage: 'Detections', }); export const TIMELINES = i18n.translate('xpack.siem.navigation.timelines', { diff --git a/x-pack/legacy/plugins/siem/public/pages/home/types.ts b/x-pack/legacy/plugins/siem/public/pages/home/types.ts index 101c6a69b08d1..678de6dbcc128 100644 --- a/x-pack/legacy/plugins/siem/public/pages/home/types.ts +++ b/x-pack/legacy/plugins/siem/public/pages/home/types.ts @@ -10,7 +10,7 @@ export enum SiemPageName { overview = 'overview', hosts = 'hosts', network = 'network', - detectionEngine = 'detection-engine', + detections = 'detections', timelines = 'timelines', } @@ -18,7 +18,7 @@ export type SiemNavTabKey = | SiemPageName.overview | SiemPageName.hosts | SiemPageName.network - | SiemPageName.detectionEngine + | SiemPageName.detections | SiemPageName.timelines; export type SiemNavTab = Record; From ec31611911c8d7630d61361f6aa22d57f86a964f Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Sat, 18 Jan 2020 14:17:36 +0100 Subject: [PATCH 08/39] Enforce camelCase format for a plugin id (#53759) * add isCamelCase function * add a warning if id is not in camelCase * document pluginId expected in camelCase * regen docs * add a test for logging * update tests. warn can be called several times for different reasons * pluginPath falls back to plugin id in snake_case * update tests * update docs * add example with id & configPath different formats Co-authored-by: Elastic Machine --- ...ugin-server.discoveredplugin.configpath.md | 2 +- .../kibana-plugin-server.discoveredplugin.md | 2 +- ...plugin-server.pluginmanifest.configpath.md | 7 +- .../kibana-plugin-server.pluginmanifest.id.md | 2 +- .../kibana-plugin-server.pluginmanifest.md | 4 +- src/core/CONVENTIONS.md | 1 + .../config_deprecation.test.ts | 25 +-- .../plugins/discovery/is_camel_case.test.ts | 42 +++++ .../server/plugins/discovery/is_camel_case.ts | 22 +++ .../discovery/plugin_manifest_parser.test.ts | 158 +++++++++++++----- .../discovery/plugin_manifest_parser.ts | 11 +- .../plugins/discovery/plugins_discovery.ts | 2 +- src/core/server/plugins/types.ts | 11 +- src/core/server/server.api.md | 8 +- 14 files changed, 224 insertions(+), 73 deletions(-) create mode 100644 src/core/server/plugins/discovery/is_camel_case.test.ts create mode 100644 src/core/server/plugins/discovery/is_camel_case.ts diff --git a/docs/development/core/server/kibana-plugin-server.discoveredplugin.configpath.md b/docs/development/core/server/kibana-plugin-server.discoveredplugin.configpath.md index b0830b8d72238..4de20b9c6cccf 100644 --- a/docs/development/core/server/kibana-plugin-server.discoveredplugin.configpath.md +++ b/docs/development/core/server/kibana-plugin-server.discoveredplugin.configpath.md @@ -4,7 +4,7 @@ ## DiscoveredPlugin.configPath property -Root configuration path used by the plugin, defaults to "id". +Root configuration path used by the plugin, defaults to "id" in snake\_case format. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.discoveredplugin.md b/docs/development/core/server/kibana-plugin-server.discoveredplugin.md index aadf9763b1604..ea13422458c7f 100644 --- a/docs/development/core/server/kibana-plugin-server.discoveredplugin.md +++ b/docs/development/core/server/kibana-plugin-server.discoveredplugin.md @@ -16,7 +16,7 @@ export interface DiscoveredPlugin | Property | Type | Description | | --- | --- | --- | -| [configPath](./kibana-plugin-server.discoveredplugin.configpath.md) | ConfigPath | Root configuration path used by the plugin, defaults to "id". | +| [configPath](./kibana-plugin-server.discoveredplugin.configpath.md) | ConfigPath | Root configuration path used by the plugin, defaults to "id" in snake\_case format. | | [id](./kibana-plugin-server.discoveredplugin.id.md) | PluginName | Identifier of the plugin. | | [optionalPlugins](./kibana-plugin-server.discoveredplugin.optionalplugins.md) | readonly PluginName[] | An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. | | [requiredPlugins](./kibana-plugin-server.discoveredplugin.requiredplugins.md) | readonly PluginName[] | An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. | diff --git a/docs/development/core/server/kibana-plugin-server.pluginmanifest.configpath.md b/docs/development/core/server/kibana-plugin-server.pluginmanifest.configpath.md index 39c1eeda47e0e..6ffe396aa2ed1 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginmanifest.configpath.md +++ b/docs/development/core/server/kibana-plugin-server.pluginmanifest.configpath.md @@ -4,10 +4,15 @@ ## PluginManifest.configPath property -Root [configuration path](./kibana-plugin-server.configpath.md) used by the plugin, defaults to "id". +Root [configuration path](./kibana-plugin-server.configpath.md) used by the plugin, defaults to "id" in snake\_case format. Signature: ```typescript readonly configPath: ConfigPath; ``` + +## Example + +id: myPlugin configPath: my\_plugin + diff --git a/docs/development/core/server/kibana-plugin-server.pluginmanifest.id.md b/docs/development/core/server/kibana-plugin-server.pluginmanifest.id.md index 44e61f11fa215..104046f3ce7d0 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginmanifest.id.md +++ b/docs/development/core/server/kibana-plugin-server.pluginmanifest.id.md @@ -4,7 +4,7 @@ ## PluginManifest.id property -Identifier of the plugin. +Identifier of the plugin. Must be a string in camelCase. Part of a plugin public contract. Other plugins leverage it to access plugin API, navigate to the plugin, etc. Signature: diff --git a/docs/development/core/server/kibana-plugin-server.pluginmanifest.md b/docs/development/core/server/kibana-plugin-server.pluginmanifest.md index 9bb208a809b22..c39a702389fb3 100644 --- a/docs/development/core/server/kibana-plugin-server.pluginmanifest.md +++ b/docs/development/core/server/kibana-plugin-server.pluginmanifest.md @@ -20,8 +20,8 @@ Should never be used in code outside of Core but is exported for documentation p | Property | Type | Description | | --- | --- | --- | -| [configPath](./kibana-plugin-server.pluginmanifest.configpath.md) | ConfigPath | Root [configuration path](./kibana-plugin-server.configpath.md) used by the plugin, defaults to "id". | -| [id](./kibana-plugin-server.pluginmanifest.id.md) | PluginName | Identifier of the plugin. | +| [configPath](./kibana-plugin-server.pluginmanifest.configpath.md) | ConfigPath | Root [configuration path](./kibana-plugin-server.configpath.md) used by the plugin, defaults to "id" in snake\_case format. | +| [id](./kibana-plugin-server.pluginmanifest.id.md) | PluginName | Identifier of the plugin. Must be a string in camelCase. Part of a plugin public contract. Other plugins leverage it to access plugin API, navigate to the plugin, etc. | | [kibanaVersion](./kibana-plugin-server.pluginmanifest.kibanaversion.md) | string | The version of Kibana the plugin is compatible with, defaults to "version". | | [optionalPlugins](./kibana-plugin-server.pluginmanifest.optionalplugins.md) | readonly PluginName[] | An optional list of the other plugins that if installed and enabled \*\*may be\*\* leveraged by this plugin for some additional functionality but otherwise are not required for this plugin to work properly. | | [requiredPlugins](./kibana-plugin-server.pluginmanifest.requiredplugins.md) | readonly PluginName[] | An optional list of the other plugins that \*\*must be\*\* installed and enabled for this plugin to function properly. | diff --git a/src/core/CONVENTIONS.md b/src/core/CONVENTIONS.md index 61c5d5b076a44..dd83ab2daca82 100644 --- a/src/core/CONVENTIONS.md +++ b/src/core/CONVENTIONS.md @@ -38,6 +38,7 @@ my_plugin/    ├── index.ts    └── plugin.ts ``` +- [Manifest file](/docs/development/core/server/kibana-plugin-server.pluginmanifest.md) should be defined on top level. - Both `server` and `public` should have an `index.ts` and a `plugin.ts` file: - `index.ts` should only contain: - The `plugin` export diff --git a/src/core/server/config/integration_tests/config_deprecation.test.ts b/src/core/server/config/integration_tests/config_deprecation.test.ts index e85f8567bfc68..5bc3887f05f93 100644 --- a/src/core/server/config/integration_tests/config_deprecation.test.ts +++ b/src/core/server/config/integration_tests/config_deprecation.test.ts @@ -36,7 +36,13 @@ describe('configuration deprecations', () => { await root.setup(); const logs = loggingServiceMock.collect(mockLoggingService); - expect(logs.warn).toMatchInlineSnapshot(`Array []`); + const warnings = logs.warn.flatMap(i => i); + expect(warnings).not.toContain( + '"optimize.lazy" is deprecated and has been replaced by "optimize.watch"' + ); + expect(warnings).not.toContain( + '"optimize.lazyPort" is deprecated and has been replaced by "optimize.watchPort"' + ); }); it('should log deprecation warnings for core deprecations', async () => { @@ -50,15 +56,12 @@ describe('configuration deprecations', () => { await root.setup(); const logs = loggingServiceMock.collect(mockLoggingService); - expect(logs.warn).toMatchInlineSnapshot(` - Array [ - Array [ - "\\"optimize.lazy\\" is deprecated and has been replaced by \\"optimize.watch\\"", - ], - Array [ - "\\"optimize.lazyPort\\" is deprecated and has been replaced by \\"optimize.watchPort\\"", - ], - ] - `); + const warnings = logs.warn.flatMap(i => i); + expect(warnings).toContain( + '"optimize.lazy" is deprecated and has been replaced by "optimize.watch"' + ); + expect(warnings).toContain( + '"optimize.lazyPort" is deprecated and has been replaced by "optimize.watchPort"' + ); }); }); diff --git a/src/core/server/plugins/discovery/is_camel_case.test.ts b/src/core/server/plugins/discovery/is_camel_case.test.ts new file mode 100644 index 0000000000000..eb8cb8f170dac --- /dev/null +++ b/src/core/server/plugins/discovery/is_camel_case.test.ts @@ -0,0 +1,42 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { isCamelCase } from './is_camel_case'; + +describe('isCamelCase', () => { + it('matches a string in camelCase', () => { + expect(isCamelCase('foo')).toBe(true); + expect(isCamelCase('foo1')).toBe(true); + expect(isCamelCase('fooBar')).toBe(true); + expect(isCamelCase('fooBarBaz')).toBe(true); + expect(isCamelCase('fooBAR')).toBe(true); + }); + + it('does not match strings in other cases', () => { + expect(isCamelCase('AAA')).toBe(false); + expect(isCamelCase('FooBar')).toBe(false); + expect(isCamelCase('3Foo')).toBe(false); + expect(isCamelCase('o_O')).toBe(false); + expect(isCamelCase('foo_bar')).toBe(false); + expect(isCamelCase('foo_')).toBe(false); + expect(isCamelCase('_fooBar')).toBe(false); + expect(isCamelCase('fooBar_')).toBe(false); + expect(isCamelCase('_fooBar_')).toBe(false); + }); +}); diff --git a/src/core/server/plugins/discovery/is_camel_case.ts b/src/core/server/plugins/discovery/is_camel_case.ts new file mode 100644 index 0000000000000..12069ec473f8d --- /dev/null +++ b/src/core/server/plugins/discovery/is_camel_case.ts @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +const camelCaseRegExp = /^[a-z]{1}([a-zA-Z0-9]{1,})$/; +export function isCamelCase(candidate: string) { + return camelCaseRegExp.test(candidate); +} diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts index 17b1ac7b86045..979accb1f769e 100644 --- a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts +++ b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts @@ -20,10 +20,12 @@ import { PluginDiscoveryErrorType } from './plugin_discovery_error'; import { mockReadFile } from './plugin_manifest_parser.test.mocks'; +import { loggingServiceMock } from '../../logging/logging_service.mock'; import { resolve } from 'path'; import { parseManifest } from './plugin_manifest_parser'; +const logger = loggingServiceMock.createLogger(); const pluginPath = resolve('path', 'existent-dir'); const pluginManifestPath = resolve(pluginPath, 'kibana.json'); const packageInfo = { @@ -43,7 +45,7 @@ test('return error when manifest is empty', async () => { cb(null, Buffer.from('')); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ message: `Unexpected end of JSON input (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, @@ -55,7 +57,7 @@ test('return error when manifest content is null', async () => { cb(null, Buffer.from('null')); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ message: `Plugin manifest must contain a JSON encoded object. (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, @@ -67,7 +69,7 @@ test('return error when manifest content is not a valid JSON', async () => { cb(null, Buffer.from('not-json')); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ message: `Unexpected token o in JSON at position 1 (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, @@ -79,7 +81,7 @@ test('return error when plugin id is missing', async () => { cb(null, Buffer.from(JSON.stringify({ version: 'some-version' }))); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ message: `Plugin manifest must contain an "id" property. (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, @@ -91,20 +93,36 @@ test('return error when plugin id includes `.` characters', async () => { cb(null, Buffer.from(JSON.stringify({ id: 'some.name', version: 'some-version' }))); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ message: `Plugin "id" must not include \`.\` characters. (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, }); }); +test('logs warning if pluginId is not in camelCase format', async () => { + mockReadFile.mockImplementation((path, cb) => { + cb(null, Buffer.from(JSON.stringify({ id: 'some_name', version: 'kibana', server: true }))); + }); + + expect(loggingServiceMock.collect(logger).warn).toHaveLength(0); + await parseManifest(pluginPath, packageInfo, logger); + expect(loggingServiceMock.collect(logger).warn).toMatchInlineSnapshot(` + Array [ + Array [ + "Expect plugin \\"id\\" in camelCase, but found: some_name", + ], + ] + `); +}); + test('return error when plugin version is missing', async () => { mockReadFile.mockImplementation((path, cb) => { - cb(null, Buffer.from(JSON.stringify({ id: 'some-id' }))); + cb(null, Buffer.from(JSON.stringify({ id: 'someId' }))); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ - message: `Plugin manifest for "some-id" must contain a "version" property. (invalid-manifest, ${pluginManifestPath})`, + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ + message: `Plugin manifest for "someId" must contain a "version" property. (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, }); @@ -112,11 +130,11 @@ test('return error when plugin version is missing', async () => { test('return error when plugin expected Kibana version is lower than actual version', async () => { mockReadFile.mockImplementation((path, cb) => { - cb(null, Buffer.from(JSON.stringify({ id: 'some-id', version: '6.4.2' }))); + cb(null, Buffer.from(JSON.stringify({ id: 'someId', version: '6.4.2' }))); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ - message: `Plugin "some-id" is only compatible with Kibana version "6.4.2", but used Kibana version is "7.0.0-alpha1". (incompatible-version, ${pluginManifestPath})`, + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ + message: `Plugin "someId" is only compatible with Kibana version "6.4.2", but used Kibana version is "7.0.0-alpha1". (incompatible-version, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.IncompatibleVersion, path: pluginManifestPath, }); @@ -126,12 +144,12 @@ test('return error when plugin expected Kibana version cannot be interpreted as mockReadFile.mockImplementation((path, cb) => { cb( null, - Buffer.from(JSON.stringify({ id: 'some-id', version: '1.0.0', kibanaVersion: 'non-sem-ver' })) + Buffer.from(JSON.stringify({ id: 'someId', version: '1.0.0', kibanaVersion: 'non-sem-ver' })) ); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ - message: `Plugin "some-id" is only compatible with Kibana version "non-sem-ver", but used Kibana version is "7.0.0-alpha1". (incompatible-version, ${pluginManifestPath})`, + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ + message: `Plugin "someId" is only compatible with Kibana version "non-sem-ver", but used Kibana version is "7.0.0-alpha1". (incompatible-version, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.IncompatibleVersion, path: pluginManifestPath, }); @@ -139,11 +157,11 @@ test('return error when plugin expected Kibana version cannot be interpreted as test('return error when plugin config path is not a string', async () => { mockReadFile.mockImplementation((path, cb) => { - cb(null, Buffer.from(JSON.stringify({ id: 'some-id', version: '7.0.0', configPath: 2 }))); + cb(null, Buffer.from(JSON.stringify({ id: 'someId', version: '7.0.0', configPath: 2 }))); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ - message: `The "configPath" in plugin manifest for "some-id" should either be a string or an array of strings. (invalid-manifest, ${pluginManifestPath})`, + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ + message: `The "configPath" in plugin manifest for "someId" should either be a string or an array of strings. (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, }); @@ -153,12 +171,12 @@ test('return error when plugin config path is an array that contains non-string mockReadFile.mockImplementation((path, cb) => { cb( null, - Buffer.from(JSON.stringify({ id: 'some-id', version: '7.0.0', configPath: ['config', 2] })) + Buffer.from(JSON.stringify({ id: 'someId', version: '7.0.0', configPath: ['config', 2] })) ); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ - message: `The "configPath" in plugin manifest for "some-id" should either be a string or an array of strings. (invalid-manifest, ${pluginManifestPath})`, + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ + message: `The "configPath" in plugin manifest for "someId" should either be a string or an array of strings. (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, }); @@ -166,11 +184,11 @@ test('return error when plugin config path is an array that contains non-string test('return error when plugin expected Kibana version is higher than actual version', async () => { mockReadFile.mockImplementation((path, cb) => { - cb(null, Buffer.from(JSON.stringify({ id: 'some-id', version: '7.0.1' }))); + cb(null, Buffer.from(JSON.stringify({ id: 'someId', version: '7.0.1' }))); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ - message: `Plugin "some-id" is only compatible with Kibana version "7.0.1", but used Kibana version is "7.0.0-alpha1". (incompatible-version, ${pluginManifestPath})`, + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ + message: `Plugin "someId" is only compatible with Kibana version "7.0.1", but used Kibana version is "7.0.0-alpha1". (incompatible-version, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.IncompatibleVersion, path: pluginManifestPath, }); @@ -178,11 +196,11 @@ test('return error when plugin expected Kibana version is higher than actual ver test('return error when both `server` and `ui` are set to `false` or missing', async () => { mockReadFile.mockImplementation((path, cb) => { - cb(null, Buffer.from(JSON.stringify({ id: 'some-id', version: '7.0.0' }))); + cb(null, Buffer.from(JSON.stringify({ id: 'someId', version: '7.0.0' }))); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ - message: `Both "server" and "ui" are missing or set to "false" in plugin manifest for "some-id", but at least one of these must be set to "true". (invalid-manifest, ${pluginManifestPath})`, + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ + message: `Both "server" and "ui" are missing or set to "false" in plugin manifest for "someId", but at least one of these must be set to "true". (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, }); @@ -190,12 +208,12 @@ test('return error when both `server` and `ui` are set to `false` or missing', a mockReadFile.mockImplementation((path, cb) => { cb( null, - Buffer.from(JSON.stringify({ id: 'some-id', version: '7.0.0', server: false, ui: false })) + Buffer.from(JSON.stringify({ id: 'someId', version: '7.0.0', server: false, ui: false })) ); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ - message: `Both "server" and "ui" are missing or set to "false" in plugin manifest for "some-id", but at least one of these must be set to "true". (invalid-manifest, ${pluginManifestPath})`, + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ + message: `Both "server" and "ui" are missing or set to "false" in plugin manifest for "someId", but at least one of these must be set to "true". (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, }); @@ -207,7 +225,7 @@ test('return error when manifest contains unrecognized properties', async () => null, Buffer.from( JSON.stringify({ - id: 'some-id', + id: 'someId', version: '7.0.0', server: true, unknownOne: 'one', @@ -217,21 +235,69 @@ test('return error when manifest contains unrecognized properties', async () => ); }); - await expect(parseManifest(pluginPath, packageInfo)).rejects.toMatchObject({ - message: `Manifest for plugin "some-id" contains the following unrecognized properties: unknownOne,unknownTwo. (invalid-manifest, ${pluginManifestPath})`, + await expect(parseManifest(pluginPath, packageInfo, logger)).rejects.toMatchObject({ + message: `Manifest for plugin "someId" contains the following unrecognized properties: unknownOne,unknownTwo. (invalid-manifest, ${pluginManifestPath})`, type: PluginDiscoveryErrorType.InvalidManifest, path: pluginManifestPath, }); }); +describe('configPath', () => { + test('falls back to plugin id if not specified', async () => { + mockReadFile.mockImplementation((path, cb) => { + cb(null, Buffer.from(JSON.stringify({ id: 'plugin', version: '7.0.0', server: true }))); + }); + + const manifest = await parseManifest(pluginPath, packageInfo, logger); + expect(manifest.configPath).toBe(manifest.id); + }); + + test('falls back to plugin id in snakeCase format', async () => { + mockReadFile.mockImplementation((path, cb) => { + cb(null, Buffer.from(JSON.stringify({ id: 'SomeId', version: '7.0.0', server: true }))); + }); + + const manifest = await parseManifest(pluginPath, packageInfo, logger); + expect(manifest.configPath).toBe('some_id'); + }); + + test('not formated to snakeCase if defined explicitly as string', async () => { + mockReadFile.mockImplementation((path, cb) => { + cb( + null, + Buffer.from( + JSON.stringify({ id: 'someId', configPath: 'somePath', version: '7.0.0', server: true }) + ) + ); + }); + + const manifest = await parseManifest(pluginPath, packageInfo, logger); + expect(manifest.configPath).toBe('somePath'); + }); + + test('not formated to snakeCase if defined explicitly as an array of strings', async () => { + mockReadFile.mockImplementation((path, cb) => { + cb( + null, + Buffer.from( + JSON.stringify({ id: 'someId', configPath: ['somePath'], version: '7.0.0', server: true }) + ) + ); + }); + + const manifest = await parseManifest(pluginPath, packageInfo, logger); + expect(manifest.configPath).toEqual(['somePath']); + }); +}); + test('set defaults for all missing optional fields', async () => { mockReadFile.mockImplementation((path, cb) => { - cb(null, Buffer.from(JSON.stringify({ id: 'some-id', version: '7.0.0', server: true }))); + cb(null, Buffer.from(JSON.stringify({ id: 'someId', version: '7.0.0', server: true }))); }); - await expect(parseManifest(pluginPath, packageInfo)).resolves.toEqual({ - id: 'some-id', - configPath: 'some-id', + await expect(parseManifest(pluginPath, packageInfo, logger)).resolves.toEqual({ + id: 'someId', + configPath: 'some_id', version: '7.0.0', kibanaVersion: '7.0.0', optionalPlugins: [], @@ -247,7 +313,7 @@ test('return all set optional fields as they are in manifest', async () => { null, Buffer.from( JSON.stringify({ - id: 'some-id', + id: 'someId', configPath: ['some', 'path'], version: 'some-version', kibanaVersion: '7.0.0', @@ -259,8 +325,8 @@ test('return all set optional fields as they are in manifest', async () => { ); }); - await expect(parseManifest(pluginPath, packageInfo)).resolves.toEqual({ - id: 'some-id', + await expect(parseManifest(pluginPath, packageInfo, logger)).resolves.toEqual({ + id: 'someId', configPath: ['some', 'path'], version: 'some-version', kibanaVersion: '7.0.0', @@ -277,7 +343,7 @@ test('return manifest when plugin expected Kibana version matches actual version null, Buffer.from( JSON.stringify({ - id: 'some-id', + id: 'someId', configPath: 'some-path', version: 'some-version', kibanaVersion: '7.0.0-alpha2', @@ -288,8 +354,8 @@ test('return manifest when plugin expected Kibana version matches actual version ); }); - await expect(parseManifest(pluginPath, packageInfo)).resolves.toEqual({ - id: 'some-id', + await expect(parseManifest(pluginPath, packageInfo, logger)).resolves.toEqual({ + id: 'someId', configPath: 'some-path', version: 'some-version', kibanaVersion: '7.0.0-alpha2', @@ -306,7 +372,7 @@ test('return manifest when plugin expected Kibana version is `kibana`', async () null, Buffer.from( JSON.stringify({ - id: 'some-id', + id: 'someId', version: 'some-version', kibanaVersion: 'kibana', requiredPlugins: ['some-required-plugin'], @@ -317,9 +383,9 @@ test('return manifest when plugin expected Kibana version is `kibana`', async () ); }); - await expect(parseManifest(pluginPath, packageInfo)).resolves.toEqual({ - id: 'some-id', - configPath: 'some-id', + await expect(parseManifest(pluginPath, packageInfo, logger)).resolves.toEqual({ + id: 'someId', + configPath: 'some_id', version: 'some-version', kibanaVersion: 'kibana', optionalPlugins: [], diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.ts index 93c993a0fa373..573109c9db35a 100644 --- a/src/core/server/plugins/discovery/plugin_manifest_parser.ts +++ b/src/core/server/plugins/discovery/plugin_manifest_parser.ts @@ -21,9 +21,12 @@ import { readFile, stat } from 'fs'; import { resolve } from 'path'; import { coerce } from 'semver'; import { promisify } from 'util'; +import { snakeCase } from 'lodash'; import { isConfigPath, PackageInfo } from '../../config'; +import { Logger } from '../../logging'; import { PluginManifest } from '../types'; import { PluginDiscoveryError } from './plugin_discovery_error'; +import { isCamelCase } from './is_camel_case'; const fsReadFileAsync = promisify(readFile); const fsStatAsync = promisify(stat); @@ -67,7 +70,7 @@ const KNOWN_MANIFEST_FIELDS = (() => { * @param packageInfo Kibana package info. * @internal */ -export async function parseManifest(pluginPath: string, packageInfo: PackageInfo) { +export async function parseManifest(pluginPath: string, packageInfo: PackageInfo, log: Logger) { const manifestPath = resolve(pluginPath, MANIFEST_FILE_NAME); let manifestContent; @@ -107,6 +110,10 @@ export async function parseManifest(pluginPath: string, packageInfo: PackageInfo ); } + if (!isCamelCase(manifest.id)) { + log.warn(`Expect plugin "id" in camelCase, but found: ${manifest.id}`); + } + if (!manifest.version || typeof manifest.version !== 'string') { throw PluginDiscoveryError.invalidManifest( manifestPath, @@ -161,7 +168,7 @@ export async function parseManifest(pluginPath: string, packageInfo: PackageInfo id: manifest.id, version: manifest.version, kibanaVersion: expectedKibanaVersion, - configPath: manifest.configPath || manifest.id, + configPath: manifest.configPath || snakeCase(manifest.id), requiredPlugins: Array.isArray(manifest.requiredPlugins) ? manifest.requiredPlugins : [], optionalPlugins: Array.isArray(manifest.optionalPlugins) ? manifest.optionalPlugins : [], ui: includesUiPlugin, diff --git a/src/core/server/plugins/discovery/plugins_discovery.ts b/src/core/server/plugins/discovery/plugins_discovery.ts index 79238afdf5c81..e7f82c9dc15ad 100644 --- a/src/core/server/plugins/discovery/plugins_discovery.ts +++ b/src/core/server/plugins/discovery/plugins_discovery.ts @@ -112,7 +112,7 @@ function processPluginSearchPaths$(pluginDirs: readonly string[], log: Logger) { * @param coreContext Kibana core context. */ function createPlugin$(path: string, log: Logger, coreContext: CoreContext) { - return from(parseManifest(path, coreContext.env.packageInfo)).pipe( + return from(parseManifest(path, coreContext.env.packageInfo, log)).pipe( map(manifest => { log.debug(`Successfully discovered plugin "${manifest.id}" at "${path}"`); const opaqueId = Symbol(manifest.id); diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index e717871912f46..a89e2f8c684e4 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -105,7 +105,8 @@ export type PluginOpaqueId = symbol; */ export interface PluginManifest { /** - * Identifier of the plugin. + * Identifier of the plugin. Must be a string in camelCase. Part of a plugin public contract. + * Other plugins leverage it to access plugin API, navigate to the plugin, etc. */ readonly id: PluginName; @@ -121,7 +122,11 @@ export interface PluginManifest { /** * Root {@link ConfigPath | configuration path} used by the plugin, defaults - * to "id". + * to "id" in snake_case format. + * + * @example + * id: myPlugin + * configPath: my_plugin */ readonly configPath: ConfigPath; @@ -162,7 +167,7 @@ export interface DiscoveredPlugin { readonly id: PluginName; /** - * Root configuration path used by the plugin, defaults to "id". + * Root configuration path used by the plugin, defaults to "id" in snake_case format. */ readonly configPath: ConfigPath; diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 6a58666716f42..7e3fc2eb66001 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -2005,9 +2005,9 @@ export const validBodyOutput: readonly ["data", "stream"]; // src/core/server/legacy/types.ts:161:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts // src/core/server/legacy/types.ts:162:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts // src/core/server/plugins/plugins_service.ts:43:5 - (ae-forgotten-export) The symbol "InternalPluginInfo" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:221:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:221:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:222:3 - (ae-forgotten-export) The symbol "ElasticsearchConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:223:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:226:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:226:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:227:3 - (ae-forgotten-export) The symbol "ElasticsearchConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:228:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts ``` From 8ae51abe193e7e5274eb04426150c3e1d79f17ba Mon Sep 17 00:00:00 2001 From: Matt Bargar Date: Sat, 18 Jan 2020 10:25:51 -0500 Subject: [PATCH 09/39] Fix KQL value suggestions for nested fields (#54820) * Revert "Flag nested fields as non-aggregatable (#51774)" This reverts commit c7046a08 * Filter out nested fields at the agg param level * Forbid nested fields in TSVB * Revert "Forbid nested fields in TSVB" This reverts commit afcaa348 Co-authored-by: Tim Roes Co-authored-by: Elastic Machine --- src/legacy/ui/public/agg_types/param_types/field.ts | 5 ++++- .../lib/field_capabilities/field_caps_response.test.js | 7 ------- .../fetcher/lib/field_capabilities/field_caps_response.ts | 8 -------- .../index_patterns/fields_for_wildcard_route/response.js | 4 ++-- 4 files changed, 6 insertions(+), 18 deletions(-) diff --git a/src/legacy/ui/public/agg_types/param_types/field.ts b/src/legacy/ui/public/agg_types/param_types/field.ts index 0ca60267becec..a0fa6ad6e3189 100644 --- a/src/legacy/ui/public/agg_types/param_types/field.ts +++ b/src/legacy/ui/public/agg_types/param_types/field.ts @@ -115,7 +115,10 @@ export class FieldParamType extends BaseParamType { const filteredFields = fields.filter((field: Field) => { const { onlyAggregatable, scriptable, filterFieldTypes } = this; - if ((onlyAggregatable && !field.aggregatable) || (!scriptable && field.scripted)) { + if ( + (onlyAggregatable && (!field.aggregatable || field.subType?.nested)) || + (!scriptable && field.scripted) + ) { return false; } diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js index 0d787fa56b400..8ddd18c2c67f4 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.test.js @@ -143,13 +143,6 @@ describe('index_patterns/field_capabilities/field_caps_response', () => { expect(child).toHaveProperty('subType', { nested: { path: 'nested_object_parent' } }); }); - it('returns nested sub-fields as non-aggregatable', () => { - const fields = readFieldCapsResponse(esResponse); - // Normally a keyword field would be aggregatable, but the fact that it is nested overrides that - const child = fields.find(f => f.name === 'nested_object_parent.child.keyword'); - expect(child).toHaveProperty('aggregatable', false); - }); - it('handles fields that are both nested and multi', () => { const fields = readFieldCapsResponse(esResponse); const child = fields.find(f => f.name === 'nested_object_parent.child.keyword'); diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts index 2215bd8a95a1d..06eb30db0b24b 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_caps_response.ts @@ -182,14 +182,6 @@ export function readFieldCapsResponse(fieldCapsResponse: FieldCapsResponse): Fie if (Object.keys(subType).length > 0) { field.subType = subType; - - // We don't support aggregating on nested fields, trying to do so in the UI will return - // blank results. For now we will stop showing nested fields as an option for aggregation. - // Once we add support for nested fields this condition should be removed and old index - // patterns should be migrated. - if (field.subType.nested) { - field.aggregatable = false; - } } } }); diff --git a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js index 25a533d39dd81..c4c71abdae125 100644 --- a/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js +++ b/test/api_integration/apis/index_patterns/fields_for_wildcard_route/response.js @@ -72,7 +72,7 @@ export default function({ getService }) { readFromDocValues: true, }, { - aggregatable: false, + aggregatable: true, esTypes: ['keyword'], name: 'nestedField.child', readFromDocValues: true, @@ -154,7 +154,7 @@ export default function({ getService }) { readFromDocValues: true, }, { - aggregatable: false, + aggregatable: true, esTypes: ['keyword'], name: 'nestedField.child', readFromDocValues: true, From 9567cca7d0a2def8df05994a981cd1c7f6f59fe2 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Sat, 18 Jan 2020 12:41:47 -0500 Subject: [PATCH 10/39] [SIEM] [Detection Engine] Update status on rule details page (#55201) * adds logic for returning / updating status when a rule is switched from enabled to disabled and vice versa. * update response for find rules statuses to include current status and failures * update status on demand and on enable/disable * adds ternary to allow removal of 'let' * adds savedObjectsClient to the add and upate prepackaged rules and import rules route. * fix bug where convertToSnakeCase would throw error 'cannot convert null or undefined to object' if passed null * genericize snake_case converter and updates isAuthorized to snake_case (different situation) * renaming to 'going to run' instead of executing because when task manager exits because of api key error it won't write the error status so the actual status is 'going to run' on the next interval. This is more accurate than being stuck on 'executing' because of an error we don't control and can't write a status for. * fix missed merge conflict Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> --- .../containers/detection_engine/rules/api.ts | 2 +- .../detection_engine/rules/index.ts | 1 + .../detection_engine/rules/types.ts | 8 +- .../rules/use_rule_status.tsx | 15 +-- .../detection_engine/signals/types.ts | 2 +- .../signals/use_privilege_user.tsx | 2 +- .../detection_engine/rules/all/columns.tsx | 15 +-- .../rules/components/rule_status/helpers.ts | 18 ++++ .../rules/components/rule_status/index.tsx | 99 +++++++++++++++++++ .../components/rule_status/translations.ts | 29 ++++++ .../rules/components/rule_switch/index.tsx | 5 + .../rules/details/failure_history.tsx | 9 +- .../detection_engine/rules/details/index.tsx | 58 +++-------- .../rules/details/translations.ts | 18 ---- .../routes/__mocks__/request_responses.ts | 2 +- .../privileges/read_privileges_route.ts | 2 +- .../rules/add_prepackaged_rules_route.ts | 14 ++- .../routes/rules/find_rules_status_route.ts | 30 ++++-- .../routes/rules/import_rules_route.ts | 7 +- .../routes/rules/update_rules_bulk_route.ts | 25 ++++- .../routes/rules/update_rules_route.ts | 1 + .../lib/detection_engine/rules/types.ts | 20 +++- .../rules/update_prepacked_rules.ts | 3 + .../detection_engine/rules/update_rules.ts | 33 ++++++- .../signals/signal_rule_alert_type.ts | 4 +- 25 files changed, 310 insertions(+), 112 deletions(-) create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx create mode 100644 x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts index 8cd3e8f2d45c7..a83e874437c10 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/api.ts @@ -322,7 +322,7 @@ export const getRuleStatusById = async ({ }: { id: string; signal: AbortSignal; -}): Promise> => { +}): Promise> => { const response = await fetch( `${chrome.getBasePath()}${DETECTION_ENGINE_RULES_STATUS}?ids=${encodeURIComponent( JSON.stringify([id]) diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts index a61cbabd80626..e9a0f27b34696 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/index.ts @@ -10,3 +10,4 @@ export * from './persist_rule'; export * from './types'; export * from './use_rule'; export * from './use_rules'; +export * from './use_rule_status'; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts index 334daa8d1d028..0dcd0da5be8f6 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/types.ts @@ -181,9 +181,15 @@ export interface ExportRulesProps { } export interface RuleStatus { + current_status: RuleInfoStatus; + failures: RuleInfoStatus[]; +} + +export type RuleStatusType = 'executing' | 'failed' | 'going to run' | 'succeeded'; +export interface RuleInfoStatus { alert_id: string; status_date: string; - status: string; + status: RuleStatusType | null; last_failure_at: string | null; last_success_at: string | null; last_failure_message: string | null; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx index 216fbcea861a3..466c2cddac97d 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useStateToaster } from '../../../components/toasters'; import { errorToToaster } from '../../../components/ml/api/error_to_toaster'; @@ -12,7 +12,8 @@ import { getRuleStatusById } from './api'; import * as i18n from './translations'; import { RuleStatus } from './types'; -type Return = [boolean, RuleStatus[] | null]; +type Func = (ruleId: string) => void; +type Return = [boolean, RuleStatus | null, Func | null]; /** * Hook for using to get a Rule from the Detection Engine API @@ -21,7 +22,8 @@ type Return = [boolean, RuleStatus[] | null]; * */ export const useRuleStatus = (id: string | undefined | null): Return => { - const [ruleStatus, setRuleStatus] = useState(null); + const [ruleStatus, setRuleStatus] = useState(null); + const fetchRuleStatus = useRef(null); const [loading, setLoading] = useState(true); const [, dispatchToaster] = useStateToaster(); @@ -29,7 +31,7 @@ export const useRuleStatus = (id: string | undefined | null): Return => { let isSubscribed = true; const abortCtrl = new AbortController(); - async function fetchData(idToFetch: string) { + const fetchData = async (idToFetch: string) => { try { setLoading(true); const ruleStatusResponse = await getRuleStatusById({ @@ -49,15 +51,16 @@ export const useRuleStatus = (id: string | undefined | null): Return => { if (isSubscribed) { setLoading(false); } - } + }; if (id != null) { fetchData(id); } + fetchRuleStatus.current = fetchData; return () => { isSubscribed = false; abortCtrl.abort(); }; }, [id]); - return [loading, ruleStatus]; + return [loading, ruleStatus, fetchRuleStatus.current]; }; diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts index 34cb7684a0399..ea4860dafd40f 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/types.ts @@ -96,5 +96,5 @@ export interface Privilege { write: boolean; }; }; - isAuthenticated: boolean; + is_authenticated: boolean; } diff --git a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx index 792ff29ad2488..7d0e331200d55 100644 --- a/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx +++ b/x-pack/legacy/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx @@ -42,7 +42,7 @@ export const usePrivilegeUser = (): Return => { }); if (isSubscribed && privilege != null) { - setAuthenticated(privilege.isAuthenticated); + setAuthenticated(privilege.is_authenticated); if (privilege.index != null && Object.keys(privilege.index).length > 0) { const indexName = Object.keys(privilege.index)[0]; setHasIndexManage(privilege.index[indexName].manage); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx index f62343f77c499..d546c4edb55d3 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx @@ -30,6 +30,7 @@ import { FormattedDate } from '../../../../components/formatted_date'; import { RuleSwitch } from '../components/rule_switch'; import { SeverityBadge } from '../components/severity_badge'; import { ActionToaster } from '../../../../components/toasters'; +import { getStatusColor } from '../components/rule_status/helpers'; const getActions = ( dispatch: React.Dispatch, @@ -113,19 +114,11 @@ export const getColumns = ( field: 'status', name: i18n.COLUMN_LAST_RESPONSE, render: (value: TableData['status']) => { - const color = - value == null - ? 'subdued' - : value === 'succeeded' - ? 'success' - : value === 'failed' - ? 'danger' - : value === 'executing' - ? 'warning' - : 'subdued'; return ( <> - {value ?? getEmptyTagValue()} + + {value ?? getEmptyTagValue()} + ); }, diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts new file mode 100644 index 0000000000000..263f602251ea7 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RuleStatusType } from '../../../../../containers/detection_engine/rules'; + +export const getStatusColor = (status: RuleStatusType | string | null) => + status == null + ? 'subdued' + : status === 'succeeded' + ? 'success' + : status === 'failed' + ? 'danger' + : status === 'executing' || status === 'going to run' + ? 'warning' + : 'subdued'; diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx new file mode 100644 index 0000000000000..2c9173cbeb694 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiLoadingSpinner, + EuiText, +} from '@elastic/eui'; +import { isEqual } from 'lodash/fp'; +import React, { memo, useCallback, useEffect, useState } from 'react'; + +import { useRuleStatus, RuleInfoStatus } from '../../../../../containers/detection_engine/rules'; +import { FormattedDate } from '../../../../../components/formatted_date'; +import { getEmptyTagValue } from '../../../../../components/empty_value'; +import { getStatusColor } from './helpers'; +import * as i18n from './translations'; + +interface RuleStatusProps { + ruleId: string | null; + ruleEnabled?: boolean | null; +} + +const RuleStatusComponent: React.FC = ({ ruleId, ruleEnabled }) => { + const [loading, ruleStatus, fetchRuleStatus] = useRuleStatus(ruleId); + const [myRuleEnabled, setMyRuleEnabled] = useState(ruleEnabled ?? null); + const [currentStatus, setCurrentStatus] = useState( + ruleStatus?.current_status ?? null + ); + + useEffect(() => { + if (myRuleEnabled !== ruleEnabled && fetchRuleStatus != null && ruleId != null) { + fetchRuleStatus(ruleId); + if (myRuleEnabled !== ruleEnabled) { + setMyRuleEnabled(ruleEnabled ?? null); + } + } + }, [fetchRuleStatus, myRuleEnabled, ruleId, ruleEnabled, setMyRuleEnabled]); + + useEffect(() => { + if (!isEqual(currentStatus, ruleStatus?.current_status)) { + setCurrentStatus(ruleStatus?.current_status ?? null); + } + }, [currentStatus, ruleStatus, setCurrentStatus]); + + const handleRefresh = useCallback(() => { + if (fetchRuleStatus != null && ruleId != null) { + fetchRuleStatus(ruleId); + } + }, [fetchRuleStatus, ruleId]); + + return ( + + + {i18n.STATUS} + {':'} + + {loading && ( + + + + )} + {!loading && ( + <> + + + {currentStatus?.status ?? getEmptyTagValue()} + + + {currentStatus?.status_date != null && currentStatus?.status != null && ( + <> + + <>{i18n.STATUS_AT} + + + + + + )} + + + + + )} + + ); +}; + +export const RuleStatus = memo(RuleStatusComponent); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts new file mode 100644 index 0000000000000..e03cc252ad729 --- /dev/null +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const STATUS = i18n.translate('xpack.siem.detectionEngine.ruleStatus.statusDescription', { + defaultMessage: 'Status', +}); + +export const STATUS_AT = i18n.translate( + 'xpack.siem.detectionEngine.ruleStatus.statusAtDescription', + { + defaultMessage: 'at', + } +); + +export const STATUS_DATE = i18n.translate( + 'xpack.siem.detectionEngine.ruleStatus.statusDateDescription', + { + defaultMessage: 'Status date', + } +); + +export const REFRESH = i18n.translate('xpack.siem.detectionEngine.ruleStatus.refreshButton', { + defaultMessage: 'Refresh', +}); diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx index 9cb0323ed8987..09b7ecc9df982 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx @@ -36,6 +36,7 @@ export interface RuleSwitchProps { isDisabled?: boolean; isLoading?: boolean; optionLabel?: string; + onChange?: (enabled: boolean) => void; } /** @@ -48,6 +49,7 @@ export const RuleSwitchComponent = ({ isLoading, enabled, optionLabel, + onChange, }: RuleSwitchProps) => { const [myIsLoading, setMyIsLoading] = useState(false); const [myEnabled, setMyEnabled] = useState(enabled ?? false); @@ -65,6 +67,9 @@ export const RuleSwitchComponent = ({ enabled: event.target.checked!, }); setMyEnabled(updatedRules[0].enabled); + if (onChange != null) { + onChange(updatedRules[0].enabled); + } } catch { setMyIsLoading(false); } diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx index 3b49cd30c9aab..f660c1763d5e0 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx @@ -15,8 +15,7 @@ import { } from '@elastic/eui'; import React, { memo } from 'react'; -import { useRuleStatus } from '../../../../containers/detection_engine/rules/use_rule_status'; -import { RuleStatus } from '../../../../containers/detection_engine/rules'; +import { useRuleStatus, RuleInfoStatus } from '../../../../containers/detection_engine/rules'; import { HeaderSection } from '../../../../components/header_section'; import * as i18n from './translations'; import { FormattedDate } from '../../../../components/formatted_date'; @@ -35,7 +34,7 @@ const FailureHistoryComponent: React.FC = ({ id }) => { ); } - const columns: Array> = [ + const columns: Array> = [ { name: i18n.COLUMN_STATUS_TYPE, render: () => {i18n.TYPE_FAILED}, @@ -65,7 +64,9 @@ const FailureHistoryComponent: React.FC = ({ id }) => { rs.last_failure_at != null) : []} + items={ + ruleStatus != null ? ruleStatus?.failures.filter(rs => rs.last_failure_at != null) : [] + } sorting={{ sort: { field: 'status_date', direction: 'desc' } }} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx index 32e40c7547400..a23c681a5aab2 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/index.tsx @@ -10,9 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiSpacer, - EuiHealth, EuiTab, - EuiText, EuiTabs, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -62,10 +60,10 @@ import { inputsSelectors } from '../../../../store/inputs'; import { State } from '../../../../store'; import { InputsRange } from '../../../../store/inputs/model'; import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../../../store/inputs/actions'; -import { getEmptyTagValue } from '../../../../components/empty_value'; +import { RuleActionsOverflow } from '../components/rule_actions_overflow'; import { RuleStatusFailedCallOut } from './status_failed_callout'; import { FailureHistory } from './failure_history'; -import { RuleActionsOverflow } from '../components/rule_actions_overflow'; +import { RuleStatus } from '../components/rule_status'; interface ReduxProps { filters: esFilters.Filter[]; @@ -113,6 +111,8 @@ const RuleDetailsComponent = memo( } = useUserInfo(); const { ruleId } = useParams(); const [isLoading, rule] = useRule(ruleId); + // This is used to re-trigger api rule status when user de/activate rule + const [ruleEnabled, setRuleEnabled] = useState(null); const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.signals); const { aboutRuleData, defineRuleData, scheduleRuleData } = getStepsData({ rule, @@ -182,17 +182,6 @@ const RuleDetailsComponent = memo( filters, ]); - const statusColor = - rule?.status == null - ? 'subdued' - : rule?.status === 'succeeded' - ? 'success' - : rule?.status === 'failed' - ? 'danger' - : rule?.status === 'executing' - ? 'warning' - : 'subdued'; - const tabs = useMemo( () => ( @@ -230,6 +219,15 @@ const RuleDetailsComponent = memo( [setAbsoluteRangeDatePicker] ); + const handleOnChangeEnabledRule = useCallback( + (enabled: boolean) => { + if (ruleEnabled == null || enabled !== ruleEnabled) { + setRuleEnabled(enabled); + } + }, + [ruleEnabled, setRuleEnabled] + ); + return ( <> {hasIndexWrite != null && !hasIndexWrite && } @@ -262,34 +260,7 @@ const RuleDetailsComponent = memo( , ] : []), - - - {i18n.STATUS} - {':'} - - - - {rule?.status ?? getEmptyTagValue()} - - - {rule?.status_date && ( - <> - - <>{i18n.STATUS_AT} - - - - - - )} - , + , ]} title={title} > @@ -300,6 +271,7 @@ const RuleDetailsComponent = memo( isDisabled={userHasNoPermissions} enabled={rule?.enabled ?? false} optionLabel={i18n.ACTIVATE_RULE} + onChange={handleOnChangeEnabledRule} /> diff --git a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts index 7b349ec646ba9..46b6984ab323f 100644 --- a/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts +++ b/x-pack/legacy/plugins/siem/public/pages/detection_engine/rules/details/translations.ts @@ -35,24 +35,6 @@ export const UNKNOWN = i18n.translate('xpack.siem.detectionEngine.ruleDetails.un defaultMessage: 'Unknown', }); -export const STATUS = i18n.translate('xpack.siem.detectionEngine.ruleDetails.statusDescription', { - defaultMessage: 'Status', -}); - -export const STATUS_AT = i18n.translate( - 'xpack.siem.detectionEngine.ruleDetails.statusAtDescription', - { - defaultMessage: 'at', - } -); - -export const STATUS_DATE = i18n.translate( - 'xpack.siem.detectionEngine.ruleDetails.statusDateDescription', - { - defaultMessage: 'Status date', - } -); - export const ERROR_CALLOUT_TITLE = i18n.translate( 'xpack.siem.detectionEngine.ruleDetails.errorCalloutTitle', { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 0c6ab1c82bcb8..a84fcb64d9ff7 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -391,7 +391,7 @@ export const getMockPrivileges = () => ({ }, }, application: {}, - isAuthenticated: false, + is_authenticated: false, }); export const getFindResultStatus = (): SavedObjectsFindResponse => ({ diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts index 240200af8b585..803d9d645aadb 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/privileges/read_privileges_route.ts @@ -30,7 +30,7 @@ export const createReadPrivilegesRulesRoute = (server: ServerFacade): Hapi.Serve const index = getIndex(request, server); const permissions = await readPrivileges(callWithRequest, index); return merge(permissions, { - isAuthenticated: request?.auth?.isAuthenticated ?? false, + is_authenticated: request?.auth?.isAuthenticated ?? false, }); } catch (err) { return transformError(err); diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index 5ceecdb058e5f..3c9cad8dc4d4b 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -36,8 +36,10 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - - if (!alertsClient || !actionsClient) { + const savedObjectsClient = isFunction(request.getSavedObjectsClient) + ? request.getSavedObjectsClient() + : null; + if (!alertsClient || !actionsClient || !savedObjectsClient) { return headers.response().code(404); } @@ -59,7 +61,13 @@ export const createAddPrepackedRulesRoute = (server: ServerFacade): Hapi.ServerR } } await installPrepackagedRules(alertsClient, actionsClient, rulesToInstall, spaceIndex); - await updatePrepackagedRules(alertsClient, actionsClient, rulesToUpdate, spaceIndex); + await updatePrepackagedRules( + alertsClient, + actionsClient, + savedObjectsClient, + rulesToUpdate, + spaceIndex + ); return { rules_installed: rulesToInstall.length, rules_updated: rulesToUpdate.length, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts index e56c440f5a415..545c2e488b1c8 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/find_rules_status_route.ts @@ -13,10 +13,16 @@ import { findRulesStatusesSchema } from '../schemas/find_rules_statuses_schema'; import { FindRulesStatusesRequest, IRuleSavedAttributesSavedObjectAttributes, + RuleStatusResponse, + IRuleStatusAttributes, } from '../../rules/types'; import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; -const convertToSnakeCase = (obj: IRuleSavedAttributesSavedObjectAttributes) => { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const convertToSnakeCase = >(obj: T): Partial | null => { + if (!obj) { + return null; + } return Object.keys(obj).reduce((acc, item) => { const newKey = snakeCase(item); return { ...acc, [newKey]: obj[item] }; @@ -53,7 +59,7 @@ export const createFindRulesStatusRoute: Hapi.ServerRoute = { "anotherAlertId": ... } */ - const statuses = await query.ids.reduce(async (acc, id) => { + const statuses = await query.ids.reduce>(async (acc, id) => { const lastFiveErrorsForId = await savedObjectsClient.find< IRuleSavedAttributesSavedObjectAttributes >({ @@ -64,15 +70,21 @@ export const createFindRulesStatusRoute: Hapi.ServerRoute = { search: id, searchFields: ['alertId'], }); - const toDisplay = - lastFiveErrorsForId.saved_objects.length <= 5 - ? lastFiveErrorsForId.saved_objects - : lastFiveErrorsForId.saved_objects.slice(1); + const accumulated = await acc; + const currentStatus = convertToSnakeCase( + lastFiveErrorsForId.saved_objects[0]?.attributes + ); + const failures = lastFiveErrorsForId.saved_objects + .slice(1) + .map(errorItem => convertToSnakeCase(errorItem.attributes)); return { - ...(await acc), - [id]: toDisplay.map(errorItem => convertToSnakeCase(errorItem.attributes)), + ...accumulated, + [id]: { + current_status: currentStatus, + failures, + }, }; - }, {}); + }, Promise.resolve({})); return statuses; }, }; diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index e312b5fc6bb10..6efaa1fea60d0 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -52,8 +52,10 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - - if (!alertsClient || !actionsClient) { + const savedObjectsClient = isFunction(request.getSavedObjectsClient) + ? request.getSavedObjectsClient() + : null; + if (!alertsClient || !actionsClient || !savedObjectsClient) { return headers.response().code(404); } const { filename } = request.payload.file.hapi; @@ -161,6 +163,7 @@ export const createImportRulesRoute = (server: ServerFacade): Hapi.ServerRoute = const updatedRule = await updateRules({ alertsClient, actionsClient, + savedObjectsClient, description, enabled, falsePositives, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index b01108f0de21f..e0d2672cf356a 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -7,12 +7,16 @@ import Hapi from 'hapi'; import { isFunction } from 'lodash/fp'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { BulkUpdateRulesRequest } from '../../rules/types'; +import { + BulkUpdateRulesRequest, + IRuleSavedAttributesSavedObjectAttributes, +} from '../../rules/types'; import { ServerFacade } from '../../../../types'; import { transformOrBulkError, getIdBulkError } from './utils'; import { transformBulkError } from '../utils'; import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema'; import { updateRules } from '../../rules/update_rules'; +import { ruleStatusSavedObjectType } from '../../rules/saved_object_mappings'; export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRoute => { return { @@ -32,8 +36,10 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou const actionsClient = isFunction(request.getActionsClient) ? request.getActionsClient() : null; - - if (!alertsClient || !actionsClient) { + const savedObjectsClient = isFunction(request.getSavedObjectsClient) + ? request.getSavedObjectsClient() + : null; + if (!alertsClient || !actionsClient || !savedObjectsClient) { return headers.response().code(404); } @@ -80,6 +86,7 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou language, outputIndex, savedId, + savedObjectsClient, timelineId, timelineTitle, meta, @@ -100,7 +107,17 @@ export const createUpdateRulesBulkRoute = (server: ServerFacade): Hapi.ServerRou version, }); if (rule != null) { - return transformOrBulkError(rule.id, rule); + const ruleStatuses = await savedObjectsClient.find< + IRuleSavedAttributesSavedObjectAttributes + >({ + type: ruleStatusSavedObjectType, + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }); + return transformOrBulkError(rule.id, rule, ruleStatuses.saved_objects[0]); } else { return getIdBulkError({ id, ruleId }); } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index 533fe9b724943..49c9304ae2d25 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -78,6 +78,7 @@ export const createUpdateRulesRoute: Hapi.ServerRoute = { language, outputIndex, savedId, + savedObjectsClient, timelineId, timelineTitle, meta, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts index 5a3f19c0bf0ef..e238e6398845c 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -7,7 +7,12 @@ import { get } from 'lodash/fp'; import { Readable } from 'stream'; -import { SavedObject, SavedObjectAttributes, SavedObjectsFindResponse } from 'kibana/server'; +import { + SavedObject, + SavedObjectAttributes, + SavedObjectsFindResponse, + SavedObjectsClientContract, +} from 'kibana/server'; import { SIGNALS_ID } from '../../../../common/constants'; import { AlertsClient } from '../../../../../alerting/server/alerts_client'; import { ActionsClient } from '../../../../../actions/server/actions_client'; @@ -41,14 +46,22 @@ export interface RuleAlertType extends Alert { params: RuleTypeParams; } -export interface IRuleStatusAttributes { +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export interface IRuleStatusAttributes extends Record { alertId: string; // created alert id. statusDate: string; lastFailureAt: string | null | undefined; lastFailureMessage: string | null | undefined; lastSuccessAt: string | null | undefined; lastSuccessMessage: string | null | undefined; - status: RuleStatusString; + status: RuleStatusString | null | undefined; +} + +export interface RuleStatusResponse { + [key: string]: { + current_status: IRuleStatusAttributes | null | undefined; + failures: IRuleStatusAttributes[] | null | undefined; + }; } export interface IRuleSavedAttributesSavedObjectAttributes @@ -142,6 +155,7 @@ export interface Clients { export type UpdateRuleParams = Partial & { id: string | undefined | null; + savedObjectsClient: SavedObjectsClientContract; } & Clients; export type DeleteRuleParams = Clients & { diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts index 756634c8fa042..0d7fb7918b67e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SavedObjectsClientContract } from 'kibana/server'; import { ActionsClient } from '../../../../../actions'; import { AlertsClient } from '../../../../../alerting'; import { updateRules } from './update_rules'; @@ -12,6 +13,7 @@ import { PrepackagedRules } from '../types'; export const updatePrepackagedRules = async ( alertsClient: AlertsClient, actionsClient: ActionsClient, + savedObjectsClient: SavedObjectsClientContract, rules: PrepackagedRules[], outputIndex: string ): Promise => { @@ -55,6 +57,7 @@ export const updatePrepackagedRules = async ( outputIndex, id: undefined, // We never have an id when updating from pre-packaged rules savedId, + savedObjectsClient, meta, filters, ruleId, diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts index 0fe4b15437af8..e2632791f859e 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/rules/update_rules.ts @@ -7,8 +7,9 @@ import { defaults } from 'lodash/fp'; import { AlertAction, IntervalSchedule } from '../../../../../alerting/server/types'; import { readRules } from './read_rules'; -import { UpdateRuleParams } from './types'; +import { UpdateRuleParams, IRuleSavedAttributesSavedObjectAttributes } from './types'; import { addTags } from './add_tags'; +import { ruleStatusSavedObjectType } from './saved_object_mappings'; export const calculateInterval = ( interval: string | undefined, @@ -66,6 +67,7 @@ export const calculateName = ({ export const updateRules = async ({ alertsClient, actionsClient, // TODO: Use this whenever we add feature support for different action types + savedObjectsClient, description, falsePositives, enabled, @@ -135,10 +137,39 @@ export const updateRules = async ({ } ); + const ruleCurrentStatus = savedObjectsClient + ? await savedObjectsClient.find({ + type: ruleStatusSavedObjectType, + perPage: 1, + sortField: 'statusDate', + sortOrder: 'desc', + search: rule.id, + searchFields: ['alertId'], + }) + : null; + if (rule.enabled && enabled === false) { await alertsClient.disable({ id: rule.id }); + // set current status for this rule to null to represent disabled, + // but keep last_success_at / last_failure_at properties intact for + // use on frontend while rule is disabled. + if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) { + const currentStatusToDisable = ruleCurrentStatus.saved_objects[0]; + currentStatusToDisable.attributes.status = null; + await savedObjectsClient?.update(ruleStatusSavedObjectType, currentStatusToDisable.id, { + ...currentStatusToDisable.attributes, + }); + } } else if (!rule.enabled && enabled === true) { await alertsClient.enable({ id: rule.id }); + // set current status for this rule to be 'going to run' + if (ruleCurrentStatus && ruleCurrentStatus.saved_objects.length > 0) { + const currentStatusToDisable = ruleCurrentStatus.saved_objects[0]; + currentStatusToDisable.attributes.status = 'going to run'; + await savedObjectsClient?.update(ruleStatusSavedObjectType, currentStatusToDisable.id, { + ...currentStatusToDisable.attributes, + }); + } } else { // enabled is null or undefined and we do not touch the rule } diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index d80eadd2c088b..32f2c86914770 100644 --- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -96,7 +96,7 @@ export const signalRulesAlertType = ({ >(ruleStatusSavedObjectType, { alertId, // do a search for this id. statusDate: date, - status: 'executing', + status: 'going to run', lastFailureAt: null, lastSuccessAt: null, lastFailureMessage: null, @@ -106,7 +106,7 @@ export const signalRulesAlertType = ({ // update 0th to executing. currentStatusSavedObject = ruleStatusSavedObjects.saved_objects[0]; const sDate = new Date().toISOString(); - currentStatusSavedObject.attributes.status = 'executing'; + currentStatusSavedObject.attributes.status = 'going to run'; currentStatusSavedObject.attributes.statusDate = sDate; await services.savedObjectsClient.update( ruleStatusSavedObjectType, From 22e7ae80dced26665f7572c6f377e8faa0466d8f Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Mon, 20 Jan 2020 11:00:33 +0300 Subject: [PATCH 11/39] Move vis_vega_type/data_model tests to jest (#55186) Co-authored-by: Elastic Machine --- .../data_model/__tests__/vega_parser.js | 317 ----------------- ...uery_parser.js => es_query_parser.test.js} | 165 +++++---- .../search_cache.js => search_cache.test.js} | 23 +- .../time_cache.js => time_cache.test.js} | 27 +- .../public/data_model/vega_parser.test.js | 327 ++++++++++++++++++ 5 files changed, 442 insertions(+), 417 deletions(-) delete mode 100644 src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/vega_parser.js rename src/legacy/core_plugins/vis_type_vega/public/data_model/{__tests__/es_query_parser.js => es_query_parser.test.js} (59%) rename src/legacy/core_plugins/vis_type_vega/public/data_model/{__tests__/search_cache.js => search_cache.test.js} (75%) rename src/legacy/core_plugins/vis_type_vega/public/data_model/{__tests__/time_cache.js => time_cache.test.js} (72%) create mode 100644 src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.test.js diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/vega_parser.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/vega_parser.js deleted file mode 100644 index 50bcff2469710..0000000000000 --- a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/vega_parser.js +++ /dev/null @@ -1,317 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import expect from '@kbn/expect'; -import { VegaParser } from '../vega_parser'; -import { bypassExternalUrlCheck } from '../../vega_view/vega_base_view'; - -describe(`VegaParser._setDefaultValue`, () => { - function test(spec, expected, ...params) { - return () => { - const vp = new VegaParser(spec); - vp._setDefaultValue(...params); - expect(vp.spec).to.eql(expected); - expect(vp.warnings).to.have.length(0); - }; - } - - it(`empty`, test({}, { config: { test: 42 } }, 42, 'config', 'test')); - it(`exists`, test({ config: { test: 42 } }, { config: { test: 42 } }, 1, 'config', 'test')); - it(`exists non-obj`, test({ config: false }, { config: false }, 42, 'config', 'test')); -}); - -describe(`VegaParser._setDefaultColors`, () => { - function test(spec, isVegaLite, expected) { - return () => { - const vp = new VegaParser(spec); - vp.isVegaLite = isVegaLite; - vp._setDefaultColors(); - expect(vp.spec).to.eql(expected); - expect(vp.warnings).to.have.length(0); - }; - } - - it( - `vegalite`, - test({}, true, { - config: { - range: { category: { scheme: 'elastic' } }, - mark: { color: '#54B399' }, - }, - }) - ); - - it( - `vega`, - test({}, false, { - config: { - range: { category: { scheme: 'elastic' } }, - arc: { fill: '#54B399' }, - area: { fill: '#54B399' }, - line: { stroke: '#54B399' }, - path: { stroke: '#54B399' }, - rect: { fill: '#54B399' }, - rule: { stroke: '#54B399' }, - shape: { stroke: '#54B399' }, - symbol: { fill: '#54B399' }, - trail: { fill: '#54B399' }, - }, - }) - ); -}); - -describe('VegaParser._resolveEsQueries', () => { - function test(spec, expected, warnCount) { - return async () => { - const vp = new VegaParser(spec, { search: async () => [[42]] }, 0, 0, { - getFileLayers: async () => [{ name: 'file1', url: 'url1' }], - getUrlForRegionLayer: async layer => { - return layer.url; - }, - }); - await vp._resolveDataUrls(); - - expect(vp.spec).to.eql(expected); - expect(vp.warnings).to.have.length(warnCount || 0); - }; - } - - it('no data', test({}, {})); - it('no data2', test({ a: 1 }, { a: 1 })); - it('non-es data', test({ data: { a: 10 } }, { data: { a: 10 } })); - it('es', test({ data: { url: { index: 'a' }, x: 1 } }, { data: { values: [42], x: 1 } })); - it( - 'es', - test({ data: { url: { '%type%': 'elasticsearch', index: 'a' } } }, { data: { values: [42] } }) - ); - it( - 'es arr', - test( - { arr: [{ data: { url: { index: 'a' }, x: 1 } }] }, - { arr: [{ data: { values: [42], x: 1 } }] } - ) - ); - it( - 'emsfile', - test( - { data: { url: { '%type%': 'emsfile', name: 'file1' } } }, - { data: { url: bypassExternalUrlCheck('url1') } } - ) - ); -}); - -describe('VegaParser._parseSchema', () => { - function test(schema, isVegaLite, warningCount) { - return () => { - const vp = new VegaParser({ $schema: schema }); - expect(vp._parseSchema()).to.be(isVegaLite); - expect(vp.spec).to.eql({ $schema: schema }); - expect(vp.warnings).to.have.length(warningCount); - }; - } - - it('should warn on no vega version specified', () => { - const vp = new VegaParser({}); - expect(vp._parseSchema()).to.be(false); - expect(vp.spec).to.eql({ $schema: 'https://vega.github.io/schema/vega/v3.0.json' }); - expect(vp.warnings).to.have.length(1); - }); - - it( - 'should not warn on current vega version', - test('https://vega.github.io/schema/vega/v4.0.json', false, 0) - ); - it( - 'should not warn on older vega version', - test('https://vega.github.io/schema/vega/v3.0.json', false, 0) - ); - it( - 'should warn on vega version too new to be supported', - test('https://vega.github.io/schema/vega/v5.0.json', false, 1) - ); - - it( - 'should not warn on current vega-lite version', - test('https://vega.github.io/schema/vega-lite/v2.0.json', true, 0) - ); - it( - 'should warn on vega-lite version too new to be supported', - test('https://vega.github.io/schema/vega-lite/v3.0.json', true, 1) - ); -}); - -describe('VegaParser._parseTooltips', () => { - function test(tooltips, position, padding, centerOnMark) { - return () => { - const vp = new VegaParser(tooltips !== undefined ? { config: { kibana: { tooltips } } } : {}); - vp._config = vp._parseConfig(); - if (position === undefined) { - // error - expect(() => vp._parseTooltips()).to.throwError(); - } else if (position === false) { - expect(vp._parseTooltips()).to.eql(false); - } else { - expect(vp._parseTooltips()).to.eql({ position, padding, centerOnMark }); - } - }; - } - - it('undefined', test(undefined, 'top', 16, 50)); - it('{}', test({}, 'top', 16, 50)); - it('left', test({ position: 'left' }, 'left', 16, 50)); - it('padding', test({ position: 'bottom', padding: 60 }, 'bottom', 60, 50)); - it('padding2', test({ padding: 70 }, 'top', 70, 50)); - it('centerOnMark', test({}, 'top', 16, 50)); - it('centerOnMark=10', test({ centerOnMark: 10 }, 'top', 16, 10)); - it('centerOnMark=true', test({ centerOnMark: true }, 'top', 16, Number.MAX_VALUE)); - it('centerOnMark=false', test({ centerOnMark: false }, 'top', 16, -1)); - - it('false', test(false, false)); - - it('err1', test(true, undefined)); - it('err2', test({ position: 'foo' }, undefined)); - it('err3', test({ padding: 'foo' }, undefined)); - it('err4', test({ centerOnMark: {} }, undefined)); -}); - -describe('VegaParser._parseMapConfig', () => { - function test(config, expected, warnCount) { - return () => { - const vp = new VegaParser(); - vp._config = config; - expect(vp._parseMapConfig()).to.eql(expected); - expect(vp.warnings).to.have.length(warnCount); - }; - } - - it( - 'empty', - test( - {}, - { - delayRepaint: true, - latitude: 0, - longitude: 0, - mapStyle: 'default', - zoomControl: true, - scrollWheelZoom: false, - }, - 0 - ) - ); - - it( - 'filled', - test( - { - delayRepaint: true, - latitude: 0, - longitude: 0, - mapStyle: 'default', - zoomControl: true, - scrollWheelZoom: false, - maxBounds: [1, 2, 3, 4], - }, - { - delayRepaint: true, - latitude: 0, - longitude: 0, - mapStyle: 'default', - zoomControl: true, - scrollWheelZoom: false, - maxBounds: [1, 2, 3, 4], - }, - 0 - ) - ); - - it( - 'warnings', - test( - { - delayRepaint: true, - latitude: 0, - longitude: 0, - zoom: 'abc', // ignored - mapStyle: 'abc', - zoomControl: 'abc', - scrollWheelZoom: 'abc', - maxBounds: [2, 3, 4], - }, - { - delayRepaint: true, - latitude: 0, - longitude: 0, - mapStyle: 'default', - zoomControl: true, - scrollWheelZoom: false, - }, - 5 - ) - ); -}); - -describe('VegaParser._parseConfig', () => { - function test(spec, expectedConfig, expectedSpec, warnCount) { - return async () => { - expectedSpec = expectedSpec || _.cloneDeep(spec); - const vp = new VegaParser(spec); - const config = await vp._parseConfig(); - expect(config).to.eql(expectedConfig); - expect(vp.spec).to.eql(expectedSpec); - expect(vp.warnings).to.have.length(warnCount || 0); - }; - } - - it('no config', test({}, {}, {})); - it('simple config', test({ config: { a: 1 } }, {})); - it('kibana config', test({ config: { kibana: { a: 1 } } }, { a: 1 }, { config: {} })); - it('_hostConfig', test({ _hostConfig: { a: 1 } }, { a: 1 }, {}, 1)); -}); - -describe('VegaParser._calcSizing', () => { - function test(spec, useResize, paddingWidth, paddingHeight, isVegaLite, expectedSpec, warnCount) { - return async () => { - expectedSpec = expectedSpec || _.cloneDeep(spec); - const vp = new VegaParser(spec); - vp.isVegaLite = !!isVegaLite; - vp._calcSizing(); - expect(vp.useResize).to.eql(useResize); - expect(vp.paddingWidth).to.eql(paddingWidth); - expect(vp.paddingHeight).to.eql(paddingHeight); - expect(vp.spec).to.eql(expectedSpec); - expect(vp.warnings).to.have.length(warnCount || 0); - }; - } - - it('no size', test({ autosize: {} }, false, 0, 0)); - it('fit', test({ autosize: 'fit' }, true, 0, 0)); - it('fit obj', test({ autosize: { type: 'fit' } }, true, 0, 0)); - it('padding const', test({ autosize: 'fit', padding: 10 }, true, 20, 20)); - it( - 'padding obj', - test({ autosize: 'fit', padding: { left: 5, bottom: 7, right: 6, top: 8 } }, true, 11, 15) - ); - it('width height', test({ autosize: 'fit', width: 1, height: 2 }, true, 0, 0, false, false, 1)); - it( - 'VL width height', - test({ autosize: 'fit', width: 1, height: 2 }, true, 0, 0, true, { autosize: 'fit' }, 0) - ); -}); diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/es_query_parser.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.test.js similarity index 59% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/es_query_parser.js rename to src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.test.js index a725bb9ed4cb5..691e5e8944241 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/es_query_parser.js +++ b/src/legacy/core_plugins/vis_type_vega/public/data_model/es_query_parser.test.js @@ -17,11 +17,13 @@ * under the License. */ -import _ from 'lodash'; -import expect from '@kbn/expect'; -import sinon from 'sinon'; +import { cloneDeep } from 'lodash'; import moment from 'moment'; -import { EsQueryParser } from '../es_query_parser'; +import { EsQueryParser } from './es_query_parser'; + +jest.mock('../helpers', () => ({ + getEsShardTimeout: jest.fn(() => '10000'), +})); const second = 1000; const minute = 60 * second; @@ -39,41 +41,57 @@ function create(min, max, dashboardCtx) { getTimeBounds: () => ({ min, max }), }, () => {}, - _.cloneDeep(dashboardCtx), + cloneDeep(dashboardCtx), () => (inst.$$$warnCount = (inst.$$$warnCount || 0) + 1) ); return inst; } describe(`EsQueryParser time`, () => { - it(`roundInterval(4s)`, () => expect(EsQueryParser._roundInterval(4 * second)).to.be(`1s`)); - it(`roundInterval(4hr)`, () => expect(EsQueryParser._roundInterval(4 * hour)).to.be(`3h`)); - it(`getTimeBound`, () => expect(create(1000, 2000)._getTimeBound({}, `min`)).to.be(1000)); - it(`getTimeBound(shift 2d)`, () => - expect(create(5, 2000)._getTimeBound({ shift: 2 }, `min`)).to.be(5 + 2 * day)); - it(`getTimeBound(shift -2hr)`, () => - expect(create(10 * day, 20 * day)._getTimeBound({ shift: -2, unit: `h` }, `min`)).to.be( + test(`roundInterval(4s)`, () => { + expect(EsQueryParser._roundInterval(4 * second)).toBe(`1s`); + }); + + test(`roundInterval(4hr)`, () => { + expect(EsQueryParser._roundInterval(4 * hour)).toBe(`3h`); + }); + + test(`getTimeBound`, () => { + expect(create(1000, 2000)._getTimeBound({}, `min`)).toBe(1000); + }); + + test(`getTimeBound(shift 2d)`, () => { + expect(create(5, 2000)._getTimeBound({ shift: 2 }, `min`)).toBe(5 + 2 * day); + }); + + test(`getTimeBound(shift -2hr)`, () => { + expect(create(10 * day, 20 * day)._getTimeBound({ shift: -2, unit: `h` }, `min`)).toBe( 10 * day - 2 * hour - )); - it(`createRangeFilter({})`, () => { + ); + }); + + test(`createRangeFilter({})`, () => { const obj = {}; - expect(create(1000, 2000)._createRangeFilter(obj)) - .to.eql({ - format: 'strict_date_optional_time', - gte: moment(1000).toISOString(), - lte: moment(2000).toISOString(), - }) - .and.to.be(obj); + const result = create(1000, 2000)._createRangeFilter(obj); + + expect(result).toEqual({ + format: 'strict_date_optional_time', + gte: moment(1000).toISOString(), + lte: moment(2000).toISOString(), + }); + expect(result).toBe(obj); }); - it(`createRangeFilter(shift 1s)`, () => { + + test(`createRangeFilter(shift 1s)`, () => { const obj = { shift: 5, unit: 's' }; - expect(create(1000, 2000)._createRangeFilter(obj)) - .to.eql({ - format: 'strict_date_optional_time', - gte: moment(6000).toISOString(), - lte: moment(7000).toISOString(), - }) - .and.to.be(obj); + const result = create(1000, 2000)._createRangeFilter(obj); + + expect(result).toEqual({ + format: 'strict_date_optional_time', + gte: moment(6000).toISOString(), + lte: moment(7000).toISOString(), + }); + expect(result).toBe(obj); }); }); @@ -82,79 +100,78 @@ describe('EsQueryParser.populateData', () => { let parser; beforeEach(() => { - searchStub = sinon.stub(); + searchStub = jest.fn(() => Promise.resolve([{}, {}])); parser = new EsQueryParser({}, { search: searchStub }, undefined, undefined); - - searchStub.returns(Promise.resolve([{}, {}])); }); - it('should set the timeout for each request', async () => { + + test('should set the timeout for each request', async () => { await parser.populateData([ { url: { body: {} }, dataObject: {} }, { url: { body: {} }, dataObject: {} }, ]); - expect(searchStub.firstCall.args[0][0].body.timeout).to.be.defined; + expect(searchStub.mock.calls[0][0][0].body.timeout).toBe.defined; }); - it('should remove possible timeout parameters on a request', async () => { + test('should remove possible timeout parameters on a request', async () => { await parser.populateData([ { url: { timeout: '500h', body: { timeout: '500h' } }, dataObject: {} }, ]); - expect(searchStub.firstCall.args[0][0].body.timeout).to.be.defined; - expect(searchStub.firstCall.args[0][0].timeout).to.be(undefined); + expect(searchStub.mock.calls[0][0][0].body.timeout).toBe.defined; + expect(searchStub.mock.calls[0][0][0].timeout).toBe(undefined); }); }); describe(`EsQueryParser.injectQueryContextVars`, () => { - function test(obj, expected, ctx) { + function check(obj, expected, ctx) { return () => { create(rangeStart, rangeEnd, ctx)._injectContextVars(obj, true); - expect(obj).to.eql(expected); + expect(obj).toEqual(expected); }; } - it(`empty`, test({}, {})); - it(`simple`, () => { + test(`empty`, check({}, {})); + test(`simple`, () => { const obj = { a: { c: 10 }, b: [{ d: 2 }, 4, 5], c: [], d: {} }; - test(obj, _.cloneDeep(obj)); + check(obj, cloneDeep(obj)); }); - it(`must clause empty`, test({ arr: ['%dashboard_context-must_clause%'] }, { arr: [] }, {})); - it( + test(`must clause empty`, check({ arr: ['%dashboard_context-must_clause%'] }, { arr: [] }, {})); + test( `must clause arr`, - test({ arr: ['%dashboard_context-must_clause%'] }, { arr: [...ctxArr.bool.must] }, ctxArr) + check({ arr: ['%dashboard_context-must_clause%'] }, { arr: [...ctxArr.bool.must] }, ctxArr) ); - it( + test( `must clause obj`, - test({ arr: ['%dashboard_context-must_clause%'] }, { arr: [ctxObj.bool.must] }, ctxObj) + check({ arr: ['%dashboard_context-must_clause%'] }, { arr: [ctxObj.bool.must] }, ctxObj) ); - it( + test( `mixed clause arr`, - test( + check( { arr: [1, '%dashboard_context-must_clause%', 2, '%dashboard_context-must_not_clause%'] }, { arr: [1, ...ctxArr.bool.must, 2, ...ctxArr.bool.must_not] }, ctxArr ) ); - it( + test( `mixed clause obj`, - test( + check( { arr: ['%dashboard_context-must_clause%', 1, '%dashboard_context-must_not_clause%', 2] }, { arr: [ctxObj.bool.must, 1, ctxObj.bool.must_not, 2] }, ctxObj ) ); - it( + test( `%autointerval% = true`, - test({ interval: { '%autointerval%': true } }, { interval: `1h` }, ctxObj) + check({ interval: { '%autointerval%': true } }, { interval: `1h` }, ctxObj) ); - it( + test( `%autointerval% = 10`, - test({ interval: { '%autointerval%': 10 } }, { interval: `3h` }, ctxObj) + check({ interval: { '%autointerval%': 10 } }, { interval: `3h` }, ctxObj) ); - it(`%timefilter% = min`, test({ a: { '%timefilter%': 'min' } }, { a: rangeStart })); - it(`%timefilter% = max`, test({ a: { '%timefilter%': 'max' } }, { a: rangeEnd })); - it( + test(`%timefilter% = min`, check({ a: { '%timefilter%': 'min' } }, { a: rangeStart })); + test(`%timefilter% = max`, check({ a: { '%timefilter%': 'max' } }, { a: rangeEnd })); + test( `%timefilter% = true`, - test( + check( { a: { '%timefilter%': true } }, { a: { @@ -168,24 +185,24 @@ describe(`EsQueryParser.injectQueryContextVars`, () => { }); describe(`EsQueryParser.parseEsRequest`, () => { - function test(req, ctx, expected) { + function check(req, ctx, expected) { return () => { create(rangeStart, rangeEnd, ctx).parseUrl({}, req); - expect(req).to.eql(expected); + expect(req).toEqual(expected); }; } - it( + test( `%context_query%=true`, - test({ index: '_all', '%context_query%': true }, ctxArr, { + check({ index: '_all', '%context_query%': true }, ctxArr, { index: '_all', body: { query: ctxArr }, }) ); - it( + test( `%context%=true`, - test({ index: '_all', '%context%': true }, ctxArr, { index: '_all', body: { query: ctxArr } }) + check({ index: '_all', '%context%': true }, ctxArr, { index: '_all', body: { query: ctxArr } }) ); const expectedForCtxAndTimefield = { @@ -211,23 +228,23 @@ describe(`EsQueryParser.parseEsRequest`, () => { }, }; - it( + test( `%context_query%='abc'`, - test({ index: '_all', '%context_query%': 'abc' }, ctxArr, expectedForCtxAndTimefield) + check({ index: '_all', '%context_query%': 'abc' }, ctxArr, expectedForCtxAndTimefield) ); - it( + test( `%context%=true, %timefield%='abc'`, - test( + check( { index: '_all', '%context%': true, '%timefield%': 'abc' }, ctxArr, expectedForCtxAndTimefield ) ); - it( + test( `%timefield%='abc'`, - test({ index: '_all', '%timefield%': 'abc' }, ctxArr, { + check({ index: '_all', '%timefield%': 'abc' }, ctxArr, { index: '_all', body: { query: { @@ -243,11 +260,11 @@ describe(`EsQueryParser.parseEsRequest`, () => { }) ); - it(`no esRequest`, test({ index: '_all' }, ctxArr, { index: '_all', body: {} })); + test(`no esRequest`, check({ index: '_all' }, ctxArr, { index: '_all', body: {} })); - it( + test( `esRequest`, - test({ index: '_all', body: { query: 2 } }, ctxArr, { + check({ index: '_all', body: { query: 2 } }, ctxArr, { index: '_all', body: { query: 2 }, }) diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/search_cache.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.test.js similarity index 75% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/search_cache.js rename to src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.test.js index 2b28d2cadfa3f..0ec018f46c02b 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/search_cache.js +++ b/src/legacy/core_plugins/vis_type_vega/public/data_model/search_cache.test.js @@ -17,8 +17,7 @@ * under the License. */ -import expect from '@kbn/expect'; -import { SearchCache } from '../search_cache'; +import { SearchCache } from './search_cache'; describe(`SearchCache`, () => { class FauxEs { @@ -45,27 +44,27 @@ describe(`SearchCache`, () => { // empty request let res = await sc.search([]); - expect(res).to.eql([]); - expect(sc._es.searches).to.eql([]); + expect(res).toEqual([]); + expect(sc._es.searches).toEqual([]); // single request res = await sc.search([request1]); - expect(res).to.eql([expected1]); - expect(sc._es.searches).to.eql([request1]); + expect(res).toEqual([expected1]); + expect(sc._es.searches).toEqual([request1]); // repeat the same search, use array notation res = await sc.search([request1]); - expect(res).to.eql([expected1]); - expect(sc._es.searches).to.eql([request1]); // no new entries + expect(res).toEqual([expected1]); + expect(sc._es.searches).toEqual([request1]); // no new entries // new single search res = await sc.search([request2]); - expect(res).to.eql([expected2]); - expect(sc._es.searches).to.eql([request1, request2]); + expect(res).toEqual([expected2]); + expect(sc._es.searches).toEqual([request1, request2]); // multiple search, some new, some old res = await sc.search([request1, request3, request2]); - expect(res).to.eql([expected1, expected3, expected2]); - expect(sc._es.searches).to.eql([request1, request2, request3]); + expect(res).toEqual([expected1, expected3, expected2]); + expect(sc._es.searches).toEqual([request1, request2, request3]); }); }); diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/time_cache.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.test.js similarity index 72% rename from src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/time_cache.js rename to src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.test.js index 21e9256295382..b76709ea2c934 100644 --- a/src/legacy/core_plugins/vis_type_vega/public/data_model/__tests__/time_cache.js +++ b/src/legacy/core_plugins/vis_type_vega/public/data_model/time_cache.test.js @@ -17,8 +17,7 @@ * under the License. */ -import expect from '@kbn/expect'; -import { TimeCache } from '../time_cache'; +import { TimeCache } from './time_cache'; describe(`TimeCache`, () => { class FauxTimefilter { @@ -70,29 +69,29 @@ describe(`TimeCache`, () => { let filterAccess = 0; // first call - gets bounds - expect(tc.getTimeBounds()).to.eql({ min: 10000, max: 20000 }); - expect(time._accessCount).to.be(++timeAccess); - expect(timefilter._accessCount).to.be(++filterAccess); + expect(tc.getTimeBounds()).toEqual({ min: 10000, max: 20000 }); + expect(time._accessCount).toBe(++timeAccess); + expect(timefilter._accessCount).toBe(++filterAccess); // short diff, same result time.increment(10); timefilter.setTime(10010, 20010); - expect(tc.getTimeBounds()).to.eql({ min: 10000, max: 20000 }); - expect(time._accessCount).to.be(++timeAccess); - expect(timefilter._accessCount).to.be(filterAccess); + expect(tc.getTimeBounds()).toEqual({ min: 10000, max: 20000 }); + expect(time._accessCount).toBe(++timeAccess); + expect(timefilter._accessCount).toBe(filterAccess); // longer diff, gets bounds but returns original time.increment(200); timefilter.setTime(10210, 20210); - expect(tc.getTimeBounds()).to.eql({ min: 10000, max: 20000 }); - expect(time._accessCount).to.be(++timeAccess); - expect(timefilter._accessCount).to.be(++filterAccess); + expect(tc.getTimeBounds()).toEqual({ min: 10000, max: 20000 }); + expect(time._accessCount).toBe(++timeAccess); + expect(timefilter._accessCount).toBe(++filterAccess); // long diff, new result time.increment(10000); timefilter.setTime(20220, 30220); - expect(tc.getTimeBounds()).to.eql({ min: 20220, max: 30220 }); - expect(time._accessCount).to.be(++timeAccess); - expect(timefilter._accessCount).to.be(++filterAccess); + expect(tc.getTimeBounds()).toEqual({ min: 20220, max: 30220 }); + expect(time._accessCount).toBe(++timeAccess); + expect(timefilter._accessCount).toBe(++filterAccess); }); }); diff --git a/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.test.js b/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.test.js new file mode 100644 index 0000000000000..1bc8b1f90daab --- /dev/null +++ b/src/legacy/core_plugins/vis_type_vega/public/data_model/vega_parser.test.js @@ -0,0 +1,327 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { cloneDeep } from 'lodash'; +import { VegaParser } from './vega_parser'; +import { bypassExternalUrlCheck } from '../vega_view/vega_base_view'; + +describe(`VegaParser._setDefaultValue`, () => { + function check(spec, expected, ...params) { + return () => { + const vp = new VegaParser(spec); + vp._setDefaultValue(...params); + expect(vp.spec).toEqual(expected); + expect(vp.warnings).toHaveLength(0); + }; + } + + test(`empty`, check({}, { config: { test: 42 } }, 42, 'config', 'test')); + test(`exists`, check({ config: { test: 42 } }, { config: { test: 42 } }, 1, 'config', 'test')); + test(`exists non-obj`, check({ config: false }, { config: false }, 42, 'config', 'test')); +}); + +describe(`VegaParser._setDefaultColors`, () => { + function check(spec, isVegaLite, expected) { + return () => { + const vp = new VegaParser(spec); + vp.isVegaLite = isVegaLite; + vp._setDefaultColors(); + expect(vp.spec).toEqual(expected); + expect(vp.warnings).toHaveLength(0); + }; + } + + test( + `vegalite`, + check({}, true, { + config: { + range: { category: { scheme: 'elastic' } }, + mark: { color: '#54B399' }, + }, + }) + ); + + test( + `vega`, + check({}, false, { + config: { + range: { category: { scheme: 'elastic' } }, + arc: { fill: '#54B399' }, + area: { fill: '#54B399' }, + line: { stroke: '#54B399' }, + path: { stroke: '#54B399' }, + rect: { fill: '#54B399' }, + rule: { stroke: '#54B399' }, + shape: { stroke: '#54B399' }, + symbol: { fill: '#54B399' }, + trail: { fill: '#54B399' }, + }, + }) + ); +}); + +describe('VegaParser._resolveEsQueries', () => { + function check(spec, expected, warnCount) { + return async () => { + const vp = new VegaParser(spec, { search: async () => [[42]] }, 0, 0, { + getFileLayers: async () => [{ name: 'file1', url: 'url1' }], + getUrlForRegionLayer: async layer => { + return layer.url; + }, + }); + await vp._resolveDataUrls(); + + expect(vp.spec).toEqual(expected); + expect(vp.warnings).toHaveLength(warnCount || 0); + }; + } + + test('no data', check({}, {})); + test('no data2', check({ a: 1 }, { a: 1 })); + test('non-es data', check({ data: { a: 10 } }, { data: { a: 10 } })); + test('es', check({ data: { url: { index: 'a' }, x: 1 } }, { data: { values: [42], x: 1 } })); + test( + 'es 2', + check({ data: { url: { '%type%': 'elasticsearch', index: 'a' } } }, { data: { values: [42] } }) + ); + test( + 'es arr', + check( + { arr: [{ data: { url: { index: 'a' }, x: 1 } }] }, + { arr: [{ data: { values: [42], x: 1 } }] } + ) + ); + test( + 'emsfile', + check( + { data: { url: { '%type%': 'emsfile', name: 'file1' } } }, + { data: { url: bypassExternalUrlCheck('url1') } } + ) + ); +}); + +describe('VegaParser._parseSchema', () => { + function check(schema, isVegaLite, warningCount) { + return () => { + const vp = new VegaParser({ $schema: schema }); + expect(vp._parseSchema()).toBe(isVegaLite); + expect(vp.spec).toEqual({ $schema: schema }); + expect(vp.warnings).toHaveLength(warningCount); + }; + } + + test('should warn on no vega version specified', () => { + const vp = new VegaParser({}); + expect(vp._parseSchema()).toBe(false); + expect(vp.spec).toEqual({ $schema: 'https://vega.github.io/schema/vega/v3.0.json' }); + expect(vp.warnings).toHaveLength(1); + }); + + test( + 'should not warn on current vega version', + check('https://vega.github.io/schema/vega/v4.0.json', false, 0) + ); + test( + 'should not warn on older vega version', + check('https://vega.github.io/schema/vega/v3.0.json', false, 0) + ); + test( + 'should warn on vega version too new to be supported', + check('https://vega.github.io/schema/vega/v5.0.json', false, 1) + ); + + test( + 'should not warn on current vega-lite version', + check('https://vega.github.io/schema/vega-lite/v2.0.json', true, 0) + ); + test( + 'should warn on vega-lite version too new to be supported', + check('https://vega.github.io/schema/vega-lite/v3.0.json', true, 1) + ); +}); + +describe('VegaParser._parseTooltips', () => { + function check(tooltips, position, padding, centerOnMark) { + return () => { + const vp = new VegaParser(tooltips !== undefined ? { config: { kibana: { tooltips } } } : {}); + vp._config = vp._parseConfig(); + if (position === undefined) { + // error + expect(() => vp._parseTooltips()).toThrow(); + } else if (position === false) { + expect(vp._parseTooltips()).toEqual(false); + } else { + expect(vp._parseTooltips()).toEqual({ position, padding, centerOnMark }); + } + }; + } + + test('undefined', check(undefined, 'top', 16, 50)); + test('{}', check({}, 'top', 16, 50)); + test('left', check({ position: 'left' }, 'left', 16, 50)); + test('padding', check({ position: 'bottom', padding: 60 }, 'bottom', 60, 50)); + test('padding2', check({ padding: 70 }, 'top', 70, 50)); + test('centerOnMark', check({}, 'top', 16, 50)); + test('centerOnMark=10', check({ centerOnMark: 10 }, 'top', 16, 10)); + test('centerOnMark=true', check({ centerOnMark: true }, 'top', 16, Number.MAX_VALUE)); + test('centerOnMark=false', check({ centerOnMark: false }, 'top', 16, -1)); + + test('false', check(false, false)); + + test('err1', check(true, undefined)); + test('err2', check({ position: 'foo' }, undefined)); + test('err3', check({ padding: 'foo' }, undefined)); + test('err4', check({ centerOnMark: {} }, undefined)); +}); + +describe('VegaParser._parseMapConfig', () => { + function check(config, expected, warnCount) { + return () => { + const vp = new VegaParser(); + vp._config = config; + expect(vp._parseMapConfig()).toEqual(expected); + expect(vp.warnings).toHaveLength(warnCount); + }; + } + + test( + 'empty', + check( + {}, + { + delayRepaint: true, + latitude: 0, + longitude: 0, + mapStyle: 'default', + zoomControl: true, + scrollWheelZoom: false, + }, + 0 + ) + ); + + test( + 'filled', + check( + { + delayRepaint: true, + latitude: 0, + longitude: 0, + mapStyle: 'default', + zoomControl: true, + scrollWheelZoom: false, + maxBounds: [1, 2, 3, 4], + }, + { + delayRepaint: true, + latitude: 0, + longitude: 0, + mapStyle: 'default', + zoomControl: true, + scrollWheelZoom: false, + maxBounds: [1, 2, 3, 4], + }, + 0 + ) + ); + + test( + 'warnings', + check( + { + delayRepaint: true, + latitude: 0, + longitude: 0, + zoom: 'abc', // ignored + mapStyle: 'abc', + zoomControl: 'abc', + scrollWheelZoom: 'abc', + maxBounds: [2, 3, 4], + }, + { + delayRepaint: true, + latitude: 0, + longitude: 0, + mapStyle: 'default', + zoomControl: true, + scrollWheelZoom: false, + }, + 5 + ) + ); +}); + +describe('VegaParser._parseConfig', () => { + function check(spec, expectedConfig, expectedSpec, warnCount) { + return async () => { + expectedSpec = expectedSpec || cloneDeep(spec); + const vp = new VegaParser(spec); + const config = await vp._parseConfig(); + expect(config).toEqual(expectedConfig); + expect(vp.spec).toEqual(expectedSpec); + expect(vp.warnings).toHaveLength(warnCount || 0); + }; + } + + test('no config', check({}, {}, {})); + test('simple config', check({ config: { a: 1 } }, {})); + test('kibana config', check({ config: { kibana: { a: 1 } } }, { a: 1 }, { config: {} })); + test('_hostConfig', check({ _hostConfig: { a: 1 } }, { a: 1 }, {}, 1)); +}); + +describe('VegaParser._calcSizing', () => { + function check( + spec, + useResize, + paddingWidth, + paddingHeight, + isVegaLite, + expectedSpec, + warnCount + ) { + return async () => { + expectedSpec = expectedSpec || cloneDeep(spec); + const vp = new VegaParser(spec); + vp.isVegaLite = !!isVegaLite; + vp._calcSizing(); + expect(vp.useResize).toEqual(useResize); + expect(vp.paddingWidth).toEqual(paddingWidth); + expect(vp.paddingHeight).toEqual(paddingHeight); + expect(vp.spec).toEqual(expectedSpec); + expect(vp.warnings).toHaveLength(warnCount || 0); + }; + } + + test('no size', check({ autosize: {} }, false, 0, 0)); + test('fit', check({ autosize: 'fit' }, true, 0, 0)); + test('fit obj', check({ autosize: { type: 'fit' } }, true, 0, 0)); + test('padding const', check({ autosize: 'fit', padding: 10 }, true, 20, 20)); + test( + 'padding obj', + check({ autosize: 'fit', padding: { left: 5, bottom: 7, right: 6, top: 8 } }, true, 11, 15) + ); + test( + 'width height', + check({ autosize: 'fit', width: 1, height: 2 }, true, 0, 0, false, false, 1) + ); + test( + 'VL width height', + check({ autosize: 'fit', width: 1, height: 2 }, true, 0, 0, true, { autosize: 'fit' }, 0) + ); +}); From 2d10350d7701aeebd87c25fbb0f1d577bb1263bd Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 20 Jan 2020 11:19:40 +0100 Subject: [PATCH 12/39] Implements `getStartServices` on server-side (#55156) * implements server-side getStartServices * add unit test * add integration test * update generated doc * improve test --- ...lugin-server.coresetup.getstartservices.md | 17 ++ .../server/kibana-plugin-server.coresetup.md | 58 +++--- src/core/server/index.ts | 9 +- src/core/server/legacy/legacy_service.ts | 12 +- src/core/server/mocks.ts | 7 +- .../plugins_service.test.mocks.ts | 27 +++ .../integration_tests/plugins_service.test.ts | 167 ++++++++++++++++++ src/core/server/plugins/plugin.test.ts | 37 ++++ src/core/server/plugins/plugin.ts | 14 +- src/core/server/plugins/plugin_context.ts | 1 + src/core/server/server.api.md | 3 +- x-pack/plugins/endpoint/server/plugin.test.ts | 4 +- x-pack/plugins/security/server/plugin.test.ts | 4 +- 13 files changed, 318 insertions(+), 42 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.coresetup.getstartservices.md create mode 100644 src/core/server/plugins/integration_tests/plugins_service.test.mocks.ts create mode 100644 src/core/server/plugins/integration_tests/plugins_service.test.ts diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.getstartservices.md b/docs/development/core/server/kibana-plugin-server.coresetup.getstartservices.md new file mode 100644 index 0000000000000..b05d28899f9d2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.coresetup.getstartservices.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) > [getStartServices](./kibana-plugin-server.coresetup.getstartservices.md) + +## CoreSetup.getStartServices() method + +Allows plugins to get access to APIs available in start inside async handlers. Promise will not resolve until Core and plugin dependencies have completed `start`. This should only be used inside handlers registered during `setup` that will only be executed after `start` lifecycle. + +Signature: + +```typescript +getStartServices(): Promise<[CoreStart, TPluginsStart]>; +``` +Returns: + +`Promise<[CoreStart, TPluginsStart]>` + diff --git a/docs/development/core/server/kibana-plugin-server.coresetup.md b/docs/development/core/server/kibana-plugin-server.coresetup.md index 3f7f5b727ee80..c36d649837e8a 100644 --- a/docs/development/core/server/kibana-plugin-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-server.coresetup.md @@ -1,26 +1,32 @@ - - -[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) - -## CoreSetup interface - -Context passed to the plugins `setup` method. - -Signature: - -```typescript -export interface CoreSetup -``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [capabilities](./kibana-plugin-server.coresetup.capabilities.md) | CapabilitiesSetup | [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) | -| [context](./kibana-plugin-server.coresetup.context.md) | ContextSetup | [ContextSetup](./kibana-plugin-server.contextsetup.md) | -| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | ElasticsearchServiceSetup | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | -| [http](./kibana-plugin-server.coresetup.http.md) | HttpServiceSetup | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | -| [savedObjects](./kibana-plugin-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) | -| [uiSettings](./kibana-plugin-server.coresetup.uisettings.md) | UiSettingsServiceSetup | [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) | -| [uuid](./kibana-plugin-server.coresetup.uuid.md) | UuidServiceSetup | [UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md) | - + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [CoreSetup](./kibana-plugin-server.coresetup.md) + +## CoreSetup interface + +Context passed to the plugins `setup` method. + +Signature: + +```typescript +export interface CoreSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [capabilities](./kibana-plugin-server.coresetup.capabilities.md) | CapabilitiesSetup | [CapabilitiesSetup](./kibana-plugin-server.capabilitiessetup.md) | +| [context](./kibana-plugin-server.coresetup.context.md) | ContextSetup | [ContextSetup](./kibana-plugin-server.contextsetup.md) | +| [elasticsearch](./kibana-plugin-server.coresetup.elasticsearch.md) | ElasticsearchServiceSetup | [ElasticsearchServiceSetup](./kibana-plugin-server.elasticsearchservicesetup.md) | +| [http](./kibana-plugin-server.coresetup.http.md) | HttpServiceSetup | [HttpServiceSetup](./kibana-plugin-server.httpservicesetup.md) | +| [savedObjects](./kibana-plugin-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-server.savedobjectsservicesetup.md) | +| [uiSettings](./kibana-plugin-server.coresetup.uisettings.md) | UiSettingsServiceSetup | [UiSettingsServiceSetup](./kibana-plugin-server.uisettingsservicesetup.md) | +| [uuid](./kibana-plugin-server.coresetup.uuid.md) | UuidServiceSetup | [UuidServiceSetup](./kibana-plugin-server.uuidservicesetup.md) | + +## Methods + +| Method | Description | +| --- | --- | +| [getStartServices()](./kibana-plugin-server.coresetup.getstartservices.md) | Allows plugins to get access to APIs available in start inside async handlers. Promise will not resolve until Core and plugin dependencies have completed start. This should only be used inside handlers registered during setup that will only be executed after start lifecycle. | + diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 50d291b173640..3f67b9a656bb7 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -283,7 +283,7 @@ export interface RequestHandlerContext { * * @public */ -export interface CoreSetup { +export interface CoreSetup { /** {@link CapabilitiesSetup} */ capabilities: CapabilitiesSetup; /** {@link ContextSetup} */ @@ -298,6 +298,13 @@ export interface CoreSetup { uiSettings: UiSettingsServiceSetup; /** {@link UuidServiceSetup} */ uuid: UuidServiceSetup; + /** + * Allows plugins to get access to APIs available in start inside async handlers. + * Promise will not resolve until Core and plugin dependencies have completed `start`. + * This should only be used inside handlers registered during `setup` that will only be executed + * after `start` lifecycle. + */ + getStartServices(): Promise<[CoreStart, TPluginsStart]>; } /** diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index ffcbf1662ee85..07cc933033054 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -256,6 +256,12 @@ export class LegacyService implements CoreService { startDeps: LegacyServiceStartDeps, legacyPlugins: LegacyPlugins ) { + const coreStart: CoreStart = { + capabilities: startDeps.core.capabilities, + savedObjects: { getScopedClient: startDeps.core.savedObjects.getScopedClient }, + uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient }, + }; + const coreSetup: CoreSetup = { capabilities: setupDeps.core.capabilities, context: setupDeps.core.context, @@ -291,11 +297,7 @@ export class LegacyService implements CoreService { uuid: { getInstanceUuid: setupDeps.core.uuid.getInstanceUuid, }, - }; - const coreStart: CoreStart = { - capabilities: startDeps.core.capabilities, - savedObjects: { getScopedClient: startDeps.core.savedObjects.getScopedClient }, - uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient }, + getStartServices: () => Promise.resolve([coreStart, startDeps.plugins]), }; // eslint-disable-next-line @typescript-eslint/no-var-requires diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index c7082d46313ae..c0a8973d98a54 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -86,6 +86,8 @@ function pluginInitializerContextMock(config: T = {} as T) { return mock; } +type CoreSetupMockType = MockedKeys & jest.Mocked>; + function createCoreSetupMock() { const httpService = httpServiceMock.createSetupContract(); const httpMock: jest.Mocked = { @@ -105,7 +107,7 @@ function createCoreSetupMock() { const uiSettingsMock = { register: uiSettingsServiceMock.createSetupContract().register, }; - const mock: MockedKeys = { + const mock: CoreSetupMockType = { capabilities: capabilitiesServiceMock.createSetupContract(), context: contextServiceMock.createSetupContract(), elasticsearch: elasticsearchServiceMock.createSetup(), @@ -113,6 +115,9 @@ function createCoreSetupMock() { savedObjects: savedObjectsServiceMock.createSetupContract(), uiSettings: uiSettingsMock, uuid: uuidServiceMock.createSetupContract(), + getStartServices: jest + .fn, object]>, []>() + .mockResolvedValue([createCoreStartMock(), {}]), }; return mock; diff --git a/src/core/server/plugins/integration_tests/plugins_service.test.mocks.ts b/src/core/server/plugins/integration_tests/plugins_service.test.mocks.ts new file mode 100644 index 0000000000000..d81a7eb5db4ae --- /dev/null +++ b/src/core/server/plugins/integration_tests/plugins_service.test.mocks.ts @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const mockPackage = new Proxy( + { raw: { __dirname: '/tmp' } as any }, + { get: (obj, prop) => obj.raw[prop] } +); +jest.mock('../../../../core/server/utils/package_json', () => ({ pkg: mockPackage })); + +export const mockDiscover = jest.fn(); +jest.mock('../discovery/plugins_discovery', () => ({ discover: mockDiscover })); diff --git a/src/core/server/plugins/integration_tests/plugins_service.test.ts b/src/core/server/plugins/integration_tests/plugins_service.test.ts new file mode 100644 index 0000000000000..d5531478f03c5 --- /dev/null +++ b/src/core/server/plugins/integration_tests/plugins_service.test.ts @@ -0,0 +1,167 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { mockPackage, mockDiscover } from './plugins_service.test.mocks'; + +import { join } from 'path'; + +import { PluginsService } from '../plugins_service'; +import { ConfigPath, ConfigService, Env } from '../../config'; +import { getEnvOptions } from '../../config/__mocks__/env'; +import { BehaviorSubject, from } from 'rxjs'; +import { rawConfigServiceMock } from '../../config/raw_config_service.mock'; +import { config } from '../plugins_config'; +import { loggingServiceMock } from '../../logging/logging_service.mock'; +import { coreMock } from '../../mocks'; +import { Plugin } from '../types'; +import { PluginWrapper } from '../plugin'; + +describe('PluginsService', () => { + const logger = loggingServiceMock.create(); + let pluginsService: PluginsService; + + const createPlugin = ( + id: string, + { + path = id, + disabled = false, + version = 'some-version', + requiredPlugins = [], + optionalPlugins = [], + kibanaVersion = '7.0.0', + configPath = [path], + server = true, + ui = true, + }: { + path?: string; + disabled?: boolean; + version?: string; + requiredPlugins?: string[]; + optionalPlugins?: string[]; + kibanaVersion?: string; + configPath?: ConfigPath; + server?: boolean; + ui?: boolean; + } + ): PluginWrapper => { + return new PluginWrapper({ + path, + manifest: { + id, + version, + configPath: `${configPath}${disabled ? '-disabled' : ''}`, + kibanaVersion, + requiredPlugins, + optionalPlugins, + server, + ui, + }, + opaqueId: Symbol(id), + initializerContext: { logger } as any, + }); + }; + + beforeEach(async () => { + mockPackage.raw = { + branch: 'feature-v1', + version: 'v1', + build: { + distributable: true, + number: 100, + sha: 'feature-v1-build-sha', + }, + }; + + const env = Env.createDefault(getEnvOptions()); + const config$ = new BehaviorSubject>({ + plugins: { + initialize: true, + }, + }); + const rawConfigService = rawConfigServiceMock.create({ rawConfig$: config$ }); + const configService = new ConfigService(rawConfigService, env, logger); + await configService.setSchema(config.path, config.schema); + + pluginsService = new PluginsService({ + coreId: Symbol('core'), + env, + logger, + configService, + }); + }); + + it("properly resolves `getStartServices` in plugin's lifecycle", async () => { + expect.assertions(5); + + const pluginPath = 'plugin-path'; + + mockDiscover.mockReturnValue({ + error$: from([]), + plugin$: from([ + createPlugin('plugin-id', { + path: pluginPath, + configPath: 'path', + }), + ]), + }); + + let startDependenciesResolved = false; + let contextFromStart: any = null; + let contextFromStartService: any = null; + + const pluginInitializer = () => + ({ + setup: async (coreSetup, deps) => { + coreSetup.getStartServices().then(([core, plugins]) => { + startDependenciesResolved = true; + contextFromStartService = { core, plugins }; + }); + }, + start: async (core, plugins) => { + contextFromStart = { core, plugins }; + await new Promise(resolve => setTimeout(resolve, 10)); + expect(startDependenciesResolved).toBe(false); + }, + } as Plugin); + + jest.doMock( + join(pluginPath, 'server'), + () => ({ + plugin: pluginInitializer, + }), + { + virtual: true, + } + ); + + await pluginsService.discover(); + + const setupDeps = coreMock.createInternalSetup(); + await pluginsService.setup(setupDeps); + + expect(startDependenciesResolved).toBe(false); + + const startDeps = coreMock.createInternalStart(); + await pluginsService.start(startDeps); + + expect(startDependenciesResolved).toBe(true); + expect(contextFromStart!.core).toEqual(contextFromStartService!.core); + expect(contextFromStart!.plugins).toEqual(contextFromStartService!.plugins); + }); +}); diff --git a/src/core/server/plugins/plugin.test.ts b/src/core/server/plugins/plugin.test.ts index 10259b718577c..6875302f88a9d 100644 --- a/src/core/server/plugins/plugin.test.ts +++ b/src/core/server/plugins/plugin.test.ts @@ -237,6 +237,43 @@ test('`start` calls plugin.start with context and dependencies', async () => { expect(mockPluginInstance.start).toHaveBeenCalledWith(context, deps); }); +test("`start` resolves `startDependencies` Promise after plugin's start", async () => { + expect.assertions(2); + + const manifest = createPluginManifest(); + const opaqueId = Symbol(); + const plugin = new PluginWrapper({ + path: 'plugin-with-initializer-path', + manifest, + opaqueId, + initializerContext: createPluginInitializerContext(coreContext, opaqueId, manifest), + }); + const startContext = { any: 'thing' } as any; + const pluginDeps = { someDep: 'value' }; + + let startDependenciesResolved = false; + + const mockPluginInstance = { + setup: jest.fn(), + start: async () => { + // delay to ensure startDependencies is not resolved until after the plugin instance's start resolves. + await new Promise(resolve => setTimeout(resolve, 10)); + expect(startDependenciesResolved).toBe(false); + }, + }; + mockPluginInitializer.mockReturnValue(mockPluginInstance); + + await plugin.setup({} as any, {} as any); + + const startDependenciesCheck = plugin.startDependencies.then(resolvedStartDeps => { + startDependenciesResolved = true; + expect(resolvedStartDeps).toEqual([startContext, pluginDeps]); + }); + + await plugin.start(startContext, pluginDeps); + await startDependenciesCheck; +}); + test('`stop` fails if plugin is not set up', async () => { const manifest = createPluginManifest(); const opaqueId = Symbol(); diff --git a/src/core/server/plugins/plugin.ts b/src/core/server/plugins/plugin.ts index c0b484515ccce..d6c774f6fc41c 100644 --- a/src/core/server/plugins/plugin.ts +++ b/src/core/server/plugins/plugin.ts @@ -19,7 +19,8 @@ import { join } from 'path'; import typeDetect from 'type-detect'; - +import { Subject } from 'rxjs'; +import { first } from 'rxjs/operators'; import { Type } from '@kbn/config-schema'; import { Logger } from '../logging'; @@ -60,6 +61,9 @@ export class PluginWrapper< private instance?: Plugin; + private readonly startDependencies$ = new Subject<[CoreStart, TPluginsStart]>(); + public readonly startDependencies = this.startDependencies$.pipe(first()).toPromise(); + constructor( public readonly params: { readonly path: string; @@ -88,12 +92,12 @@ export class PluginWrapper< * @param plugins The dictionary where the key is the dependency name and the value * is the contract returned by the dependency's `setup` function. */ - public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { + public async setup(setupContext: CoreSetup, plugins: TPluginsSetup) { this.instance = this.createPluginInstance(); this.log.info('Setting up plugin'); - return await this.instance.setup(setupContext, plugins); + return this.instance.setup(setupContext, plugins); } /** @@ -108,7 +112,9 @@ export class PluginWrapper< throw new Error(`Plugin "${this.name}" can't be started since it isn't set up.`); } - return await this.instance.start(startContext, plugins); + const startContract = await this.instance.start(startContext, plugins); + this.startDependencies$.next([startContext, plugins]); + return startContract; } /** diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index 6d82a8d3ec6cf..f266172cb4bd9 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -176,6 +176,7 @@ export function createPluginSetupContext( uuid: { getInstanceUuid: deps.uuid.getInstanceUuid, }, + getStartServices: () => plugin.startDependencies, }; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 7e3fc2eb66001..6e41a4aefba30 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -552,13 +552,14 @@ export interface ContextSetup { export type CoreId = symbol; // @public -export interface CoreSetup { +export interface CoreSetup { // (undocumented) capabilities: CapabilitiesSetup; // (undocumented) context: ContextSetup; // (undocumented) elasticsearch: ElasticsearchServiceSetup; + getStartServices(): Promise<[CoreStart, TPluginsStart]>; // (undocumented) http: HttpServiceSetup; // (undocumented) diff --git a/x-pack/plugins/endpoint/server/plugin.test.ts b/x-pack/plugins/endpoint/server/plugin.test.ts index 87d373d3a4f34..7dd878d579043 100644 --- a/x-pack/plugins/endpoint/server/plugin.test.ts +++ b/x-pack/plugins/endpoint/server/plugin.test.ts @@ -3,14 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'kibana/server'; + import { EndpointPlugin, EndpointPluginSetupDependencies } from './plugin'; import { coreMock } from '../../../../src/core/server/mocks'; import { PluginSetupContract } from '../../features/server'; describe('test endpoint plugin', () => { let plugin: EndpointPlugin; - let mockCoreSetup: MockedKeys; + let mockCoreSetup: ReturnType; let mockedEndpointPluginSetupDependencies: jest.Mocked; let mockedPluginSetupContract: jest.Mocked; beforeEach(() => { diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 5e32a0e90198a..56aad4ece3e95 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -6,7 +6,7 @@ import { of } from 'rxjs'; import { ByteSizeValue } from '@kbn/config-schema'; -import { ICustomClusterClient, CoreSetup } from '../../../../src/core/server'; +import { ICustomClusterClient } from '../../../../src/core/server'; import { elasticsearchClientPlugin } from './elasticsearch_client_plugin'; import { Plugin, PluginSetupDependencies } from './plugin'; @@ -14,7 +14,7 @@ import { coreMock, elasticsearchServiceMock } from '../../../../src/core/server/ describe('Security Plugin', () => { let plugin: Plugin; - let mockCoreSetup: MockedKeys; + let mockCoreSetup: ReturnType; let mockClusterClient: jest.Mocked; let mockDependencies: PluginSetupDependencies; beforeEach(() => { From 940b53399ecedd96845f9d249276be593e4f3e03 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Mon, 20 Jan 2020 11:40:53 +0100 Subject: [PATCH 13/39] [State Management] Remove AppState from edit_index_pattern page (#54104) Replaces AppState in edit_index_pattern with state containers and state syncing utils. In addition makes tab navigation on edit_index_pattern work with browser's undo history Co-authored-by: Elastic Machine --- .../edit_index_pattern.html | 8 +- .../edit_index_pattern/edit_index_pattern.js | 280 ++++++++++-------- .../edit_index_pattern_state_container.ts | 89 ++++++ 3 files changed, 243 insertions(+), 134 deletions(-) create mode 100644 src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern_state_container.ts diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html index 4b3014fd28a51..625227be3c2d2 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/edit_index_pattern.html @@ -83,8 +83,8 @@ - - - - - - diff --git a/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.js b/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.js deleted file mode 100644 index d9aa59f6df142..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/change_password_form/change_password_form.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { uiModules } from 'ui/modules'; -import template from './change_password_form.html'; - -const module = uiModules.get('security', ['kibana']); -module.directive('kbnChangePasswordForm', function() { - return { - template, - scope: { - requireCurrentPassword: '=', - showKibanaWarning: '=', - onChangePassword: '&', - }, - restrict: 'E', - replace: true, - controllerAs: 'changePasswordController', - controller: function($scope) { - this.currentPassword = null; - this.newPassword = null; - this.newPasswordConfirmation = null; - this.isFormVisible = false; - this.isIncorrectPassword = false; - - this.showForm = () => { - this.isFormVisible = true; - }; - - this.hideForm = () => { - $scope.changePasswordForm.$setPristine(); - $scope.changePasswordForm.$setUntouched(); - this.currentPassword = null; - this.newPassword = null; - this.newPasswordConfirmation = null; - this.isFormVisible = false; - this.isIncorrectPassword = false; - }; - - this.onIncorrectPassword = () => { - this.isIncorrectPassword = true; - }; - }, - }; -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/_index.scss b/x-pack/legacy/plugins/security/public/views/management/edit_role/_index.scss deleted file mode 100644 index 192091fb04e3c..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './components/index'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/_index.scss b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/_index.scss deleted file mode 100644 index 32b3832e7a9fa..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/_index.scss +++ /dev/null @@ -1,9 +0,0 @@ -@import './collapsible_panel/collapsible_panel'; -@import './privileges/kibana/space_aware_privilege_section/index'; -@import './privileges/kibana/feature_table/index'; -@import './spaces_popover_list/spaces_popover_list'; - -.secPrivilegeFeatureIcon { - flex-shrink: 0; - margin-right: $euiSizeS; -} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx deleted file mode 100644 index 67c32c8393171..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.test.tsx +++ /dev/null @@ -1,716 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ReactWrapper } from 'enzyme'; -import React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { UICapabilities } from 'ui/capabilities'; -import { Space } from '../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../plugins/features/public'; -// These modules should be moved into a common directory -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { Actions } from '../../../../../../../../plugins/security/server/authorization/actions'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { privilegesFactory } from '../../../../../../../../plugins/security/server/authorization/privileges'; -import { RawKibanaPrivileges, Role } from '../../../../../common/model'; -import { EditRolePage } from './edit_role_page'; -import { SimplePrivilegeSection } from './privileges/kibana/simple_privilege_section'; -import { SpaceAwarePrivilegeSection } from './privileges/kibana/space_aware_privilege_section'; -import { TransformErrorSection } from './privileges/kibana/transform_error_section'; - -const buildFeatures = () => { - return [ - { - id: 'feature1', - name: 'Feature 1', - icon: 'addDataApp', - app: ['feature1App'], - privileges: { - all: { - app: ['feature1App'], - ui: ['feature1-ui'], - savedObject: { - all: [], - read: [], - }, - }, - }, - }, - { - id: 'feature2', - name: 'Feature 2', - icon: 'addDataApp', - app: ['feature2App'], - privileges: { - all: { - app: ['feature2App'], - ui: ['feature2-ui'], - savedObject: { - all: ['feature2'], - read: ['config'], - }, - }, - }, - }, - ] as Feature[]; -}; - -const buildRawKibanaPrivileges = () => { - return privilegesFactory(new Actions('unit_test_version'), { - getFeatures: () => buildFeatures(), - }).get(); -}; - -const buildBuiltinESPrivileges = () => { - return { - cluster: ['all', 'manage', 'monitor'], - index: ['all', 'read', 'write', 'index'], - }; -}; - -const buildUICapabilities = (canManageSpaces = true) => { - return { - catalogue: {}, - management: {}, - navLinks: {}, - spaces: { - manage: canManageSpaces, - }, - } as UICapabilities; -}; - -const buildSpaces = () => { - return [ - { - id: 'default', - name: 'Default', - disabledFeatures: [], - _reserved: true, - }, - { - id: 'space_1', - name: 'Space 1', - disabledFeatures: [], - }, - { - id: 'space_2', - name: 'Space 2', - disabledFeatures: ['feature2'], - }, - ] as Space[]; -}; - -const expectReadOnlyFormButtons = (wrapper: ReactWrapper) => { - expect(wrapper.find('button[data-test-subj="roleFormReturnButton"]')).toHaveLength(1); - expect(wrapper.find('button[data-test-subj="roleFormSaveButton"]')).toHaveLength(0); -}; - -const expectSaveFormButtons = (wrapper: ReactWrapper) => { - expect(wrapper.find('button[data-test-subj="roleFormReturnButton"]')).toHaveLength(0); - expect(wrapper.find('button[data-test-subj="roleFormSaveButton"]')).toHaveLength(1); -}; - -describe('', () => { - describe('with spaces enabled', () => { - it('can render a reserved role', () => { - const role: Role = { - name: 'superuser', - metadata: { - _reserved: true, - }, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1); - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectReadOnlyFormButtons(wrapper); - }); - - it('can render a user defined role', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectSaveFormButtons(wrapper); - }); - - it('can render when creating a new role', () => { - // @ts-ignore - const role: Role = { - metadata: {}, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectSaveFormButtons(wrapper); - }); - - it('can render when cloning an existing role', () => { - const role: Role = { - metadata: { - _reserved: false, - }, - name: '', - elasticsearch: { - cluster: ['all', 'manage'], - indices: [ - { - names: ['foo*'], - privileges: ['all'], - field_security: { - except: ['f'], - grant: ['b*'], - }, - }, - ], - run_as: ['elastic'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectSaveFormButtons(wrapper); - }); - - it('renders an auth error when not authorized to manage spaces', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(false); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); - - expect( - wrapper.find('EuiCallOut[data-test-subj="userCannotManageSpacesCallout"]') - ).toHaveLength(1); - - expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); - expectSaveFormButtons(wrapper); - }); - - it('renders a partial read-only view when there is a transform error', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [], - _transform_error: ['kibana'], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const spaces: Space[] = buildSpaces(); - const uiCapabilities: UICapabilities = buildUICapabilities(false); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(TransformErrorSection)).toHaveLength(1); - expectReadOnlyFormButtons(wrapper); - }); - }); - - describe('with spaces disabled', () => { - it('can render a reserved role', () => { - const role: Role = { - name: 'superuser', - metadata: { - _reserved: true, - }, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1); - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectReadOnlyFormButtons(wrapper); - }); - - it('can render a user defined role', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); - expectSaveFormButtons(wrapper); - }); - - it('can render when creating a new role', () => { - // @ts-ignore - const role: Role = { - metadata: {}, - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expectSaveFormButtons(wrapper); - }); - - it('can render when cloning an existing role', () => { - const role: Role = { - metadata: { - _reserved: false, - }, - name: '', - elasticsearch: { - cluster: ['all', 'manage'], - indices: [ - { - names: ['foo*'], - privileges: ['all'], - field_security: { - except: ['f'], - grant: ['b*'], - }, - }, - ], - run_as: ['elastic'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expectSaveFormButtons(wrapper); - }); - - it('does not care if user cannot manage spaces', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [ - { - spaces: ['*'], - base: ['all'], - feature: {}, - }, - ], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(false); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); - - expect( - wrapper.find('EuiCallOut[data-test-subj="userCannotManageSpacesCallout"]') - ).toHaveLength(0); - - expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); - expectSaveFormButtons(wrapper); - }); - - it('renders a partial read-only view when there is a transform error', () => { - const role: Role = { - name: 'my custom role', - metadata: {}, - elasticsearch: { - cluster: ['all'], - indices: [], - run_as: ['*'], - }, - kibana: [], - _transform_error: ['kibana'], - }; - - const features: Feature[] = buildFeatures(); - const mockHttpClient = jest.fn(); - const indexPatterns: string[] = ['foo*', 'bar*']; - const kibanaPrivileges: RawKibanaPrivileges = buildRawKibanaPrivileges(); - const builtinESPrivileges = buildBuiltinESPrivileges(); - const uiCapabilities: UICapabilities = buildUICapabilities(false); - - const wrapper = mountWithIntl( - - ); - - expect(wrapper.find(TransformErrorSection)).toHaveLength(1); - expectReadOnlyFormButtons(wrapper); - }); - }); -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx deleted file mode 100644 index 2ba012afa689d..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/edit_role_page.tsx +++ /dev/null @@ -1,409 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiButton, - EuiButtonEmpty, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiForm, - EuiFormRow, - EuiPanel, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import { get } from 'lodash'; -import React, { ChangeEvent, Component, Fragment, HTMLProps } from 'react'; -import { UICapabilities } from 'ui/capabilities'; -import { toastNotifications } from 'ui/notify'; -import { Space } from '../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../plugins/features/public'; -import { - KibanaPrivileges, - RawKibanaPrivileges, - Role, - BuiltinESPrivileges, -} from '../../../../../common/model'; -import { - isReadOnlyRole, - isReservedRole, - copyRole, - prepareRoleClone, -} from '../../../../lib/role_utils'; -import { deleteRole, saveRole } from '../../../../objects'; -import { ROLES_PATH } from '../../management_urls'; -import { RoleValidationResult, RoleValidator } from '../lib/validate_role'; -import { DeleteRoleButton } from './delete_role_button'; -import { ElasticsearchPrivileges, KibanaPrivilegesRegion } from './privileges'; -import { ReservedRoleBadge } from './reserved_role_badge'; - -interface Props { - action: 'edit' | 'clone'; - role: Role; - runAsUsers: string[]; - indexPatterns: string[]; - httpClient: any; - allowDocumentLevelSecurity: boolean; - allowFieldLevelSecurity: boolean; - kibanaPrivileges: RawKibanaPrivileges; - builtinESPrivileges: BuiltinESPrivileges; - spaces?: Space[]; - spacesEnabled: boolean; - intl: InjectedIntl; - uiCapabilities: UICapabilities; - features: Feature[]; -} - -interface State { - role: Role; - formError: RoleValidationResult | null; -} - -class EditRolePageUI extends Component { - private validator: RoleValidator; - - constructor(props: Props) { - super(props); - - this.validator = new RoleValidator({ shouldValidate: false }); - - let role: Role; - if (props.action === 'clone') { - role = prepareRoleClone(props.role); - } else { - role = copyRole(props.role); - } - - this.state = { - role, - formError: null, - }; - } - - public componentDidMount() { - if (this.props.action === 'clone' && isReservedRole(this.props.role)) { - this.backToRoleList(); - } - } - - public render() { - const description = this.props.spacesEnabled ? ( - - ) : ( - - ); - - return ( -
- - {this.getFormTitle()} - - - - {description} - - {isReservedRole(this.state.role) && ( - - - -

- -

-
-
- )} - - - - {this.getRoleName()} - - {this.getElasticsearchPrivileges()} - - {this.getKibanaPrivileges()} - - - - {this.getFormButtons()} -
-
- ); - } - - private getFormTitle = () => { - let titleText; - const props: HTMLProps = { - tabIndex: 0, - }; - if (isReservedRole(this.state.role)) { - titleText = ( - - ); - props['aria-describedby'] = 'reservedRoleDescription'; - } else if (this.editingExistingRole()) { - titleText = ( - - ); - } else { - titleText = ( - - ); - } - - return ( - -

- {titleText} -

-
- ); - }; - - private getActionButton = () => { - if (this.editingExistingRole() && !isReadOnlyRole(this.state.role)) { - return ( - - - - ); - } - - return null; - }; - - private getRoleName = () => { - return ( - - - } - helpText={ - !isReservedRole(this.state.role) && this.editingExistingRole() ? ( - - ) : ( - undefined - ) - } - {...this.validator.validateRoleName(this.state.role)} - > - - - - ); - }; - - private onNameChange = (e: ChangeEvent) => { - const rawValue = e.target.value; - const name = rawValue.replace(/\s/g, '_'); - - this.setState({ - role: { - ...this.state.role, - name, - }, - }); - }; - - private getElasticsearchPrivileges() { - return ( -
- - -
- ); - } - - private onRoleChange = (role: Role) => { - this.setState({ - role, - }); - }; - - private getKibanaPrivileges = () => { - return ( -
- - -
- ); - }; - - private getFormButtons = () => { - if (isReadOnlyRole(this.state.role)) { - return this.getReturnToRoleListButton(); - } - - return ( - - {this.getSaveButton()} - {this.getCancelButton()} - - {this.getActionButton()} - - ); - }; - - private getReturnToRoleListButton = () => { - return ( - - - - ); - }; - - private getSaveButton = () => { - const saveText = this.editingExistingRole() ? ( - - ) : ( - - ); - - return ( - - {saveText} - - ); - }; - - private getCancelButton = () => { - return ( - - - - ); - }; - - private editingExistingRole = () => { - return !!this.props.role.name && this.props.action === 'edit'; - }; - - private saveRole = () => { - this.validator.enableValidation(); - - const result = this.validator.validateForSave(this.state.role); - if (result.isInvalid) { - this.setState({ - formError: result, - }); - } else { - this.setState({ - formError: null, - }); - - const { httpClient, intl, spacesEnabled } = this.props; - - saveRole(httpClient, this.state.role, spacesEnabled) - .then(() => { - toastNotifications.addSuccess( - intl.formatMessage({ - id: 'xpack.security.management.editRole.roleSuccessfullySavedNotificationMessage', - defaultMessage: 'Saved role', - }) - ); - this.backToRoleList(); - }) - .catch((error: any) => { - toastNotifications.addDanger(get(error, 'data.message')); - }); - } - }; - - private handleDeleteRole = () => { - const { httpClient, role, intl } = this.props; - - deleteRole(httpClient, role.name) - .then(() => { - toastNotifications.addSuccess( - intl.formatMessage({ - id: 'xpack.security.management.editRole.roleSuccessfullyDeletedNotificationMessage', - defaultMessage: 'Deleted role', - }) - ); - this.backToRoleList(); - }) - .catch((error: any) => { - toastNotifications.addDanger(get(error, 'data.message')); - }); - }; - - private backToRoleList = () => { - window.location.hash = ROLES_PATH; - }; -} - -export const EditRolePage = injectI18n(EditRolePageUI); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.test.tsx b/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.test.tsx deleted file mode 100644 index 5ba3d1daf61ac..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.test.tsx +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { RoleValidator } from '../../../lib/validate_role'; -import { ClusterPrivileges } from './cluster_privileges'; -import { ElasticsearchPrivileges } from './elasticsearch_privileges'; -import { IndexPrivileges } from './index_privileges'; - -test('it renders without crashing', () => { - const props = { - role: { - name: '', - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }, - editable: true, - httpClient: jest.fn(), - onChange: jest.fn(), - runAsUsers: [], - indexPatterns: [], - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, - validator: new RoleValidator(), - builtinESPrivileges: { - cluster: ['all', 'manage', 'monitor'], - index: ['all', 'read', 'write', 'index'], - }, - }; - const wrapper = shallowWithIntl(); - expect(wrapper).toMatchSnapshot(); -}); - -test('it renders ClusterPrivileges', () => { - const props = { - role: { - name: '', - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }, - editable: true, - httpClient: jest.fn(), - onChange: jest.fn(), - runAsUsers: [], - indexPatterns: [], - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, - validator: new RoleValidator(), - builtinESPrivileges: { - cluster: ['all', 'manage', 'monitor'], - index: ['all', 'read', 'write', 'index'], - }, - }; - const wrapper = mountWithIntl(); - expect(wrapper.find(ClusterPrivileges)).toHaveLength(1); -}); - -test('it renders IndexPrivileges', () => { - const props = { - role: { - name: '', - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - }, - editable: true, - httpClient: jest.fn(), - onChange: jest.fn(), - runAsUsers: [], - indexPatterns: [], - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, - validator: new RoleValidator(), - builtinESPrivileges: { - cluster: ['all', 'manage', 'monitor'], - index: ['all', 'read', 'write', 'index'], - }, - }; - const wrapper = mountWithIntl(); - expect(wrapper.find(IndexPrivileges)).toHaveLength(1); -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/edit_role.html b/x-pack/legacy/plugins/security/public/views/management/edit_role/edit_role.html deleted file mode 100644 index ca4073dcad6f5..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/edit_role.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js b/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js deleted file mode 100644 index 27c9beb4ba828..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/index.js +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import _ from 'lodash'; -import routes from 'ui/routes'; -import { capabilities } from 'ui/capabilities'; -import { kfetch } from 'ui/kfetch'; -import { fatalError, toastNotifications } from 'ui/notify'; -import { npStart } from 'ui/new_platform'; -import template from 'plugins/security/views/management/edit_role/edit_role.html'; -import 'plugins/security/services/shield_role'; -import 'plugins/security/services/shield_indices'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { UserAPIClient } from '../../../lib/api'; -import { ROLES_PATH, CLONE_ROLES_PATH, EDIT_ROLES_PATH } from '../management_urls'; -import { getEditRoleBreadcrumbs, getCreateRoleBreadcrumbs } from '../breadcrumbs'; - -import { EditRolePage } from './components'; - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { I18nContext } from 'ui/i18n'; -import { i18n } from '@kbn/i18n'; - -const routeDefinition = action => ({ - template, - k7Breadcrumbs: ($injector, $route) => - $injector.invoke( - action === 'edit' && $route.current.params.name - ? getEditRoleBreadcrumbs - : getCreateRoleBreadcrumbs - ), - resolve: { - role($route, ShieldRole, Promise, kbnUrl) { - const name = $route.current.params.name; - - let role; - - if (name != null) { - role = ShieldRole.get({ name }).$promise.catch(response => { - if (response.status === 404) { - toastNotifications.addDanger({ - title: i18n.translate('xpack.security.management.roles.roleNotFound', { - defaultMessage: 'No "{roleName}" role found.', - values: { roleName: name }, - }), - }); - kbnUrl.redirect(ROLES_PATH); - } else { - return fatalError(response); - } - }); - } else { - role = Promise.resolve( - new ShieldRole({ - elasticsearch: { - cluster: [], - indices: [], - run_as: [], - }, - kibana: [], - _unrecognized_applications: [], - }) - ); - } - - return role.then(res => res.toJSON()); - }, - users() { - return new UserAPIClient().getUsers().then(users => _.map(users, 'username')); - }, - indexPatterns() { - return npStart.plugins.data.indexPatterns.getTitles(); - }, - spaces(spacesEnabled) { - if (spacesEnabled) { - return kfetch({ method: 'get', pathname: '/api/spaces/space' }); - } - return []; - }, - kibanaPrivileges() { - return kfetch({ - method: 'get', - pathname: '/api/security/privileges', - query: { includeActions: true }, - }); - }, - builtinESPrivileges() { - return kfetch({ method: 'get', pathname: '/internal/security/esPrivileges/builtin' }); - }, - features() { - return kfetch({ method: 'get', pathname: '/api/features' }).catch(e => { - // TODO: This check can be removed once all of these `resolve` entries are moved out of Angular and into the React app. - const unauthorizedForFeatures = _.get(e, 'body.statusCode') === 404; - if (unauthorizedForFeatures) { - return []; - } - throw e; - }); - }, - }, - controllerAs: 'editRole', - controller($injector, $scope, $http, enableSpaceAwarePrivileges) { - const $route = $injector.get('$route'); - const role = $route.current.locals.role; - - const allowDocumentLevelSecurity = xpackInfo.get( - 'features.security.allowRoleDocumentLevelSecurity' - ); - const allowFieldLevelSecurity = xpackInfo.get('features.security.allowRoleFieldLevelSecurity'); - if (role.elasticsearch.indices.length === 0) { - const emptyOption = { - names: [], - privileges: [], - }; - - if (allowFieldLevelSecurity) { - emptyOption.field_security = { - grant: ['*'], - except: [], - }; - } - - if (allowDocumentLevelSecurity) { - emptyOption.query = ''; - } - - role.elasticsearch.indices.push(emptyOption); - } - - const { - users, - indexPatterns, - spaces, - kibanaPrivileges, - builtinESPrivileges, - features, - } = $route.current.locals; - - $scope.$$postDigest(async () => { - const domNode = document.getElementById('editRoleReactRoot'); - - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); - }); - }, -}); - -routes.when(`${CLONE_ROLES_PATH}/:name`, routeDefinition('clone')); -routes.when(`${EDIT_ROLES_PATH}/:name?`, routeDefinition('edit')); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/_index.scss b/x-pack/legacy/plugins/security/public/views/management/edit_user/_index.scss deleted file mode 100644 index c5da74aa3f785..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './users'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.html b/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.html deleted file mode 100644 index 4fa2768480874..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.js b/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.js deleted file mode 100644 index ab218022c6ee6..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/edit_user.js +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import routes from 'ui/routes'; -import template from 'plugins/security/views/management/edit_user/edit_user.html'; -import 'angular-resource'; -import 'ui/angular_ui_select'; -import 'plugins/security/services/shield_role'; -import { EDIT_USERS_PATH } from '../management_urls'; -import { EditUserPage } from './components'; -import { UserAPIClient } from '../../../lib/api'; -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import { I18nContext } from 'ui/i18n'; -import { npSetup } from 'ui/new_platform'; -import { getEditUserBreadcrumbs, getCreateUserBreadcrumbs } from '../breadcrumbs'; - -const renderReact = (elem, changeUrl, username) => { - render( - - - , - elem - ); -}; - -routes.when(`${EDIT_USERS_PATH}/:username?`, { - template, - k7Breadcrumbs: ($injector, $route) => - $injector.invoke( - $route.current.params.username ? getEditUserBreadcrumbs : getCreateUserBreadcrumbs - ), - controllerAs: 'editUser', - controller($scope, $route, kbnUrl) { - $scope.$on('$destroy', () => { - const elem = document.getElementById('editUserReactRoot'); - if (elem) { - unmountComponentAtNode(elem); - } - }); - $scope.$$postDigest(() => { - const elem = document.getElementById('editUserReactRoot'); - const username = $route.current.params.username; - const changeUrl = url => { - kbnUrl.change(url); - $scope.$apply(); - }; - renderReact(elem, changeUrl, username); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/management.js b/x-pack/legacy/plugins/security/public/views/management/management.js deleted file mode 100644 index f0369f232aeba..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/management.js +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import 'plugins/security/views/management/change_password_form/change_password_form'; -import 'plugins/security/views/management/password_form/password_form'; -import 'plugins/security/views/management/users_grid/users'; -import 'plugins/security/views/management/roles_grid/roles'; -import 'plugins/security/views/management/api_keys_grid/api_keys'; -import 'plugins/security/views/management/edit_user/edit_user'; -import 'plugins/security/views/management/edit_role/index'; -import 'plugins/security/views/management/role_mappings/role_mappings_grid'; -import 'plugins/security/views/management/role_mappings/edit_role_mapping'; -import routes from 'ui/routes'; -import { xpackInfo } from 'plugins/xpack_main/services/xpack_info'; -import { ROLES_PATH, USERS_PATH, API_KEYS_PATH, ROLE_MAPPINGS_PATH } from './management_urls'; - -import { management } from 'ui/management'; -import { npSetup } from 'ui/new_platform'; -import { i18n } from '@kbn/i18n'; -import { toastNotifications } from 'ui/notify'; - -routes - .defaults(/^\/management\/security(\/|$)/, { - resolve: { - showLinks(kbnUrl, Promise) { - if (!xpackInfo.get('features.security.showLinks')) { - toastNotifications.addDanger({ - title: xpackInfo.get('features.security.linksMessage'), - }); - kbnUrl.redirect('/management'); - return Promise.halt(); - } - }, - }, - }) - .defaults(/\/management/, { - resolve: { - securityManagementSection: function() { - const showSecurityLinks = xpackInfo.get('features.security.showLinks'); - const showRoleMappingsManagementLink = xpackInfo.get( - 'features.security.showRoleMappingsManagement' - ); - - function deregisterSecurity() { - management.deregister('security'); - } - - function deregisterRoleMappingsManagement() { - if (management.hasItem('security')) { - const security = management.getSection('security'); - if (security.hasItem('roleMappings')) { - security.deregister('roleMappings'); - } - } - } - - function ensureSecurityRegistered() { - const registerSecurity = () => - management.register('security', { - display: i18n.translate('xpack.security.management.securityTitle', { - defaultMessage: 'Security', - }), - order: 100, - icon: 'securityApp', - }); - const getSecurity = () => management.getSection('security'); - - const security = management.hasItem('security') ? getSecurity() : registerSecurity(); - - if (!security.hasItem('users')) { - security.register('users', { - name: 'securityUsersLink', - order: 10, - display: i18n.translate('xpack.security.management.usersTitle', { - defaultMessage: 'Users', - }), - url: `#${USERS_PATH}`, - }); - } - - if (!security.hasItem('roles')) { - security.register('roles', { - name: 'securityRolesLink', - order: 20, - display: i18n.translate('xpack.security.management.rolesTitle', { - defaultMessage: 'Roles', - }), - url: `#${ROLES_PATH}`, - }); - } - - if (!security.hasItem('apiKeys')) { - security.register('apiKeys', { - name: 'securityApiKeysLink', - order: 30, - display: i18n.translate('xpack.security.management.apiKeysTitle', { - defaultMessage: 'API Keys', - }), - url: `#${API_KEYS_PATH}`, - }); - } - - if (showRoleMappingsManagementLink && !security.hasItem('roleMappings')) { - security.register('roleMappings', { - name: 'securityRoleMappingLink', - order: 30, - display: i18n.translate('xpack.security.management.roleMappingsTitle', { - defaultMessage: 'Role Mappings', - }), - url: `#${ROLE_MAPPINGS_PATH}`, - }); - } - } - - if (!showSecurityLinks) { - deregisterSecurity(); - } else { - if (!showRoleMappingsManagementLink) { - deregisterRoleMappingsManagement(); - } - - // getCurrentUser will reject if there is no authenticated user, so we prevent them from - // seeing the security management screens. - return npSetup.plugins.security.authc - .getCurrentUser() - .then(ensureSecurityRegistered) - .catch(deregisterSecurity); - } - }, - }, - }); diff --git a/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.html b/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.html deleted file mode 100644 index 72956992100f5..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.html +++ /dev/null @@ -1,53 +0,0 @@ - - -
- - - - -
-
- - -
- - - - -
-
-
diff --git a/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.js b/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.js deleted file mode 100644 index edcccdb5e6e69..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/password_form/password_form.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { uiModules } from 'ui/modules'; -import template from './password_form.html'; - -const module = uiModules.get('security', ['kibana']); -module.directive('kbnPasswordForm', function() { - return { - template, - scope: { - password: '=', - }, - restrict: 'E', - replace: true, - controllerAs: 'passwordController', - controller: function() { - this.confirmation = null; - }, - }; -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/_index.scss b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/_index.scss deleted file mode 100644 index 80e08ebcf1226..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import './components/rule_editor_panel/index'; \ No newline at end of file diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.test.tsx deleted file mode 100644 index 375a8d9f374a8..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.test.tsx +++ /dev/null @@ -1,341 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { findTestSubject } from 'test_utils/find_test_subject'; - -// brace/ace uses the Worker class, which is not currently provided by JSDOM. -// This is not required for the tests to pass, but it rather suppresses lengthy -// warnings in the console which adds unnecessary noise to the test output. -import 'test_utils/stub_web_worker'; - -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; -import { EditRoleMappingPage } from '.'; -import { NoCompatibleRealms, SectionLoading, PermissionDenied } from '../../components'; -import { VisualRuleEditor } from './rule_editor_panel/visual_rule_editor'; -import { JSONRuleEditor } from './rule_editor_panel/json_rule_editor'; -import { EuiComboBox } from '@elastic/eui'; - -jest.mock('../../../../../lib/roles_api', () => { - return { - RolesApi: { - getRoles: () => Promise.resolve([{ name: 'foo_role' }, { name: 'bar role' }]), - }, - }; -}); - -describe('EditRoleMappingPage', () => { - it('allows a role mapping to be created', async () => { - const roleMappingsAPI = ({ - saveRoleMapping: jest.fn().mockResolvedValue(null), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - - await nextTick(); - wrapper.update(); - - findTestSubject(wrapper, 'roleMappingFormNameInput').simulate('change', { - target: { value: 'my-role-mapping' }, - }); - - (wrapper - .find(EuiComboBox) - .filter('[data-test-subj="roleMappingFormRoleComboBox"]') - .props() as any).onChange([{ label: 'foo_role' }]); - - findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); - - findTestSubject(wrapper, 'saveRoleMappingButton').simulate('click'); - - expect(roleMappingsAPI.saveRoleMapping).toHaveBeenCalledWith({ - name: 'my-role-mapping', - enabled: true, - roles: ['foo_role'], - role_templates: [], - rules: { - all: [{ field: { username: '*' } }], - }, - metadata: {}, - }); - }); - - it('allows a role mapping to be updated', async () => { - const roleMappingsAPI = ({ - saveRoleMapping: jest.fn().mockResolvedValue(null), - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - role_templates: [ - { - template: { id: 'foo' }, - }, - ], - enabled: true, - rules: { - any: [{ field: { 'metadata.someCustomOption': [false, true, 'asdf'] } }], - }, - metadata: { - foo: 'bar', - bar: 'baz', - }, - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - await nextTick(); - wrapper.update(); - - findTestSubject(wrapper, 'switchToRolesButton').simulate('click'); - - (wrapper - .find(EuiComboBox) - .filter('[data-test-subj="roleMappingFormRoleComboBox"]') - .props() as any).onChange([{ label: 'foo_role' }]); - - findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); - wrapper.find('button[id="addRuleOption"]').simulate('click'); - - findTestSubject(wrapper, 'saveRoleMappingButton').simulate('click'); - - expect(roleMappingsAPI.saveRoleMapping).toHaveBeenCalledWith({ - name: 'foo', - enabled: true, - roles: ['foo_role'], - role_templates: [], - rules: { - any: [ - { field: { 'metadata.someCustomOption': [false, true, 'asdf'] } }, - { field: { username: '*' } }, - ], - }, - metadata: { - foo: 'bar', - bar: 'baz', - }, - }); - }); - - it('renders a permission denied message when unauthorized to manage role mappings', async () => { - const roleMappingsAPI = ({ - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: false, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(PermissionDenied)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - expect(wrapper.find(PermissionDenied)).toHaveLength(1); - }); - - it('renders a warning when there are no compatible realms enabled', async () => { - const roleMappingsAPI = ({ - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: false, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); - }); - - it('renders a warning when editing a mapping with a stored role template, when stored scripts are disabled', async () => { - const roleMappingsAPI = ({ - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - role_templates: [ - { - template: { id: 'foo' }, - }, - ], - enabled: true, - rules: { - field: { username: '*' }, - }, - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: false, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); - expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); - expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(1); - }); - - it('renders a warning when editing a mapping with an inline role template, when inline scripts are disabled', async () => { - const roleMappingsAPI = ({ - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - role_templates: [ - { - template: { source: 'foo' }, - }, - ], - enabled: true, - rules: { - field: { username: '*' }, - }, - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: false, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); - expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(1); - expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); - }); - - it('renders the visual editor by default for simple rule sets', async () => { - const roleMappingsAPI = ({ - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - roles: ['superuser'], - enabled: true, - rules: { - all: [ - { - field: { - username: '*', - }, - }, - { - field: { - dn: null, - }, - }, - { - field: { - realm: ['ldap', 'pki', null, 12], - }, - }, - ], - }, - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); - expect(wrapper.find(JSONRuleEditor)).toHaveLength(0); - }); - - it('renders the JSON editor by default for complex rule sets', async () => { - const createRule = (depth: number): Record => { - if (depth > 0) { - const rule = { - all: [ - { - field: { - username: '*', - }, - }, - ], - } as Record; - - const subRule = createRule(depth - 1); - if (subRule) { - rule.all.push(subRule); - } - - return rule; - } - return null as any; - }; - - const roleMappingsAPI = ({ - getRoleMapping: jest.fn().mockResolvedValue({ - name: 'foo', - roles: ['superuser'], - enabled: true, - rules: createRule(10), - }), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - canUseInlineScripts: true, - canUseStoredScripts: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl( - - ); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(VisualRuleEditor)).toHaveLength(0); - expect(wrapper.find(JSONRuleEditor)).toHaveLength(1); - }); -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/edit_role_mapping.html b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/edit_role_mapping.html deleted file mode 100644 index ca8ab9c35c49b..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/edit_role_mapping.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/index.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/index.tsx deleted file mode 100644 index b064a4dc50a22..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/index.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import routes from 'ui/routes'; -import { I18nContext } from 'ui/i18n'; -import { npSetup } from 'ui/new_platform'; -import { RoleMappingsAPI } from '../../../../lib/role_mappings_api'; -// @ts-ignore -import template from './edit_role_mapping.html'; -import { CREATE_ROLE_MAPPING_PATH } from '../../management_urls'; -import { getEditRoleMappingBreadcrumbs } from '../../breadcrumbs'; -import { EditRoleMappingPage } from './components'; - -routes.when(`${CREATE_ROLE_MAPPING_PATH}/:name?`, { - template, - k7Breadcrumbs: getEditRoleMappingBreadcrumbs, - controller($scope, $route) { - $scope.$$postDigest(() => { - const domNode = document.getElementById('editRoleMappingReactRoot'); - - const { name } = $route.current.params; - - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - if (domNode) { - unmountComponentAtNode(domNode); - } - }); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.test.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.test.tsx deleted file mode 100644 index 259cdc71e25a2..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.test.tsx +++ /dev/null @@ -1,182 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { RoleMappingsGridPage } from '.'; -import { SectionLoading, PermissionDenied, NoCompatibleRealms } from '../../components'; -import { EmptyPrompt } from './empty_prompt'; -import { findTestSubject } from 'test_utils/find_test_subject'; -import { EuiLink } from '@elastic/eui'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; -import { act } from '@testing-library/react'; - -describe('RoleMappingsGridPage', () => { - it('renders an empty prompt when no role mappings exist', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(EmptyPrompt)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - expect(wrapper.find(EmptyPrompt)).toHaveLength(1); - }); - - it('renders a permission denied message when unauthorized to manage role mappings', async () => { - const roleMappingsAPI = ({ - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: false, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(PermissionDenied)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - expect(wrapper.find(PermissionDenied)).toHaveLength(1); - }); - - it('renders a warning when there are no compatible realms enabled', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([ - { - name: 'some realm', - enabled: true, - roles: [], - rules: { field: { username: '*' } }, - }, - ]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: false, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - expect(wrapper.find(SectionLoading)).toHaveLength(1); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); - - await nextTick(); - wrapper.update(); - - expect(wrapper.find(SectionLoading)).toHaveLength(0); - expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); - }); - - it('renders links to mapped roles', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([ - { - name: 'some realm', - enabled: true, - roles: ['superuser'], - rules: { field: { username: '*' } }, - }, - ]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - await nextTick(); - wrapper.update(); - - const links = findTestSubject(wrapper, 'roleMappingRoles').find(EuiLink); - expect(links).toHaveLength(1); - expect(links.at(0).props()).toMatchObject({ - href: '#/management/security/roles/edit/superuser', - }); - }); - - it('describes the number of mapped role templates', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([ - { - name: 'some realm', - enabled: true, - role_templates: [{}, {}], - rules: { field: { username: '*' } }, - }, - ]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - }), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - await nextTick(); - wrapper.update(); - - const templates = findTestSubject(wrapper, 'roleMappingRoles'); - expect(templates).toHaveLength(1); - expect(templates.text()).toEqual(`2 role templates defined`); - }); - - it('allows role mappings to be deleted, refreshing the grid after', async () => { - const roleMappingsAPI = ({ - getRoleMappings: jest.fn().mockResolvedValue([ - { - name: 'some-realm', - enabled: true, - roles: ['superuser'], - rules: { field: { username: '*' } }, - }, - ]), - checkRoleMappingFeatures: jest.fn().mockResolvedValue({ - canManageRoleMappings: true, - hasCompatibleRealms: true, - }), - deleteRoleMappings: jest.fn().mockReturnValue( - Promise.resolve([ - { - name: 'some-realm', - success: true, - }, - ]) - ), - } as unknown) as RoleMappingsAPI; - - const wrapper = mountWithIntl(); - await nextTick(); - wrapper.update(); - - expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(1); - expect(roleMappingsAPI.deleteRoleMappings).not.toHaveBeenCalled(); - - findTestSubject(wrapper, `deleteRoleMappingButton-some-realm`).simulate('click'); - expect(findTestSubject(wrapper, 'deleteRoleMappingConfirmationModal')).toHaveLength(1); - - await act(async () => { - findTestSubject(wrapper, 'confirmModalConfirmButton').simulate('click'); - await nextTick(); - wrapper.update(); - }); - - expect(roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith(['some-realm']); - // Expect an additional API call to refresh the grid - expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(2); - }); -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/index.tsx b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/index.tsx deleted file mode 100644 index 9e925d0fa6dc0..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import routes from 'ui/routes'; -import { I18nContext } from 'ui/i18n'; -import { npSetup } from 'ui/new_platform'; -import { RoleMappingsAPI } from '../../../../lib/role_mappings_api'; -// @ts-ignore -import template from './role_mappings.html'; -import { ROLE_MAPPINGS_PATH } from '../../management_urls'; -import { getRoleMappingBreadcrumbs } from '../../breadcrumbs'; -import { RoleMappingsGridPage } from './components'; - -routes.when(ROLE_MAPPINGS_PATH, { - template, - k7Breadcrumbs: getRoleMappingBreadcrumbs, - controller($scope) { - $scope.$$postDigest(() => { - const domNode = document.getElementById('roleMappingsGridReactRoot'); - - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - if (domNode) { - unmountComponentAtNode(domNode); - } - }); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/role_mappings.html b/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/role_mappings.html deleted file mode 100644 index cff3b821d132c..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/role_mappings.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.html b/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.html deleted file mode 100644 index 0552b655afafd..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.js b/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.js deleted file mode 100644 index e9c42824711b3..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/roles.js +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import routes from 'ui/routes'; -import template from 'plugins/security/views/management/roles_grid/roles.html'; -import { ROLES_PATH } from '../management_urls'; -import { getRolesBreadcrumbs } from '../breadcrumbs'; -import { I18nContext } from 'ui/i18n'; -import { RolesGridPage } from './components'; - -routes.when(ROLES_PATH, { - template, - k7Breadcrumbs: getRolesBreadcrumbs, - controller($scope) { - $scope.$$postDigest(() => { - const domNode = document.getElementById('rolesGridReactRoot'); - - render( - - - , - domNode - ); - - // unmount react on controller destroy - $scope.$on('$destroy', () => { - unmountComponentAtNode(domNode); - }); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/users.html b/x-pack/legacy/plugins/security/public/views/management/users_grid/users.html deleted file mode 100644 index 3dce7326d001a..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/users.html +++ /dev/null @@ -1,3 +0,0 @@ - -
- diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/users.js b/x-pack/legacy/plugins/security/public/views/management/users_grid/users.js deleted file mode 100644 index 8d4e0526251d7..0000000000000 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/users.js +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { render, unmountComponentAtNode } from 'react-dom'; -import routes from 'ui/routes'; -import template from 'plugins/security/views/management/users_grid/users.html'; -import { SECURITY_PATH, USERS_PATH } from '../management_urls'; -import { UsersListPage } from './components'; -import { UserAPIClient } from '../../../lib/api'; -import { I18nContext } from 'ui/i18n'; -import { getUsersBreadcrumbs } from '../breadcrumbs'; - -routes.when(SECURITY_PATH, { - redirectTo: USERS_PATH, -}); - -const renderReact = (elem, changeUrl) => { - render( - - - , - elem - ); -}; - -routes.when(USERS_PATH, { - template, - k7Breadcrumbs: getUsersBreadcrumbs, - controller($scope, $http, kbnUrl) { - $scope.$on('$destroy', () => { - const elem = document.getElementById('usersReactRoot'); - if (elem) unmountComponentAtNode(elem); - }); - $scope.$$postDigest(() => { - const elem = document.getElementById('usersReactRoot'); - const changeUrl = url => { - kbnUrl.change(url); - $scope.$apply(); - }; - renderReact(elem, $http, changeUrl); - }); - }, -}); diff --git a/x-pack/legacy/plugins/security/public/views/overwritten_session/overwritten_session.tsx b/x-pack/legacy/plugins/security/public/views/overwritten_session/overwritten_session.tsx index fb39c517e1c2c..4c79c499cc0e6 100644 --- a/x-pack/legacy/plugins/security/public/views/overwritten_session/overwritten_session.tsx +++ b/x-pack/legacy/plugins/security/public/views/overwritten_session/overwritten_session.tsx @@ -11,8 +11,7 @@ import { render } from 'react-dom'; import chrome from 'ui/chrome'; import { I18nContext } from 'ui/i18n'; import { npSetup } from 'ui/new_platform'; -import { SecurityPluginSetup } from '../../../../../../plugins/security/public'; -import { AuthenticatedUser } from '../../../common/model'; +import { AuthenticatedUser, SecurityPluginSetup } from '../../../../../../plugins/security/public'; import { AuthenticationStatePage } from '../../components/authentication_state_page'; chrome diff --git a/x-pack/plugins/security/common/model/index.ts b/x-pack/plugins/security/common/model/index.ts index f3c65ed7e3cf1..121791d113bd5 100644 --- a/x-pack/plugins/security/common/model/index.ts +++ b/x-pack/plugins/security/common/model/index.ts @@ -10,7 +10,16 @@ export { AuthenticatedUser, canUserChangePassword } from './authenticated_user'; export { BuiltinESPrivileges } from './builtin_es_privileges'; export { FeaturesPrivileges } from './features_privileges'; export { RawKibanaPrivileges, RawKibanaFeaturePrivileges } from './raw_kibana_privileges'; -export { Role, RoleIndexPrivilege, RoleKibanaPrivilege } from './role'; +export { + Role, + RoleIndexPrivilege, + RoleKibanaPrivilege, + copyRole, + isReadOnlyRole, + isReservedRole, + isRoleEnabled, + prepareRoleClone, +} from './role'; export { KibanaPrivileges } from './kibana_privileges'; export { InlineRoleTemplate, diff --git a/x-pack/legacy/plugins/security/public/lib/role_utils.test.ts b/x-pack/plugins/security/common/model/role.test.ts similarity index 96% rename from x-pack/legacy/plugins/security/public/lib/role_utils.test.ts rename to x-pack/plugins/security/common/model/role.test.ts index 9d94017c3f0fe..d4a910a1785eb 100644 --- a/x-pack/legacy/plugins/security/public/lib/role_utils.test.ts +++ b/x-pack/plugins/security/common/model/role.test.ts @@ -4,14 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Role } from '../../common/model'; -import { - copyRole, - isReadOnlyRole, - isReservedRole, - isRoleEnabled, - prepareRoleClone, -} from './role_utils'; +import { Role, isReadOnlyRole, isReservedRole, isRoleEnabled, copyRole, prepareRoleClone } from '.'; describe('role', () => { describe('isRoleEnabled', () => { diff --git a/x-pack/plugins/security/common/model/role.ts b/x-pack/plugins/security/common/model/role.ts index 89f68aaa55b5c..1edcf147262ed 100644 --- a/x-pack/plugins/security/common/model/role.ts +++ b/x-pack/plugins/security/common/model/role.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { cloneDeep } from 'lodash'; import { FeaturesPrivileges } from './features_privileges'; export interface RoleIndexPrivilege { @@ -40,3 +41,53 @@ export interface Role { _transform_error?: string[]; _unrecognized_applications?: string[]; } + +/** + * Returns whether given role is enabled or not + * + * @param role Object Role JSON, as returned by roles API + * @return Boolean true if role is enabled; false otherwise + */ +export function isRoleEnabled(role: Partial) { + return role.transient_metadata?.enabled ?? true; +} + +/** + * Returns whether given role is reserved or not. + * + * @param role Role as returned by roles API + */ +export function isReservedRole(role: Partial) { + return (role.metadata?._reserved as boolean) ?? false; +} + +/** + * Returns whether given role is editable through the UI or not. + * + * @param role the Role as returned by roles API + */ +export function isReadOnlyRole(role: Partial): boolean { + return isReservedRole(role) || (role._transform_error?.length ?? 0) > 0; +} + +/** + * Returns a deep copy of the role. + * + * @param role the Role to copy. + */ +export function copyRole(role: Role) { + return cloneDeep(role); +} + +/** + * Creates a deep copy of the role suitable for cloning. + * + * @param role the Role to clone. + */ +export function prepareRoleClone(role: Role): Role { + const clone = copyRole(role); + + clone.name = ''; + + return clone; +} diff --git a/x-pack/plugins/security/kibana.json b/x-pack/plugins/security/kibana.json index 32f860b1423d3..7d1940e393bec 100644 --- a/x-pack/plugins/security/kibana.json +++ b/x-pack/plugins/security/kibana.json @@ -3,7 +3,8 @@ "version": "8.0.0", "kibanaVersion": "kibana", "configPath": ["xpack", "security"], - "requiredPlugins": ["features", "licensing"], + "requiredPlugins": ["data", "features", "licensing"], + "optionalPlugins": ["home", "management"], "server": true, "ui": true } diff --git a/x-pack/plugins/security/public/_index.scss b/x-pack/plugins/security/public/_index.scss new file mode 100644 index 0000000000000..9fa81bad7c3f4 --- /dev/null +++ b/x-pack/plugins/security/public/_index.scss @@ -0,0 +1,2 @@ +// Management styles +@import './management/index'; diff --git a/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.test.tsx b/x-pack/plugins/security/public/account_management/account_management_page.test.tsx similarity index 72% rename from x-pack/legacy/plugins/security/public/views/account/components/account_management_page.test.tsx rename to x-pack/plugins/security/public/account_management/account_management_page.test.tsx index 366842e58e9e4..b7cf8e6dd1418 100644 --- a/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.test.tsx +++ b/x-pack/plugins/security/public/account_management/account_management_page.test.tsx @@ -6,11 +6,12 @@ import React from 'react'; import { act } from '@testing-library/react'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { securityMock } from '../../../../../../../plugins/security/public/mocks'; +import { AuthenticatedUser } from '../../common/model'; import { AccountManagementPage } from './account_management_page'; -import { AuthenticatedUser } from '../../../../common/model'; -jest.mock('ui/kfetch'); +import { coreMock } from 'src/core/public/mocks'; +import { securityMock } from '../mocks'; +import { userAPIClientMock } from '../management/users/index.mock'; interface Options { withFullName?: boolean; @@ -45,7 +46,11 @@ describe('', () => { it(`displays users full name, username, and email address`, async () => { const user = createUser(); const wrapper = mountWithIntl( - + ); await act(async () => { @@ -63,7 +68,11 @@ describe('', () => { it(`displays username when full_name is not provided`, async () => { const user = createUser({ withFullName: false }); const wrapper = mountWithIntl( - + ); await act(async () => { @@ -77,7 +86,11 @@ describe('', () => { it(`displays a placeholder when no email address is provided`, async () => { const user = createUser({ withEmail: false }); const wrapper = mountWithIntl( - + ); await act(async () => { @@ -91,7 +104,11 @@ describe('', () => { it(`displays change password form for users in the native realm`, async () => { const user = createUser(); const wrapper = mountWithIntl( - + ); await act(async () => { @@ -106,7 +123,11 @@ describe('', () => { it(`does not display change password form for users in the saml realm`, async () => { const user = createUser({ realm: 'saml' }); const wrapper = mountWithIntl( - + ); await act(async () => { diff --git a/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.tsx b/x-pack/plugins/security/public/account_management/account_management_page.tsx similarity index 62% rename from x-pack/legacy/plugins/security/public/views/account/components/account_management_page.tsx rename to x-pack/plugins/security/public/account_management/account_management_page.tsx index 6abee73e0b353..3f764adc7949f 100644 --- a/x-pack/legacy/plugins/security/public/views/account/components/account_management_page.tsx +++ b/x-pack/plugins/security/public/account_management/account_management_page.tsx @@ -5,20 +5,24 @@ */ import { EuiPage, EuiPageBody, EuiPanel, EuiSpacer, EuiText } from '@elastic/eui'; import React, { useEffect, useState } from 'react'; -import { SecurityPluginSetup } from '../../../../../../../plugins/security/public'; -import { getUserDisplayName, AuthenticatedUser } from '../../../../common/model'; +import { NotificationsStart } from 'src/core/public'; +import { getUserDisplayName, AuthenticatedUser } from '../../common/model'; +import { AuthenticationServiceSetup } from '../authentication'; import { ChangePassword } from './change_password'; +import { UserAPIClient } from '../management'; import { PersonalInfo } from './personal_info'; interface Props { - securitySetup: SecurityPluginSetup; + authc: AuthenticationServiceSetup; + apiClient: PublicMethodsOf; + notifications: NotificationsStart; } -export const AccountManagementPage = (props: Props) => { +export const AccountManagementPage = ({ apiClient, authc, notifications }: Props) => { const [currentUser, setCurrentUser] = useState(null); useEffect(() => { - props.securitySetup.authc.getCurrentUser().then(setCurrentUser); - }, [props]); + authc.getCurrentUser().then(setCurrentUser); + }, [authc]); if (!currentUser) { return null; @@ -36,7 +40,7 @@ export const AccountManagementPage = (props: Props) => { - + diff --git a/x-pack/legacy/plugins/security/public/views/account/components/change_password/change_password.tsx b/x-pack/plugins/security/public/account_management/change_password/change_password.tsx similarity index 81% rename from x-pack/legacy/plugins/security/public/views/account/components/change_password/change_password.tsx rename to x-pack/plugins/security/public/account_management/change_password/change_password.tsx index 63abb4539470d..f5ac5f3b21d2e 100644 --- a/x-pack/legacy/plugins/security/public/views/account/components/change_password/change_password.tsx +++ b/x-pack/plugins/security/public/account_management/change_password/change_password.tsx @@ -3,18 +3,18 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - // @ts-ignore - EuiDescribedFormGroup, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { UserAPIClient } from '../../../../lib/api'; -import { AuthenticatedUser, canUserChangePassword } from '../../../../../common/model'; -import { ChangePasswordForm } from '../../../../components/management/change_password_form'; +import { EuiDescribedFormGroup } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsSetup } from 'src/core/public'; +import { AuthenticatedUser, canUserChangePassword } from '../../../common/model'; +import { UserAPIClient } from '../../management/users'; +import { ChangePasswordForm } from '../../management/users/components/change_password_form'; interface Props { user: AuthenticatedUser; + apiClient: PublicMethodsOf; + notifications: NotificationsSetup; } export class ChangePassword extends Component { @@ -48,7 +48,8 @@ export class ChangePassword extends Component { ); diff --git a/x-pack/legacy/plugins/security/public/views/account/components/change_password/index.ts b/x-pack/plugins/security/public/account_management/change_password/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/account/components/change_password/index.ts rename to x-pack/plugins/security/public/account_management/change_password/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/account/components/index.ts b/x-pack/plugins/security/public/account_management/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/account/components/index.ts rename to x-pack/plugins/security/public/account_management/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/account/components/personal_info/index.ts b/x-pack/plugins/security/public/account_management/personal_info/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/account/components/personal_info/index.ts rename to x-pack/plugins/security/public/account_management/personal_info/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/account/components/personal_info/personal_info.tsx b/x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/account/components/personal_info/personal_info.tsx rename to x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx index 7121bf7ab28ee..9cbbc242e8400 100644 --- a/x-pack/legacy/plugins/security/public/views/account/components/personal_info/personal_info.tsx +++ b/x-pack/plugins/security/public/account_management/personal_info/personal_info.tsx @@ -3,15 +3,10 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { - // @ts-ignore - EuiDescribedFormGroup, - EuiFormRow, - EuiText, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { AuthenticatedUser } from '../../../../../common/model'; +import { EuiDescribedFormGroup, EuiFormRow, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { AuthenticatedUser } from '../../../common/model'; interface Props { user: AuthenticatedUser; diff --git a/x-pack/plugins/security/public/management/_index.scss b/x-pack/plugins/security/public/management/_index.scss new file mode 100644 index 0000000000000..5d419b5323079 --- /dev/null +++ b/x-pack/plugins/security/public/management/_index.scss @@ -0,0 +1,3 @@ +@import './roles/index'; +@import './users/index'; +@import './role_mappings/index'; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.ts new file mode 100644 index 0000000000000..2a45d497029f4 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.mock.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const apiKeysAPIClientMock = { + create: () => ({ + checkPrivileges: jest.fn(), + getApiKeys: jest.fn(), + invalidateApiKeys: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.test.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.test.ts new file mode 100644 index 0000000000000..7d51a80459a6e --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { APIKeysAPIClient } from './api_keys_api_client'; + +import { httpServiceMock } from '../../../../../../src/core/public/mocks'; + +describe('APIKeysAPIClient', () => { + it('checkPrivileges() queries correct endpoint', async () => { + const httpMock = httpServiceMock.createStartContract(); + + const mockResponse = Symbol('mockResponse'); + httpMock.get.mockResolvedValue(mockResponse); + + const apiClient = new APIKeysAPIClient(httpMock); + + await expect(apiClient.checkPrivileges()).resolves.toBe(mockResponse); + expect(httpMock.get).toHaveBeenCalledTimes(1); + expect(httpMock.get).toHaveBeenCalledWith('/internal/security/api_key/privileges'); + }); + + it('getApiKeys() queries correct endpoint', async () => { + const httpMock = httpServiceMock.createStartContract(); + + const mockResponse = Symbol('mockResponse'); + httpMock.get.mockResolvedValue(mockResponse); + + const apiClient = new APIKeysAPIClient(httpMock); + + await expect(apiClient.getApiKeys()).resolves.toBe(mockResponse); + expect(httpMock.get).toHaveBeenCalledTimes(1); + expect(httpMock.get).toHaveBeenCalledWith('/internal/security/api_key', { + query: { isAdmin: false }, + }); + httpMock.get.mockClear(); + + await expect(apiClient.getApiKeys(false)).resolves.toBe(mockResponse); + expect(httpMock.get).toHaveBeenCalledTimes(1); + expect(httpMock.get).toHaveBeenCalledWith('/internal/security/api_key', { + query: { isAdmin: false }, + }); + httpMock.get.mockClear(); + + await expect(apiClient.getApiKeys(true)).resolves.toBe(mockResponse); + expect(httpMock.get).toHaveBeenCalledTimes(1); + expect(httpMock.get).toHaveBeenCalledWith('/internal/security/api_key', { + query: { isAdmin: true }, + }); + }); + + it('invalidateApiKeys() queries correct endpoint', async () => { + const httpMock = httpServiceMock.createStartContract(); + + const mockResponse = Symbol('mockResponse'); + httpMock.post.mockResolvedValue(mockResponse); + + const apiClient = new APIKeysAPIClient(httpMock); + const mockAPIKeys = [ + { id: 'one', name: 'name-one' }, + { id: 'two', name: 'name-two' }, + ]; + + await expect(apiClient.invalidateApiKeys(mockAPIKeys)).resolves.toBe(mockResponse); + expect(httpMock.post).toHaveBeenCalledTimes(1); + expect(httpMock.post).toHaveBeenCalledWith('/internal/security/api_key/invalidate', { + body: JSON.stringify({ apiKeys: mockAPIKeys, isAdmin: false }), + }); + httpMock.post.mockClear(); + + await expect(apiClient.invalidateApiKeys(mockAPIKeys, false)).resolves.toBe(mockResponse); + expect(httpMock.post).toHaveBeenCalledTimes(1); + expect(httpMock.post).toHaveBeenCalledWith('/internal/security/api_key/invalidate', { + body: JSON.stringify({ apiKeys: mockAPIKeys, isAdmin: false }), + }); + httpMock.post.mockClear(); + + await expect(apiClient.invalidateApiKeys(mockAPIKeys, true)).resolves.toBe(mockResponse); + expect(httpMock.post).toHaveBeenCalledTimes(1); + expect(httpMock.post).toHaveBeenCalledWith('/internal/security/api_key/invalidate', { + body: JSON.stringify({ apiKeys: mockAPIKeys, isAdmin: true }), + }); + }); +}); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts new file mode 100644 index 0000000000000..372b1e56a73c4 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_api_client.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpStart } from 'src/core/public'; +import { ApiKey, ApiKeyToInvalidate } from '../../../common/model'; + +interface CheckPrivilegesResponse { + areApiKeysEnabled: boolean; + isAdmin: boolean; +} + +interface InvalidateApiKeysResponse { + itemsInvalidated: ApiKeyToInvalidate[]; + errors: any[]; +} + +interface GetApiKeysResponse { + apiKeys: ApiKey[]; +} + +const apiKeysUrl = '/internal/security/api_key'; + +export class APIKeysAPIClient { + constructor(private readonly http: HttpStart) {} + + public async checkPrivileges() { + return await this.http.get(`${apiKeysUrl}/privileges`); + } + + public async getApiKeys(isAdmin = false) { + return await this.http.get(apiKeysUrl, { query: { isAdmin } }); + } + + public async invalidateApiKeys(apiKeys: ApiKeyToInvalidate[], isAdmin = false) { + return await this.http.post(`${apiKeysUrl}/invalidate`, { + body: JSON.stringify({ apiKeys, isAdmin }), + }); + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/__snapshots__/api_keys_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap similarity index 91% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/__snapshots__/api_keys_grid_page.test.tsx.snap rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap index c2537235c99f6..42fd4417e238b 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/__snapshots__/api_keys_grid_page.test.tsx.snap +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/__snapshots__/api_keys_grid_page.test.tsx.snap @@ -1,7 +1,13 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ApiKeysGridPage renders a callout when API keys are not enabled 1`] = ` - +exports[`APIKeysGridPage renders a callout when API keys are not enabled 1`] = ` + Contact your system administrator and refer to the `; -exports[`ApiKeysGridPage renders permission denied if user does not have required permissions 1`] = ` +exports[`APIKeysGridPage renders permission denied if user does not have required permissions 1`] = ` ({ body: { statusCode: 403 } }); -const mock500 = () => ({ body: { error: 'Internal Server Error', message: '', statusCode: 500 } }); - -jest.mock('../../../../lib/api_keys_api', () => { - return { - ApiKeysApi: { - async checkPrivileges() { - if (mockSimulate403) { - throw mock403(); - } - - return { - isAdmin: mockIsAdmin, - areApiKeysEnabled: mockAreApiKeysEnabled, - }; - }, - async getApiKeys() { - if (mockSimulate500) { - throw mock500(); - } - - return { - apiKeys: [ - { - creation: 1571322182082, - expiration: 1571408582082, - id: '0QQZ2m0BO2XZwgJFuWTT', - invalidated: false, - name: 'my-api-key', - realm: 'reserved', - username: 'elastic', - }, - ], - }; - }, - }, - }; -}); - import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { ApiKeysGridPage } from './api_keys_grid_page'; import React from 'react'; import { ReactWrapper } from 'enzyme'; import { EuiCallOut } from '@elastic/eui'; import { NotEnabled } from './not_enabled'; import { PermissionDenied } from './permission_denied'; +import { APIKeysAPIClient } from '../api_keys_api_client'; +import { DocumentationLinksService } from '../documentation_links'; +import { APIKeysGridPage } from './api_keys_grid_page'; + +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { apiKeysAPIClientMock } from '../index.mock'; + +const mock403 = () => ({ body: { statusCode: 403 } }); +const mock500 = () => ({ body: { error: 'Internal Server Error', message: '', statusCode: 500 } }); const waitForRender = async ( wrapper: ReactWrapper, @@ -77,23 +41,51 @@ const waitForRender = async ( }); }; -describe('ApiKeysGridPage', () => { +describe('APIKeysGridPage', () => { + let apiClientMock: jest.Mocked>; beforeEach(() => { - mockSimulate403 = false; - mockSimulate500 = false; - mockAreApiKeysEnabled = true; - mockIsAdmin = true; + apiClientMock = apiKeysAPIClientMock.create(); + apiClientMock.checkPrivileges.mockResolvedValue({ + isAdmin: true, + areApiKeysEnabled: true, + }); + apiClientMock.getApiKeys.mockResolvedValue({ + apiKeys: [ + { + creation: 1571322182082, + expiration: 1571408582082, + id: '0QQZ2m0BO2XZwgJFuWTT', + invalidated: false, + name: 'my-api-key', + realm: 'reserved', + username: 'elastic', + }, + ], + }); }); + const getViewProperties = () => { + const { docLinks, notifications } = coreMock.createStart(); + return { + docLinks: new DocumentationLinksService(docLinks), + notifications, + apiKeysAPIClient: apiClientMock, + }; + }; + it('renders a loading state when fetching API keys', async () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl(); expect(wrapper.find('[data-test-subj="apiKeysSectionLoading"]')).toHaveLength(1); }); it('renders a callout when API keys are not enabled', async () => { - mockAreApiKeysEnabled = false; - const wrapper = mountWithIntl(); + apiClientMock.checkPrivileges.mockResolvedValue({ + isAdmin: true, + areApiKeysEnabled: false, + }); + + const wrapper = mountWithIntl(); await waitForRender(wrapper, updatedWrapper => { return updatedWrapper.find(NotEnabled).length > 0; @@ -103,8 +95,9 @@ describe('ApiKeysGridPage', () => { }); it('renders permission denied if user does not have required permissions', async () => { - mockSimulate403 = true; - const wrapper = mountWithIntl(); + apiClientMock.checkPrivileges.mockRejectedValue(mock403()); + + const wrapper = mountWithIntl(); await waitForRender(wrapper, updatedWrapper => { return updatedWrapper.find(PermissionDenied).length > 0; @@ -114,8 +107,9 @@ describe('ApiKeysGridPage', () => { }); it('renders error callout if error fetching API keys', async () => { - mockSimulate500 = true; - const wrapper = mountWithIntl(); + apiClientMock.getApiKeys.mockRejectedValue(mock500()); + + const wrapper = mountWithIntl(); await waitForRender(wrapper, updatedWrapper => { return updatedWrapper.find(EuiCallOut).length > 0; @@ -125,7 +119,10 @@ describe('ApiKeysGridPage', () => { }); describe('Admin view', () => { - const wrapper = mountWithIntl(); + let wrapper: ReactWrapper; + beforeEach(() => { + wrapper = mountWithIntl(); + }); it('renders a callout indicating the user is an administrator', async () => { const calloutEl = 'EuiCallOut[data-test-subj="apiKeyAdminDescriptionCallOut"]'; @@ -151,8 +148,15 @@ describe('ApiKeysGridPage', () => { }); describe('Non-admin view', () => { - mockIsAdmin = false; - const wrapper = mountWithIntl(); + let wrapper: ReactWrapper; + beforeEach(() => { + apiClientMock.checkPrivileges.mockResolvedValue({ + isAdmin: false, + areApiKeysEnabled: true, + }); + + wrapper = mountWithIntl(); + }); it('does NOT render a callout indicating the user is an administrator', async () => { const descriptionEl = 'EuiText[data-test-subj="apiKeysDescriptionText"]'; diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx similarity index 90% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index 92633a4b0ef57..779a2302cfadf 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -27,16 +27,23 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment-timezone'; import _ from 'lodash'; -import { toastNotifications } from 'ui/notify'; +import { NotificationsStart } from 'src/core/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SectionLoading } from '../../../../../../../../../src/plugins/es_ui_shared/public/components/section_loading'; -import { ApiKey, ApiKeyToInvalidate } from '../../../../../common/model'; -import { ApiKeysApi } from '../../../../lib/api_keys_api'; +import { SectionLoading } from '../../../../../../../src/plugins/es_ui_shared/public/components/section_loading'; +import { ApiKey, ApiKeyToInvalidate } from '../../../../common/model'; +import { APIKeysAPIClient } from '../api_keys_api_client'; +import { DocumentationLinksService } from '../documentation_links'; import { PermissionDenied } from './permission_denied'; import { EmptyPrompt } from './empty_prompt'; import { NotEnabled } from './not_enabled'; import { InvalidateProvider } from './invalidate_provider'; +interface Props { + notifications: NotificationsStart; + docLinks: DocumentationLinksService; + apiKeysAPIClient: PublicMethodsOf; +} + interface State { isLoadingApp: boolean; isLoadingTable: boolean; @@ -50,7 +57,7 @@ interface State { const DATE_FORMAT = 'MMMM Do YYYY HH:mm:ss'; -export class ApiKeysGridPage extends Component { +export class APIKeysGridPage extends Component { constructor(props: any) { super(props); this.state = { @@ -124,7 +131,7 @@ export class ApiKeysGridPage extends Component { if (!areApiKeysEnabled) { return ( - + ); } @@ -132,7 +139,7 @@ export class ApiKeysGridPage extends Component { if (!isLoadingTable && apiKeys && apiKeys.length === 0) { return ( - + ); } @@ -210,7 +217,11 @@ export class ApiKeysGridPage extends Component { const search: EuiInMemoryTableProps['search'] = { toolsLeft: selectedItems.length ? ( - + {invalidateApiKeyPrompt => { return ( { return ( - + {invalidateApiKeyPrompt => { return ( { private async checkPrivileges() { try { - const { isAdmin, areApiKeysEnabled } = await ApiKeysApi.checkPrivileges(); + const { isAdmin, areApiKeysEnabled } = await this.props.apiKeysAPIClient.checkPrivileges(); this.setState({ isAdmin, areApiKeysEnabled }); if (areApiKeysEnabled) { @@ -494,14 +509,11 @@ export class ApiKeysGridPage extends Component { if (_.get(e, 'body.statusCode') === 403) { this.setState({ permissionDenied: true, isLoadingApp: false }); } else { - toastNotifications.addDanger( - this.props.i18n.translate( - 'xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage', - { - defaultMessage: 'Error checking privileges: {message}', - values: { message: _.get(e, 'body.message', '') }, - } - ) + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.apiKeys.table.fetchingApiKeysErrorMessage', { + defaultMessage: 'Error checking privileges: {message}', + values: { message: _.get(e, 'body.message', '') }, + }) ); } } @@ -520,7 +532,7 @@ export class ApiKeysGridPage extends Component { private loadApiKeys = async () => { try { const { isAdmin } = this.state; - const { apiKeys } = await ApiKeysApi.getApiKeys(isAdmin); + const { apiKeys } = await this.props.apiKeysAPIClient.getApiKeys(isAdmin); this.setState({ apiKeys }); } catch (e) { this.setState({ error: e }); diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/empty_prompt/empty_prompt.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/empty_prompt/empty_prompt.tsx rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx index 957ca7010a1a0..7d762a1ceb04e 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/empty_prompt/empty_prompt.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/empty_prompt/empty_prompt.tsx @@ -7,13 +7,14 @@ import React, { Fragment } from 'react'; import { EuiEmptyPrompt, EuiButton, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { documentationLinks } from '../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; interface Props { isAdmin: boolean; + docLinks: DocumentationLinksService; } -export const EmptyPrompt: React.FunctionComponent = ({ isAdmin }) => ( +export const EmptyPrompt: React.FunctionComponent = ({ isAdmin, docLinks }) => ( = ({ isAdmin }) => ( defaultMessage="You can create an {link} from Console." values={{ link: ( - + React.ReactElement; + notifications: NotificationsStart; + apiKeysAPIClient: PublicMethodsOf; } export type InvalidateApiKeys = ( @@ -23,7 +25,12 @@ export type InvalidateApiKeys = ( type OnSuccessCallback = (apiKeysInvalidated: ApiKeyToInvalidate[]) => void; -export const InvalidateProvider: React.FunctionComponent = ({ isAdmin, children }) => { +export const InvalidateProvider: React.FunctionComponent = ({ + isAdmin, + children, + notifications, + apiKeysAPIClient, +}) => { const [apiKeys, setApiKeys] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); const onSuccessCallback = useRef(null); @@ -48,7 +55,7 @@ export const InvalidateProvider: React.FunctionComponent = ({ isAdmin, ch let errors; try { - result = await ApiKeysApi.invalidateApiKeys(apiKeys, isAdmin); + result = await apiKeysAPIClient.invalidateApiKeys(apiKeys, isAdmin); } catch (e) { error = e; } @@ -77,7 +84,7 @@ export const InvalidateProvider: React.FunctionComponent = ({ isAdmin, ch values: { name: itemsInvalidated[0].name }, } ); - toastNotifications.addSuccess(successMessage); + notifications.toasts.addSuccess(successMessage); if (onSuccessCallback.current) { onSuccessCallback.current([...itemsInvalidated]); } @@ -106,7 +113,7 @@ export const InvalidateProvider: React.FunctionComponent = ({ isAdmin, ch values: { name: (errors && errors[0].name) || apiKeys[0].name }, } ); - toastNotifications.addDanger(errorMessage); + notifications.toasts.addDanger(errorMessage); } }; diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/index.ts b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/index.ts rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/not_enabled.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/not_enabled.tsx similarity index 78% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/not_enabled.tsx rename to x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/not_enabled.tsx index c419e15450c1e..08fe542557757 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/components/not_enabled/not_enabled.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/not_enabled/not_enabled.tsx @@ -7,9 +7,13 @@ import React from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { documentationLinks } from '../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; -export const NotEnabled: React.FunctionComponent = () => ( +interface Props { + docLinks: DocumentationLinksService; +} + +export const NotEnabled: React.FunctionComponent = ({ docLinks }) => ( ( defaultMessage="Contact your system administrator and refer to the {link} to enable API keys." values={{ link: ( - + ({ + APIKeysGridPage: (props: any) => `Page: ${JSON.stringify(props)}`, +})); + +import { apiKeysManagementApp } from './api_keys_management_app'; +import { coreMock } from '../../../../../../src/core/public/mocks'; + +describe('apiKeysManagementApp', () => { + it('create() returns proper management app descriptor', () => { + const { getStartServices } = coreMock.createSetup(); + + expect(apiKeysManagementApp.create({ getStartServices: getStartServices as any })) + .toMatchInlineSnapshot(` + Object { + "id": "api_keys", + "mount": [Function], + "order": 30, + "title": "API Keys", + } + `); + }); + + it('mount() works for the `grid` page', async () => { + const { getStartServices } = coreMock.createSetup(); + const container = document.createElement('div'); + + const setBreadcrumbs = jest.fn(); + const unmount = await apiKeysManagementApp + .create({ getStartServices: getStartServices as any }) + .mount({ + basePath: '/some-base-path', + element: container, + setBreadcrumbs, + }); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: '#/some-base-path', text: 'API Keys' }]); + expect(container).toMatchInlineSnapshot(` +
+ Page: {"notifications":{"toasts":{}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"apiKeysAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); +}); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx new file mode 100644 index 0000000000000..35de732b84ce9 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_management_app.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup } from 'src/core/public'; +import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { PluginStartDependencies } from '../../plugin'; +import { APIKeysGridPage } from './api_keys_grid'; +import { APIKeysAPIClient } from './api_keys_api_client'; +import { DocumentationLinksService } from './documentation_links'; + +interface CreateParams { + getStartServices: CoreSetup['getStartServices']; +} + +export const apiKeysManagementApp = Object.freeze({ + id: 'api_keys', + create({ getStartServices }: CreateParams) { + return { + id: this.id, + order: 30, + title: i18n.translate('xpack.security.management.apiKeysTitle', { + defaultMessage: 'API Keys', + }), + async mount({ basePath, element, setBreadcrumbs }) { + const [{ docLinks, http, notifications, i18n: i18nStart }] = await getStartServices(); + setBreadcrumbs([ + { + text: i18n.translate('xpack.security.apiKeys.breadcrumb', { + defaultMessage: 'API Keys', + }), + href: `#${basePath}`, + }, + ]); + + render( + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; + }, + } as RegisterManagementAppArgs; + }, +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/services/documentation_links.ts b/x-pack/plugins/security/public/management/api_keys/documentation_links.ts similarity index 51% rename from x-pack/legacy/plugins/security/public/views/management/api_keys_grid/services/documentation_links.ts rename to x-pack/plugins/security/public/management/api_keys/documentation_links.ts index 1f03763eb542a..4165c2a2372c9 100644 --- a/x-pack/legacy/plugins/security/public/views/management/api_keys_grid/services/documentation_links.ts +++ b/x-pack/plugins/security/public/management/api_keys/documentation_links.ts @@ -4,18 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } from 'ui/documentation_links'; +import { DocLinksStart } from 'src/core/public'; -class DocumentationLinksService { - private esDocBasePath = `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${DOC_LINK_VERSION}/`; +export class DocumentationLinksService { + private readonly esDocBasePath: string; - public getApiKeyServiceSettingsDocUrl(): string { + constructor(docLinks: DocLinksStart) { + this.esDocBasePath = `${docLinks.ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${docLinks.DOC_LINK_VERSION}/`; + } + + public getApiKeyServiceSettingsDocUrl() { return `${this.esDocBasePath}security-settings.html#api-key-service-settings`; } - public getCreateApiKeyDocUrl(): string { + public getCreateApiKeyDocUrl() { return `${this.esDocBasePath}security-api-create-api-key.html`; } } - -export const documentationLinks = new DocumentationLinksService(); diff --git a/x-pack/plugins/security/public/management/api_keys/index.mock.ts b/x-pack/plugins/security/public/management/api_keys/index.mock.ts new file mode 100644 index 0000000000000..3c11cd6bb9c65 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/index.mock.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { apiKeysAPIClientMock } from './api_keys_api_client.mock'; diff --git a/x-pack/plugins/security/public/management/api_keys/index.ts b/x-pack/plugins/security/public/management/api_keys/index.ts new file mode 100644 index 0000000000000..e15da7d5eb409 --- /dev/null +++ b/x-pack/plugins/security/public/management/api_keys/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { apiKeysManagementApp } from './api_keys_management_app'; diff --git a/x-pack/plugins/security/public/management/index.ts b/x-pack/plugins/security/public/management/index.ts new file mode 100644 index 0000000000000..e1a13d66e6883 --- /dev/null +++ b/x-pack/plugins/security/public/management/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ManagementService } from './management_service'; +export { UserAPIClient } from './users/user_api_client'; diff --git a/x-pack/plugins/security/public/management/management_service.test.ts b/x-pack/plugins/security/public/management/management_service.test.ts new file mode 100644 index 0000000000000..53c12ad7ab12c --- /dev/null +++ b/x-pack/plugins/security/public/management/management_service.test.ts @@ -0,0 +1,226 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { BehaviorSubject } from 'rxjs'; +import { ManagementApp } from '../../../../../src/plugins/management/public'; +import { SecurityLicenseFeatures } from '../../common/licensing/license_features'; +import { ManagementService } from './management_service'; +import { usersManagementApp } from './users'; + +import { coreMock } from '../../../../../src/core/public/mocks'; +import { licenseMock } from '../../common/licensing/index.mock'; +import { securityMock } from '../mocks'; +import { rolesManagementApp } from './roles'; +import { apiKeysManagementApp } from './api_keys'; +import { roleMappingsManagementApp } from './role_mappings'; + +describe('ManagementService', () => { + describe('setup()', () => { + it('properly registers security section and its applications', () => { + const { fatalErrors, getStartServices } = coreMock.createSetup(); + const { authc } = securityMock.createSetup(); + const license = licenseMock.create(); + + const mockSection = { registerApp: jest.fn() }; + const managementSetup = { + sections: { + getSection: jest.fn(), + getAllSections: jest.fn(), + register: jest.fn().mockReturnValue(mockSection), + }, + }; + + const service = new ManagementService(); + service.setup({ + getStartServices: getStartServices as any, + license, + fatalErrors, + authc, + management: managementSetup, + }); + + expect(managementSetup.sections.register).toHaveBeenCalledTimes(1); + expect(managementSetup.sections.register).toHaveBeenCalledWith({ + id: 'security', + title: 'Security', + order: 100, + euiIconType: 'securityApp', + }); + + expect(mockSection.registerApp).toHaveBeenCalledTimes(4); + expect(mockSection.registerApp).toHaveBeenCalledWith({ + id: 'users', + mount: expect.any(Function), + order: 10, + title: 'Users', + }); + expect(mockSection.registerApp).toHaveBeenCalledWith({ + id: 'roles', + mount: expect.any(Function), + order: 20, + title: 'Roles', + }); + expect(mockSection.registerApp).toHaveBeenCalledWith({ + id: 'api_keys', + mount: expect.any(Function), + order: 30, + title: 'API Keys', + }); + expect(mockSection.registerApp).toHaveBeenCalledWith({ + id: 'role_mappings', + mount: expect.any(Function), + order: 40, + title: 'Role Mappings', + }); + }); + }); + + describe('start()', () => { + function startService(initialFeatures: Partial) { + const { fatalErrors, getStartServices } = coreMock.createSetup(); + + const licenseSubject = new BehaviorSubject( + (initialFeatures as unknown) as SecurityLicenseFeatures + ); + const license = licenseMock.create(); + license.features$ = licenseSubject; + + const service = new ManagementService(); + service.setup({ + getStartServices: getStartServices as any, + license, + fatalErrors, + authc: securityMock.createSetup().authc, + management: { + sections: { + getSection: jest.fn(), + getAllSections: jest.fn(), + register: jest.fn().mockReturnValue({ registerApp: jest.fn() }), + }, + }, + }); + + const getMockedApp = () => { + // All apps are enabled by default. + let enabled = true; + return ({ + get enabled() { + return enabled; + }, + enable: jest.fn().mockImplementation(() => { + enabled = true; + }), + disable: jest.fn().mockImplementation(() => { + enabled = false; + }), + } as unknown) as jest.Mocked; + }; + const mockApps = new Map>([ + [usersManagementApp.id, getMockedApp()], + [rolesManagementApp.id, getMockedApp()], + [apiKeysManagementApp.id, getMockedApp()], + [roleMappingsManagementApp.id, getMockedApp()], + ] as Array<[string, jest.Mocked]>); + + service.start({ + management: { + sections: { + getSection: jest + .fn() + .mockReturnValue({ getApp: jest.fn().mockImplementation(id => mockApps.get(id)) }), + getAllSections: jest.fn(), + navigateToApp: jest.fn(), + }, + legacy: undefined, + }, + }); + + return { + mockApps, + updateFeatures(features: Partial) { + licenseSubject.next((features as unknown) as SecurityLicenseFeatures); + }, + }; + } + + it('does not do anything if `showLinks` is `true` at `start`', () => { + const { mockApps } = startService({ showLinks: true, showRoleMappingsManagement: true }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enable).not.toHaveBeenCalled(); + expect(mockApp.disable).not.toHaveBeenCalled(); + expect(mockApp.enabled).toBe(true); + } + }); + + it('disables all apps if `showLinks` is `false` at `start`', () => { + const { mockApps } = startService({ showLinks: false, showRoleMappingsManagement: true }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(false); + } + }); + + it('disables only Role Mappings app if `showLinks` is `true`, but `showRoleMappingsManagement` is `false` at `start`', () => { + const { mockApps } = startService({ showLinks: true, showRoleMappingsManagement: false }); + for (const [appId, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(appId !== roleMappingsManagementApp.id); + } + }); + + it('apps are disabled if `showLinks` changes after `start`', () => { + const { mockApps, updateFeatures } = startService({ + showLinks: true, + showRoleMappingsManagement: true, + }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(true); + } + + updateFeatures({ showLinks: false, showRoleMappingsManagement: false }); + + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(false); + } + }); + + it('role mappings app is disabled if `showRoleMappingsManagement` changes after `start`', () => { + const { mockApps, updateFeatures } = startService({ + showLinks: true, + showRoleMappingsManagement: true, + }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(true); + } + + updateFeatures({ showLinks: true, showRoleMappingsManagement: false }); + + for (const [appId, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(appId !== roleMappingsManagementApp.id); + } + }); + + it('apps are re-enabled if `showLinks` eventually transitions to `true` after `start`', () => { + const { mockApps, updateFeatures } = startService({ + showLinks: true, + showRoleMappingsManagement: true, + }); + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(true); + } + + updateFeatures({ showLinks: false, showRoleMappingsManagement: false }); + + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(false); + } + + updateFeatures({ showLinks: true, showRoleMappingsManagement: true }); + + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(true); + } + }); + }); +}); diff --git a/x-pack/plugins/security/public/management/management_service.ts b/x-pack/plugins/security/public/management/management_service.ts new file mode 100644 index 0000000000000..5ad3681590fbf --- /dev/null +++ b/x-pack/plugins/security/public/management/management_service.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Subscription } from 'rxjs'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup, FatalErrorsSetup } from 'src/core/public'; +import { + ManagementApp, + ManagementSetup, + ManagementStart, +} from '../../../../../src/plugins/management/public'; +import { SecurityLicense } from '../../common/licensing'; +import { AuthenticationServiceSetup } from '../authentication'; +import { PluginStartDependencies } from '../plugin'; +import { apiKeysManagementApp } from './api_keys'; +import { roleMappingsManagementApp } from './role_mappings'; +import { rolesManagementApp } from './roles'; +import { usersManagementApp } from './users'; + +interface SetupParams { + management: ManagementSetup; + license: SecurityLicense; + authc: AuthenticationServiceSetup; + fatalErrors: FatalErrorsSetup; + getStartServices: CoreSetup['getStartServices']; +} + +interface StartParams { + management: ManagementStart; +} + +export class ManagementService { + private license!: SecurityLicense; + private licenseFeaturesSubscription?: Subscription; + + setup({ getStartServices, management, authc, license, fatalErrors }: SetupParams) { + this.license = license; + + const securitySection = management.sections.register({ + id: 'security', + title: i18n.translate('xpack.security.management.securityTitle', { + defaultMessage: 'Security', + }), + order: 100, + euiIconType: 'securityApp', + }); + + securitySection.registerApp(usersManagementApp.create({ authc, getStartServices })); + securitySection.registerApp( + rolesManagementApp.create({ fatalErrors, license, getStartServices }) + ); + securitySection.registerApp(apiKeysManagementApp.create({ getStartServices })); + securitySection.registerApp(roleMappingsManagementApp.create({ getStartServices })); + } + + start({ management }: StartParams) { + this.licenseFeaturesSubscription = this.license.features$.subscribe(async features => { + const securitySection = management.sections.getSection('security')!; + + const securityManagementAppsStatuses: Array<[ManagementApp, boolean]> = [ + [securitySection.getApp(usersManagementApp.id)!, features.showLinks], + [securitySection.getApp(rolesManagementApp.id)!, features.showLinks], + [securitySection.getApp(apiKeysManagementApp.id)!, features.showLinks], + [ + securitySection.getApp(roleMappingsManagementApp.id)!, + features.showLinks && features.showRoleMappingsManagement, + ], + ]; + + // Iterate over all registered apps and update their enable status depending on the available + // license features. + for (const [app, enableStatus] of securityManagementAppsStatuses) { + if (app.enabled === enableStatus) { + continue; + } + + if (enableStatus) { + app.enable(); + } else { + app.disable(); + } + } + }); + } + + stop() { + if (this.licenseFeaturesSubscription) { + this.licenseFeaturesSubscription.unsubscribe(); + this.licenseFeaturesSubscription = undefined; + } + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/management_urls.ts b/x-pack/plugins/security/public/management/management_urls.ts similarity index 74% rename from x-pack/legacy/plugins/security/public/views/management/management_urls.ts rename to x-pack/plugins/security/public/management/management_urls.ts index 881740c0b2895..0d4e3fc920bdb 100644 --- a/x-pack/legacy/plugins/security/public/views/management/management_urls.ts +++ b/x-pack/plugins/security/public/management/management_urls.ts @@ -4,19 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -export const MANAGEMENT_PATH = '/management'; -export const SECURITY_PATH = `${MANAGEMENT_PATH}/security`; +const MANAGEMENT_PATH = '/management'; +const SECURITY_PATH = `${MANAGEMENT_PATH}/security`; export const ROLES_PATH = `${SECURITY_PATH}/roles`; export const EDIT_ROLES_PATH = `${ROLES_PATH}/edit`; export const CLONE_ROLES_PATH = `${ROLES_PATH}/clone`; export const USERS_PATH = `${SECURITY_PATH}/users`; export const EDIT_USERS_PATH = `${USERS_PATH}/edit`; -export const API_KEYS_PATH = `${SECURITY_PATH}/api_keys`; export const ROLE_MAPPINGS_PATH = `${SECURITY_PATH}/role_mappings`; -export const CREATE_ROLE_MAPPING_PATH = `${ROLE_MAPPINGS_PATH}/edit`; +const CREATE_ROLE_MAPPING_PATH = `${ROLE_MAPPINGS_PATH}/edit`; export const getEditRoleHref = (roleName: string) => - `#${EDIT_ROLES_PATH}/${encodeURIComponent(roleName)}`; + `#${ROLES_PATH}/edit/${encodeURIComponent(roleName)}`; export const getCreateRoleMappingHref = () => `#${CREATE_ROLE_MAPPING_PATH}`; diff --git a/x-pack/plugins/security/public/management/role_mappings/_index.scss b/x-pack/plugins/security/public/management/role_mappings/_index.scss new file mode 100644 index 0000000000000..bae6effcd2ec5 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/_index.scss @@ -0,0 +1 @@ +@import './edit_role_mapping/index'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.test.tsx b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.test.tsx similarity index 64% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.test.tsx index b826d68053e27..69142b1ad610e 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.test.tsx @@ -5,24 +5,15 @@ */ import React from 'react'; -import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { DeleteProvider } from '.'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; -import { RoleMapping } from '../../../../../../common/model'; import { EuiConfirmModal } from '@elastic/eui'; -import { findTestSubject } from 'test_utils/find_test_subject'; import { act } from '@testing-library/react'; -import { toastNotifications } from 'ui/notify'; - -jest.mock('ui/notify', () => { - return { - toastNotifications: { - addError: jest.fn(), - addSuccess: jest.fn(), - addDanger: jest.fn(), - }, - }; -}); +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { RoleMapping } from '../../../../../common/model'; +import { DeleteProvider } from '.'; + +import { roleMappingsAPIClientMock } from '../../index.mock'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; describe('DeleteProvider', () => { beforeEach(() => { @@ -30,17 +21,14 @@ describe('DeleteProvider', () => { }); it('allows a single role mapping to be deleted', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.deleteRoleMappings.mockResolvedValue([{ name: 'delete-me', success: true }]); + + const notifications = coreMock.createStart().notifications; + const props = { - roleMappingsAPI: ({ - deleteRoleMappings: jest.fn().mockReturnValue( - Promise.resolve([ - { - name: 'delete-me', - success: true, - }, - ]) - ), - } as unknown) as RoleMappingsAPI, + roleMappingsAPI, + notifications, }; const roleMappingsToDelete = [ @@ -79,11 +67,10 @@ describe('DeleteProvider', () => { expect(props.roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith(['delete-me']); - const notifications = toastNotifications as jest.Mocked; - expect(notifications.addError).toHaveBeenCalledTimes(0); - expect(notifications.addDanger).toHaveBeenCalledTimes(0); - expect(notifications.addSuccess).toHaveBeenCalledTimes(1); - expect(notifications.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` + expect(notifications.toasts.addError).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addDanger).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "data-test-subj": "deletedRoleMappingSuccessToast", @@ -94,21 +81,23 @@ describe('DeleteProvider', () => { }); it('allows multiple role mappings to be deleted', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.deleteRoleMappings.mockResolvedValue([ + { + name: 'delete-me', + success: true, + }, + { + name: 'delete-me-too', + success: true, + }, + ]); + + const notifications = coreMock.createStart().notifications; + const props = { - roleMappingsAPI: ({ - deleteRoleMappings: jest.fn().mockReturnValue( - Promise.resolve([ - { - name: 'delete-me', - success: true, - }, - { - name: 'delete-me-too', - success: true, - }, - ]) - ), - } as unknown) as RoleMappingsAPI, + roleMappingsAPI, + notifications, }; const roleMappingsToDelete = [ @@ -152,11 +141,11 @@ describe('DeleteProvider', () => { 'delete-me', 'delete-me-too', ]); - const notifications = toastNotifications as jest.Mocked; - expect(notifications.addError).toHaveBeenCalledTimes(0); - expect(notifications.addDanger).toHaveBeenCalledTimes(0); - expect(notifications.addSuccess).toHaveBeenCalledTimes(1); - expect(notifications.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` + + expect(notifications.toasts.addError).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addDanger).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "data-test-subj": "deletedRoleMappingSuccessToast", @@ -167,22 +156,24 @@ describe('DeleteProvider', () => { }); it('handles mixed success/failure conditions', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.deleteRoleMappings.mockResolvedValue([ + { + name: 'delete-me', + success: true, + }, + { + name: 'i-wont-work', + success: false, + error: new Error('something went wrong. sad.'), + }, + ]); + + const notifications = coreMock.createStart().notifications; + const props = { - roleMappingsAPI: ({ - deleteRoleMappings: jest.fn().mockReturnValue( - Promise.resolve([ - { - name: 'delete-me', - success: true, - }, - { - name: 'i-wont-work', - success: false, - error: new Error('something went wrong. sad.'), - }, - ]) - ), - } as unknown) as RoleMappingsAPI, + roleMappingsAPI, + notifications, }; const roleMappingsToDelete = [ @@ -223,10 +214,9 @@ describe('DeleteProvider', () => { 'i-wont-work', ]); - const notifications = toastNotifications as jest.Mocked; - expect(notifications.addError).toHaveBeenCalledTimes(0); - expect(notifications.addSuccess).toHaveBeenCalledTimes(1); - expect(notifications.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` + expect(notifications.toasts.addError).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addSuccess).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addSuccess.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { "data-test-subj": "deletedRoleMappingSuccessToast", @@ -235,8 +225,8 @@ describe('DeleteProvider', () => { ] `); - expect(notifications.addDanger).toHaveBeenCalledTimes(1); - expect(notifications.addDanger.mock.calls[0]).toMatchInlineSnapshot(` + expect(notifications.toasts.addDanger).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addDanger.mock.calls[0]).toMatchInlineSnapshot(` Array [ "Error deleting role mapping 'i-wont-work'", ] @@ -244,12 +234,13 @@ describe('DeleteProvider', () => { }); it('handles errors calling the API', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.deleteRoleMappings.mockRejectedValue(new Error('AHHHHH')); + + const notifications = coreMock.createStart().notifications; const props = { - roleMappingsAPI: ({ - deleteRoleMappings: jest.fn().mockImplementation(() => { - throw new Error('AHHHHH'); - }), - } as unknown) as RoleMappingsAPI, + roleMappingsAPI, + notifications, }; const roleMappingsToDelete = [ @@ -284,12 +275,11 @@ describe('DeleteProvider', () => { expect(props.roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith(['delete-me']); - const notifications = toastNotifications as jest.Mocked; - expect(notifications.addDanger).toHaveBeenCalledTimes(0); - expect(notifications.addSuccess).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addDanger).toHaveBeenCalledTimes(0); + expect(notifications.toasts.addSuccess).toHaveBeenCalledTimes(0); - expect(notifications.addError).toHaveBeenCalledTimes(1); - expect(notifications.addError.mock.calls[0]).toMatchInlineSnapshot(` + expect(notifications.toasts.addError).toHaveBeenCalledTimes(1); + expect(notifications.toasts.addError.mock.calls[0]).toMatchInlineSnapshot(` Array [ [Error: AHHHHH], Object { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.tsx b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.tsx rename to x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx index 2072cedeab462..860fe22cb8032 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/delete_provider.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/delete_provider.tsx @@ -6,13 +6,14 @@ import React, { Fragment, useRef, useState, ReactElement } from 'react'; import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; import { i18n } from '@kbn/i18n'; -import { RoleMapping } from '../../../../../../common/model'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; +import { NotificationsStart } from 'src/core/public'; +import { RoleMapping } from '../../../../../common/model'; +import { RoleMappingsAPIClient } from '../../role_mappings_api_client'; interface Props { - roleMappingsAPI: RoleMappingsAPI; + roleMappingsAPI: PublicMethodsOf; + notifications: NotificationsStart; children: (deleteMappings: DeleteRoleMappings) => ReactElement; } @@ -23,7 +24,11 @@ export type DeleteRoleMappings = ( type OnSuccessCallback = (deletedRoleMappings: string[]) => void; -export const DeleteProvider: React.FunctionComponent = ({ roleMappingsAPI, children }) => { +export const DeleteProvider: React.FunctionComponent = ({ + roleMappingsAPI, + children, + notifications, +}) => { const [roleMappings, setRoleMappings] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); const [isDeleteInProgress, setIsDeleteInProgress] = useState(false); @@ -55,7 +60,7 @@ export const DeleteProvider: React.FunctionComponent = ({ roleMappingsAPI try { result = await roleMappingsAPI.deleteRoleMappings(roleMappings.map(rm => rm.name)); } catch (e) { - toastNotifications.addError(e, { + notifications.toasts.addError(e, { title: i18n.translate( 'xpack.security.management.roleMappings.deleteRoleMapping.unknownError', { @@ -92,7 +97,7 @@ export const DeleteProvider: React.FunctionComponent = ({ roleMappingsAPI values: { name: successfulDeletes[0].name }, } ); - toastNotifications.addSuccess({ + notifications.toasts.addSuccess({ title: successMessage, 'data-test-subj': 'deletedRoleMappingSuccessToast', }); @@ -121,7 +126,7 @@ export const DeleteProvider: React.FunctionComponent = ({ roleMappingsAPI values: { name: erroredDeletes[0].name }, } ); - toastNotifications.addDanger(errorMessage); + notifications.toasts.addDanger(errorMessage); } }; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/index.ts b/x-pack/plugins/security/public/management/role_mappings/components/delete_provider/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/delete_provider/index.ts rename to x-pack/plugins/security/public/management/role_mappings/components/delete_provider/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/index.ts b/x-pack/plugins/security/public/management/role_mappings/components/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/index.ts rename to x-pack/plugins/security/public/management/role_mappings/components/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/index.ts b/x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/index.ts rename to x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx b/x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx similarity index 78% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx rename to x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx index 969832b3ecbae..5e14b0c179bfd 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/components/no_compatible_realms/no_compatible_realms.tsx @@ -7,9 +7,13 @@ import React from 'react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { documentationLinks } from '../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; -export const NoCompatibleRealms: React.FunctionComponent = () => ( +interface Props { + docLinks: DocumentationLinksService; +} + +export const NoCompatibleRealms: React.FunctionComponent = ({ docLinks }: Props) => ( ( defaultMessage="Role mappings will not be applied to any users. Contact your system administrator and refer to the {link} for more information." values={{ link: ( - + { + let rolesAPI: PublicMethodsOf; + beforeEach(() => { + rolesAPI = rolesAPIClientMock.create(); + (rolesAPI as jest.Mocked).getRoles.mockResolvedValue([ + { name: 'foo_role' }, + { name: 'bar role' }, + ] as Role[]); + }); + + it('allows a role mapping to be created', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.saveRoleMapping.mockResolvedValue(null); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + await nextTick(); + wrapper.update(); + + findTestSubject(wrapper, 'roleMappingFormNameInput').simulate('change', { + target: { value: 'my-role-mapping' }, + }); + + (wrapper + .find(EuiComboBox) + .filter('[data-test-subj="roleMappingFormRoleComboBox"]') + .props() as any).onChange([{ label: 'foo_role' }]); + + findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); + + findTestSubject(wrapper, 'saveRoleMappingButton').simulate('click'); + + expect(roleMappingsAPI.saveRoleMapping).toHaveBeenCalledWith({ + name: 'my-role-mapping', + enabled: true, + roles: ['foo_role'], + role_templates: [], + rules: { + all: [{ field: { username: '*' } }], + }, + metadata: {}, + }); + }); + + it('allows a role mapping to be updated', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.saveRoleMapping.mockResolvedValue(null); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + role_templates: [ + { + template: { id: 'foo' }, + }, + ], + enabled: true, + rules: { + any: [{ field: { 'metadata.someCustomOption': [false, true, 'asdf'] } }], + }, + metadata: { + foo: 'bar', + bar: 'baz', + }, + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + await nextTick(); + wrapper.update(); + + findTestSubject(wrapper, 'switchToRolesButton').simulate('click'); + + (wrapper + .find(EuiComboBox) + .filter('[data-test-subj="roleMappingFormRoleComboBox"]') + .props() as any).onChange([{ label: 'foo_role' }]); + + findTestSubject(wrapper, 'roleMappingsAddRuleButton').simulate('click'); + wrapper.find('button[id="addRuleOption"]').simulate('click'); + + findTestSubject(wrapper, 'saveRoleMappingButton').simulate('click'); + + expect(roleMappingsAPI.saveRoleMapping).toHaveBeenCalledWith({ + name: 'foo', + enabled: true, + roles: ['foo_role'], + role_templates: [], + rules: { + any: [ + { field: { 'metadata.someCustomOption': [false, true, 'asdf'] } }, + { field: { username: '*' } }, + ], + }, + metadata: { + foo: 'bar', + bar: 'baz', + }, + }); + }); + + it('renders a permission denied message when unauthorized to manage role mappings', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: false, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(PermissionDenied)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + expect(wrapper.find(PermissionDenied)).toHaveLength(1); + }); + + it('renders a warning when there are no compatible realms enabled', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: false, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); + }); + + it('renders a warning when editing a mapping with a stored role template, when stored scripts are disabled', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + role_templates: [ + { + template: { id: 'foo' }, + }, + ], + enabled: true, + rules: { + field: { username: '*' }, + }, + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: false, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(1); + }); + + it('renders a warning when editing a mapping with an inline role template, when inline scripts are disabled', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + role_templates: [ + { + template: { source: 'foo' }, + }, + ], + enabled: true, + rules: { + field: { username: '*' }, + }, + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: false, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(0); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(findTestSubject(wrapper, 'roleMappingInlineScriptsDisabled')).toHaveLength(1); + expect(findTestSubject(wrapper, 'roleMappingStoredScriptsDisabled')).toHaveLength(0); + }); + + it('renders the visual editor by default for simple rule sets', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + roles: ['superuser'], + enabled: true, + rules: { + all: [ + { + field: { + username: '*', + }, + }, + { + field: { + dn: null, + }, + }, + { + field: { + realm: ['ldap', 'pki', null, 12], + }, + }, + ], + }, + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); + expect(wrapper.find(JSONRuleEditor)).toHaveLength(0); + }); + + it('renders the JSON editor by default for complex rule sets', async () => { + const createRule = (depth: number): Record => { + if (depth > 0) { + const rule = { + all: [ + { + field: { + username: '*', + }, + }, + ], + } as Record; + + const subRule = createRule(depth - 1); + if (subRule) { + rule.all.push(subRule); + } + + return rule; + } + return null as any; + }; + + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMapping.mockResolvedValue({ + name: 'foo', + roles: ['superuser'], + enabled: true, + rules: createRule(10), + }); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + canUseInlineScripts: true, + canUseStoredScripts: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(VisualRuleEditor)).toHaveLength(0); + expect(wrapper.find(JSONRuleEditor)).toHaveLength(1); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx similarity index 87% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx index b8a75a4ad9fdf..142b53cbb50f2 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/edit_role_mapping_page.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/edit_role_mapping_page.tsx @@ -19,20 +19,21 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { toastNotifications } from 'ui/notify'; -import { RoleMapping } from '../../../../../../common/model'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; +import { NotificationsStart } from 'src/core/public'; +import { RoleMapping } from '../../../../common/model'; import { RuleEditorPanel } from './rule_editor_panel'; import { NoCompatibleRealms, PermissionDenied, DeleteProvider, SectionLoading, -} from '../../components'; -import { ROLE_MAPPINGS_PATH } from '../../../management_urls'; -import { validateRoleMappingForSave } from '../services/role_mapping_validation'; +} from '../components'; +import { RolesAPIClient } from '../../roles'; +import { ROLE_MAPPINGS_PATH } from '../../management_urls'; +import { validateRoleMappingForSave } from './services/role_mapping_validation'; import { MappingInfoPanel } from './mapping_info_panel'; -import { documentationLinks } from '../../services/documentation_links'; +import { DocumentationLinksService } from '../documentation_links'; +import { RoleMappingsAPIClient } from '../role_mappings_api_client'; interface State { loadState: 'loading' | 'permissionDenied' | 'ready' | 'saveInProgress'; @@ -50,7 +51,10 @@ interface State { interface Props { name?: string; - roleMappingsAPI: RoleMappingsAPI; + roleMappingsAPI: PublicMethodsOf; + rolesAPIClient: PublicMethodsOf; + notifications: NotificationsStart; + docLinks: DocumentationLinksService; } export class EditRoleMappingPage extends Component { @@ -74,6 +78,12 @@ export class EditRoleMappingPage extends Component { this.loadAppData(); } + public async componentDidUpdate(prevProps: Props) { + if (prevProps.name !== this.props.name) { + await this.loadAppData(); + } + } + public render() { const { loadState } = this.state; @@ -101,6 +111,8 @@ export class EditRoleMappingPage extends Component { validateForm={this.state.validateForm} canUseInlineScripts={this.state.canUseInlineScripts} canUseStoredScripts={this.state.canUseStoredScripts} + rolesAPIClient={this.props.rolesAPIClient} + docLinks={this.props.docLinks} /> { }, }) } + docLinks={this.props.docLinks} /> {this.getFormButtons()} @@ -149,7 +162,7 @@ export class EditRoleMappingPage extends Component { values={{ learnMoreLink: ( @@ -166,7 +179,7 @@ export class EditRoleMappingPage extends Component { {!this.state.hasCompatibleRealms && ( <> - + )} @@ -201,7 +214,10 @@ export class EditRoleMappingPage extends Component { {this.editingExistingRoleMapping() && ( - + {deleteRoleMappingsPrompt => { return ( { this.props.roleMappingsAPI .saveRoleMapping(this.state.roleMapping) .then(() => { - toastNotifications.addSuccess({ + this.props.notifications.toasts.addSuccess({ title: i18n.translate('xpack.security.management.editRoleMapping.saveSuccess', { defaultMessage: `Saved role mapping '{roleMappingName}'`, values: { @@ -264,7 +280,7 @@ export class EditRoleMappingPage extends Component { this.backToRoleMappingsList(); }) .catch(e => { - toastNotifications.addError(e, { + this.props.notifications.toasts.addError(e, { title: i18n.translate('xpack.security.management.editRoleMapping.saveError', { defaultMessage: `Error saving role mapping`, }), @@ -312,7 +328,7 @@ export class EditRoleMappingPage extends Component { roleMapping, }); } catch (e) { - toastNotifications.addDanger({ + this.props.notifications.toasts.addDanger({ title: i18n.translate( 'xpack.security.management.editRoleMapping.table.fetchingRoleMappingsErrorMessage', { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/index.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/index.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/index.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/index.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx similarity index 82% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx index d821b33ace6a7..9b62ca27ca569 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.test.tsx @@ -6,21 +6,27 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { MappingInfoPanel } from '.'; -import { RoleMapping } from '../../../../../../../common/model'; import { findTestSubject } from 'test_utils/find_test_subject'; +import { Role, RoleMapping } from '../../../../../common/model'; +import { RolesAPIClient } from '../../../roles'; +import { DocumentationLinksService } from '../../documentation_links'; import { RoleSelector } from '../role_selector'; import { RoleTemplateEditor } from '../role_selector/role_template_editor'; +import { MappingInfoPanel } from '.'; -jest.mock('../../../../../../lib/roles_api', () => { - return { - RolesApi: { - getRoles: () => Promise.resolve([{ name: 'foo_role' }, { name: 'bar role' }]), - }, - }; -}); +import { rolesAPIClientMock } from '../../../roles/roles_api_client.mock'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; describe('MappingInfoPanel', () => { + let rolesAPI: PublicMethodsOf; + beforeEach(() => { + rolesAPI = rolesAPIClientMock.create(); + (rolesAPI as jest.Mocked).getRoles.mockResolvedValue([ + { name: 'foo_role' }, + { name: 'bar role' }, + ] as Role[]); + }); + it('renders when creating a role mapping, default to the "roles" view', () => { const props = { roleMapping: { @@ -32,6 +38,8 @@ describe('MappingInfoPanel', () => { metadata: {}, } as RoleMapping, mode: 'create', + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, } as MappingInfoPanel['props']; const wrapper = mountWithIntl(); @@ -77,6 +85,8 @@ describe('MappingInfoPanel', () => { metadata: {}, } as RoleMapping, mode: 'edit', + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, } as MappingInfoPanel['props']; const wrapper = mountWithIntl(); @@ -101,6 +111,8 @@ describe('MappingInfoPanel', () => { canUseInlineScripts: true, canUseStoredScripts: false, validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, }; const wrapper = mountWithIntl(); @@ -140,6 +152,8 @@ describe('MappingInfoPanel', () => { canUseInlineScripts: false, canUseStoredScripts: true, validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, }; const wrapper = mountWithIntl(); @@ -179,6 +193,8 @@ describe('MappingInfoPanel', () => { canUseInlineScripts: false, canUseStoredScripts: false, validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, }; const wrapper = mountWithIntl(); @@ -202,6 +218,8 @@ describe('MappingInfoPanel', () => { metadata: {}, } as RoleMapping, mode: 'edit', + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), + rolesAPIClient: rolesAPI, } as MappingInfoPanel['props']; const wrapper = mountWithIntl(); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx index a02b4fc1709f0..02af6bfbafa7e 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/mapping_info_panel/mapping_info_panel.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/mapping_info_panel/mapping_info_panel.tsx @@ -18,14 +18,15 @@ import { EuiSwitch, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { RoleMapping } from '../../../../../../../common/model'; +import { RoleMapping } from '../../../../../common/model'; +import { RolesAPIClient } from '../../../roles'; import { validateRoleMappingName, validateRoleMappingRoles, validateRoleMappingRoleTemplates, -} from '../../services/role_mapping_validation'; +} from '../services/role_mapping_validation'; import { RoleSelector } from '../role_selector'; -import { documentationLinks } from '../../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; interface Props { roleMapping: RoleMapping; @@ -34,6 +35,8 @@ interface Props { validateForm: boolean; canUseInlineScripts: boolean; canUseStoredScripts: boolean; + rolesAPIClient: PublicMethodsOf; + docLinks: DocumentationLinksService; } interface State { @@ -163,6 +166,7 @@ export class MappingInfoPanel extends Component { > { defaultMessage="Create templates that describe the roles to assign to your users." />{' '} @@ -230,6 +234,7 @@ export class MappingInfoPanel extends Component { > { - return { - RolesApi: { - getRoles: () => Promise.resolve([{ name: 'foo_role' }, { name: 'bar role' }]), - }, - }; -}); +import { RolesAPIClient } from '../../../roles'; +import { rolesAPIClientMock } from '../../../roles/roles_api_client.mock'; describe('RoleSelector', () => { + let rolesAPI: PublicMethodsOf; + beforeEach(() => { + rolesAPI = rolesAPIClientMock.create(); + (rolesAPI as jest.Mocked).getRoles.mockResolvedValue([ + { name: 'foo_role' }, + { name: 'bar role' }, + ] as Role[]); + }); + it('allows roles to be selected, removing any previously selected role templates', () => { const props = { roleMapping: { @@ -36,6 +39,7 @@ describe('RoleSelector', () => { canUseInlineScripts: true, onChange: jest.fn(), mode: 'roles', + rolesAPIClient: rolesAPI, } as RoleSelector['props']; const wrapper = mountWithIntl(); @@ -57,6 +61,7 @@ describe('RoleSelector', () => { canUseInlineScripts: true, onChange: jest.fn(), mode: 'templates', + rolesAPIClient: rolesAPI, } as RoleSelector['props']; const wrapper = mountWithIntl(); @@ -87,6 +92,7 @@ describe('RoleSelector', () => { canUseInlineScripts: true, onChange: jest.fn(), mode: 'templates', + rolesAPIClient: rolesAPI, } as RoleSelector['props']; const wrapper = mountWithIntl(); @@ -122,6 +128,7 @@ describe('RoleSelector', () => { canUseInlineScripts: true, onChange: jest.fn(), mode: 'templates', + rolesAPIClient: rolesAPI, } as RoleSelector['props']; const wrapper = mountWithIntl(); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_selector.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_selector.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx index 6b92d6b4907f1..992c2741ae93e 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_selector.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_selector.tsx @@ -7,12 +7,13 @@ import React, { Fragment } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiComboBox, EuiFormRow, EuiHorizontalRule } from '@elastic/eui'; -import { RoleMapping, Role } from '../../../../../../../common/model'; -import { RolesApi } from '../../../../../../lib/roles_api'; +import { RoleMapping, Role } from '../../../../../common/model'; +import { RolesAPIClient } from '../../../roles'; import { AddRoleTemplateButton } from './add_role_template_button'; import { RoleTemplateEditor } from './role_template_editor'; interface Props { + rolesAPIClient: PublicMethodsOf; roleMapping: RoleMapping; canUseInlineScripts: boolean; canUseStoredScripts: boolean; @@ -32,7 +33,7 @@ export class RoleSelector extends React.Component { } public async componentDidMount() { - const roles = await RolesApi.getRoles(); + const roles = await this.props.rolesAPIClient.getRoles(); this.setState({ roles }); } diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.test.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.test.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.tsx similarity index 98% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.tsx index 4b8d34d271996..d79651d7b9cd6 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_editor.tsx @@ -18,12 +18,12 @@ import { EuiSpacer, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { RoleTemplate } from '../../../../../../../common/model'; +import { RoleTemplate } from '../../../../../common/model'; import { isInlineRoleTemplate, isStoredRoleTemplate, isInvalidRoleTemplate, -} from '../../services/role_template_type'; +} from '../services/role_template_type'; import { RoleTemplateTypeSelect } from './role_template_type_select'; interface Props { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_type_select.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_type_select.tsx similarity index 92% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_type_select.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_type_select.tsx index 4a06af0fb436b..aa65c5c9bcae7 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/role_selector/role_template_type_select.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/role_selector/role_template_type_select.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiComboBox } from '@elastic/eui'; -import { RoleTemplate } from '../../../../../../../common/model'; -import { isInlineRoleTemplate, isStoredRoleTemplate } from '../../services/role_template_type'; +import { RoleTemplate } from '../../../../../common/model'; +import { isInlineRoleTemplate, isStoredRoleTemplate } from '../services/role_template_type'; const templateTypeOptions = [ { diff --git a/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_index.scss b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_index.scss new file mode 100644 index 0000000000000..c3b2764e64713 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_index.scss @@ -0,0 +1 @@ +@import './rule_editor_group'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/_index.scss b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_rule_editor_group.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/_index.scss rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/_rule_editor_group.scss diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.test.tsx similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.test.tsx index 917b822acef3f..d1411bd9bf2b9 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { AddRuleButton } from './add_rule_button'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { findTestSubject } from 'test_utils/find_test_subject'; -import { FieldRule, AllRule } from '../../../model'; +import { FieldRule, AllRule } from '../../model'; describe('AddRuleButton', () => { it('allows a field rule to be created', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.tsx similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.tsx index 100c0dd3eeaee..9696fa337a74f 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/add_rule_button.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/add_rule_button.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { EuiButtonEmpty, EuiPopover, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Rule, FieldRule, AllRule } from '../../../model'; +import { Rule, FieldRule, AllRule } from '../../model'; interface Props { onClick: (newRule: Rule) => void; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.test.tsx index 8d5d5c99ee99d..5374f4625336d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { FieldRuleEditor } from './field_rule_editor'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { FieldRule } from '../../../model'; +import { FieldRule } from '../../model'; import { findTestSubject } from 'test_utils/find_test_subject'; import { ReactWrapper } from 'enzyme'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.tsx index 52cf70dbd12bd..2506c18dcaf3a 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/field_rule_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/field_rule_editor.tsx @@ -18,7 +18,7 @@ import { EuiIcon, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { FieldRule, FieldRuleValue } from '../../../model'; +import { FieldRule, FieldRuleValue } from '../../model'; interface Props { rule: FieldRule; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/index.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/index.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/index.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/index.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx index 8a9b37ab0f406..263c456e901cd 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.test.tsx @@ -17,7 +17,10 @@ import { act } from 'react-dom/test-utils'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { JSONRuleEditor } from './json_rule_editor'; import { EuiCodeEditor } from '@elastic/eui'; -import { AllRule, AnyRule, FieldRule, ExceptAnyRule, ExceptAllRule } from '../../../model'; +import { AllRule, AnyRule, FieldRule, ExceptAnyRule, ExceptAllRule } from '../../model'; +import { DocumentationLinksService } from '../../documentation_links'; + +import { coreMock } from '../../../../../../../../src/core/public/mocks'; describe('JSONRuleEditor', () => { it('renders an empty rule set', () => { @@ -25,6 +28,7 @@ describe('JSONRuleEditor', () => { rules: null, onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -46,6 +50,7 @@ describe('JSONRuleEditor', () => { ]), onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -79,6 +84,7 @@ describe('JSONRuleEditor', () => { rules: null, onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -100,6 +106,7 @@ describe('JSONRuleEditor', () => { rules: null, onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -130,6 +137,7 @@ describe('JSONRuleEditor', () => { rules: null, onChange: jest.fn(), onValidityChange: jest.fn(), + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx index 371fb59f7a5d1..e7a9149513d20 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/json_rule_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/json_rule_editor.tsx @@ -11,13 +11,14 @@ import 'brace/theme/github'; import { EuiCodeEditor, EuiFormRow, EuiButton, EuiSpacer, EuiLink, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { Rule, RuleBuilderError, generateRulesFromRaw } from '../../../model'; -import { documentationLinks } from '../../../services/documentation_links'; +import { DocumentationLinksService } from '../../documentation_links'; +import { Rule, RuleBuilderError, generateRulesFromRaw } from '../../model'; interface Props { rules: Rule | null; onChange: (updatedRules: Rule | null) => void; onValidityChange: (isValid: boolean) => void; + docLinks: DocumentationLinksService; } export const JSONRuleEditor = (props: Props) => { @@ -107,7 +108,7 @@ export const JSONRuleEditor = (props: Props) => { values={{ roleMappingAPI: ( diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx similarity index 88% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx index 809264183d30c..b9c650cc1f77a 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.test.tsx @@ -15,8 +15,11 @@ import { findTestSubject } from 'test_utils/find_test_subject'; // This is not required for the tests to pass, but it rather suppresses lengthy // warnings in the console which adds unnecessary noise to the test output. import 'test_utils/stub_web_worker'; -import { AllRule, FieldRule } from '../../../model'; +import { AllRule, FieldRule } from '../../model'; import { EuiErrorBoundary } from '@elastic/eui'; +import { DocumentationLinksService } from '../../documentation_links'; + +import { coreMock } from '../../../../../../../../src/core/public/mocks'; describe('RuleEditorPanel', () => { it('renders the visual editor when no rules are defined', () => { @@ -25,6 +28,7 @@ describe('RuleEditorPanel', () => { onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); @@ -45,6 +49,7 @@ describe('RuleEditorPanel', () => { onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); expect(wrapper.find(VisualRuleEditor)).toHaveLength(1); @@ -68,6 +73,7 @@ describe('RuleEditorPanel', () => { onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); @@ -103,6 +109,7 @@ describe('RuleEditorPanel', () => { onChange: jest.fn(), onValidityChange: jest.fn(), validateForm: false, + docLinks: new DocumentationLinksService(coreMock.createStart().docLinks), }; const wrapper = mountWithIntl(); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx index 4aab49b2b2efc..6e6641caa1f39 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_editor_panel.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_editor_panel.tsx @@ -22,19 +22,20 @@ import { } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; -import { RoleMapping } from '../../../../../../../common/model'; +import { RoleMapping } from '../../../../../common/model'; import { VisualRuleEditor } from './visual_rule_editor'; import { JSONRuleEditor } from './json_rule_editor'; -import { VISUAL_MAX_RULE_DEPTH } from '../../services/role_mapping_constants'; -import { Rule, generateRulesFromRaw } from '../../../model'; -import { validateRoleMappingRules } from '../../services/role_mapping_validation'; -import { documentationLinks } from '../../../services/documentation_links'; +import { VISUAL_MAX_RULE_DEPTH } from '../services/role_mapping_constants'; +import { Rule, generateRulesFromRaw } from '../../model'; +import { DocumentationLinksService } from '../../documentation_links'; +import { validateRoleMappingRules } from '../services/role_mapping_validation'; interface Props { rawRules: RoleMapping['rules']; onChange: (rawRules: RoleMapping['rules']) => void; onValidityChange: (isValid: boolean) => void; validateForm: boolean; + docLinks: DocumentationLinksService; } interface State { @@ -91,7 +92,7 @@ export class RuleEditorPanel extends Component { values={{ learnMoreLink: ( @@ -214,6 +215,7 @@ export class RuleEditorPanel extends Component { rules={this.state.rules} onChange={this.onRuleChange} onValidityChange={this.onValidityChange} + docLinks={this.props.docLinks} /> ); default: diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.test.tsx index 3e0e0e386e98c..5946aac4306b1 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { RuleGroupEditor } from './rule_group_editor'; import { shallowWithIntl, mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; -import { AllRule, FieldRule, AnyRule, ExceptAnyRule } from '../../../model'; +import { AllRule, FieldRule, AnyRule, ExceptAnyRule } from '../../model'; import { FieldRuleEditor } from './field_rule_editor'; import { AddRuleButton } from './add_rule_button'; import { EuiContextMenuItem } from '@elastic/eui'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx index 6fb33db179e8a..c17a853a65467 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_editor.tsx @@ -16,8 +16,8 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { AddRuleButton } from './add_rule_button'; import { RuleGroupTitle } from './rule_group_title'; import { FieldRuleEditor } from './field_rule_editor'; -import { RuleGroup, Rule, FieldRule } from '../../../model'; -import { isRuleGroup } from '../../services/is_rule_group'; +import { RuleGroup, Rule, FieldRule } from '../../model'; +import { isRuleGroup } from '../services/is_rule_group'; interface Props { rule: RuleGroup; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_title.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_title.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx index e46893afd4d86..6bef9c09eeef3 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/rule_group_title.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/rule_group_title.tsx @@ -15,14 +15,7 @@ import { EuiConfirmModal, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { - RuleGroup, - AllRule, - AnyRule, - ExceptAllRule, - ExceptAnyRule, - FieldRule, -} from '../../../model'; +import { RuleGroup, AllRule, AnyRule, ExceptAllRule, ExceptAnyRule, FieldRule } from '../../model'; interface Props { rule: RuleGroup; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.test.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.test.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.test.tsx index 7c63613ee1cc9..b40f0063acb08 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.test.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { VisualRuleEditor } from './visual_rule_editor'; import { findTestSubject } from 'test_utils/find_test_subject'; -import { AnyRule, AllRule, FieldRule, ExceptAnyRule, ExceptAllRule } from '../../../model'; +import { AnyRule, AllRule, FieldRule, ExceptAnyRule, ExceptAllRule } from '../../model'; import { RuleGroupEditor } from './rule_group_editor'; import { FieldRuleEditor } from './field_rule_editor'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.tsx b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.tsx rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.tsx index 214c583189fb8..2e3db275788ee 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/components/rule_editor_panel/visual_rule_editor.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/rule_editor_panel/visual_rule_editor.tsx @@ -9,9 +9,9 @@ import { EuiEmptyPrompt, EuiCallOut, EuiSpacer, EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { FieldRuleEditor } from './field_rule_editor'; import { RuleGroupEditor } from './rule_group_editor'; -import { VISUAL_MAX_RULE_DEPTH } from '../../services/role_mapping_constants'; -import { Rule, FieldRule, RuleGroup, AllRule } from '../../../model'; -import { isRuleGroup } from '../../services/is_rule_group'; +import { VISUAL_MAX_RULE_DEPTH } from '../services/role_mapping_constants'; +import { Rule, FieldRule, RuleGroup, AllRule } from '../../model'; +import { isRuleGroup } from '../services/is_rule_group'; interface Props { rules: Rule | null; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/is_rule_group.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/is_rule_group.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/is_rule_group.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/is_rule_group.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_constants.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts similarity index 98% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts index 9614c4338b631..0c3f988ae6b10 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.test.ts @@ -10,7 +10,7 @@ import { validateRoleMappingRules, validateRoleMappingForSave, } from './role_mapping_validation'; -import { RoleMapping } from '../../../../../../common/model'; +import { RoleMapping } from '../../../../../common/model'; describe('validateRoleMappingName', () => { it('requires a value', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts similarity index 97% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts index 5916d6fd9e189..7695f1da14881 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_mapping_validation.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { RoleMapping } from '../../../../../../common/model'; +import { RoleMapping } from '../../../../../common/model'; import { generateRulesFromRaw } from '../../model'; interface ValidationResult { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts index 8e1f47a4157ae..c093bb1b3fbbc 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.test.ts @@ -9,7 +9,7 @@ import { isInlineRoleTemplate, isInvalidRoleTemplate, } from './role_template_type'; -import { RoleTemplate } from '../../../../../../common/model'; +import { RoleTemplate } from '../../../../../common/model'; describe('#isStoredRoleTemplate', () => { it('returns true for stored templates, false otherwise', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.ts b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.ts similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.ts rename to x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.ts index 90d8d1a09e587..5e646535a6c4d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/edit_role_mapping/services/role_template_type.ts +++ b/x-pack/plugins/security/public/management/role_mappings/edit_role_mapping/services/role_template_type.ts @@ -9,7 +9,7 @@ import { StoredRoleTemplate, InlineRoleTemplate, InvalidRoleTemplate, -} from '../../../../../../common/model'; +} from '../../../../../common/model'; export function isStoredRoleTemplate( roleMappingTemplate: RoleTemplate diff --git a/x-pack/plugins/security/public/management/role_mappings/index.mock.ts b/x-pack/plugins/security/public/management/role_mappings/index.mock.ts new file mode 100644 index 0000000000000..826477a1a5b15 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/index.mock.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { roleMappingsAPIClientMock } from './role_mappings_api_client.mock'; diff --git a/x-pack/plugins/security/public/management/role_mappings/index.ts b/x-pack/plugins/security/public/management/role_mappings/index.ts new file mode 100644 index 0000000000000..f670ea6181038 --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { roleMappingsManagementApp } from './role_mappings_management_app'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap b/x-pack/plugins/security/public/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap rename to x-pack/plugins/security/public/management/role_mappings/model/__snapshots__/rule_builder.test.ts.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/all_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/all_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/all_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/all_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/all_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/any_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/any_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/any_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/any_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/any_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/except_all_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/except_all_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/except_all_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_all_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/except_all_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/except_any_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/except_any_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/except_any_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/except_any_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/except_any_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/field_rule.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/field_rule.test.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/field_rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/field_rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/field_rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/index.ts b/x-pack/plugins/security/public/management/role_mappings/model/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/index.ts rename to x-pack/plugins/security/public/management/role_mappings/model/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.test.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder.test.ts similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.test.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule_builder.test.ts index ebd48f6d15d99..ad486a8b314c4 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.test.ts +++ b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder.test.ts @@ -5,7 +5,7 @@ */ import { generateRulesFromRaw, FieldRule } from '.'; -import { RoleMapping } from '../../../../../common/model'; +import { RoleMapping } from '../../../../common/model'; import { RuleBuilderError } from './rule_builder_error'; describe('generateRulesFromRaw', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder.ts similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule_builder.ts index fe344b2ae38dd..a384e61e521ab 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder.ts +++ b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { RoleMapping } from '../../../../../common/model'; +import { RoleMapping } from '../../../../common/model'; import { FieldRule, FieldRuleValue } from './field_rule'; import { AllRule } from './all_rule'; import { AnyRule } from './any_rule'; diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder_error.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule_builder_error.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_builder_error.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule_builder_error.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_group.ts b/x-pack/plugins/security/public/management/role_mappings/model/rule_group.ts similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_group.ts rename to x-pack/plugins/security/public/management/role_mappings/model/rule_group.ts index 3e1e7fad9b36f..5077c79a543c4 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/model/rule_group.ts +++ b/x-pack/plugins/security/public/management/role_mappings/model/rule_group.ts @@ -7,7 +7,7 @@ import { Rule } from './rule'; /** - * Represents a catagory of Role Mapping rules which are capable of containing other rules. + * Represents a category of Role Mapping rules which are capable of containing other rules. */ export abstract class RuleGroup extends Rule { /** diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.mock.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.mock.ts new file mode 100644 index 0000000000000..07d583d1e983f --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const roleMappingsAPIClientMock = { + create: () => ({ + checkRoleMappingFeatures: jest.fn(), + getRoleMappings: jest.fn(), + getRoleMapping: jest.fn(), + saveRoleMapping: jest.fn(), + deleteRoleMappings: jest.fn(), + }), +}; diff --git a/x-pack/legacy/plugins/security/public/lib/role_mappings_api.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.ts similarity index 89% rename from x-pack/legacy/plugins/security/public/lib/role_mappings_api.ts rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.ts index b8bcba91388b5..0a88ed1da9ac3 100644 --- a/x-pack/legacy/plugins/security/public/lib/role_mappings_api.ts +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_api_client.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreSetup } from 'src/core/public'; -import { RoleMapping } from '../../common/model'; +import { HttpStart } from 'src/core/public'; +import { RoleMapping } from '../../../common/model'; interface CheckRoleMappingFeaturesResponse { canManageRoleMappings: boolean; @@ -20,8 +20,8 @@ type DeleteRoleMappingsResponse = Array<{ error?: Error; }>; -export class RoleMappingsAPI { - constructor(private readonly http: CoreSetup['http']) {} +export class RoleMappingsAPIClient { + constructor(private readonly http: HttpStart) {} public async checkRoleMappingFeatures(): Promise { return this.http.get(`/internal/security/_check_role_mapping_features`); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/create_role_mapping_button.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/create_role_mapping_button.tsx similarity index 90% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/create_role_mapping_button.tsx rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/create_role_mapping_button.tsx index 2342eeb97d03e..6fe4bcc7a0bbb 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/create_role_mapping_button.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/create_role_mapping_button.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiButton } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { getCreateRoleMappingHref } from '../../../../management_urls'; +import { getCreateRoleMappingHref } from '../../../management_urls'; export const CreateRoleMappingButton = () => { return ( diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/index.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/create_role_mapping_button/index.ts rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/create_role_mapping_button/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/empty_prompt.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/empty_prompt/empty_prompt.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/empty_prompt.tsx rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/empty_prompt/empty_prompt.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/index.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/empty_prompt/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/empty_prompt/index.ts rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/empty_prompt/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/index.ts b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/index.ts rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/index.ts diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx new file mode 100644 index 0000000000000..de0722b4cd85e --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.test.tsx @@ -0,0 +1,219 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { RoleMappingsGridPage } from '.'; +import { SectionLoading, PermissionDenied, NoCompatibleRealms } from '../components'; +import { EmptyPrompt } from './empty_prompt'; +import { findTestSubject } from 'test_utils/find_test_subject'; +import { EuiLink } from '@elastic/eui'; +import { act } from '@testing-library/react'; +import { DocumentationLinksService } from '../documentation_links'; + +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { roleMappingsAPIClientMock } from '../role_mappings_api_client.mock'; + +describe('RoleMappingsGridPage', () => { + it('renders an empty prompt when no role mappings exist', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(EmptyPrompt)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + expect(wrapper.find(EmptyPrompt)).toHaveLength(1); + }); + + it('renders a permission denied message when unauthorized to manage role mappings', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: false, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(PermissionDenied)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + expect(wrapper.find(PermissionDenied)).toHaveLength(1); + }); + + it('renders a warning when there are no compatible realms enabled', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([ + { + name: 'some realm', + enabled: true, + roles: [], + rules: { field: { username: '*' } }, + }, + ]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: false, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + expect(wrapper.find(SectionLoading)).toHaveLength(1); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(0); + + await nextTick(); + wrapper.update(); + + expect(wrapper.find(SectionLoading)).toHaveLength(0); + expect(wrapper.find(NoCompatibleRealms)).toHaveLength(1); + }); + + it('renders links to mapped roles', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([ + { + name: 'some realm', + enabled: true, + roles: ['superuser'], + rules: { field: { username: '*' } }, + }, + ]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + await nextTick(); + wrapper.update(); + + const links = findTestSubject(wrapper, 'roleMappingRoles').find(EuiLink); + expect(links).toHaveLength(1); + expect(links.at(0).props()).toMatchObject({ + href: '#/management/security/roles/edit/superuser', + }); + }); + + it('describes the number of mapped role templates', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([ + { + name: 'some realm', + enabled: true, + role_templates: [{}, {}], + rules: { field: { username: '*' } }, + }, + ]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + await nextTick(); + wrapper.update(); + + const templates = findTestSubject(wrapper, 'roleMappingRoles'); + expect(templates).toHaveLength(1); + expect(templates.text()).toEqual(`2 role templates defined`); + }); + + it('allows role mappings to be deleted, refreshing the grid after', async () => { + const roleMappingsAPI = roleMappingsAPIClientMock.create(); + roleMappingsAPI.getRoleMappings.mockResolvedValue([ + { + name: 'some-realm', + enabled: true, + roles: ['superuser'], + rules: { field: { username: '*' } }, + }, + ]); + roleMappingsAPI.checkRoleMappingFeatures.mockResolvedValue({ + canManageRoleMappings: true, + hasCompatibleRealms: true, + }); + roleMappingsAPI.deleteRoleMappings.mockResolvedValue([ + { + name: 'some-realm', + success: true, + }, + ]); + + const { docLinks, notifications } = coreMock.createStart(); + const wrapper = mountWithIntl( + + ); + await nextTick(); + wrapper.update(); + + expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(1); + expect(roleMappingsAPI.deleteRoleMappings).not.toHaveBeenCalled(); + + findTestSubject(wrapper, `deleteRoleMappingButton-some-realm`).simulate('click'); + expect(findTestSubject(wrapper, 'deleteRoleMappingConfirmationModal')).toHaveLength(1); + + await act(async () => { + findTestSubject(wrapper, 'confirmModalConfirmButton').simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(roleMappingsAPI.deleteRoleMappings).toHaveBeenCalledWith(['some-realm']); + // Expect an additional API call to refresh the grid + expect(roleMappingsAPI.getRoleMappings).toHaveBeenCalledTimes(2); + }); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.tsx rename to x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx index 7b23f2288d1ba..feb918cb6b301 100644 --- a/x-pack/legacy/plugins/security/public/views/management/role_mappings/role_mappings_grid/components/role_mappings_grid_page.tsx +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_grid/role_mappings_grid_page.tsx @@ -25,24 +25,27 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { RoleMapping } from '../../../../../../common/model'; -import { RoleMappingsAPI } from '../../../../../lib/role_mappings_api'; +import { NotificationsStart } from 'src/core/public'; +import { RoleMapping } from '../../../../common/model'; import { EmptyPrompt } from './empty_prompt'; import { NoCompatibleRealms, DeleteProvider, PermissionDenied, SectionLoading, -} from '../../components'; -import { documentationLinks } from '../../services/documentation_links'; +} from '../components'; import { getCreateRoleMappingHref, getEditRoleMappingHref, getEditRoleHref, -} from '../../../management_urls'; +} from '../../management_urls'; +import { DocumentationLinksService } from '../documentation_links'; +import { RoleMappingsAPIClient } from '../role_mappings_api_client'; interface Props { - roleMappingsAPI: RoleMappingsAPI; + roleMappingsAPI: PublicMethodsOf; + notifications: NotificationsStart; + docLinks: DocumentationLinksService; } interface State { @@ -140,7 +143,7 @@ export class RoleMappingsGridPage extends Component { values={{ learnMoreLink: ( @@ -168,7 +171,7 @@ export class RoleMappingsGridPage extends Component { {!this.state.hasCompatibleRealms && ( <> - + )} @@ -214,7 +217,10 @@ export class RoleMappingsGridPage extends Component { const search = { toolsLeft: selectedItems.length ? ( - + {deleteRoleMappingsPrompt => { return ( { return ( - + {deleteRoleMappingPrompt => { return ( ({ + RoleMappingsGridPage: (props: any) => `Role Mappings Page: ${JSON.stringify(props)}`, +})); + +jest.mock('./edit_role_mapping', () => ({ + EditRoleMappingPage: (props: any) => `Role Mapping Edit Page: ${JSON.stringify(props)}`, +})); + +import { roleMappingsManagementApp } from './role_mappings_management_app'; + +import { coreMock } from '../../../../../../src/core/public/mocks'; + +async function mountApp(basePath: string) { + const container = document.createElement('div'); + const setBreadcrumbs = jest.fn(); + + const unmount = await roleMappingsManagementApp + .create({ getStartServices: coreMock.createSetup().getStartServices as any }) + .mount({ basePath, element: container, setBreadcrumbs }); + + return { unmount, container, setBreadcrumbs }; +} + +describe('roleMappingsManagementApp', () => { + it('create() returns proper management app descriptor', () => { + expect( + roleMappingsManagementApp.create({ + getStartServices: coreMock.createSetup().getStartServices as any, + }) + ).toMatchInlineSnapshot(` + Object { + "id": "role_mappings", + "mount": [Function], + "order": 40, + "title": "Role Mappings", + } + `); + }); + + it('mount() works for the `grid` page', async () => { + const basePath = '/some-base-path/role_mappings'; + window.location.hash = basePath; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `#${basePath}`, text: 'Role Mappings' }]); + expect(container).toMatchInlineSnapshot(` +
+ Role Mappings Page: {"notifications":{"toasts":{}},"roleMappingsAPI":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `create role mapping` page', async () => { + const basePath = '/some-base-path/role_mappings'; + window.location.hash = `${basePath}/edit`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Role Mappings' }, + { text: 'Create' }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Mapping Edit Page: {"roleMappingsAPI":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `edit role mapping` page', async () => { + const basePath = '/some-base-path/role_mappings'; + const roleMappingName = 'someRoleMappingName'; + window.location.hash = `${basePath}/edit/${roleMappingName}`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Role Mappings' }, + { href: `#/some-base-path/role_mappings/edit/${roleMappingName}`, text: roleMappingName }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Mapping Edit Page: {"name":"someRoleMappingName","roleMappingsAPI":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() properly encodes role mapping name in `edit role mapping` page link in breadcrumbs', async () => { + const basePath = '/some-base-path/role_mappings'; + const roleMappingName = 'some 安全性 role mapping'; + window.location.hash = `${basePath}/edit/${roleMappingName}`; + + const { setBreadcrumbs } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Role Mappings' }, + { + href: + '#/some-base-path/role_mappings/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20role%20mapping', + text: roleMappingName, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx new file mode 100644 index 0000000000000..af1572cedbade --- /dev/null +++ b/x-pack/plugins/security/public/management/role_mappings/role_mappings_management_app.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { HashRouter as Router, Route, Switch, useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup } from 'src/core/public'; +import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { PluginStartDependencies } from '../../plugin'; +import { RolesAPIClient } from '../roles'; +import { RoleMappingsAPIClient } from './role_mappings_api_client'; +import { DocumentationLinksService } from './documentation_links'; +import { RoleMappingsGridPage } from './role_mappings_grid'; +import { EditRoleMappingPage } from './edit_role_mapping'; + +interface CreateParams { + getStartServices: CoreSetup['getStartServices']; +} + +export const roleMappingsManagementApp = Object.freeze({ + id: 'role_mappings', + create({ getStartServices }: CreateParams) { + return { + id: this.id, + order: 40, + title: i18n.translate('xpack.security.management.roleMappingsTitle', { + defaultMessage: 'Role Mappings', + }), + async mount({ basePath, element, setBreadcrumbs }) { + const [{ docLinks, http, notifications, i18n: i18nStart }] = await getStartServices(); + const roleMappingsBreadcrumbs = [ + { + text: i18n.translate('xpack.security.roleMapping.breadcrumb', { + defaultMessage: 'Role Mappings', + }), + href: `#${basePath}`, + }, + ]; + + const roleMappingsAPIClient = new RoleMappingsAPIClient(http); + const dockLinksService = new DocumentationLinksService(docLinks); + const RoleMappingsGridPageWithBreadcrumbs = () => { + setBreadcrumbs(roleMappingsBreadcrumbs); + return ( + + ); + }; + + const EditRoleMappingsPageWithBreadcrumbs = () => { + const { name } = useParams<{ name?: string }>(); + + setBreadcrumbs([ + ...roleMappingsBreadcrumbs, + name + ? { text: name, href: `#${basePath}/edit/${encodeURIComponent(name)}` } + : { + text: i18n.translate('xpack.security.roleMappings.createBreadcrumb', { + defaultMessage: 'Create', + }), + }, + ]); + + return ( + + ); + }; + + render( + + + + + + + + + + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; + }, + } as RegisterManagementAppArgs; + }, +}); diff --git a/x-pack/plugins/security/public/management/roles/_index.scss b/x-pack/plugins/security/public/management/roles/_index.scss new file mode 100644 index 0000000000000..5256c79f01f10 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/_index.scss @@ -0,0 +1 @@ +@import './edit_role/index'; diff --git a/x-pack/plugins/security/public/management/roles/documentation_links.ts b/x-pack/plugins/security/public/management/roles/documentation_links.ts new file mode 100644 index 0000000000000..cf46973d3541c --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/documentation_links.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DocLinksStart } from 'src/core/public'; + +export class DocumentationLinksService { + private readonly esDocBasePath: string; + + constructor(docLinks: DocLinksStart) { + this.esDocBasePath = `${docLinks.ELASTIC_WEBSITE_URL}guide/en/elasticsearch/reference/${docLinks.DOC_LINK_VERSION}/`; + } + + public getESClusterPrivilegesDocUrl() { + return `${this.esDocBasePath}security-privileges.html#privileges-list-cluster`; + } + + public getESRunAsPrivilegesDocUrl() { + return `${this.esDocBasePath}security-privileges.html#_run_as_privilege`; + } + + public getESIndicesPrivilegesDocUrl() { + return `${this.esDocBasePath}security-privileges.html#privileges-list-indices`; + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/lib/__snapshots__/validate_role.test.ts.snap b/x-pack/plugins/security/public/management/roles/edit_role/__snapshots__/validate_role.test.ts.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/lib/__snapshots__/validate_role.test.ts.snap rename to x-pack/plugins/security/public/management/roles/edit_role/__snapshots__/validate_role.test.ts.snap diff --git a/x-pack/plugins/security/public/management/roles/edit_role/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/_index.scss new file mode 100644 index 0000000000000..0153b1734ceba --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/_index.scss @@ -0,0 +1,3 @@ +@import './collapsible_panel/index'; +@import './spaces_popover_list/index'; +@import './privileges/index'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/__snapshots__/collapsible_panel.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/_collapsible_panel.scss b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_collapsible_panel.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/_collapsible_panel.scss rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_collapsible_panel.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss new file mode 100644 index 0000000000000..c0f4f8ab9a870 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/_index.scss @@ -0,0 +1 @@ +@import './collapsible_panel'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.test.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.test.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.tsx b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx index 416dd7f6c4e5c..01af7cb4509f6 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/collapsible_panel.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/collapsible_panel.tsx @@ -50,7 +50,6 @@ export class CollapsiblePanel extends Component { public getTitle = () => { return ( - // @ts-ignore diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/collapsible_panel/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/collapsible_panel/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.test.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.test.tsx index cc16866c88355..f4af935be6648 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.test.tsx @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonEmpty, - // @ts-ignore - EuiConfirmModal, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiConfirmModal } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; import { DeleteRoleButton } from './delete_role_button'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.tsx b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx index 1ae84d3fb7224..c6a10396f235c 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/delete_role_button.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/delete_role_button.tsx @@ -4,13 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EuiButtonEmpty, - // @ts-ignore - EuiConfirmModal, - // @ts-ignore - EuiOverlayMask, -} from '@elastic/eui'; +import { EuiButtonEmpty, EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx new file mode 100644 index 0000000000000..2b3d7c811f6de --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -0,0 +1,552 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ReactWrapper } from 'enzyme'; +import React from 'react'; +import { act } from '@testing-library/react'; +import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; +import { Capabilities } from 'src/core/public'; +import { Space } from '../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../features/public'; +// These modules should be moved into a common directory +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { Actions } from '../../../../server/authorization/actions'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { privilegesFactory } from '../../../../server/authorization/privileges'; +import { Role } from '../../../../common/model'; +import { DocumentationLinksService } from '../documentation_links'; +import { EditRolePage } from './edit_role_page'; +import { SimplePrivilegeSection } from './privileges/kibana/simple_privilege_section'; +import { SpaceAwarePrivilegeSection } from './privileges/kibana/space_aware_privilege_section'; + +import { TransformErrorSection } from './privileges/kibana/transform_error_section'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; +import { licenseMock } from '../../../../common/licensing/index.mock'; +import { userAPIClientMock } from '../../users/index.mock'; +import { rolesAPIClientMock, indicesAPIClientMock, privilegesAPIClientMock } from '../index.mock'; + +const buildFeatures = () => { + return [ + { + id: 'feature1', + name: 'Feature 1', + icon: 'addDataApp', + app: ['feature1App'], + privileges: { + all: { + app: ['feature1App'], + ui: ['feature1-ui'], + savedObject: { + all: [], + read: [], + }, + }, + }, + }, + { + id: 'feature2', + name: 'Feature 2', + icon: 'addDataApp', + app: ['feature2App'], + privileges: { + all: { + app: ['feature2App'], + ui: ['feature2-ui'], + savedObject: { + all: ['feature2'], + read: ['config'], + }, + }, + }, + }, + ] as Feature[]; +}; + +const buildRawKibanaPrivileges = () => { + return privilegesFactory(new Actions('unit_test_version'), { + getFeatures: () => buildFeatures(), + }).get(); +}; + +const buildBuiltinESPrivileges = () => { + return { + cluster: ['all', 'manage', 'monitor'], + index: ['all', 'read', 'write', 'index'], + }; +}; + +const buildUICapabilities = (canManageSpaces = true) => { + return { + catalogue: {}, + management: {}, + navLinks: {}, + spaces: { + manage: canManageSpaces, + }, + } as Capabilities; +}; + +const buildSpaces = () => { + return [ + { + id: 'default', + name: 'Default', + disabledFeatures: [], + _reserved: true, + }, + { + id: 'space_1', + name: 'Space 1', + disabledFeatures: [], + }, + { + id: 'space_2', + name: 'Space 2', + disabledFeatures: ['feature2'], + }, + ] as Space[]; +}; + +const expectReadOnlyFormButtons = (wrapper: ReactWrapper) => { + expect(wrapper.find('button[data-test-subj="roleFormReturnButton"]')).toHaveLength(1); + expect(wrapper.find('button[data-test-subj="roleFormSaveButton"]')).toHaveLength(0); +}; + +const expectSaveFormButtons = (wrapper: ReactWrapper) => { + expect(wrapper.find('button[data-test-subj="roleFormReturnButton"]')).toHaveLength(0); + expect(wrapper.find('button[data-test-subj="roleFormSaveButton"]')).toHaveLength(1); +}; + +function getProps({ + action, + role, + canManageSpaces = true, + spacesEnabled = true, +}: { + action: 'edit' | 'clone'; + role?: Role; + canManageSpaces?: boolean; + spacesEnabled?: boolean; +}) { + const rolesAPIClient = rolesAPIClientMock.create(); + rolesAPIClient.getRole.mockResolvedValue(role); + + const indexPatterns = dataPluginMock.createStartContract().indexPatterns; + indexPatterns.getTitles = jest.fn().mockResolvedValue(['foo*', 'bar*']); + + const indicesAPIClient = indicesAPIClientMock.create(); + + const userAPIClient = userAPIClientMock.create(); + userAPIClient.getUsers.mockResolvedValue([]); + + const privilegesAPIClient = privilegesAPIClientMock.create(); + privilegesAPIClient.getAll.mockResolvedValue(buildRawKibanaPrivileges()); + privilegesAPIClient.getBuiltIn.mockResolvedValue(buildBuiltinESPrivileges()); + + const license = licenseMock.create(); + license.getFeatures.mockReturnValue({ + allowRoleDocumentLevelSecurity: true, + allowRoleFieldLevelSecurity: true, + } as any); + + const { fatalErrors } = coreMock.createSetup(); + const { http, docLinks, notifications } = coreMock.createStart(); + http.get.mockImplementation(async path => { + if (path === '/api/features') { + return buildFeatures(); + } + + if (path === '/api/spaces/space') { + return buildSpaces(); + } + }); + + return { + action, + roleName: role?.name, + license, + http, + indexPatterns, + indicesAPIClient, + privilegesAPIClient, + rolesAPIClient, + userAPIClient, + notifications, + docLinks: new DocumentationLinksService(docLinks), + fatalErrors, + spacesEnabled, + uiCapabilities: buildUICapabilities(canManageSpaces), + }; +} + +describe('', () => { + describe('with spaces enabled', () => { + it('can render a reserved role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1); + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectReadOnlyFormButtons(wrapper); + }); + + it('can render a user defined role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('can render when creating a new role', async () => { + const wrapper = mountWithIntl(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('can render when cloning an existing role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('renders an auth error when not authorized to manage spaces', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); + + expect( + wrapper.find('EuiCallOut[data-test-subj="userCannotManageSpacesCallout"]') + ).toHaveLength(1); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expectSaveFormButtons(wrapper); + }); + + it('renders a partial read-only view when there is a transform error', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(TransformErrorSection)).toHaveLength(1); + expectReadOnlyFormButtons(wrapper); + }); + }); + + describe('with spaces disabled', () => { + it('can render a reserved role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(1); + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectReadOnlyFormButtons(wrapper); + }); + + it('can render a user defined role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('can render when creating a new role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expectSaveFormButtons(wrapper); + }); + + it('can render when cloning an existing role', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expectSaveFormButtons(wrapper); + }); + + it('does not care if user cannot manage spaces', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="reservedRoleBadgeTooltip"]')).toHaveLength(0); + + expect( + wrapper.find('EuiCallOut[data-test-subj="userCannotManageSpacesCallout"]') + ).toHaveLength(0); + + expect(wrapper.find(SimplePrivilegeSection)).toHaveLength(1); + expectSaveFormButtons(wrapper); + }); + + it('renders a partial read-only view when there is a transform error', async () => { + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(TransformErrorSection)).toHaveLength(1); + expectReadOnlyFormButtons(wrapper); + }); + }); + + it('can render if features are not available', async () => { + const { http } = coreMock.createStart(); + http.get.mockImplementation(async path => { + if (path === '/api/features') { + const error = { response: { status: 404 } }; + throw error; + } + + if (path === '/api/spaces/space') { + return buildSpaces(); + } + }); + + const wrapper = mountWithIntl(); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); + + it('can render if index patterns are not available', async () => { + const indexPatterns = dataPluginMock.createStartContract().indexPatterns; + indexPatterns.getTitles = jest.fn().mockRejectedValue({ response: { status: 403 } }); + + const wrapper = mountWithIntl( + + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find(SpaceAwarePrivilegeSection)).toHaveLength(1); + expect(wrapper.find('[data-test-subj="userCannotManageSpacesCallout"]')).toHaveLength(0); + expectSaveFormButtons(wrapper); + }); +}); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx new file mode 100644 index 0000000000000..33e69a68ca896 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -0,0 +1,594 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import _, { get } from 'lodash'; +import { + EuiButton, + EuiButtonEmpty, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { + ChangeEvent, + Fragment, + FunctionComponent, + HTMLProps, + useEffect, + useRef, + useState, +} from 'react'; +import { + Capabilities, + FatalErrorsSetup, + HttpStart, + IHttpFetchError, + NotificationsStart, +} from 'src/core/public'; +import { IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; +import { Space } from '../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../features/public'; +import { + KibanaPrivileges, + RawKibanaPrivileges, + Role, + BuiltinESPrivileges, + isReadOnlyRole as checkIfRoleReadOnly, + isReservedRole as checkIfRoleReserved, + copyRole, + prepareRoleClone, + RoleIndexPrivilege, +} from '../../../../common/model'; +import { ROLES_PATH } from '../../management_urls'; +import { RoleValidationResult, RoleValidator } from './validate_role'; +import { DeleteRoleButton } from './delete_role_button'; +import { ElasticsearchPrivileges, KibanaPrivilegesRegion } from './privileges'; +import { ReservedRoleBadge } from './reserved_role_badge'; +import { SecurityLicense } from '../../../../common/licensing'; +import { UserAPIClient } from '../../users'; +import { DocumentationLinksService } from '../documentation_links'; +import { IndicesAPIClient } from '../indices_api_client'; +import { RolesAPIClient } from '../roles_api_client'; +import { PrivilegesAPIClient } from '../privileges_api_client'; + +interface Props { + action: 'edit' | 'clone'; + roleName?: string; + indexPatterns: IndexPatternsContract; + userAPIClient: PublicMethodsOf; + indicesAPIClient: PublicMethodsOf; + rolesAPIClient: PublicMethodsOf; + privilegesAPIClient: PublicMethodsOf; + docLinks: DocumentationLinksService; + http: HttpStart; + license: SecurityLicense; + spacesEnabled: boolean; + uiCapabilities: Capabilities; + notifications: NotificationsStart; + fatalErrors: FatalErrorsSetup; +} + +function useRunAsUsers( + userAPIClient: PublicMethodsOf, + fatalErrors: FatalErrorsSetup +) { + const [userNames, setUserNames] = useState(null); + useEffect(() => { + userAPIClient.getUsers().then( + users => setUserNames(users.map(user => user.username)), + err => fatalErrors.add(err) + ); + }, [fatalErrors, userAPIClient]); + + return userNames; +} + +function useIndexPatternsTitles( + indexPatterns: IndexPatternsContract, + fatalErrors: FatalErrorsSetup, + notifications: NotificationsStart +) { + const [indexPatternsTitles, setIndexPatternsTitles] = useState(null); + useEffect(() => { + indexPatterns + .getTitles() + .catch((err: IHttpFetchError) => { + // If user doesn't have access to the index patterns they still should be able to create new + // or edit existing role. + if (err.response?.status === 403) { + notifications.toasts.addDanger({ + title: i18n.translate('xpack.security.management.roles.noIndexPatternsPermission', { + defaultMessage: 'You need permission to access the list of available index patterns.', + }), + }); + return []; + } + + fatalErrors.add(err); + throw err; + }) + .then(setIndexPatternsTitles); + }, [fatalErrors, indexPatterns, notifications]); + + return indexPatternsTitles; +} + +function usePrivileges( + privilegesAPIClient: PublicMethodsOf, + fatalErrors: FatalErrorsSetup +) { + const [privileges, setPrivileges] = useState<[RawKibanaPrivileges, BuiltinESPrivileges] | null>( + null + ); + useEffect(() => { + Promise.all([ + privilegesAPIClient.getAll({ includeActions: true }), + privilegesAPIClient.getBuiltIn(), + ]).then( + ([kibanaPrivileges, builtInESPrivileges]) => + setPrivileges([kibanaPrivileges, builtInESPrivileges]), + err => fatalErrors.add(err) + ); + }, [privilegesAPIClient, fatalErrors]); + + return privileges; +} + +function useRole( + rolesAPIClient: PublicMethodsOf, + fatalErrors: FatalErrorsSetup, + notifications: NotificationsStart, + license: SecurityLicense, + action: string, + roleName?: string +) { + const [role, setRole] = useState(null); + useEffect(() => { + const rolePromise = roleName + ? rolesAPIClient.getRole(roleName) + : Promise.resolve({ + name: '', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [], + _unrecognized_applications: [], + } as Role); + + rolePromise + .then(fetchedRole => { + if (action === 'clone' && checkIfRoleReserved(fetchedRole)) { + backToRoleList(); + return; + } + + if (fetchedRole.elasticsearch.indices.length === 0) { + const emptyOption: RoleIndexPrivilege = { + names: [], + privileges: [], + }; + + const { + allowRoleDocumentLevelSecurity, + allowRoleFieldLevelSecurity, + } = license.getFeatures(); + + if (allowRoleFieldLevelSecurity) { + emptyOption.field_security = { + grant: ['*'], + except: [], + }; + } + + if (allowRoleDocumentLevelSecurity) { + emptyOption.query = ''; + } + + fetchedRole.elasticsearch.indices.push(emptyOption); + } + + setRole(action === 'clone' ? prepareRoleClone(fetchedRole) : copyRole(fetchedRole)); + }) + .catch((err: IHttpFetchError) => { + if (err.response?.status === 404) { + notifications.toasts.addDanger({ + title: i18n.translate('xpack.security.management.roles.roleNotFound', { + defaultMessage: 'No "{roleName}" role found.', + values: { roleName }, + }), + }); + backToRoleList(); + } else { + fatalErrors.add(err); + } + }); + }, [roleName, action, fatalErrors, rolesAPIClient, notifications, license]); + + return [role, setRole] as [Role | null, typeof setRole]; +} + +function useSpaces(http: HttpStart, fatalErrors: FatalErrorsSetup, spacesEnabled: boolean) { + const [spaces, setSpaces] = useState(null); + useEffect(() => { + (spacesEnabled ? http.get('/api/spaces/space') : Promise.resolve([])).then( + fetchedSpaces => setSpaces(fetchedSpaces), + err => fatalErrors.add(err) + ); + }, [http, fatalErrors, spacesEnabled]); + + return spaces; +} + +function useFeatures(http: HttpStart, fatalErrors: FatalErrorsSetup) { + const [features, setFeatures] = useState(null); + useEffect(() => { + http + .get('/api/features') + .catch((err: IHttpFetchError) => { + // Currently, the `/api/features` endpoint effectively requires the "Global All" kibana privilege (e.g., what + // the `kibana_user` grants), because it returns information about all registered features (#35841). It's + // possible that a user with `manage_security` will attempt to visit the role management page without the + // correct Kibana privileges. If that's the case, then they receive a partial view of the role, and the UI does + // not allow them to make changes to that role's kibana privileges. When this user visits the edit role page, + // this API endpoint will throw a 404, which causes view to fail completely. So we instead attempt to detect the + // 404 here, and respond in a way that still allows the UI to render itself. + const unauthorizedForFeatures = err.response?.status === 404; + if (unauthorizedForFeatures) { + return []; + } + + fatalErrors.add(err); + throw err; + }) + .then(setFeatures); + }, [http, fatalErrors]); + + return features; +} + +function backToRoleList() { + window.location.hash = ROLES_PATH; +} + +export const EditRolePage: FunctionComponent = ({ + userAPIClient, + indexPatterns, + rolesAPIClient, + indicesAPIClient, + privilegesAPIClient, + http, + roleName, + action, + fatalErrors, + spacesEnabled, + license, + docLinks, + uiCapabilities, + notifications, +}) => { + // We should keep the same mutable instance of Validator for every re-render since we'll + // eventually enable validation after the first time user tries to save a role. + const { current: validator } = useRef(new RoleValidator({ shouldValidate: false })); + + const [formError, setFormError] = useState(null); + const runAsUsers = useRunAsUsers(userAPIClient, fatalErrors); + const indexPatternsTitles = useIndexPatternsTitles(indexPatterns, fatalErrors, notifications); + const privileges = usePrivileges(privilegesAPIClient, fatalErrors); + const spaces = useSpaces(http, fatalErrors, spacesEnabled); + const features = useFeatures(http, fatalErrors); + const [role, setRole] = useRole( + rolesAPIClient, + fatalErrors, + notifications, + license, + action, + roleName + ); + + if (!role || !runAsUsers || !indexPatternsTitles || !privileges || !spaces || !features) { + return null; + } + + const isEditingExistingRole = !!roleName && action === 'edit'; + const isReadOnlyRole = checkIfRoleReadOnly(role); + const isReservedRole = checkIfRoleReserved(role); + + const [kibanaPrivileges, builtInESPrivileges] = privileges; + + const getFormTitle = () => { + let titleText; + const props: HTMLProps = { + tabIndex: 0, + }; + if (isReservedRole) { + titleText = ( + + ); + props['aria-describedby'] = 'reservedRoleDescription'; + } else if (isEditingExistingRole) { + titleText = ( + + ); + } else { + titleText = ( + + ); + } + + return ( + +

+ {titleText} +

+
+ ); + }; + + const getActionButton = () => { + if (isEditingExistingRole && !isReadOnlyRole) { + return ( + + + + ); + } + + return null; + }; + + const getRoleName = () => { + return ( + + + } + helpText={ + !isReservedRole && isEditingExistingRole ? ( + + ) : ( + undefined + ) + } + {...validator.validateRoleName(role)} + > + + + + ); + }; + + const onNameChange = (e: ChangeEvent) => + setRole({ + ...role, + name: e.target.value.replace(/\s/g, '_'), + }); + + const getElasticsearchPrivileges = () => { + return ( +
+ + +
+ ); + }; + + const onRoleChange = (newRole: Role) => setRole(newRole); + + const getKibanaPrivileges = () => { + return ( +
+ + +
+ ); + }; + + const getFormButtons = () => { + if (isReadOnlyRole) { + return getReturnToRoleListButton(); + } + + return ( + + {getSaveButton()} + {getCancelButton()} + + {getActionButton()} + + ); + }; + + const getReturnToRoleListButton = () => { + return ( + + + + ); + }; + + const getSaveButton = () => { + const saveText = isEditingExistingRole ? ( + + ) : ( + + ); + + return ( + + {saveText} + + ); + }; + + const getCancelButton = () => { + return ( + + + + ); + }; + + const saveRole = async () => { + validator.enableValidation(); + + const result = validator.validateForSave(role); + if (result.isInvalid) { + setFormError(result); + } else { + setFormError(null); + + try { + await rolesAPIClient.saveRole({ role, spacesEnabled }); + } catch (error) { + notifications.toasts.addDanger(get(error, 'data.message')); + return; + } + + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.editRole.roleSuccessfullySavedNotificationMessage', + { defaultMessage: 'Saved role' } + ) + ); + + backToRoleList(); + } + }; + + const handleDeleteRole = async () => { + try { + await rolesAPIClient.deleteRole(role.name); + } catch (error) { + notifications.toasts.addDanger(get(error, 'data.message')); + return; + } + + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.editRole.roleSuccessfullyDeletedNotificationMessage', + { defaultMessage: 'Deleted role' } + ) + ); + + backToRoleList(); + }; + + const description = spacesEnabled ? ( + + ) : ( + + ); + + return ( +
+ + {getFormTitle()} + + + + {description} + + {isReservedRole && ( + + + +

+ +

+
+
+ )} + + + + {getRoleName()} + + {getElasticsearchPrivileges()} + + {getKibanaPrivileges()} + + + + {getFormButtons()} +
+
+ ); +}; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/index.ts diff --git a/x-pack/legacy/plugins/security/public/lib/privilege_utils.test.ts b/x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.test.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/lib/privilege_utils.test.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.test.ts diff --git a/x-pack/legacy/plugins/security/public/lib/privilege_utils.ts b/x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.ts similarity index 93% rename from x-pack/legacy/plugins/security/public/lib/privilege_utils.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.ts index 74bde71dc421a..3fd8536951967 100644 --- a/x-pack/legacy/plugins/security/public/lib/privilege_utils.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/privilege_utils.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RoleKibanaPrivilege } from '../../common/model'; +import { RoleKibanaPrivilege } from '../../../../common/model'; /** * Determines if the passed privilege spec defines global privileges. diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss new file mode 100644 index 0000000000000..a1a9d038065e6 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/_index.scss @@ -0,0 +1,2 @@ +@import './privilege_feature_icon'; +@import './kibana/index'; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/_privilege_feature_icon.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/_privilege_feature_icon.scss new file mode 100644 index 0000000000000..a7f24c96a2821 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/_privilege_feature_icon.scss @@ -0,0 +1,4 @@ +.secPrivilegeFeatureIcon { + flex-shrink: 0; + margin-right: $euiSizeS; +} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/cluster_privileges.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap similarity index 87% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap index 795131337c31f..323629de7578d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap @@ -15,7 +15,7 @@ exports[`it renders without crashing 1`] = ` /> { + expect(shallowWithIntl()).toMatchSnapshot(); +}); + +test('it renders ClusterPrivileges', () => { + expect( + mountWithIntl().find(ClusterPrivileges) + ).toHaveLength(1); +}); + +test('it renders IndexPrivileges', () => { + expect( + mountWithIntl().find(IndexPrivileges) + ).toHaveLength(1); +}); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx index c0e6db3fef21c..96249ccf3ff87 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/elasticsearch_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/elasticsearch_privileges.tsx @@ -7,7 +7,6 @@ import { EuiButton, EuiComboBox, - // @ts-ignore EuiDescribedFormGroup, EuiFormRow, EuiHorizontalRule, @@ -19,26 +18,26 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import { Role, BuiltinESPrivileges } from '../../../../../../../common/model'; -// @ts-ignore -import { documentationLinks } from '../../../../../../documentation_links'; -import { RoleValidator } from '../../../lib/validate_role'; +import { Role, BuiltinESPrivileges } from '../../../../../../common/model'; +import { SecurityLicense } from '../../../../../../common/licensing'; +import { IndicesAPIClient } from '../../../indices_api_client'; +import { RoleValidator } from '../../validate_role'; import { CollapsiblePanel } from '../../collapsible_panel'; import { ClusterPrivileges } from './cluster_privileges'; - import { IndexPrivileges } from './index_privileges'; +import { DocumentationLinksService } from '../../../documentation_links'; interface Props { role: Role; editable: boolean; - httpClient: any; + indicesAPIClient: PublicMethodsOf; + docLinks: DocumentationLinksService; + license: SecurityLicense; onChange: (role: Role) => void; runAsUsers: string[]; validator: RoleValidator; builtinESPrivileges: BuiltinESPrivileges; indexPatterns: string[]; - allowDocumentLevelSecurity: boolean; - allowFieldLevelSecurity: boolean; } export class ElasticsearchPrivileges extends Component { @@ -53,23 +52,22 @@ export class ElasticsearchPrivileges extends Component { public getForm = () => { const { role, - httpClient, + indicesAPIClient, + docLinks, validator, onChange, editable, indexPatterns, - allowDocumentLevelSecurity, - allowFieldLevelSecurity, + license, builtinESPrivileges, } = this.props; const indexProps = { role, - httpClient, + indicesAPIClient, validator, indexPatterns, - allowDocumentLevelSecurity, - allowFieldLevelSecurity, + license, onChange, availableIndexPrivileges: builtinESPrivileges.index, }; @@ -91,7 +89,7 @@ export class ElasticsearchPrivileges extends Component { id="xpack.security.management.editRole.elasticSearchPrivileges.manageRoleActionsDescription" defaultMessage="Manage the actions this role can perform against your cluster. " /> - {this.learnMore(documentationLinks.esClusterPrivileges)} + {this.learnMore(docLinks.getESClusterPrivilegesDocUrl())}

} > @@ -121,7 +119,7 @@ export class ElasticsearchPrivileges extends Component { id="xpack.security.management.editRole.elasticSearchPrivileges.howToBeSubmittedOnBehalfOfOtherUsersDescription" defaultMessage="Allow requests to be submitted on the behalf of other users. " /> - {this.learnMore(documentationLinks.esRunAsPrivileges)} + {this.learnMore(docLinks.getESRunAsPrivilegesDocUrl())}

} > @@ -165,7 +163,7 @@ export class ElasticsearchPrivileges extends Component { id="xpack.security.management.editRole.elasticSearchPrivileges.controlAccessToClusterDataDescription" defaultMessage="Control access to the data in your cluster. " /> - {this.learnMore(documentationLinks.esIndicesPrivileges)} + {this.learnMore(docLinks.getESIndicesPrivilegesDocUrl())}

diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx index 6d386fd78a11b..5e2da51314365 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.test.tsx @@ -6,7 +6,7 @@ import { EuiButtonIcon, EuiTextArea } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { RoleValidator } from '../../../lib/validate_role'; +import { RoleValidator } from '../../validate_role'; import { IndexPrivilegeForm } from './index_privilege_form'; test('it renders without crashing', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx similarity index 98% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx index bafc56dc167ea..15e0367c2b6dc 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privilege_form.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privilege_form.tsx @@ -19,8 +19,8 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { ChangeEvent, Component, Fragment } from 'react'; -import { RoleIndexPrivilege } from '../../../../../../../common/model'; -import { RoleValidator } from '../../../lib/validate_role'; +import { RoleIndexPrivilege } from '../../../../../../common/model'; +import { RoleValidator } from '../../validate_role'; const fromOption = (option: any) => option.label; const toOption = (value: string) => ({ label: value }); @@ -164,7 +164,6 @@ export class IndexPrivilegeForm extends Component { {!isReadOnlyRole && ( { - // @ts-ignore missing "compressed" prop definition { } return ( - // @ts-ignore {!this.props.isReadOnlyRole && ( { - // @ts-ignore missing "compressed" proptype new Promise(setImmediate); test('it renders without crashing', async () => { + const license = licenseMock.create(); + license.getFeatures.mockReturnValue({ + allowRoleFieldLevelSecurity: true, + allowRoleDocumentLevelSecurity: true, + } as any); + const props = { role: { name: '', @@ -25,14 +34,13 @@ test('it renders without crashing', async () => { run_as: [], }, }, - httpClient: jest.fn(), onChange: jest.fn(), indexPatterns: [], editable: true, - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, validator: new RoleValidator(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], + indicesAPIClient: indicesAPIClientMock.create(), + license, }; const wrapper = shallowWithIntl(); await flushPromises(); @@ -40,6 +48,12 @@ test('it renders without crashing', async () => { }); test('it renders a IndexPrivilegeForm for each privilege on the role', async () => { + const license = licenseMock.create(); + license.getFeatures.mockReturnValue({ + allowRoleFieldLevelSecurity: true, + allowRoleDocumentLevelSecurity: true, + } as any); + const props = { role: { name: '', @@ -59,14 +73,13 @@ test('it renders a IndexPrivilegeForm for each privilege on the role', async () run_as: [], }, }, - httpClient: jest.fn(), onChange: jest.fn(), indexPatterns: [], editable: true, - allowDocumentLevelSecurity: true, - allowFieldLevelSecurity: true, validator: new RoleValidator(), availableIndexPrivileges: ['all', 'read', 'write', 'index'], + indicesAPIClient: indicesAPIClientMock.create(), + license, }; const wrapper = mountWithIntl(); await flushPromises(); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx similarity index 84% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx index f09084ad2bb38..2c745067fede2 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/es/index_privileges.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/index_privileges.tsx @@ -5,19 +5,23 @@ */ import _ from 'lodash'; import React, { Component, Fragment } from 'react'; -import { Role, RoleIndexPrivilege } from '../../../../../../../common/model'; -import { isReadOnlyRole, isRoleEnabled } from '../../../../../../lib/role_utils'; -import { getFields } from '../../../../../../objects'; -import { RoleValidator } from '../../../lib/validate_role'; +import { + Role, + RoleIndexPrivilege, + isReadOnlyRole, + isRoleEnabled, +} from '../../../../../../common/model'; +import { SecurityLicense } from '../../../../../../common/licensing'; +import { IndicesAPIClient } from '../../../indices_api_client'; +import { RoleValidator } from '../../validate_role'; import { IndexPrivilegeForm } from './index_privilege_form'; interface Props { role: Role; indexPatterns: string[]; availableIndexPrivileges: string[]; - allowDocumentLevelSecurity: boolean; - allowFieldLevelSecurity: boolean; - httpClient: any; + indicesAPIClient: PublicMethodsOf; + license: SecurityLicense; onChange: (role: Role) => void; validator: RoleValidator; } @@ -43,20 +47,16 @@ export class IndexPrivileges extends Component { public render() { const { indices = [] } = this.props.role.elasticsearch; - const { - indexPatterns, - allowDocumentLevelSecurity, - allowFieldLevelSecurity, - availableIndexPrivileges, - } = this.props; + const { indexPatterns, license, availableIndexPrivileges } = this.props; + const { allowRoleDocumentLevelSecurity, allowRoleFieldLevelSecurity } = license.getFeatures(); const props = { indexPatterns, // If editing an existing role while that has been disabled, always show the FLS/DLS fields because currently // a role is only marked as disabled if it has FLS/DLS setup (usually before the user changed to a license that // doesn't permit FLS/DLS). - allowDocumentLevelSecurity: allowDocumentLevelSecurity || !isRoleEnabled(this.props.role), - allowFieldLevelSecurity: allowFieldLevelSecurity || !isRoleEnabled(this.props.role), + allowDocumentLevelSecurity: allowRoleDocumentLevelSecurity || !isRoleEnabled(this.props.role), + allowFieldLevelSecurity: allowRoleFieldLevelSecurity || !isRoleEnabled(this.props.role), isReadOnlyRole: isReadOnlyRole(this.props.role), }; @@ -171,7 +171,7 @@ export class IndexPrivileges extends Component { try { return { - [pattern]: await getFields(this.props.httpClient, pattern), + [pattern]: await this.props.indicesAPIClient.getFields(pattern), }; } catch (e) { return { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/__snapshots__/kibana_privileges_region.test.tsx.snap diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss new file mode 100644 index 0000000000000..19547c0e1953e --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/_index.scss @@ -0,0 +1,2 @@ +@import './feature_table/index'; +@import './space_aware_privilege_section/index'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/lib/constants.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/constants.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/lib/constants.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/constants.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/__snapshots__/feature_table.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/__snapshots__/feature_table.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/__snapshots__/feature_table.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/__snapshots__/feature_table.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_change_all_privileges.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/_index.scss rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_change_all_privileges.scss diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss new file mode 100644 index 0000000000000..6a96553742819 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/_index.scss @@ -0,0 +1 @@ +@import './change_all_privileges'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/change_all_privileges.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/change_all_privileges.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/change_all_privileges.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx index 9648bf1d111bf..dea42e16f99d4 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx @@ -3,12 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore import { EuiInMemoryTable } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; +import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { FeatureTable } from './feature_table'; const defaultPrivilegeDefinition = new KibanaPrivileges({ @@ -113,7 +112,6 @@ describe('FeatureTable', () => { onChange={jest.fn()} onChangeAll={jest.fn()} spacesIndex={0} - intl={null as any} /> ); @@ -141,7 +139,6 @@ describe('FeatureTable', () => { onChange={jest.fn()} onChangeAll={jest.fn()} spacesIndex={-1} - intl={null as any} /> ); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx similarity index 91% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx index a05dc687fce4a..8283efe23260a 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/feature_table/feature_table.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.tsx @@ -4,28 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; +import React, { Component } from 'react'; import { - // @ts-ignore EuiButtonGroup, EuiIcon, EuiIconTip, - // @ts-ignore EuiInMemoryTable, EuiText, IconType, } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; -import _ from 'lodash'; -import React, { Component } from 'react'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../../common/model'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Feature } from '../../../../../../../../features/public'; +import { FeaturesPrivileges, KibanaPrivileges, Role } from '../../../../../../../common/model'; import { AllowedPrivilege, CalculatedPrivilege, PrivilegeExplanation, -} from '../../../../../../../lib/kibana_privilege_calculator'; -import { isGlobalPrivilegeDefinition } from '../../../../../../../lib/privilege_utils'; -import { NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +} from '../kibana_privilege_calculator'; +import { isGlobalPrivilegeDefinition } from '../../../privilege_utils'; +import { NO_PRIVILEGE_VALUE } from '../constants'; import { PrivilegeDisplay } from '../space_aware_privilege_section/privilege_display'; import { ChangeAllPrivilegesControl } from './change_all_privileges'; @@ -36,7 +35,6 @@ interface Props { allowedPrivileges: AllowedPrivilege; rankedFeaturePrivileges: FeaturesPrivileges; kibanaPrivileges: KibanaPrivileges; - intl: InjectedIntl; spacesIndex: number; onChange: (featureId: string, privileges: string[]) => void; onChangeAll: (privileges: string[]) => void; @@ -100,9 +98,7 @@ export class FeatureTable extends Component { const availablePrivileges = Object.values(rankedFeaturePrivileges)[0]; return ( - // @ts-ignore missing responsive from typedef { private getColumns = (availablePrivileges: string[]) => [ { field: 'feature', - name: this.props.intl.formatMessage({ - id: 'xpack.security.management.editRole.featureTable.enabledRoleFeaturesFeatureColumnTitle', - defaultMessage: 'Feature', - }), + name: i18n.translate( + 'xpack.security.management.editRole.featureTable.enabledRoleFeaturesFeatureColumnTitle', + { defaultMessage: 'Feature' } + ), render: (feature: TableFeature) => { let tooltipElement = null; if (feature.privilegesTooltip) { @@ -239,9 +235,7 @@ export class FeatureTable extends Component { }); return ( - // @ts-ignore missing name from typedef void; validator: RoleValidator; - intl: InjectedIntl; } export class KibanaPrivilegesRegion extends Component { @@ -81,7 +79,6 @@ export class KibanaPrivilegesRegion extends Component { privilegeCalculatorFactory={privilegeCalculatorFactory} onChange={onChange} editable={editable} - intl={this.props.intl} /> ); } diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/__snapshots__/simple_privilege_section.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/privilege_selector.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/privilege_selector.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/privilege_selector.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/privilege_selector.tsx index 135419cc9a10d..bda0227372c09 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/privilege_selector.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/privilege_selector.tsx @@ -6,7 +6,7 @@ import { EuiSelect } from '@elastic/eui'; import React, { ChangeEvent, Component } from 'react'; -import { NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +import { NO_PRIVILEGE_VALUE } from '../constants'; interface Props { ['data-test-subj']: string; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx index f97fa93294ff5..db1e3cfd61621 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.test.tsx @@ -3,13 +3,13 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore + import { EuiButtonGroup, EuiButtonGroupProps, EuiComboBox, EuiSuperSelect } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; +import { Feature } from '../../../../../../../../features/public'; +import { KibanaPrivileges, Role } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { SimplePrivilegeSection } from './simple_privilege_section'; import { UnsupportedSpacePrivilegesWarning } from './unsupported_space_privileges_warning'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx index 7768dc769a32f..2221fc6bab279 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/simple_privilege_section.tsx @@ -6,21 +6,24 @@ import { EuiComboBox, - // @ts-ignore EuiDescribedFormGroup, EuiFormRow, EuiSuperSelect, EuiText, } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; +import { FormattedMessage } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; -import { isGlobalPrivilegeDefinition } from '../../../../../../../lib/privilege_utils'; -import { copyRole } from '../../../../../../../lib/role_utils'; -import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +import { Feature } from '../../../../../../../../features/public'; +import { + KibanaPrivileges, + Role, + RoleKibanaPrivilege, + copyRole, +} from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; +import { isGlobalPrivilegeDefinition } from '../../../privilege_utils'; +import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../constants'; import { FeatureTable } from '../feature_table'; import { UnsupportedSpacePrivilegesWarning } from './unsupported_space_privileges_warning'; @@ -31,7 +34,6 @@ interface Props { features: Feature[]; onChange: (role: Role) => void; editable: boolean; - intl: InjectedIntl; } interface State { @@ -230,7 +232,6 @@ export class SimplePrivilegeSection extends Component { allowedPrivileges={allowedPrivileges} rankedFeaturePrivileges={privilegeCalculator.rankedFeaturePrivileges} features={this.props.features} - intl={this.props.intl} onChange={this.onFeaturePrivilegeChange} onChangeAll={this.onChangeAllFeaturePrivileges} spacesIndex={this.props.role.kibana.findIndex(k => isGlobalPrivilegeDefinition(k))} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/simple_privilege_section/unsupported_space_privileges_warning.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts index d412ba63403e1..428836c9f181b 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__fixtures__/raw_kibana_privileges.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RawKibanaPrivileges } from '../../../../../../../../../common/model'; +import { RawKibanaPrivileges } from '../../../../../../../../common/model'; export const rawKibanaPrivileges: RawKibanaPrivileges = { global: { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_display.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_display.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_display.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_display.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap similarity index 82% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap index c20a391cdb20c..e9f2f946e9885 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/privilege_space_form.test.tsx.snap @@ -351,108 +351,6 @@ exports[` renders without crashing 1`] = ` } disabled={true} features={Array []} - intl={ - Object { - "defaultFormats": Object {}, - "defaultLocale": "en", - "formatDate": [Function], - "formatHTMLMessage": [Function], - "formatMessage": [Function], - "formatNumber": [Function], - "formatPlural": [Function], - "formatRelative": [Function], - "formatTime": [Function], - "formats": Object { - "date": Object { - "full": Object { - "day": "numeric", - "month": "long", - "weekday": "long", - "year": "numeric", - }, - "long": Object { - "day": "numeric", - "month": "long", - "year": "numeric", - }, - "medium": Object { - "day": "numeric", - "month": "short", - "year": "numeric", - }, - "short": Object { - "day": "numeric", - "month": "numeric", - "year": "2-digit", - }, - }, - "number": Object { - "currency": Object { - "style": "currency", - }, - "percent": Object { - "style": "percent", - }, - }, - "relative": Object { - "days": Object { - "units": "day", - }, - "hours": Object { - "units": "hour", - }, - "minutes": Object { - "units": "minute", - }, - "months": Object { - "units": "month", - }, - "seconds": Object { - "units": "second", - }, - "years": Object { - "units": "year", - }, - }, - "time": Object { - "full": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "long": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "medium": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - }, - "short": Object { - "hour": "numeric", - "minute": "numeric", - }, - }, - }, - "formatters": Object { - "getDateTimeFormat": [Function], - "getMessageFormat": [Function], - "getNumberFormat": [Function], - "getPluralFormat": [Function], - "getRelativeFormat": [Function], - }, - "locale": "en", - "messages": Object {}, - "now": [Function], - "onError": [Function], - "textComponent": Symbol(react.fragment), - "timeZone": null, - } - } kibanaPrivileges={ KibanaPrivileges { "rawKibanaPrivileges": Object { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/__snapshots__/space_aware_privilege_section.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_index.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_index.scss rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_index.scss diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss similarity index 87% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss index 5f9fbced5ee6a..8f47727fdf8d6 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/_privilege_matrix.scss @@ -9,6 +9,6 @@ .secPrivilegeMatrix__row--isBasePrivilege, .secPrivilegeMatrix__cell--isGlobalPrivilege, -.secPrivilegeTable__row--isGlobalSpace, { +.secPrivilegeTable__row--isGlobalSpace { background-color: $euiColorLightestShade; } diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx index 62e22050132fd..c6268e19abfd1 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.test.tsx @@ -7,7 +7,7 @@ import { EuiIconTip, EuiText, EuiToolTip } from '@elastic/eui'; import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { PRIVILEGE_SOURCE } from '../../../../../../../lib/kibana_privilege_calculator'; +import { PRIVILEGE_SOURCE } from '../kibana_privilege_calculator'; import { PrivilegeDisplay } from './privilege_display'; describe('PrivilegeDisplay', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.tsx index 6af7672f6fef8..55ac99da4c8c1 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_display.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_display.tsx @@ -7,11 +7,8 @@ import { EuiIcon, EuiIconTip, EuiText, IconType, PropsOf, EuiToolTip } from '@el import { FormattedMessage } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { ReactNode, FC } from 'react'; -import { - PRIVILEGE_SOURCE, - PrivilegeExplanation, -} from '../../../../../../../lib/kibana_privilege_calculator'; -import { NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +import { PRIVILEGE_SOURCE, PrivilegeExplanation } from '../kibana_privilege_calculator'; +import { NO_PRIVILEGE_VALUE } from '../constants'; interface Props extends PropsOf { privilege: string | string[] | undefined; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx index ee121caa13a2a..16aad4826ae44 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.test.tsx @@ -3,14 +3,14 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -// @ts-ignore + import { EuiButtonEmpty, EuiInMemoryTable } from '@elastic/eui'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../..//lib/kibana_privilege_calculator'; +import { Space } from '../../../../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../../../../features/public'; +import { KibanaPrivileges, Role } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { PrivilegeMatrix } from './privilege_matrix'; describe('PrivilegeMatrix', () => { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx index 962487312c83d..b3449e32c6c91 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_matrix.tsx @@ -8,7 +8,6 @@ import { EuiButtonEmpty, EuiIcon, EuiIconTip, - // @ts-ignore EuiInMemoryTable, EuiModal, EuiModalBody, @@ -16,18 +15,16 @@ import { EuiModalHeader, EuiModalHeaderTitle, EuiOverlayMask, - // @ts-ignore - EuiToolTip, IconType, } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { SpaceAvatar } from '../../../../../../../../../spaces/public/space_avatar'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { FeaturesPrivileges, Role } from '../../../../../../../../common/model'; -import { CalculatedPrivilege } from '../../../../../../../lib/kibana_privilege_calculator'; -import { isGlobalPrivilegeDefinition } from '../../../../../../../lib/privilege_utils'; +import { SpaceAvatar } from '../../../../../../../../../legacy/plugins/spaces/public/space_avatar'; +import { Space } from '../../../../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../../../../features/public'; +import { FeaturesPrivileges, Role } from '../../../../../../../common/model'; +import { CalculatedPrivilege } from '../kibana_privilege_calculator'; +import { isGlobalPrivilegeDefinition } from '../../../privilege_utils'; import { SpacesPopoverList } from '../../../spaces_popover_list'; import { PrivilegeDisplay } from './privilege_display'; @@ -258,11 +255,9 @@ export class PrivilegeMatrix extends Component { ]; return ( - // @ts-ignore missing rowProps from typedef { return { className: item.feature.isBase ? 'secPrivilegeMatrix__row--isBasePrivilege' : '', diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx index 2b7d87f663d72..675f02a81f9e1 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.test.tsx @@ -9,8 +9,8 @@ import { merge } from 'lodash'; // @ts-ignore import { findTestSubject } from '@elastic/eui/lib/test'; import { shallowWithIntl, mountWithIntl } from 'test_utils/enzyme_helpers'; -import { KibanaPrivileges } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; +import { KibanaPrivileges } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { PrivilegeSpaceForm } from './privilege_space_form'; import { rawKibanaPrivileges } from './__fixtures__'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx index 5abb87d23bb6e..6d1f5117c52e9 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_form.tsx @@ -24,17 +24,16 @@ import { } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component, Fragment } from 'react'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; +import { Space } from '../../../../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../../../../features/public'; +import { KibanaPrivileges, Role, copyRole } from '../../../../../../../common/model'; import { AllowedPrivilege, KibanaPrivilegeCalculatorFactory, PrivilegeExplanation, -} from '../../../../../../../lib/kibana_privilege_calculator'; -import { hasAssignedFeaturePrivileges } from '../../../../../../../lib/privilege_utils'; -import { copyRole } from '../../../../../../../lib/role_utils'; -import { CUSTOM_PRIVILEGE_VALUE } from '../../../../lib/constants'; +} from '../kibana_privilege_calculator'; +import { hasAssignedFeaturePrivileges } from '../../../privilege_utils'; +import { CUSTOM_PRIVILEGE_VALUE } from '../constants'; import { FeatureTable } from '../feature_table'; import { SpaceSelector } from './space_selector'; @@ -285,7 +284,6 @@ export class PrivilegeSpaceForm extends Component { calculatedPrivileges={calculatedPrivileges} allowedPrivileges={allowedPrivileges} rankedFeaturePrivileges={privilegeCalculator.rankedFeaturePrivileges} - intl={this.props.intl} onChange={this.onFeaturePrivilegesChange} onChangeAll={this.onChangeAllFeaturePrivileges} kibanaPrivileges={this.props.kibanaPrivileges} diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx similarity index 99% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx index 37ee43c5473b0..f0a391c98c910 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx @@ -10,8 +10,8 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import { ReactWrapper } from 'enzyme'; import { PrivilegeSpaceTable } from './privilege_space_table'; import { PrivilegeDisplay } from './privilege_display'; -import { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; +import { KibanaPrivileges, Role, RoleKibanaPrivilege } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { rawKibanaPrivileges } from './__fixtures__'; interface TableRow { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx similarity index 94% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx index 65a3df9fb47a1..1c27ec84f50dc 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.tsx @@ -12,22 +12,21 @@ import { EuiBasicTableColumn, } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; -import _ from 'lodash'; import React, { Component } from 'react'; -import { getSpaceColor } from '../../../../../../../../../spaces/public/space_avatar'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; +import { getSpaceColor } from '../../../../../../../../../legacy/plugins/spaces/public/space_avatar'; +import { Space } from '../../../../../../../../spaces/common/model/space'; import { FeaturesPrivileges, Role, RoleKibanaPrivilege, -} from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; + copyRole, +} from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; import { isGlobalPrivilegeDefinition, hasAssignedFeaturePrivileges, -} from '../../../../../../../lib/privilege_utils'; -import { copyRole } from '../../../../../../../lib/role_utils'; -import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../../../../lib/constants'; +} from '../../../privilege_utils'; +import { CUSTOM_PRIVILEGE_VALUE, NO_PRIVILEGE_VALUE } from '../constants'; import { SpacesPopoverList } from '../../../spaces_popover_list'; import { PrivilegeDisplay } from './privilege_display'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx index 2756b1c447274..e06d2a4f7dc33 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.test.tsx @@ -6,9 +6,9 @@ import React from 'react'; import { mountWithIntl, shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { KibanaPrivileges } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; -import { RoleValidator } from '../../../../lib/validate_role'; +import { KibanaPrivileges } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; +import { RoleValidator } from '../../../validate_role'; import { PrivilegeMatrix } from './privilege_matrix'; import { PrivilegeSpaceForm } from './privilege_space_form'; import { PrivilegeSpaceTable } from './privilege_space_table'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx index d324cf99c8418..21cadfafe1790 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_aware_privilege_section.tsx @@ -14,13 +14,12 @@ import { import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; import _ from 'lodash'; import React, { Component, Fragment } from 'react'; -import { UICapabilities } from 'ui/capabilities'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { Feature } from '../../../../../../../../../../../plugins/features/public'; -import { KibanaPrivileges, Role } from '../../../../../../../../common/model'; -import { KibanaPrivilegeCalculatorFactory } from '../../../../../../../lib/kibana_privilege_calculator'; -import { isReservedRole } from '../../../../../../../lib/role_utils'; -import { RoleValidator } from '../../../../lib/validate_role'; +import { Capabilities } from 'src/core/public'; +import { Space } from '../../../../../../../../spaces/common/model/space'; +import { Feature } from '../../../../../../../../features/public'; +import { KibanaPrivileges, Role, isReservedRole } from '../../../../../../../common/model'; +import { KibanaPrivilegeCalculatorFactory } from '../kibana_privilege_calculator'; +import { RoleValidator } from '../../../validate_role'; import { PrivilegeMatrix } from './privilege_matrix'; import { PrivilegeSpaceForm } from './privilege_space_form'; import { PrivilegeSpaceTable } from './privilege_space_table'; @@ -34,7 +33,7 @@ interface Props { editable: boolean; validator: RoleValidator; intl: InjectedIntl; - uiCapabilities: UICapabilities; + uiCapabilities: Capabilities; features: Feature[]; } diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_selector.tsx similarity index 93% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_selector.tsx index 0eb9cf0b0ee9d..cfeb5b9f37d8c 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/space_aware_privilege_section/space_selector.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/space_selector.tsx @@ -7,8 +7,8 @@ import { EuiComboBox, EuiComboBoxOptionProps, EuiHealth, EuiHighlight } from '@elastic/eui'; import { InjectedIntl } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { Space } from '../../../../../../../../../spaces/common/model/space'; -import { getSpaceColor } from '../../../../../../../../../spaces/public/space_avatar'; +import { getSpaceColor } from '../../../../../../../../../legacy/plugins/spaces/public/space_avatar'; +import { Space } from '../../../../../../../../spaces/common/model/space'; const spaceToOption = (space?: Space, currentSelection?: 'global' | 'spaces') => { if (!space) { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/transform_error_section/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/transform_error_section/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/transform_error_section/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/transform_error_section/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/transform_error_section/transform_error_section.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/transform_error_section/transform_error_section.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/privileges/kibana/transform_error_section/transform_error_section.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/transform_error_section/transform_error_section.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.test.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.test.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.test.tsx index 9b483d92cde41..d29b442420a90 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.test.tsx @@ -7,7 +7,7 @@ import { EuiIcon } from '@elastic/eui'; import { shallow } from 'enzyme'; import React from 'react'; -import { Role } from '../../../../../common/model'; +import { Role } from '../../../../common/model'; import { ReservedRoleBadge } from './reserved_role_badge'; const reservedRole: Role = { diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.tsx b/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.tsx similarity index 89% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.tsx index 3d817d1e07d21..501ca7589dafd 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/reserved_role_badge.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/reserved_role_badge.tsx @@ -8,8 +8,7 @@ import React from 'react'; import { EuiIcon, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Role } from '../../../../../common/model'; -import { isReservedRole } from '../../../../lib/role_utils'; +import { Role, isReservedRole } from '../../../../common/model'; interface Props { role: Role; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss new file mode 100644 index 0000000000000..b40a32cb8df96 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_index.scss @@ -0,0 +1 @@ +@import './spaces_popover_list'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/_spaces_popover_list.scss b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_spaces_popover_list.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/_spaces_popover_list.scss rename to x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/_spaces_popover_list.scss diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/index.ts b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/index.ts rename to x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/spaces_popover_list.tsx b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx similarity index 95% rename from x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/spaces_popover_list.tsx rename to x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx index a99e389044eaa..bb7a6db97f7c8 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_role/components/spaces_popover_list/spaces_popover_list.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/spaces_popover_list/spaces_popover_list.tsx @@ -14,9 +14,9 @@ import { } from '@elastic/eui'; import { FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; import React, { Component } from 'react'; -import { SpaceAvatar } from '../../../../../../../spaces/public/space_avatar'; -import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../../../../../../plugins/spaces/common/constants'; -import { Space } from '../../../../../../../../../plugins/spaces/common/model/space'; +import { SpaceAvatar } from '../../../../../../../legacy/plugins/spaces/public/space_avatar'; +import { SPACE_SEARCH_COUNT_THRESHOLD } from '../../../../../../spaces/common'; +import { Space } from '../../../../../../spaces/common/model/space'; interface Props { spaces: Space[]; @@ -146,7 +146,6 @@ export class SpacesPopoverList extends Component { return (
{ - // @ts-ignore onSearch isn't defined on the type ({ + getFields: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/public/management/roles/indices_api_client.ts b/x-pack/plugins/security/public/management/roles/indices_api_client.ts new file mode 100644 index 0000000000000..65d9a40a776eb --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/indices_api_client.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpStart } from 'src/core/public'; + +export class IndicesAPIClient { + constructor(private readonly http: HttpStart) {} + + async getFields(query: string) { + return ( + (await this.http.get(`/internal/security/fields/${encodeURIComponent(query)}`)) || + [] + ); + } +} diff --git a/x-pack/plugins/security/public/management/roles/privileges_api_client.mock.ts b/x-pack/plugins/security/public/management/roles/privileges_api_client.mock.ts new file mode 100644 index 0000000000000..2564914a1d3d8 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/privileges_api_client.mock.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const privilegesAPIClientMock = { + create: () => ({ + getAll: jest.fn(), + getBuiltIn: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/public/management/roles/privileges_api_client.ts b/x-pack/plugins/security/public/management/roles/privileges_api_client.ts new file mode 100644 index 0000000000000..45bd2fd8fb3a6 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/privileges_api_client.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpStart } from 'src/core/public'; +import { BuiltinESPrivileges, RawKibanaPrivileges } from '../../../common/model'; + +export class PrivilegesAPIClient { + constructor(private readonly http: HttpStart) {} + + async getAll({ includeActions }: { includeActions: boolean }) { + return await this.http.get('/api/security/privileges', { + query: { includeActions }, + }); + } + + async getBuiltIn() { + return await this.http.get('/internal/security/esPrivileges/builtin'); + } +} diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts new file mode 100644 index 0000000000000..c4d3724c0ecb5 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.mock.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const rolesAPIClientMock = { + create: () => ({ + getRoles: jest.fn(), + getRole: jest.fn(), + deleteRole: jest.fn(), + saveRole: jest.fn(), + }), +}; diff --git a/x-pack/legacy/plugins/security/public/lib/transform_role_for_save.test.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.test.ts similarity index 82% rename from x-pack/legacy/plugins/security/public/lib/transform_role_for_save.test.ts rename to x-pack/plugins/security/public/management/roles/roles_api_client.test.ts index 1ea19f2637305..7561161368405 100644 --- a/x-pack/legacy/plugins/security/public/lib/transform_role_for_save.test.ts +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.test.ts @@ -4,12 +4,23 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Role } from '../../common/model'; -import { transformRoleForSave } from './transform_role_for_save'; +import { Role } from '../../../common/model'; +import { RolesAPIClient } from './roles_api_client'; +import { httpServiceMock } from '../../../../../../src/core/public/mocks'; + +describe('RolesAPIClient', () => { + async function saveRole(role: Role, spacesEnabled: boolean) { + const httpMock = httpServiceMock.createStartContract(); + const rolesAPIClient = new RolesAPIClient(httpMock); + + await rolesAPIClient.saveRole({ role, spacesEnabled }); + expect(httpMock.put).toHaveBeenCalledTimes(1); + + return JSON.parse(httpMock.put.mock.calls[0][1]?.body as any); + } -describe('transformRoleForSave', () => { describe('spaces disabled', () => { - it('removes placeholder index privileges', () => { + it('removes placeholder index privileges', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -20,7 +31,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -32,7 +43,7 @@ describe('transformRoleForSave', () => { }); }); - it('removes placeholder query entries', () => { + it('removes placeholder query entries', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -43,7 +54,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -55,7 +66,7 @@ describe('transformRoleForSave', () => { }); }); - it('removes transient fields not required for save', () => { + it('removes transient fields not required for save', async () => { const role: Role = { name: 'my role', transient_metadata: { @@ -74,7 +85,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ metadata: { @@ -89,7 +100,7 @@ describe('transformRoleForSave', () => { }); }); - it('does not remove actual query entries', () => { + it('does not remove actual query entries', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -100,7 +111,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -112,7 +123,7 @@ describe('transformRoleForSave', () => { }); }); - it('should remove feature privileges if a corresponding base privilege is defined', () => { + it('should remove feature privileges if a corresponding base privilege is defined', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -132,7 +143,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -150,7 +161,7 @@ describe('transformRoleForSave', () => { }); }); - it('should not remove feature privileges if a corresponding base privilege is not defined', () => { + it('should not remove feature privileges if a corresponding base privilege is not defined', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -170,7 +181,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -191,7 +202,7 @@ describe('transformRoleForSave', () => { }); }); - it('should remove space privileges', () => { + it('should remove space privileges', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -219,7 +230,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, false); + const result = await saveRole(role, false); expect(result).toEqual({ elasticsearch: { @@ -242,7 +253,7 @@ describe('transformRoleForSave', () => { }); describe('spaces enabled', () => { - it('removes placeholder index privileges', () => { + it('removes placeholder index privileges', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -253,7 +264,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -265,7 +276,7 @@ describe('transformRoleForSave', () => { }); }); - it('removes placeholder query entries', () => { + it('removes placeholder query entries', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -276,7 +287,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -288,7 +299,7 @@ describe('transformRoleForSave', () => { }); }); - it('removes transient fields not required for save', () => { + it('removes transient fields not required for save', async () => { const role: Role = { name: 'my role', transient_metadata: { @@ -307,7 +318,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ metadata: { @@ -322,7 +333,7 @@ describe('transformRoleForSave', () => { }); }); - it('does not remove actual query entries', () => { + it('does not remove actual query entries', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -333,7 +344,7 @@ describe('transformRoleForSave', () => { kibana: [], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -345,7 +356,7 @@ describe('transformRoleForSave', () => { }); }); - it('should remove feature privileges if a corresponding base privilege is defined', () => { + it('should remove feature privileges if a corresponding base privilege is defined', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -365,7 +376,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -383,7 +394,7 @@ describe('transformRoleForSave', () => { }); }); - it('should not remove feature privileges if a corresponding base privilege is not defined', () => { + it('should not remove feature privileges if a corresponding base privilege is not defined', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -403,7 +414,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { @@ -424,7 +435,7 @@ describe('transformRoleForSave', () => { }); }); - it('should not remove space privileges', () => { + it('should not remove space privileges', async () => { const role: Role = { name: 'my role', elasticsearch: { @@ -452,7 +463,7 @@ describe('transformRoleForSave', () => { ], }; - const result = transformRoleForSave(role, true); + const result = await saveRole(role, true); expect(result).toEqual({ elasticsearch: { diff --git a/x-pack/plugins/security/public/management/roles/roles_api_client.ts b/x-pack/plugins/security/public/management/roles/roles_api_client.ts new file mode 100644 index 0000000000000..d7e98e03a965b --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/roles_api_client.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpStart } from 'src/core/public'; +import { Role, RoleIndexPrivilege, copyRole } from '../../../common/model'; +import { isGlobalPrivilegeDefinition } from './edit_role/privilege_utils'; + +export class RolesAPIClient { + constructor(private readonly http: HttpStart) {} + + public async getRoles() { + return await this.http.get('/api/security/role'); + } + + public async getRole(roleName: string) { + return await this.http.get(`/api/security/role/${encodeURIComponent(roleName)}`); + } + + public async deleteRole(roleName: string) { + await this.http.delete(`/api/security/role/${encodeURIComponent(roleName)}`); + } + + public async saveRole({ role, spacesEnabled }: { role: Role; spacesEnabled: boolean }) { + await this.http.put(`/api/security/role/${encodeURIComponent(role.name)}`, { + body: JSON.stringify(this.transformRoleForSave(copyRole(role), spacesEnabled)), + }); + } + + private transformRoleForSave(role: Role, spacesEnabled: boolean) { + // Remove any placeholder index privileges + const isPlaceholderPrivilege = (indexPrivilege: RoleIndexPrivilege) => + indexPrivilege.names.length === 0; + role.elasticsearch.indices = role.elasticsearch.indices.filter( + indexPrivilege => !isPlaceholderPrivilege(indexPrivilege) + ); + + // Remove any placeholder query entries + role.elasticsearch.indices.forEach(index => index.query || delete index.query); + + // If spaces are disabled, then do not persist any space privileges + if (!spacesEnabled) { + role.kibana = role.kibana.filter(isGlobalPrivilegeDefinition); + } + + role.kibana.forEach(kibanaPrivilege => { + // If a base privilege is defined, then do not persist feature privileges + if (kibanaPrivilege.base.length > 0) { + kibanaPrivilege.feature = {}; + } + }); + + delete role.name; + delete role.transient_metadata; + delete role._unrecognized_applications; + delete role._transform_error; + + return role; + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/__snapshots__/roles_grid_page.test.tsx.snap b/x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/__snapshots__/roles_grid_page.test.tsx.snap rename to x-pack/plugins/security/public/management/roles/roles_grid/__snapshots__/roles_grid_page.test.tsx.snap diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/confirm_delete.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx similarity index 72% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/confirm_delete.tsx rename to x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx index 34784b4b2accb..37eed3357241d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/confirm_delete.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/confirm_delete.tsx @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import React, { Component, Fragment } from 'react'; import { EuiButton, EuiButtonEmpty, @@ -15,23 +16,24 @@ import { EuiOverlayMask, EuiText, } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import React, { Component, Fragment } from 'react'; -import { toastNotifications } from 'ui/notify'; -import { RolesApi } from '../../../../../lib/roles_api'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { RolesAPIClient } from '../../roles_api_client'; interface Props { rolesToDelete: string[]; - intl: InjectedIntl; callback: (rolesToDelete: string[], errors: string[]) => void; onCancel: () => void; + notifications: NotificationsStart; + rolesAPIClient: PublicMethodsOf; } interface State { deleteInProgress: boolean; } -class ConfirmDeleteUI extends Component { +export class ConfirmDelete extends Component { constructor(props: Props) { super(props); this.state = { @@ -40,15 +42,12 @@ class ConfirmDeleteUI extends Component { } public render() { - const { rolesToDelete, intl } = this.props; + const { rolesToDelete } = this.props; const moreThanOne = rolesToDelete.length > 1; - const title = intl.formatMessage( - { - id: 'xpack.security.management.roles.deleteRoleTitle', - defaultMessage: 'Delete role{value, plural, one {{roleName}} other {s}}', - }, - { value: rolesToDelete.length, roleName: ` ${rolesToDelete[0]}` } - ); + const title = i18n.translate('xpack.security.management.roles.deleteRoleTitle', { + defaultMessage: 'Delete role{value, plural, one {{roleName}} other {s}}', + values: { value: rolesToDelete.length, roleName: ` ${rolesToDelete[0]}` }, + }); // This is largely the same as the built-in EuiConfirmModal component, but we needed the ability // to disable the buttons since this could be a long-running operation @@ -128,32 +127,24 @@ class ConfirmDeleteUI extends Component { }; private deleteRoles = async () => { - const { rolesToDelete, callback } = this.props; + const { rolesToDelete, callback, rolesAPIClient, notifications } = this.props; const errors: string[] = []; const deleteOperations = rolesToDelete.map(roleName => { const deleteRoleTask = async () => { try { - await RolesApi.deleteRole(roleName); - toastNotifications.addSuccess( - this.props.intl.formatMessage( - { - id: - 'xpack.security.management.roles.confirmDelete.roleSuccessfullyDeletedNotificationMessage', - defaultMessage: 'Deleted role {roleName}', - }, - { roleName } + await rolesAPIClient.deleteRole(roleName); + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.roles.confirmDelete.roleSuccessfullyDeletedNotificationMessage', + { defaultMessage: 'Deleted role {roleName}', values: { roleName } } ) ); } catch (e) { errors.push(roleName); - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: - 'xpack.security.management.roles.confirmDelete.roleDeletingErrorNotificationMessage', - defaultMessage: 'Error deleting role {roleName}', - }, - { roleName } + notifications.toasts.addDanger( + i18n.translate( + 'xpack.security.management.roles.confirmDelete.roleDeletingErrorNotificationMessage', + { defaultMessage: 'Error deleting role {roleName}', values: { roleName } } ) ); } @@ -167,5 +158,3 @@ class ConfirmDeleteUI extends Component { callback(rolesToDelete, errors); }; } - -export const ConfirmDelete = injectI18n(ConfirmDeleteUI); diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/index.ts b/x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/confirm_delete/index.ts rename to x-pack/plugins/security/public/management/roles/roles_grid/confirm_delete/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/index.ts b/x-pack/plugins/security/public/management/roles/roles_grid/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/index.ts rename to x-pack/plugins/security/public/management/roles/roles_grid/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/permission_denied/index.ts b/x-pack/plugins/security/public/management/roles/roles_grid/permission_denied/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/permission_denied/index.ts rename to x-pack/plugins/security/public/management/roles/roles_grid/permission_denied/index.ts diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/permission_denied/permission_denied.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/permission_denied/permission_denied.tsx similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/permission_denied/permission_denied.tsx rename to x-pack/plugins/security/public/management/roles/roles_grid/permission_denied/permission_denied.tsx diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.test.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx similarity index 63% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.test.tsx rename to x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx index 6da2f2442d488..63ace53420612 100644 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.test.tsx @@ -4,50 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ -let mockSimulate403 = false; -const mock403 = () => ({ body: { statusCode: 403 } }); -jest.mock('../../../../lib/roles_api', () => { - return { - RolesApi: { - async getRoles() { - if (mockSimulate403) { - throw mock403(); - } - return [ - { - name: 'test-role-1', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: { global: [], space: {} }, - }, - { - name: 'reserved-role', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: { global: [], space: {} }, - metadata: { - _reserved: true, - }, - }, - { - name: 'disabled-role', - elasticsearch: { cluster: [], indices: [], run_as: [] }, - kibana: { global: [], space: {} }, - transient_metadata: { - enabled: false, - }, - }, - ]; - }, - }, - }; -}); - import { EuiIcon } from '@elastic/eui'; import { ReactWrapper } from 'enzyme'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { RolesAPIClient } from '../roles_api_client'; import { PermissionDenied } from './permission_denied'; import { RolesGridPage } from './roles_grid_page'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { rolesAPIClientMock } from '../index.mock'; + +const mock403 = () => ({ body: { statusCode: 403 } }); + const waitForRender = async ( wrapper: ReactWrapper, condition: (wrapper: ReactWrapper) => boolean @@ -69,12 +38,37 @@ const waitForRender = async ( }; describe('', () => { + let apiClientMock: jest.Mocked>; beforeEach(() => { - mockSimulate403 = false; + apiClientMock = rolesAPIClientMock.create(); + apiClientMock.getRoles.mockResolvedValue([ + { + name: 'test-role-1', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + }, + { + name: 'reserved-role', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + metadata: { _reserved: true }, + }, + { + name: 'disabled-role', + elasticsearch: { cluster: [], indices: [], run_as: [] }, + kibana: [{ base: [], spaces: [], feature: {} }], + transient_metadata: { enabled: false }, + }, + ]); }); it(`renders reserved roles as such`, async () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); const initialIconCount = wrapper.find(EuiIcon).length; await waitForRender(wrapper, updatedWrapper => { @@ -87,8 +81,14 @@ describe('', () => { }); it('renders permission denied if required', async () => { - mockSimulate403 = true; - const wrapper = mountWithIntl(); + apiClientMock.getRoles.mockRejectedValue(mock403()); + + const wrapper = mountWithIntl( + + ); await waitForRender(wrapper, updatedWrapper => { return updatedWrapper.find(PermissionDenied).length > 0; }); @@ -96,7 +96,12 @@ describe('', () => { }); it('renders role actions as appropriate', async () => { - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); const initialIconCount = wrapper.find(EuiIcon).length; await waitForRender(wrapper, updatedWrapper => { diff --git a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.tsx b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx similarity index 78% rename from x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.tsx rename to x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx index 2083a93f4b33c..7c686bef391a7 100644 --- a/x-pack/legacy/plugins/security/public/views/management/roles_grid/components/roles_grid_page.tsx +++ b/x-pack/plugins/security/public/management/roles/roles_grid/roles_grid_page.tsx @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import _ from 'lodash'; +import React, { Component } from 'react'; import { EuiButton, EuiIcon, @@ -18,18 +20,17 @@ import { EuiButtonIcon, EuiBasicTableColumn, } from '@elastic/eui'; -import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react'; -import _ from 'lodash'; -import React, { Component } from 'react'; -import { toastNotifications } from 'ui/notify'; -import { Role } from '../../../../../common/model'; -import { isRoleEnabled, isReadOnlyRole, isReservedRole } from '../../../../lib/role_utils'; -import { RolesApi } from '../../../../lib/roles_api'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { Role, isRoleEnabled, isReadOnlyRole, isReservedRole } from '../../../../common/model'; +import { RolesAPIClient } from '../roles_api_client'; import { ConfirmDelete } from './confirm_delete'; import { PermissionDenied } from './permission_denied'; interface Props { - intl: InjectedIntl; + notifications: NotificationsStart; + rolesAPIClient: PublicMethodsOf; } interface State { @@ -44,7 +45,7 @@ const getRoleManagementHref = (action: 'edit' | 'clone', roleName?: string) => { return `#/management/security/roles/${action}${roleName ? `/${roleName}` : ''}`; }; -class RolesGridPageUI extends Component { +export class RolesGridPage extends Component { constructor(props: Props) { super(props); this.state = { @@ -68,7 +69,6 @@ class RolesGridPageUI extends Component { private getPageContent = () => { const { roles } = this.state; - const { intl } = this.props; return ( @@ -105,6 +105,8 @@ class RolesGridPageUI extends Component { onCancel={this.onCancelDelete} rolesToDelete={this.state.selection.map(role => role.name)} callback={this.handleDelete} + notifications={this.props.notifications} + rolesAPIClient={this.props.rolesAPIClient} /> ) : null} @@ -112,7 +114,7 @@ class RolesGridPageUI extends Component { !role.metadata || !role.metadata._reserved, @@ -155,17 +157,16 @@ class RolesGridPageUI extends Component { ); }; - private getColumnConfig = (intl: InjectedIntl) => { - const reservedRoleDesc = intl.formatMessage({ - id: 'xpack.security.management.roles.reservedColumnDescription', - defaultMessage: 'Reserved roles are built-in and cannot be edited or removed.', - }); + private getColumnConfig = () => { + const reservedRoleDesc = i18n.translate( + 'xpack.security.management.roles.reservedColumnDescription', + { defaultMessage: 'Reserved roles are built-in and cannot be edited or removed.' } + ); return [ { field: 'name', - name: intl.formatMessage({ - id: 'xpack.security.management.roles.nameColumnName', + name: i18n.translate('xpack.security.management.roles.nameColumnName', { defaultMessage: 'Role', }), sortable: true, @@ -188,8 +189,7 @@ class RolesGridPageUI extends Component { }, { field: 'metadata', - name: intl.formatMessage({ - id: 'xpack.security.management.roles.reservedColumnName', + name: i18n.translate('xpack.security.management.roles.reservedColumnName', { defaultMessage: 'Reserved', }), sortable: ({ metadata }: Role) => Boolean(metadata && metadata._reserved), @@ -197,8 +197,7 @@ class RolesGridPageUI extends Component { align: 'right', description: reservedRoleDesc, render: (metadata: Role['metadata']) => { - const label = intl.formatMessage({ - id: 'xpack.security.management.roles.reservedRoleIconLabel', + const label = i18n.translate('xpack.security.management.roles.reservedRoleIconLabel', { defaultMessage: 'Reserved role', }); @@ -210,8 +209,7 @@ class RolesGridPageUI extends Component { }, }, { - name: intl.formatMessage({ - id: 'xpack.security.management.roles.actionsColumnName', + name: i18n.translate('xpack.security.management.roles.actionsColumnName', { defaultMessage: 'Actions', }), width: '150px', @@ -219,15 +217,10 @@ class RolesGridPageUI extends Component { { available: (role: Role) => !isReadOnlyRole(role), render: (role: Role) => { - const title = intl.formatMessage( - { - id: 'xpack.security.management.roles.editRoleActionName', - defaultMessage: `Edit {roleName}`, - }, - { - roleName: role.name, - } - ); + const title = i18n.translate('xpack.security.management.roles.editRoleActionName', { + defaultMessage: `Edit {roleName}`, + values: { roleName: role.name }, + }); return ( { { available: (role: Role) => !isReservedRole(role), render: (role: Role) => { - const title = intl.formatMessage( - { - id: 'xpack.security.management.roles.cloneRoleActionName', - defaultMessage: `Clone {roleName}`, - }, - { - roleName: role.name, - } - ); + const title = i18n.translate('xpack.security.management.roles.cloneRoleActionName', { + defaultMessage: `Clone {roleName}`, + values: { roleName: role.name }, + }); return ( { private async loadRoles() { try { - const roles = await RolesApi.getRoles(); + const roles = await this.props.rolesAPIClient.getRoles(); this.setState({ roles }); } catch (e) { if (_.get(e, 'body.statusCode') === 403) { this.setState({ permissionDenied: true }); } else { - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: 'xpack.security.management.roles.fetchingRolesErrorMessage', - defaultMessage: 'Error fetching roles: {message}', - }, - { message: _.get(e, 'body.message', '') } - ) + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.roles.fetchingRolesErrorMessage', { + defaultMessage: 'Error fetching roles: {message}', + values: { message: _.get(e, 'body.message', '') }, + }) ); } } @@ -339,5 +324,3 @@ class RolesGridPageUI extends Component { this.setState({ showDeleteConfirmation: false }); }; } - -export const RolesGridPage = injectI18n(RolesGridPageUI); diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx new file mode 100644 index 0000000000000..48bc1a6580a93 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.test.tsx @@ -0,0 +1,160 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { licenseMock } from '../../../common/licensing/index.mock'; + +jest.mock('./roles_grid', () => ({ + RolesGridPage: (props: any) => `Roles Page: ${JSON.stringify(props)}`, +})); + +jest.mock('./edit_role', () => ({ + EditRolePage: (props: any) => `Role Edit Page: ${JSON.stringify(props)}`, +})); + +import { rolesManagementApp } from './roles_management_app'; + +import { coreMock } from '../../../../../../src/core/public/mocks'; + +async function mountApp(basePath: string) { + const { fatalErrors } = coreMock.createSetup(); + const container = document.createElement('div'); + const setBreadcrumbs = jest.fn(); + + const unmount = await rolesManagementApp + .create({ + license: licenseMock.create(), + fatalErrors, + getStartServices: jest.fn().mockResolvedValue([coreMock.createStart(), { data: {} }]), + }) + .mount({ basePath, element: container, setBreadcrumbs }); + + return { unmount, container, setBreadcrumbs }; +} + +describe('rolesManagementApp', () => { + it('create() returns proper management app descriptor', () => { + const { fatalErrors, getStartServices } = coreMock.createSetup(); + + expect( + rolesManagementApp.create({ + license: licenseMock.create(), + fatalErrors, + getStartServices: getStartServices as any, + }) + ).toMatchInlineSnapshot(` + Object { + "id": "roles", + "mount": [Function], + "order": 20, + "title": "Roles", + } + `); + }); + + it('mount() works for the `grid` page', async () => { + const basePath = '/some-base-path/roles'; + window.location.hash = basePath; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `#${basePath}`, text: 'Roles' }]); + expect(container).toMatchInlineSnapshot(` +
+ Roles Page: {"notifications":{"toasts":{}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `create role` page', async () => { + const basePath = '/some-base-path/roles'; + window.location.hash = `${basePath}/edit`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Roles' }, + { text: 'Create' }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Edit Page: {"action":"edit","rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"userAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"http":{"basePath":{"basePath":""},"anonymousPaths":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `edit role` page', async () => { + const basePath = '/some-base-path/roles'; + const roleName = 'someRoleName'; + window.location.hash = `${basePath}/edit/${roleName}`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Roles' }, + { href: `#/some-base-path/roles/edit/${roleName}`, text: roleName }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Edit Page: {"action":"edit","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"userAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"http":{"basePath":{"basePath":""},"anonymousPaths":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `clone role` page', async () => { + const basePath = '/some-base-path/roles'; + const roleName = 'someRoleName'; + window.location.hash = `${basePath}/clone/${roleName}`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Roles' }, + { text: 'Create' }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ Role Edit Page: {"action":"clone","roleName":"someRoleName","rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"userAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"indicesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"privilegesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"http":{"basePath":{"basePath":""},"anonymousPaths":{}},"notifications":{"toasts":{}},"fatalErrors":{},"license":{"features$":{"_isScalar":false}},"docLinks":{"esDocBasePath":"https://www.elastic.co/guide/en/elasticsearch/reference/mocked-test-branch/"},"uiCapabilities":{"catalogue":{},"management":{},"navLinks":{}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() properly encodes role name in `edit role` page link in breadcrumbs', async () => { + const basePath = '/some-base-path/roles'; + const roleName = 'some 安全性 role'; + window.location.hash = `${basePath}/edit/${roleName}`; + + const { setBreadcrumbs } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Roles' }, + { + href: '#/some-base-path/roles/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20role', + text: roleName, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security/public/management/roles/roles_management_app.tsx b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx new file mode 100644 index 0000000000000..4e8c95b61c2f1 --- /dev/null +++ b/x-pack/plugins/security/public/management/roles/roles_management_app.tsx @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { HashRouter as Router, Route, Switch, useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup, FatalErrorsSetup } from 'src/core/public'; +import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { SecurityLicense } from '../../../common/licensing'; +import { PluginStartDependencies } from '../../plugin'; +import { UserAPIClient } from '../users'; +import { RolesAPIClient } from './roles_api_client'; +import { RolesGridPage } from './roles_grid'; +import { EditRolePage } from './edit_role'; +import { DocumentationLinksService } from './documentation_links'; +import { IndicesAPIClient } from './indices_api_client'; +import { PrivilegesAPIClient } from './privileges_api_client'; + +interface CreateParams { + fatalErrors: FatalErrorsSetup; + license: SecurityLicense; + getStartServices: CoreSetup['getStartServices']; +} + +export const rolesManagementApp = Object.freeze({ + id: 'roles', + create({ license, fatalErrors, getStartServices }: CreateParams) { + return { + id: this.id, + order: 20, + title: i18n.translate('xpack.security.management.rolesTitle', { defaultMessage: 'Roles' }), + async mount({ basePath, element, setBreadcrumbs }) { + const [ + { application, docLinks, http, i18n: i18nStart, injectedMetadata, notifications }, + { data }, + ] = await getStartServices(); + + const rolesBreadcrumbs = [ + { + text: i18n.translate('xpack.security.roles.breadcrumb', { defaultMessage: 'Roles' }), + href: `#${basePath}`, + }, + ]; + + const rolesAPIClient = new RolesAPIClient(http); + const RolesGridPageWithBreadcrumbs = () => { + setBreadcrumbs(rolesBreadcrumbs); + return ; + }; + + const EditRolePageWithBreadcrumbs = ({ action }: { action: 'edit' | 'clone' }) => { + const { roleName } = useParams<{ roleName?: string }>(); + + setBreadcrumbs([ + ...rolesBreadcrumbs, + action === 'edit' && roleName + ? { text: roleName, href: `#${basePath}/edit/${encodeURIComponent(roleName)}` } + : { + text: i18n.translate('xpack.security.roles.createBreadcrumb', { + defaultMessage: 'Create', + }), + }, + ]); + + return ( + + ); + }; + + render( + + + + + + + + + + + + + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; + }, + } as RegisterManagementAppArgs; + }, +}); diff --git a/x-pack/plugins/security/public/management/users/_index.scss b/x-pack/plugins/security/public/management/users/_index.scss new file mode 100644 index 0000000000000..35df0c1b96583 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/_index.scss @@ -0,0 +1 @@ +@import './edit_user/index'; diff --git a/x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.test.tsx b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx similarity index 82% rename from x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.test.tsx rename to x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx index 221120532318c..be46612767a59 100644 --- a/x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.test.tsx +++ b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.test.tsx @@ -7,10 +7,12 @@ import { EuiFieldText } from '@elastic/eui'; import { ReactWrapper } from 'enzyme'; import React from 'react'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { User } from '../../../../common/model'; -import { UserAPIClient } from '../../../lib/api'; +import { User } from '../../../../../common/model'; import { ChangePasswordForm } from './change_password_form'; +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { userAPIClientMock } from '../../index.mock'; + function getCurrentPasswordField(wrapper: ReactWrapper) { return wrapper.find(EuiFieldText).filter('[data-test-subj="currentPassword"]'); } @@ -23,8 +25,6 @@ function getConfirmPasswordField(wrapper: ReactWrapper) { return wrapper.find(EuiFieldText).filter('[data-test-subj="confirmNewPassword"]'); } -jest.mock('ui/kfetch'); - describe('', () => { describe('for the current user', () => { it('shows fields for current and new passwords', () => { @@ -40,7 +40,8 @@ describe('', () => { ); @@ -60,15 +61,15 @@ describe('', () => { const callback = jest.fn(); - const apiClient = new UserAPIClient(); - apiClient.changePassword = jest.fn(); + const apiClientMock = userAPIClientMock.create(); const wrapper = mountWithIntl( ); @@ -83,8 +84,8 @@ describe('', () => { wrapper.find('button[data-test-subj="changePasswordButton"]').simulate('click'); - expect(apiClient.changePassword).toHaveBeenCalledTimes(1); - expect(apiClient.changePassword).toHaveBeenCalledWith( + expect(apiClientMock.changePassword).toHaveBeenCalledTimes(1); + expect(apiClientMock.changePassword).toHaveBeenCalledWith( 'user', 'myNewPassword', 'myCurrentPassword' @@ -106,7 +107,8 @@ describe('', () => { ); diff --git a/x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.tsx b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx similarity index 96% rename from x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.tsx rename to x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx index 61c0b77decd56..6dcf330ec6f9e 100644 --- a/x-pack/legacy/plugins/security/public/components/management/change_password_form/change_password_form.tsx +++ b/x-pack/plugins/security/public/management/users/components/change_password_form/change_password_form.tsx @@ -5,10 +5,7 @@ */ import { EuiButton, - // @ts-ignore EuiButtonEmpty, - // @ts-ignore - EuiDescribedFormGroup, EuiFieldText, EuiFlexGroup, EuiFlexItem, @@ -18,15 +15,16 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { ChangeEvent, Component } from 'react'; -import { toastNotifications } from 'ui/notify'; -import { User } from '../../../../common/model'; -import { UserAPIClient } from '../../../lib/api'; +import { NotificationsStart } from 'src/core/public'; +import { User } from '../../../../../common/model'; +import { UserAPIClient } from '../..'; interface Props { user: User; isUserChangingOwnPassword: boolean; onChangePassword?: () => void; - apiClient: UserAPIClient; + apiClient: PublicMethodsOf; + notifications: NotificationsStart; } interface State { @@ -294,7 +292,7 @@ export class ChangePasswordForm extends Component { }; private handleChangePasswordSuccess = () => { - toastNotifications.addSuccess({ + this.props.notifications.toasts.addSuccess({ title: i18n.translate('xpack.security.account.changePasswordSuccess', { defaultMessage: 'Your password has been changed.', }), @@ -317,7 +315,7 @@ export class ChangePasswordForm extends Component { if (error.body && error.body.statusCode === 403) { this.setState({ currentPasswordError: true }); } else { - toastNotifications.addDanger( + this.props.notifications.toasts.addDanger( i18n.translate('xpack.security.management.users.editUser.settingPasswordErrorMessage', { defaultMessage: 'Error setting password: {message}', values: { message: _.get(error, 'body.message') }, diff --git a/x-pack/legacy/plugins/security/public/components/management/change_password_form/index.ts b/x-pack/plugins/security/public/management/users/components/change_password_form/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/components/management/change_password_form/index.ts rename to x-pack/plugins/security/public/management/users/components/change_password_form/index.ts diff --git a/x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.test.tsx b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.test.tsx similarity index 58% rename from x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.test.tsx rename to x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.test.tsx index 9f69fc7a7551f..bcec707b03f93 100644 --- a/x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.test.tsx +++ b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.test.tsx @@ -5,16 +5,21 @@ */ import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { ConfirmDeleteUsers } from './confirm_delete'; +import { ConfirmDeleteUsers } from './confirm_delete_users'; import React from 'react'; -import { UserAPIClient } from '../../../lib/api'; -jest.mock('ui/kfetch'); +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { userAPIClientMock } from '../../index.mock'; describe('ConfirmDeleteUsers', () => { it('renders a warning for a single user', () => { const wrapper = mountWithIntl( - + ); expect(wrapper.find('EuiModalHeaderTitle').text()).toMatchInlineSnapshot(`"Delete user foo"`); @@ -23,7 +28,8 @@ describe('ConfirmDeleteUsers', () => { it('renders a warning for a multiple users', () => { const wrapper = mountWithIntl( @@ -35,7 +41,12 @@ describe('ConfirmDeleteUsers', () => { it('fires onCancel when the operation is cancelled', () => { const onCancel = jest.fn(); const wrapper = mountWithIntl( - + ); expect(onCancel).toBeCalledTimes(0); @@ -47,50 +58,48 @@ describe('ConfirmDeleteUsers', () => { it('deletes the requested users when confirmed', () => { const onCancel = jest.fn(); - const deleteUser = jest.fn(); - - const apiClient = new UserAPIClient(); - apiClient.deleteUser = deleteUser; + const apiClientMock = userAPIClientMock.create(); const wrapper = mountWithIntl( ); wrapper.find('EuiButton[data-test-subj="confirmModalConfirmButton"]').simulate('click'); - expect(deleteUser).toBeCalledTimes(2); - expect(deleteUser).toBeCalledWith('foo'); - expect(deleteUser).toBeCalledWith('bar'); + expect(apiClientMock.deleteUser).toBeCalledTimes(2); + expect(apiClientMock.deleteUser).toBeCalledWith('foo'); + expect(apiClientMock.deleteUser).toBeCalledWith('bar'); }); it('attempts to delete all users even if some fail', () => { const onCancel = jest.fn(); - const deleteUser = jest.fn().mockImplementation(user => { + + const apiClientMock = userAPIClientMock.create(); + apiClientMock.deleteUser.mockImplementation(user => { if (user === 'foo') { return Promise.reject('something terrible happened'); } return Promise.resolve(); }); - const apiClient = new UserAPIClient(); - apiClient.deleteUser = deleteUser; - const wrapper = mountWithIntl( ); wrapper.find('EuiButton[data-test-subj="confirmModalConfirmButton"]').simulate('click'); - expect(deleteUser).toBeCalledTimes(2); - expect(deleteUser).toBeCalledWith('foo'); - expect(deleteUser).toBeCalledWith('bar'); + expect(apiClientMock.deleteUser).toBeCalledTimes(2); + expect(apiClientMock.deleteUser).toBeCalledWith('foo'); + expect(apiClientMock.deleteUser).toBeCalledWith('bar'); }); }); diff --git a/x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.tsx b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx similarity index 50% rename from x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.tsx rename to x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx index 53bb022afb513..b7269e0168d7d 100644 --- a/x-pack/legacy/plugins/security/public/components/management/users/confirm_delete.tsx +++ b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/confirm_delete_users.tsx @@ -6,51 +6,46 @@ import React, { Component, Fragment } from 'react'; import { EuiOverlayMask, EuiConfirmModal } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; -import { FormattedMessage, injectI18n, InjectedIntl } from '@kbn/i18n/react'; -import { UserAPIClient } from '../../../lib/api'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { UserAPIClient } from '../..'; interface Props { - intl: InjectedIntl; usersToDelete: string[]; - apiClient: UserAPIClient; + apiClient: PublicMethodsOf; + notifications: NotificationsStart; onCancel: () => void; callback?: (usersToDelete: string[], errors: string[]) => void; } -class ConfirmDeleteUI extends Component { +export class ConfirmDeleteUsers extends Component { public render() { - const { usersToDelete, onCancel, intl } = this.props; + const { usersToDelete, onCancel } = this.props; const moreThanOne = usersToDelete.length > 1; const title = moreThanOne - ? intl.formatMessage( - { - id: 'xpack.security.management.users.confirmDelete.deleteMultipleUsersTitle', - defaultMessage: 'Delete {userLength} users', - }, - { userLength: usersToDelete.length } - ) - : intl.formatMessage( - { - id: 'xpack.security.management.users.confirmDelete.deleteOneUserTitle', - defaultMessage: 'Delete user {userLength}', - }, - { userLength: usersToDelete[0] } - ); + ? i18n.translate('xpack.security.management.users.confirmDelete.deleteMultipleUsersTitle', { + defaultMessage: 'Delete {userLength} users', + values: { userLength: usersToDelete.length }, + }) + : i18n.translate('xpack.security.management.users.confirmDelete.deleteOneUserTitle', { + defaultMessage: 'Delete user {userLength}', + values: { userLength: usersToDelete[0] }, + }); return (
@@ -82,31 +77,23 @@ class ConfirmDeleteUI extends Component { } private deleteUsers = () => { - const { usersToDelete, callback, apiClient } = this.props; + const { usersToDelete, callback, apiClient, notifications } = this.props; const errors: string[] = []; usersToDelete.forEach(async username => { try { await apiClient.deleteUser(username); - toastNotifications.addSuccess( - this.props.intl.formatMessage( - { - id: - 'xpack.security.management.users.confirmDelete.userSuccessfullyDeletedNotificationMessage', - defaultMessage: 'Deleted user {username}', - }, - { username } + notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.users.confirmDelete.userSuccessfullyDeletedNotificationMessage', + { defaultMessage: 'Deleted user {username}', values: { username } } ) ); } catch (e) { errors.push(username); - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: - 'xpack.security.management.users.confirmDelete.userDeletingErrorNotificationMessage', - defaultMessage: 'Error deleting user {username}', - }, - { username } + notifications.toasts.addDanger( + i18n.translate( + 'xpack.security.management.users.confirmDelete.userDeletingErrorNotificationMessage', + { defaultMessage: 'Error deleting user {username}', values: { username } } ) ); } @@ -116,5 +103,3 @@ class ConfirmDeleteUI extends Component { }); }; } - -export const ConfirmDeleteUsers = injectI18n(ConfirmDeleteUI); diff --git a/x-pack/legacy/plugins/security/public/components/management/users/index.ts b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/index.ts similarity index 79% rename from x-pack/legacy/plugins/security/public/components/management/users/index.ts rename to x-pack/plugins/security/public/management/users/components/confirm_delete_users/index.ts index 813e671c05ccf..fde35ab0f0d02 100644 --- a/x-pack/legacy/plugins/security/public/components/management/users/index.ts +++ b/x-pack/plugins/security/public/management/users/components/confirm_delete_users/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { ConfirmDeleteUsers } from './confirm_delete'; +export { ConfirmDeleteUsers } from './confirm_delete_users'; diff --git a/x-pack/plugins/security/public/management/users/components/index.ts b/x-pack/plugins/security/public/management/users/components/index.ts new file mode 100644 index 0000000000000..54011a6a24cbd --- /dev/null +++ b/x-pack/plugins/security/public/management/users/components/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { ChangePasswordForm } from './change_password_form'; +export { ConfirmDeleteUsers } from './confirm_delete_users'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/_users.scss b/x-pack/plugins/security/public/management/users/edit_user/_edit_user_page.scss similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_user/_users.scss rename to x-pack/plugins/security/public/management/users/edit_user/_edit_user_page.scss diff --git a/x-pack/plugins/security/public/management/users/edit_user/_index.scss b/x-pack/plugins/security/public/management/users/edit_user/_index.scss new file mode 100644 index 0000000000000..734ba7882ba72 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/edit_user/_index.scss @@ -0,0 +1 @@ +@import './edit_user_page'; diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.test.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx similarity index 70% rename from x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.test.tsx rename to x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx index 639646ce48e22..543d20bb92afe 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.test.tsx @@ -8,13 +8,14 @@ import { act } from '@testing-library/react'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { EditUserPage } from './edit_user_page'; import React from 'react'; -import { securityMock } from '../../../../../../../../plugins/security/public/mocks'; -import { UserAPIClient } from '../../../../lib/api'; -import { User, Role } from '../../../../../common/model'; +import { User, Role } from '../../../../common/model'; import { ReactWrapper } from 'enzyme'; -import { mockAuthenticatedUser } from '../../../../../../../../plugins/security/common/model/authenticated_user.mock'; -jest.mock('ui/kfetch'); +import { coreMock } from '../../../../../../../src/core/public/mocks'; +import { mockAuthenticatedUser } from '../../../../common/model/authenticated_user.mock'; +import { securityMock } from '../../../mocks'; +import { rolesAPIClientMock } from '../../roles/index.mock'; +import { userAPIClientMock } from '../index.mock'; const createUser = (username: string) => { const user: User = { @@ -34,14 +35,12 @@ const createUser = (username: string) => { return user; }; -const buildClient = () => { - const apiClient = new UserAPIClient(); +const buildClients = () => { + const apiClient = userAPIClientMock.create(); + apiClient.getUser.mockImplementation(async (username: string) => createUser(username)); - apiClient.getUser = jest - .fn() - .mockImplementation(async (username: string) => createUser(username)); - - apiClient.getRoles = jest.fn().mockImplementation(() => { + const rolesAPIClient = rolesAPIClientMock.create(); + rolesAPIClient.getRoles.mockImplementation(() => { return Promise.resolve([ { name: 'role 1', @@ -64,7 +63,7 @@ const buildClient = () => { ] as Role[]); }); - return apiClient; + return { apiClient, rolesAPIClient }; }; function buildSecuritySetup() { @@ -85,15 +84,15 @@ function expectMissingSaveButton(wrapper: ReactWrapper) { describe('EditUserPage', () => { it('allows reserved users to be viewed', async () => { - const apiClient = buildClient(); + const { apiClient, rolesAPIClient } = buildClients(); const securitySetup = buildSecuritySetup(); const wrapper = mountWithIntl( - path} - intl={null as any} + rolesAPIClient={rolesAPIClient} + authc={securitySetup.authc} + notifications={coreMock.createStart().notifications} /> ); @@ -106,15 +105,15 @@ describe('EditUserPage', () => { }); it('allows new users to be created', async () => { - const apiClient = buildClient(); + const { apiClient, rolesAPIClient } = buildClients(); const securitySetup = buildSecuritySetup(); const wrapper = mountWithIntl( - path} - intl={null as any} + rolesAPIClient={rolesAPIClient} + authc={securitySetup.authc} + notifications={coreMock.createStart().notifications} /> ); @@ -127,15 +126,15 @@ describe('EditUserPage', () => { }); it('allows existing users to be edited', async () => { - const apiClient = buildClient(); + const { apiClient, rolesAPIClient } = buildClients(); const securitySetup = buildSecuritySetup(); const wrapper = mountWithIntl( - path} - intl={null as any} + rolesAPIClient={rolesAPIClient} + authc={securitySetup.authc} + notifications={coreMock.createStart().notifications} /> ); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx similarity index 77% rename from x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx rename to x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx index bbffe07485f8d..576f3ff9e6008 100644 --- a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/edit_user_page.tsx +++ b/x-pack/plugins/security/public/management/users/edit_user/edit_user_page.tsx @@ -26,22 +26,23 @@ import { EuiHorizontalRule, EuiSpacer, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; -import { FormattedMessage, injectI18n, InjectedIntl } from '@kbn/i18n/react'; -import { SecurityPluginSetup } from '../../../../../../../../plugins/security/public'; -import { UserValidator, UserValidationResult } from '../../../../lib/validate_user'; -import { User, EditUser, Role } from '../../../../../common/model'; -import { USERS_PATH } from '../../../../views/management/management_urls'; -import { ConfirmDeleteUsers } from '../../../../components/management/users'; -import { UserAPIClient } from '../../../../lib/api'; -import { ChangePasswordForm } from '../../../../components/management/change_password_form'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { User, EditUser, Role } from '../../../../common/model'; +import { AuthenticationServiceSetup } from '../../../authentication'; +import { USERS_PATH } from '../../management_urls'; +import { RolesAPIClient } from '../../roles'; +import { ConfirmDeleteUsers, ChangePasswordForm } from '../components'; +import { UserValidator, UserValidationResult } from './validate_user'; +import { UserAPIClient } from '..'; interface Props { - username: string; - intl: InjectedIntl; - changeUrl: (path: string) => void; - apiClient: UserAPIClient; - securitySetup: SecurityPluginSetup; + username?: string; + apiClient: PublicMethodsOf; + rolesAPIClient: PublicMethodsOf; + authc: AuthenticationServiceSetup; + notifications: NotificationsStart; } interface State { @@ -56,7 +57,11 @@ interface State { formError: UserValidationResult | null; } -class EditUserPageUI extends Component { +function backToUserList() { + window.location.hash = USERS_PATH; +} + +export class EditUserPage extends Component { private validator: UserValidator; constructor(props: Props) { @@ -84,7 +89,17 @@ class EditUserPageUI extends Component { } public async componentDidMount() { - const { username, apiClient, securitySetup } = this.props; + await this.setCurrentUser(); + } + + public async componentDidUpdate(prevProps: Props) { + if (prevProps.username !== this.props.username) { + await this.setCurrentUser(); + } + } + + private async setCurrentUser() { + const { username, apiClient, rolesAPIClient, notifications, authc } = this.props; let { user, currentUser } = this.state; if (username) { try { @@ -93,26 +108,24 @@ class EditUserPageUI extends Component { password: '', confirmPassword: '', }; - currentUser = await securitySetup.authc.getCurrentUser(); + currentUser = await authc.getCurrentUser(); } catch (err) { - toastNotifications.addDanger({ - title: this.props.intl.formatMessage({ - id: 'xpack.security.management.users.editUser.errorLoadingUserTitle', + notifications.toasts.addDanger({ + title: i18n.translate('xpack.security.management.users.editUser.errorLoadingUserTitle', { defaultMessage: 'Error loading user', }), text: get(err, 'body.message') || err.message, }); - return; + return backToUserList(); } } let roles: Role[] = []; try { - roles = await apiClient.getRoles(); + roles = await rolesAPIClient.getRoles(); } catch (err) { - toastNotifications.addDanger({ - title: this.props.intl.formatMessage({ - id: 'xpack.security.management.users.editUser.errorLoadingRolesTitle', + notifications.toasts.addDanger({ + title: i18n.translate('xpack.security.management.users.editUser.errorLoadingRolesTitle', { defaultMessage: 'Error loading roles', }), text: get(err, 'body.message') || err.message, @@ -131,8 +144,7 @@ class EditUserPageUI extends Component { private handleDelete = (usernames: string[], errors: string[]) => { if (errors.length === 0) { - const { changeUrl } = this.props; - changeUrl(USERS_PATH); + backToUserList(); } }; @@ -148,7 +160,7 @@ class EditUserPageUI extends Component { this.setState({ formError: null, }); - const { changeUrl, apiClient } = this.props; + const { apiClient } = this.props; const { user, isNewUser, selectedRoles } = this.state; const userToSave: EditUser = { ...user }; if (!isNewUser) { @@ -160,26 +172,23 @@ class EditUserPageUI extends Component { }); try { await apiClient.saveUser(userToSave); - toastNotifications.addSuccess( - this.props.intl.formatMessage( + this.props.notifications.toasts.addSuccess( + i18n.translate( + 'xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage', { - id: - 'xpack.security.management.users.editUser.userSuccessfullySavedNotificationMessage', defaultMessage: 'Saved user {message}', - }, - { message: user.username } + values: { message: user.username }, + } ) ); - changeUrl(USERS_PATH); + + backToUserList(); } catch (e) { - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: 'xpack.security.management.users.editUser.savingUserErrorMessage', - defaultMessage: 'Error saving user: {message}', - }, - { message: get(e, 'body.message', 'Unknown error') } - ) + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.users.editUser.savingUserErrorMessage', { + defaultMessage: 'Error saving user: {message}', + values: { message: get(e, 'body.message', 'Unknown error') }, + }) ); } } @@ -189,8 +198,7 @@ class EditUserPageUI extends Component { return ( { /> { {user.username === 'kibana' ? ( @@ -260,6 +268,7 @@ class EditUserPageUI extends Component { isUserChangingOwnPassword={userIsLoggedInUser} onChangePassword={this.toggleChangePasswordForm} apiClient={this.props.apiClient} + notifications={this.props.notifications} /> ); @@ -352,7 +361,6 @@ class EditUserPageUI extends Component { }; public render() { - const { changeUrl, intl } = this.props; const { user, roles, @@ -417,6 +425,7 @@ class EditUserPageUI extends Component { usersToDelete={[user.username]} callback={this.handleDelete} apiClient={this.props.apiClient} + notifications={this.props.notifications} /> ) : null} @@ -425,17 +434,16 @@ class EditUserPageUI extends Component { {...this.validator.validateUsername(this.state.user)} helpText={ !isNewUser && !reserved - ? intl.formatMessage({ - id: - 'xpack.security.management.users.editUser.changingUserNameAfterCreationDescription', - defaultMessage: `Usernames can't be changed after creation.`, - }) + ? i18n.translate( + 'xpack.security.management.users.editUser.changingUserNameAfterCreationDescription', + { defaultMessage: `Usernames can't be changed after creation.` } + ) : null } - label={intl.formatMessage({ - id: 'xpack.security.management.users.editUser.usernameFormRowLabel', - defaultMessage: 'Username', - })} + label={i18n.translate( + 'xpack.security.management.users.editUser.usernameFormRowLabel', + { defaultMessage: 'Username' } + )} > { {reserved ? null : ( { { )} { @@ -513,7 +521,7 @@ class EditUserPageUI extends Component { {reserved && ( - changeUrl(USERS_PATH)}> + { - changeUrl(USERS_PATH)} - > + { ); } } - -export const EditUserPage = injectI18n(EditUserPageUI); diff --git a/x-pack/legacy/plugins/security/public/views/management/edit_user/components/index.ts b/x-pack/plugins/security/public/management/users/edit_user/index.ts similarity index 100% rename from x-pack/legacy/plugins/security/public/views/management/edit_user/components/index.ts rename to x-pack/plugins/security/public/management/users/edit_user/index.ts diff --git a/x-pack/legacy/plugins/security/public/lib/validate_user.test.ts b/x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts similarity index 98% rename from x-pack/legacy/plugins/security/public/lib/validate_user.test.ts rename to x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts index 0535248fede88..6050e1868a759 100644 --- a/x-pack/legacy/plugins/security/public/lib/validate_user.test.ts +++ b/x-pack/plugins/security/public/management/users/edit_user/validate_user.test.ts @@ -5,7 +5,7 @@ */ import { UserValidator, UserValidationResult } from './validate_user'; -import { User, EditUser } from '../../common/model'; +import { User, EditUser } from '../../../../common/model'; function expectValid(result: UserValidationResult) { expect(result.isInvalid).toBe(false); diff --git a/x-pack/legacy/plugins/security/public/lib/validate_user.ts b/x-pack/plugins/security/public/management/users/edit_user/validate_user.ts similarity index 98% rename from x-pack/legacy/plugins/security/public/lib/validate_user.ts rename to x-pack/plugins/security/public/management/users/edit_user/validate_user.ts index 113aaacdcbf96..5edd96c68bf0d 100644 --- a/x-pack/legacy/plugins/security/public/lib/validate_user.ts +++ b/x-pack/plugins/security/public/management/users/edit_user/validate_user.ts @@ -5,7 +5,7 @@ */ import { i18n } from '@kbn/i18n'; -import { User, EditUser } from '../../common/model'; +import { User, EditUser } from '../../../../common/model'; interface UserValidatorOptions { shouldValidate?: boolean; diff --git a/x-pack/legacy/plugins/security/public/views/management/index.js b/x-pack/plugins/security/public/management/users/index.mock.ts similarity index 80% rename from x-pack/legacy/plugins/security/public/views/management/index.js rename to x-pack/plugins/security/public/management/users/index.mock.ts index 0ed6fe09ef80a..f090f88da500d 100644 --- a/x-pack/legacy/plugins/security/public/views/management/index.js +++ b/x-pack/plugins/security/public/management/users/index.mock.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -import './management'; +export { userAPIClientMock } from './user_api_client.mock'; diff --git a/x-pack/legacy/plugins/security/public/objects/index.ts b/x-pack/plugins/security/public/management/users/index.ts similarity index 68% rename from x-pack/legacy/plugins/security/public/objects/index.ts rename to x-pack/plugins/security/public/management/users/index.ts index a6238ca879901..c8b4d41973da6 100644 --- a/x-pack/legacy/plugins/security/public/objects/index.ts +++ b/x-pack/plugins/security/public/management/users/index.ts @@ -4,6 +4,5 @@ * you may not use this file except in compliance with the Elastic License. */ -export { saveRole, deleteRole } from './lib/roles'; - -export { getFields } from './lib/get_fields'; +export { UserAPIClient } from './user_api_client'; +export { usersManagementApp } from './users_management_app'; diff --git a/x-pack/plugins/security/public/management/users/user_api_client.mock.ts b/x-pack/plugins/security/public/management/users/user_api_client.mock.ts new file mode 100644 index 0000000000000..7223f78d57fdc --- /dev/null +++ b/x-pack/plugins/security/public/management/users/user_api_client.mock.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const userAPIClientMock = { + create: () => ({ + getUsers: jest.fn(), + getUser: jest.fn(), + deleteUser: jest.fn(), + saveUser: jest.fn(), + changePassword: jest.fn(), + }), +}; diff --git a/x-pack/plugins/security/public/management/users/user_api_client.ts b/x-pack/plugins/security/public/management/users/user_api_client.ts new file mode 100644 index 0000000000000..61dd09d2c5e3d --- /dev/null +++ b/x-pack/plugins/security/public/management/users/user_api_client.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpStart } from 'src/core/public'; +import { User, EditUser } from '../../../common/model'; + +const usersUrl = '/internal/security/users'; + +export class UserAPIClient { + constructor(private readonly http: HttpStart) {} + + public async getUsers() { + return await this.http.get(usersUrl); + } + + public async getUser(username: string) { + return await this.http.get(`${usersUrl}/${encodeURIComponent(username)}`); + } + + public async deleteUser(username: string) { + await this.http.delete(`${usersUrl}/${encodeURIComponent(username)}`); + } + + public async saveUser(user: EditUser) { + await this.http.post(`${usersUrl}/${encodeURIComponent(user.username)}`, { + body: JSON.stringify(user), + }); + } + + public async changePassword(username: string, password: string, currentPassword: string) { + const data: Record = { + newPassword: password, + }; + if (currentPassword) { + data.password = currentPassword; + } + + await this.http.post(`${usersUrl}/${encodeURIComponent(username)}/password`, { + body: JSON.stringify(data), + }); + } +} diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/index.ts b/x-pack/plugins/security/public/management/users/users_grid/index.ts similarity index 82% rename from x-pack/legacy/plugins/security/public/views/management/users_grid/components/index.ts rename to x-pack/plugins/security/public/management/users/users_grid/index.ts index 03721f5ce93b1..90e16248e19c3 100644 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/index.ts +++ b/x-pack/plugins/security/public/management/users/users_grid/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { UsersListPage } from './users_list_page'; +export { UsersGridPage } from './users_grid_page'; diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.test.tsx b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx similarity index 63% rename from x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.test.tsx rename to x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx index bdc0df9bae67c..def0649953437 100644 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.test.tsx +++ b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.test.tsx @@ -4,19 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { UserAPIClient } from '../../../../lib/api'; -import { User } from '../../../../../common/model'; +import { User } from '../../../../common/model'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { UsersListPage } from './users_list_page'; +import { UsersGridPage } from './users_grid_page'; import React from 'react'; import { ReactWrapper } from 'enzyme'; +import { userAPIClientMock } from '../index.mock'; +import { coreMock } from '../../../../../../../src/core/public/mocks'; -jest.mock('ui/kfetch'); - -describe('UsersListPage', () => { +describe('UsersGridPage', () => { it('renders the list of users', async () => { - const apiClient = new UserAPIClient(); - apiClient.getUsers = jest.fn().mockImplementation(() => { + const apiClientMock = userAPIClientMock.create(); + apiClientMock.getUsers.mockImplementation(() => { return Promise.resolve([ { username: 'foo', @@ -38,22 +37,27 @@ describe('UsersListPage', () => { ]); }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); await waitForRender(wrapper); - expect(apiClient.getUsers).toBeCalledTimes(1); + expect(apiClientMock.getUsers).toBeCalledTimes(1); expect(wrapper.find('EuiInMemoryTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(2); }); it('renders a forbidden message if user is not authorized', async () => { - const apiClient = new UserAPIClient(); - apiClient.getUsers = jest.fn().mockImplementation(() => { - return Promise.reject({ body: { statusCode: 403 } }); - }); + const apiClient = userAPIClientMock.create(); + apiClient.getUsers.mockRejectedValue({ body: { statusCode: 403 } }); - const wrapper = mountWithIntl(); + const wrapper = mountWithIntl( + + ); await waitForRender(wrapper); diff --git a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.tsx b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx similarity index 85% rename from x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.tsx rename to x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx index df8522e5f32f9..fa15c3388fcc9 100644 --- a/x-pack/legacy/plugins/security/public/views/management/users_grid/components/users_list_page.tsx +++ b/x-pack/plugins/security/public/management/users/users_grid/users_grid_page.tsx @@ -19,15 +19,16 @@ import { EuiEmptyPrompt, EuiBasicTableColumn, } from '@elastic/eui'; -import { toastNotifications } from 'ui/notify'; -import { injectI18n, FormattedMessage, InjectedIntl } from '@kbn/i18n/react'; -import { ConfirmDeleteUsers } from '../../../../components/management/users'; -import { User } from '../../../../../common/model'; -import { UserAPIClient } from '../../../../lib/api'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { NotificationsStart } from 'src/core/public'; +import { User } from '../../../../common/model'; +import { ConfirmDeleteUsers } from '../components'; +import { UserAPIClient } from '..'; interface Props { - intl: InjectedIntl; - apiClient: UserAPIClient; + apiClient: PublicMethodsOf; + notifications: NotificationsStart; } interface State { @@ -38,7 +39,7 @@ interface State { filter: string; } -class UsersListPageUI extends Component { +export class UsersGridPage extends Component { constructor(props: Props) { super(props); this.state = { @@ -56,7 +57,6 @@ class UsersListPageUI extends Component { public render() { const { users, filter, permissionDenied, showDeleteConfirmation, selection } = this.state; - const { intl } = this.props; if (permissionDenied) { return ( @@ -88,8 +88,7 @@ class UsersListPageUI extends Component { const columns: Array> = [ { field: 'full_name', - name: intl.formatMessage({ - id: 'xpack.security.management.users.fullNameColumnName', + name: i18n.translate('xpack.security.management.users.fullNameColumnName', { defaultMessage: 'Full Name', }), sortable: true, @@ -100,8 +99,7 @@ class UsersListPageUI extends Component { }, { field: 'username', - name: intl.formatMessage({ - id: 'xpack.security.management.users.userNameColumnName', + name: i18n.translate('xpack.security.management.users.userNameColumnName', { defaultMessage: 'User Name', }), sortable: true, @@ -114,8 +112,7 @@ class UsersListPageUI extends Component { }, { field: 'email', - name: intl.formatMessage({ - id: 'xpack.security.management.users.emailAddressColumnName', + name: i18n.translate('xpack.security.management.users.emailAddressColumnName', { defaultMessage: 'Email Address', }), sortable: true, @@ -126,8 +123,7 @@ class UsersListPageUI extends Component { }, { field: 'roles', - name: intl.formatMessage({ - id: 'xpack.security.management.users.rolesColumnName', + name: i18n.translate('xpack.security.management.users.rolesColumnName', { defaultMessage: 'Roles', }), render: (rolenames: string[]) => { @@ -144,15 +140,13 @@ class UsersListPageUI extends Component { }, { field: 'metadata', - name: intl.formatMessage({ - id: 'xpack.security.management.users.reservedColumnName', + name: i18n.translate('xpack.security.management.users.reservedColumnName', { defaultMessage: 'Reserved', }), sortable: ({ metadata }: User) => Boolean(metadata && metadata._reserved), width: '100px', align: 'right', - description: intl.formatMessage({ - id: 'xpack.security.management.users.reservedColumnDescription', + description: i18n.translate('xpack.security.management.users.reservedColumnDescription', { defaultMessage: 'Reserved users are built-in and cannot be removed. Only the password can be changed.', }), @@ -233,6 +227,7 @@ class UsersListPageUI extends Component { usersToDelete={selection.map(user => user.username)} callback={this.handleDelete} apiClient={this.props.apiClient} + notifications={this.props.notifications} /> ) : null} @@ -275,14 +270,11 @@ class UsersListPageUI extends Component { if (e.body.statusCode === 403) { this.setState({ permissionDenied: true }); } else { - toastNotifications.addDanger( - this.props.intl.formatMessage( - { - id: 'xpack.security.management.users.fetchingUsersErrorMessage', - defaultMessage: 'Error fetching users: {message}', - }, - { message: e.body.message } - ) + this.props.notifications.toasts.addDanger( + i18n.translate('xpack.security.management.users.fetchingUsersErrorMessage', { + defaultMessage: 'Error fetching users: {message}', + values: { message: e.body.message }, + }) ); } } @@ -315,5 +307,3 @@ class UsersListPageUI extends Component { this.setState({ showDeleteConfirmation: false }); }; } - -export const UsersListPage = injectI18n(UsersListPageUI); diff --git a/x-pack/plugins/security/public/management/users/users_management_app.test.tsx b/x-pack/plugins/security/public/management/users/users_management_app.test.tsx new file mode 100644 index 0000000000000..48ffcfc550a84 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/users_management_app.test.tsx @@ -0,0 +1,131 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('./users_grid', () => ({ + UsersGridPage: (props: any) => `Users Page: ${JSON.stringify(props)}`, +})); + +jest.mock('./edit_user', () => ({ + EditUserPage: (props: any) => `User Edit Page: ${JSON.stringify(props)}`, +})); + +import { usersManagementApp } from './users_management_app'; + +import { coreMock } from '../../../../../../src/core/public/mocks'; +import { securityMock } from '../../mocks'; + +async function mountApp(basePath: string) { + const container = document.createElement('div'); + const setBreadcrumbs = jest.fn(); + + const unmount = await usersManagementApp + .create({ + authc: securityMock.createSetup().authc, + getStartServices: coreMock.createSetup().getStartServices as any, + }) + .mount({ basePath, element: container, setBreadcrumbs }); + + return { unmount, container, setBreadcrumbs }; +} + +describe('usersManagementApp', () => { + it('create() returns proper management app descriptor', () => { + expect( + usersManagementApp.create({ + authc: securityMock.createSetup().authc, + getStartServices: coreMock.createSetup().getStartServices as any, + }) + ).toMatchInlineSnapshot(` + Object { + "id": "users", + "mount": [Function], + "order": 10, + "title": "Users", + } + `); + }); + + it('mount() works for the `grid` page', async () => { + const basePath = '/some-base-path/users'; + window.location.hash = basePath; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([{ href: `#${basePath}`, text: 'Users' }]); + expect(container).toMatchInlineSnapshot(` +
+ Users Page: {"notifications":{"toasts":{}},"apiClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `create user` page', async () => { + const basePath = '/some-base-path/users'; + window.location.hash = `${basePath}/edit`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Users' }, + { text: 'Create' }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ User Edit Page: {"authc":{},"apiClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}}} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() works for the `edit user` page', async () => { + const basePath = '/some-base-path/users'; + const userName = 'someUserName'; + window.location.hash = `${basePath}/edit/${userName}`; + + const { setBreadcrumbs, container, unmount } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Users' }, + { href: `#/some-base-path/users/edit/${userName}`, text: userName }, + ]); + expect(container).toMatchInlineSnapshot(` +
+ User Edit Page: {"authc":{},"apiClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"rolesAPIClient":{"http":{"basePath":{"basePath":""},"anonymousPaths":{}}},"notifications":{"toasts":{}},"username":"someUserName"} +
+ `); + + unmount(); + + expect(container).toMatchInlineSnapshot(`
`); + }); + + it('mount() properly encodes user name in `edit user` page link in breadcrumbs', async () => { + const basePath = '/some-base-path/users'; + const username = 'some 安全性 user'; + window.location.hash = `${basePath}/edit/${username}`; + + const { setBreadcrumbs } = await mountApp(basePath); + + expect(setBreadcrumbs).toHaveBeenCalledTimes(1); + expect(setBreadcrumbs).toHaveBeenCalledWith([ + { href: `#${basePath}`, text: 'Users' }, + { + href: '#/some-base-path/users/edit/some%20%E5%AE%89%E5%85%A8%E6%80%A7%20user', + text: username, + }, + ]); + }); +}); diff --git a/x-pack/plugins/security/public/management/users/users_management_app.tsx b/x-pack/plugins/security/public/management/users/users_management_app.tsx new file mode 100644 index 0000000000000..9aebb396ce9a9 --- /dev/null +++ b/x-pack/plugins/security/public/management/users/users_management_app.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { HashRouter as Router, Route, Switch, useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { CoreSetup } from 'src/core/public'; +import { RegisterManagementAppArgs } from '../../../../../../src/plugins/management/public'; +import { AuthenticationServiceSetup } from '../../authentication'; +import { PluginStartDependencies } from '../../plugin'; +import { RolesAPIClient } from '../roles'; +import { UserAPIClient } from './user_api_client'; +import { UsersGridPage } from './users_grid'; +import { EditUserPage } from './edit_user'; + +interface CreateParams { + authc: AuthenticationServiceSetup; + getStartServices: CoreSetup['getStartServices']; +} + +export const usersManagementApp = Object.freeze({ + id: 'users', + create({ authc, getStartServices }: CreateParams) { + return { + id: this.id, + order: 10, + title: i18n.translate('xpack.security.management.usersTitle', { defaultMessage: 'Users' }), + async mount({ basePath, element, setBreadcrumbs }) { + const [{ http, notifications, i18n: i18nStart }] = await getStartServices(); + const usersBreadcrumbs = [ + { + text: i18n.translate('xpack.security.users.breadcrumb', { defaultMessage: 'Users' }), + href: `#${basePath}`, + }, + ]; + + const userAPIClient = new UserAPIClient(http); + const UsersGridPageWithBreadcrumbs = () => { + setBreadcrumbs(usersBreadcrumbs); + return ; + }; + + const EditUserPageWithBreadcrumbs = () => { + const { username } = useParams<{ username?: string }>(); + + setBreadcrumbs([ + ...usersBreadcrumbs, + username + ? { text: username, href: `#${basePath}/edit/${encodeURIComponent(username)}` } + : { + text: i18n.translate('xpack.security.users.createBreadcrumb', { + defaultMessage: 'Create', + }), + }, + ]); + + return ( + + ); + }; + + render( + + + + + + + + + + + + , + element + ); + + return () => { + unmountComponentAtNode(element); + }; + }, + } as RegisterManagementAppArgs; + }, +}); diff --git a/x-pack/plugins/security/public/plugin.ts b/x-pack/plugins/security/public/plugin.ts deleted file mode 100644 index 50e0b838c750f..0000000000000 --- a/x-pack/plugins/security/public/plugin.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Plugin, CoreSetup, CoreStart } from 'src/core/public'; -import { LicensingPluginSetup } from '../../licensing/public'; -import { - SessionExpired, - SessionTimeout, - ISessionTimeout, - SessionTimeoutHttpInterceptor, - UnauthorizedResponseHttpInterceptor, -} from './session'; -import { SecurityLicenseService } from '../common/licensing'; -import { SecurityNavControlService } from './nav_control'; -import { AuthenticationService } from './authentication'; - -export interface PluginSetupDependencies { - licensing: LicensingPluginSetup; -} - -export class SecurityPlugin implements Plugin { - private sessionTimeout!: ISessionTimeout; - - private navControlService!: SecurityNavControlService; - - private securityLicenseService!: SecurityLicenseService; - - public setup(core: CoreSetup, { licensing }: PluginSetupDependencies) { - const { http, notifications, injectedMetadata } = core; - const { basePath, anonymousPaths } = http; - anonymousPaths.register('/login'); - anonymousPaths.register('/logout'); - anonymousPaths.register('/logged_out'); - - const tenant = `${injectedMetadata.getInjectedVar('session.tenant', '')}`; - const sessionExpired = new SessionExpired(basePath, tenant); - http.intercept(new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths)); - this.sessionTimeout = new SessionTimeout(notifications, sessionExpired, http, tenant); - http.intercept(new SessionTimeoutHttpInterceptor(this.sessionTimeout, anonymousPaths)); - - this.navControlService = new SecurityNavControlService(); - this.securityLicenseService = new SecurityLicenseService(); - const { license } = this.securityLicenseService.setup({ license$: licensing.license$ }); - - const authc = new AuthenticationService().setup({ http: core.http }); - - this.navControlService.setup({ - securityLicense: license, - authc, - }); - - return { - authc, - sessionTimeout: this.sessionTimeout, - }; - } - - public start(core: CoreStart) { - this.sessionTimeout.start(); - this.navControlService.start({ core }); - } - - public stop() { - this.sessionTimeout.stop(); - this.navControlService.stop(); - this.securityLicenseService.stop(); - } -} - -export type SecurityPluginSetup = ReturnType; -export type SecurityPluginStart = ReturnType; diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx new file mode 100644 index 0000000000000..394e23cbbf646 --- /dev/null +++ b/x-pack/plugins/security/public/plugin.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Plugin, CoreSetup, CoreStart } from 'src/core/public'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { DataPublicPluginStart } from '../../../../src/plugins/data/public'; +import { + FeatureCatalogueCategory, + HomePublicPluginSetup, +} from '../../../../src/plugins/home/public'; +import { LicensingPluginSetup } from '../../licensing/public'; +import { ManagementSetup, ManagementStart } from '../../../../src/plugins/management/public'; +import { + SessionExpired, + SessionTimeout, + ISessionTimeout, + SessionTimeoutHttpInterceptor, + UnauthorizedResponseHttpInterceptor, +} from './session'; +import { SecurityLicenseService } from '../common/licensing'; +import { SecurityNavControlService } from './nav_control'; +import { AccountManagementPage } from './account_management'; +import { AuthenticationService, AuthenticationServiceSetup } from './authentication'; +import { ManagementService, UserAPIClient } from './management'; + +export interface PluginSetupDependencies { + licensing: LicensingPluginSetup; + home?: HomePublicPluginSetup; + management?: ManagementSetup; +} + +export interface PluginStartDependencies { + data: DataPublicPluginStart; + management?: ManagementStart; +} + +export class SecurityPlugin + implements + Plugin< + SecurityPluginSetup, + SecurityPluginStart, + PluginSetupDependencies, + PluginStartDependencies + > { + private sessionTimeout!: ISessionTimeout; + private readonly navControlService = new SecurityNavControlService(); + private readonly securityLicenseService = new SecurityLicenseService(); + private readonly managementService = new ManagementService(); + private authc!: AuthenticationServiceSetup; + + public setup( + core: CoreSetup, + { home, licensing, management }: PluginSetupDependencies + ) { + const { http, notifications, injectedMetadata } = core; + const { basePath, anonymousPaths } = http; + anonymousPaths.register('/login'); + anonymousPaths.register('/logout'); + anonymousPaths.register('/logged_out'); + + const tenant = `${injectedMetadata.getInjectedVar('session.tenant', '')}`; + const sessionExpired = new SessionExpired(basePath, tenant); + http.intercept(new UnauthorizedResponseHttpInterceptor(sessionExpired, anonymousPaths)); + this.sessionTimeout = new SessionTimeout(notifications, sessionExpired, http, tenant); + http.intercept(new SessionTimeoutHttpInterceptor(this.sessionTimeout, anonymousPaths)); + + const { license } = this.securityLicenseService.setup({ license$: licensing.license$ }); + + this.authc = new AuthenticationService().setup({ http: core.http }); + + this.navControlService.setup({ + securityLicense: license, + authc: this.authc, + }); + + if (management) { + this.managementService.setup({ + license, + management, + authc: this.authc, + fatalErrors: core.fatalErrors, + getStartServices: core.getStartServices, + }); + } + + if (management && home) { + home.featureCatalogue.register({ + id: 'security', + title: i18n.translate('xpack.security.registerFeature.securitySettingsTitle', { + defaultMessage: 'Security Settings', + }), + description: i18n.translate('xpack.security.registerFeature.securitySettingsDescription', { + defaultMessage: + 'Protect your data and easily manage who has access to what with users and roles.', + }), + icon: 'securityApp', + path: '/app/kibana#/management/security/users', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + }); + } + + return { + authc: this.authc, + sessionTimeout: this.sessionTimeout, + }; + } + + public start(core: CoreStart, { data, management }: PluginStartDependencies) { + this.sessionTimeout.start(); + this.navControlService.start({ core }); + + if (management) { + this.managementService.start({ management }); + } + + return { + __legacyCompat: { + account_management: { + AccountManagementPage: () => ( + + + + ), + }, + }, + }; + } + + public stop() { + this.sessionTimeout.stop(); + this.navControlService.stop(); + this.securityLicenseService.stop(); + this.managementService.stop(); + } +} + +export type SecurityPluginSetup = ReturnType; +export type SecurityPluginStart = ReturnType; diff --git a/x-pack/plugins/security/server/routes/role_mapping/get.ts b/x-pack/plugins/security/server/routes/role_mapping/get.ts index 9cd5cf83092e1..def6fabc0e322 100644 --- a/x-pack/plugins/security/server/routes/role_mapping/get.ts +++ b/x-pack/plugins/security/server/routes/role_mapping/get.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { schema } from '@kbn/config-schema'; -import { RoleMapping } from '../../../../../legacy/plugins/security/common/model'; +import { RoleMapping } from '../../../common/model'; import { createLicensedRouteHandler } from '../licensed_route_handler'; import { wrapError } from '../../errors'; import { RouteDefinitionParams } from '..'; diff --git a/x-pack/plugins/spaces/common/index.ts b/x-pack/plugins/spaces/common/index.ts index 65baa1bd99102..c1f0f8bd3ece4 100644 --- a/x-pack/plugins/spaces/common/index.ts +++ b/x-pack/plugins/spaces/common/index.ts @@ -5,5 +5,5 @@ */ export { isReservedSpace } from './is_reserved_space'; -export { MAX_SPACE_INITIALS } from './constants'; +export { MAX_SPACE_INITIALS, SPACE_SEARCH_COUNT_THRESHOLD } from './constants'; export { addSpaceIdToPath, getSpaceIdFromPath } from './lib/spaces_url_parser'; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index ce3edbbb59828..c3e46a9c04e1c 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -10595,17 +10595,6 @@ "xpack.security.management.apiKeys.table.userFilterLabel": "ユーザー", "xpack.security.management.apiKeys.table.userNameColumnName": "ユーザー", "xpack.security.management.apiKeysTitle": "API キー", - "xpack.security.management.changePasswordForm.cancelButtonLabel": "キャンセル", - "xpack.security.management.changePasswordForm.changePasswordLinkLabel": "パスワードを変更", - "xpack.security.management.changePasswordForm.confirmPasswordLabel": "パスワードの確認", - "xpack.security.management.changePasswordForm.currentPasswordLabel": "現在のパスワード", - "xpack.security.management.changePasswordForm.incorrectPasswordDescription": "入力された現在のパスワードが正しくありません。", - "xpack.security.management.changePasswordForm.newPasswordLabel": "新しいパスワード", - "xpack.security.management.changePasswordForm.passwordDontMatchDescription": "パスワードが一致しません", - "xpack.security.management.changePasswordForm.passwordLabel": "パスワード", - "xpack.security.management.changePasswordForm.passwordLengthDescription": "パスワードは最低 6 文字必要です", - "xpack.security.management.changePasswordForm.saveChangesButtonLabel": "変更を保存", - "xpack.security.management.changePasswordForm.updateAndRestartKibanaDescription": "Kibana ユーザーのパスワードを変更後、kibana.yml ファイルを更新し Kibana を再起動する必要があります。", "xpack.security.management.editRole.cancelButtonLabel": "キャンセル", "xpack.security.management.editRole.changeAllPrivilegesLink": "(すべて変更)", "xpack.security.management.editRole.collapsiblePanel.hideLinkText": "非表示", @@ -10730,10 +10719,6 @@ "xpack.security.management.editRolespacePrivilegeForm.createPrivilegeButton": "スペース権限を作成", "xpack.security.management.editRolespacePrivilegeForm.updateGlobalPrivilegeButton": "グローバル特権を更新", "xpack.security.management.editRolespacePrivilegeForm.updatePrivilegeButton": "スペース権限を更新", - "xpack.security.management.passwordForm.confirmPasswordLabel": "パスワードの確認", - "xpack.security.management.passwordForm.passwordDontMatchDescription": "パスワードが一致しません", - "xpack.security.management.passwordForm.passwordLabel": "パスワード", - "xpack.security.management.passwordForm.passwordLengthDescription": "パスワードは最低 6 文字必要です", "xpack.security.management.roles.actionsColumnName": "アクション", "xpack.security.management.roles.cloneRoleActionName": "{roleName} を複製", "xpack.security.management.roles.confirmDelete.cancelButtonLabel": "キャンセル", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index dc72e83ebe3c3..c40b8c8d0393c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -10594,17 +10594,6 @@ "xpack.security.management.apiKeys.table.userFilterLabel": "用户", "xpack.security.management.apiKeys.table.userNameColumnName": "用户", "xpack.security.management.apiKeysTitle": "API 密钥", - "xpack.security.management.changePasswordForm.cancelButtonLabel": "取消", - "xpack.security.management.changePasswordForm.changePasswordLinkLabel": "更改密码", - "xpack.security.management.changePasswordForm.confirmPasswordLabel": "确认密码", - "xpack.security.management.changePasswordForm.currentPasswordLabel": "当前密码", - "xpack.security.management.changePasswordForm.incorrectPasswordDescription": "您输入的当前密码不正确。", - "xpack.security.management.changePasswordForm.newPasswordLabel": "新密码", - "xpack.security.management.changePasswordForm.passwordDontMatchDescription": "密码不匹配", - "xpack.security.management.changePasswordForm.passwordLabel": "密码", - "xpack.security.management.changePasswordForm.passwordLengthDescription": "密码长度必须至少为 6 个字符", - "xpack.security.management.changePasswordForm.saveChangesButtonLabel": "保存更改", - "xpack.security.management.changePasswordForm.updateAndRestartKibanaDescription": "更改 Kibana 用户的密码后,必须更新 kibana.yml 文件并重新启动 Kibana", "xpack.security.management.editRole.cancelButtonLabel": "取消", "xpack.security.management.editRole.changeAllPrivilegesLink": "(全部更改)", "xpack.security.management.editRole.collapsiblePanel.hideLinkText": "隐藏", @@ -10729,10 +10718,6 @@ "xpack.security.management.editRolespacePrivilegeForm.createPrivilegeButton": "创建工作区权限", "xpack.security.management.editRolespacePrivilegeForm.updateGlobalPrivilegeButton": "更新全局权限", "xpack.security.management.editRolespacePrivilegeForm.updatePrivilegeButton": "更新工作区权限", - "xpack.security.management.passwordForm.confirmPasswordLabel": "确认密码", - "xpack.security.management.passwordForm.passwordDontMatchDescription": "密码不匹配", - "xpack.security.management.passwordForm.passwordLabel": "密码", - "xpack.security.management.passwordForm.passwordLengthDescription": "密码长度必须至少为 6 个字符", "xpack.security.management.roles.actionsColumnName": "鎿嶄綔", "xpack.security.management.roles.cloneRoleActionName": "克隆 {roleName}", "xpack.security.management.roles.confirmDelete.cancelButtonLabel": "取消", diff --git a/x-pack/test/functional/apps/security/management.js b/x-pack/test/functional/apps/security/management.js index 45a35029ffba2..8ab84126b2b30 100644 --- a/x-pack/test/functional/apps/security/management.js +++ b/x-pack/test/functional/apps/security/management.js @@ -11,7 +11,7 @@ import { ROLES_PATH, EDIT_ROLES_PATH, CLONE_ROLES_PATH, -} from '../../../../legacy/plugins/security/public/views/management/management_urls'; +} from '../../../../plugins/security/public/management/management_urls'; export default function({ getService, getPageObjects }) { const kibanaServer = getService('kibanaServer'); diff --git a/x-pack/test/functional/apps/security/role_mappings.ts b/x-pack/test/functional/apps/security/role_mappings.ts index 5fed56ee79e3d..a1517e1934a28 100644 --- a/x-pack/test/functional/apps/security/role_mappings.ts +++ b/x-pack/test/functional/apps/security/role_mappings.ts @@ -93,7 +93,7 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const url = parse(await browser.getCurrentUrl()); - expect(url.hash).to.eql('#/management/security/role_mappings?_g=()'); + expect(url.hash).to.eql('#/management/security/role_mappings'); }); describe('with role mappings', () => { From a131f1dbcfa4838653d587e948ef0365036e5dd6 Mon Sep 17 00:00:00 2001 From: Dima Arnautov Date: Tue, 21 Jan 2020 13:50:18 +0100 Subject: [PATCH 30/39] [ML] Formatting for additional timing and model size stats (#55062) * [ML] formatting for additional timing and model size stats * [ML] roundToDecimalPlace only average search time * [ML] adjust functional tests * [ML] remove debug tag, fix assert value * [ML] check for no decimal place * [ML] fix functional tests Co-authored-by: Elastic Machine --- .../formatters/round_to_decimal_place.test.ts | 2 ++ .../formatters/round_to_decimal_place.ts | 7 ++++++- .../components/job_details/format_values.js | 10 ++++++++++ .../anomaly_detection/advanced_job.ts | 8 ++++---- .../anomaly_detection/multi_metric_job.ts | 4 ++-- .../anomaly_detection/population_job.ts | 4 ++-- .../anomaly_detection/saved_search_job.ts | 20 +++++++++---------- .../anomaly_detection/single_metric_job.ts | 4 ++-- 8 files changed, 38 insertions(+), 21 deletions(-) diff --git a/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.test.ts b/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.test.ts index 99b9aceab3696..663a3f3d8f955 100644 --- a/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.test.ts +++ b/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.test.ts @@ -20,6 +20,7 @@ describe('ML - roundToDecimalPlace formatter', () => { expect(roundToDecimalPlace(0.0005)).toBe('5.00e-4'); expect(roundToDecimalPlace(-0.0005)).toBe('-5.00e-4'); expect(roundToDecimalPlace(-12.045)).toBe(-12.04); + expect(roundToDecimalPlace(0)).toBe(0); }); it('returns the correct format using specified decimal place', () => { @@ -31,5 +32,6 @@ describe('ML - roundToDecimalPlace formatter', () => { expect(roundToDecimalPlace(0.0005, 4)).toBe(0.0005); expect(roundToDecimalPlace(0.00005, 4)).toBe('5.00e-5'); expect(roundToDecimalPlace(-0.00005, 4)).toBe('-5.00e-5'); + expect(roundToDecimalPlace(0, 4)).toBe(0); }); }); diff --git a/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.ts b/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.ts index f863fe6d76e57..5a030d7619e98 100644 --- a/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.ts +++ b/x-pack/legacy/plugins/ml/public/application/formatters/round_to_decimal_place.ts @@ -4,7 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -export function roundToDecimalPlace(num: number, dp: number = 2) { +export function roundToDecimalPlace(num: number, dp: number = 2): number | string { + if (num % 1 === 0) { + // no decimal place + return num; + } + if (Math.abs(num) < Math.pow(10, -dp)) { return Number.parseFloat(String(num)).toExponential(2); } diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js index eb0a905725d75..9984f3be299ae 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js +++ b/x-pack/legacy/plugins/ml/public/application/jobs/jobs_list/components/job_details/format_values.js @@ -6,6 +6,7 @@ import numeral from '@elastic/numeral'; import { formatDate } from '@elastic/eui/lib/services/format'; +import { roundToDecimalPlace } from '../../../../formatters/round_to_decimal_place'; import { toLocaleString } from '../../../../util/string_utils'; const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; @@ -35,6 +36,8 @@ export function formatValues([key, value]) { case 'established_model_memory': case 'input_bytes': case 'model_bytes': + case 'model_bytes_exceeded': + case 'model_bytes_memory_limit': value = formatData(value); break; @@ -53,9 +56,16 @@ export function formatValues([key, value]) { case 'total_over_field_count': case 'total_partition_field_count': case 'bucket_allocation_failures_count': + case 'search_count': value = toLocaleString(value); break; + // numbers rounded to 3 decimal places + case 'average_search_time_per_bucket_ms': + case 'exponential_average_search_time_per_hour_ms': + value = typeof value === 'number' ? roundToDecimalPlace(value, 3).toLocaleString() : value; + break; + default: break; } diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts index 386914b735554..acb92b270c4a1 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/advanced_job.ts @@ -194,8 +194,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '10485760', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '10.0 MB', total_by_field_count: '37', total_over_field_count: '92', total_partition_field_count: '8', @@ -261,8 +261,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '104857600', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '100.0 MB', total_by_field_count: '994', total_over_field_count: '0', total_partition_field_count: '2', diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts index d41d96e40e2be..6a12a28e8ac49 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/multi_metric_job.ts @@ -60,8 +60,8 @@ export default function({ getService }: FtrProviderContext) { return { job_id: expectedJobId, result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '20971520', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '20.0 MB', total_by_field_count: '59', total_over_field_count: '0', total_partition_field_count: '58', diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts index 296af3179ce3e..6593dd10928b4 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/population_job.ts @@ -74,8 +74,8 @@ export default function({ getService }: FtrProviderContext) { return { job_id: expectedJobId, result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '8388608', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '8.0 MB', total_by_field_count: '25', total_over_field_count: '92', total_partition_field_count: '3', diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts index 7d989bc6244b8..348910a2a8f84 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/saved_search_job.ts @@ -53,8 +53,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '20971520', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '20.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', @@ -104,8 +104,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '20971520', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '20.0 MB', total_by_field_count: '7', total_over_field_count: '0', total_partition_field_count: '6', @@ -155,8 +155,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '20971520', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '20.0 MB', total_by_field_count: '7', total_over_field_count: '0', total_partition_field_count: '6', @@ -207,8 +207,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '20971520', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '20.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', @@ -258,8 +258,8 @@ export default function({ getService }: FtrProviderContext) { }, modelSizeStats: { result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '20971520', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '20.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', diff --git a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts index f6cd7b40bc7b1..13cac36d99a1b 100644 --- a/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts +++ b/x-pack/test/functional/apps/machine_learning/anomaly_detection/single_metric_job.ts @@ -59,8 +59,8 @@ export default function({ getService }: FtrProviderContext) { return { job_id: expectedJobId, result_type: 'model_size_stats', - model_bytes_exceeded: '0', - model_bytes_memory_limit: '15728640', + model_bytes_exceeded: '0.0 B', + model_bytes_memory_limit: '15.0 MB', total_by_field_count: '3', total_over_field_count: '0', total_partition_field_count: '2', From c1960583500d28fc3002b4959804ad52cf723b4a Mon Sep 17 00:00:00 2001 From: patrykkopycinski Date: Tue, 21 Jan 2020 14:04:52 +0100 Subject: [PATCH 31/39] [SIEM] Update ml_conditional_links cypress tests (#55373) --- .../ml_conditional_links/ml_conditional_links.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts index afeb8c3c13a4f..142729189e49b 100644 --- a/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts +++ b/x-pack/legacy/plugins/siem/cypress/integration/smoke_tests/ml_conditional_links/ml_conditional_links.spec.ts @@ -99,7 +99,7 @@ describe('ml conditional links', () => { loginAndWaitForPage(mlNetworkSingleIpNullKqlQuery); cy.url().should( 'include', - '/app/siem#/network/ip/127.0.0.1?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' + '/app/siem#/network/ip/127.0.0.1/source?timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))' ); }); @@ -107,7 +107,7 @@ describe('ml conditional links', () => { loginAndWaitForPage(mlNetworkSingleIpKqlQuery); cy.url().should( 'include', - "/app/siem#/network/ip/127.0.0.1?query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" + "/app/siem#/network/ip/127.0.0.1/source?query=(language:kuery,query:'(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)')&timerange=(global:(linkTo:!(timeline),timerange:(from:1566990000000,kind:absolute,to:1567000799999)),timeline:(linkTo:!(global),timerange:(from:1566990000000,kind:absolute,to:1567000799999)))" ); }); From c88aa5a505ced73ffabd4a4136f6172ee23073c8 Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 21 Jan 2020 14:49:30 +0100 Subject: [PATCH 32/39] Migration: Separate legacy and index entrypoint (#54124) --- src/legacy/core_plugins/kibana/index.js | 7 +- .../core_plugins/kibana/public/.eslintrc.js | 1 + .../kibana/public/dashboard/index.ts | 40 ++---------- .../kibana/public/dashboard/legacy.ts | 56 ++++++++++++++++ .../__tests__/directives/discover_field.js | 2 +- .../__tests__/directives/field_calculator.js | 2 +- .../__tests__/directives/field_chooser.js | 2 +- .../discover/__tests__/doc_table/doc_table.js | 2 +- .../__tests__/doc_table/lib/rows_headers.js | 2 +- .../query_parameters/action_add_filter.js | 2 +- .../action_set_predecessor_count.js | 2 +- .../action_set_query_parameters.js | 2 +- .../action_set_successor_count.js | 2 +- .../kibana/public/discover/index.ts | 17 +---- .../kibana/public/discover/legacy.ts | 32 +++++++++ .../angular/context/api/__tests__/anchor.js | 2 +- .../context/api/__tests__/predecessors.js | 2 +- .../context/api/__tests__/successors.js | 2 +- .../core_plugins/kibana/public/home/index.ts | 5 -- .../kibana/public/home/kibana_services.ts | 1 - .../home/np_ready/components/welcome.test.tsx | 4 -- .../home/np_ready/components/welcome.tsx | 7 +- .../core_plugins/kibana/public/home/plugin.ts | 8 ++- .../core_plugins/kibana/public/kibana.js | 6 +- .../kibana/public/visualize/index.ts | 49 ++------------ .../kibana/public/visualize/legacy.ts | 65 +++++++++++++++++++ .../directives/__tests__/css_truncate.js | 2 +- .../dashboard_mode/public/dashboard_viewer.js | 1 + 28 files changed, 202 insertions(+), 123 deletions(-) create mode 100644 src/legacy/core_plugins/kibana/public/dashboard/legacy.ts create mode 100644 src/legacy/core_plugins/kibana/public/discover/legacy.ts create mode 100644 src/legacy/core_plugins/kibana/public/visualize/legacy.ts diff --git a/src/legacy/core_plugins/kibana/index.js b/src/legacy/core_plugins/kibana/index.js index e6a0420534ef2..0366d8b27f211 100644 --- a/src/legacy/core_plugins/kibana/index.js +++ b/src/legacy/core_plugins/kibana/index.js @@ -60,7 +60,12 @@ export default function(kibana) { }, uiExports: { - hacks: ['plugins/kibana/discover', 'plugins/kibana/dev_tools', 'plugins/kibana/visualize'], + hacks: [ + 'plugins/kibana/discover/legacy', + 'plugins/kibana/dev_tools', + 'plugins/kibana/visualize/legacy', + 'plugins/kibana/dashboard/legacy', + ], savedObjectTypes: ['plugins/kibana/dashboard/saved_dashboard/saved_dashboard_register'], app: { id: 'kibana', diff --git a/src/legacy/core_plugins/kibana/public/.eslintrc.js b/src/legacy/core_plugins/kibana/public/.eslintrc.js index 160adcc5b63f1..9b45217287dc8 100644 --- a/src/legacy/core_plugins/kibana/public/.eslintrc.js +++ b/src/legacy/core_plugins/kibana/public/.eslintrc.js @@ -51,6 +51,7 @@ function buildRestrictedPaths(shimmedPlugins) { from: [ `src/legacy/core_plugins/kibana/public/${shimmedPlugin}/**/*`, `!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/index.ts`, + `!src/legacy/core_plugins/kibana/public/${shimmedPlugin}/legacy.ts`, ], allowSameFolder: false, errorMessage: `kibana/public/${shimmedPlugin} is behaving like a NP plugin and does not allow deep imports. If you need something from within ${shimmedPlugin} in another plugin, consider re-exporting it from the top level index module`, diff --git a/src/legacy/core_plugins/kibana/public/dashboard/index.ts b/src/legacy/core_plugins/kibana/public/dashboard/index.ts index fd39f28a7673a..4a8decab6b00e 100644 --- a/src/legacy/core_plugins/kibana/public/dashboard/index.ts +++ b/src/legacy/core_plugins/kibana/public/dashboard/index.ts @@ -17,40 +17,12 @@ * under the License. */ -import { npSetup, npStart, legacyChrome } from './legacy_imports'; -import { DashboardPlugin, LegacyAngularInjectedDependencies } from './plugin'; -import { start as data } from '../../../data/public/legacy'; -import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy'; -import './saved_dashboard/saved_dashboard_register'; -import './dashboard_config'; +import { PluginInitializerContext } from 'kibana/public'; +import { DashboardPlugin } from './plugin'; export * from './np_ready/dashboard_constants'; -/** - * Get dependencies relying on the global angular context. - * They also have to get resolved together with the legacy imports above - */ -async function getAngularDependencies(): Promise { - const injector = await legacyChrome.dangerouslyGetActiveInjector(); - - return { - dashboardConfig: injector.get('dashboardConfig'), - }; -} - -(async () => { - const instance = new DashboardPlugin(); - instance.setup(npSetup.core, { - ...npSetup.plugins, - __LEGACY: { - getAngularDependencies, - }, - }); - instance.start(npStart.core, { - ...npStart.plugins, - data, - npData: npStart.plugins.data, - embeddables, - navigation: npStart.plugins.navigation, - }); -})(); +// Core will be looking for this when loading our plugin in the new platform +export const plugin = (context: PluginInitializerContext) => { + return new DashboardPlugin(); +}; diff --git a/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts b/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts new file mode 100644 index 0000000000000..068a8378f936a --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/dashboard/legacy.ts @@ -0,0 +1,56 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginInitializerContext } from 'kibana/public'; +import { npSetup, npStart, legacyChrome } from './legacy_imports'; +import { LegacyAngularInjectedDependencies } from './plugin'; +import { start as data } from '../../../data/public/legacy'; +import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy'; +import './saved_dashboard/saved_dashboard_register'; +import './dashboard_config'; +import { plugin } from './index'; + +/** + * Get dependencies relying on the global angular context. + * They also have to get resolved together with the legacy imports above + */ +async function getAngularDependencies(): Promise { + const injector = await legacyChrome.dangerouslyGetActiveInjector(); + + return { + dashboardConfig: injector.get('dashboardConfig'), + }; +} + +(async () => { + const instance = plugin({} as PluginInitializerContext); + instance.setup(npSetup.core, { + ...npSetup.plugins, + __LEGACY: { + getAngularDependencies, + }, + }); + instance.start(npStart.core, { + ...npStart.plugins, + data, + npData: npStart.plugins.data, + embeddables, + navigation: npStart.plugins.navigation, + }); +})(); diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js index 18d83595f8fa3..6ffda87ac2be8 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/discover_field.js @@ -22,7 +22,7 @@ import _ from 'lodash'; import sinon from 'sinon'; import ngMock from 'ng_mock'; import expect from '@kbn/expect'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; // Load the kibana app dependencies. diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js index 378a9e9325655..f302d684135f6 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_calculator.js @@ -18,7 +18,7 @@ */ import _ from 'lodash'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import ngMock from 'ng_mock'; import { fieldCalculator } from '../../np_ready/components/field_chooser/lib/field_calculator'; import expect from '@kbn/expect'; diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js index 5f6898ae2bd16..f74e145865475 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/directives/field_chooser.js @@ -23,7 +23,7 @@ import _ from 'lodash'; import sinon from 'sinon'; import expect from '@kbn/expect'; import $ from 'jquery'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import FixturesHitsProvider from 'fixtures/hits'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import { SimpleSavedObject } from '../../../../../../../core/public'; diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/doc_table.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/doc_table.js index b57f452b637af..6b97da79fc589 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/doc_table.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/doc_table.js @@ -22,7 +22,7 @@ import expect from '@kbn/expect'; import _ from 'lodash'; import ngMock from 'ng_mock'; import 'ui/private'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; import hits from 'fixtures/real_hits'; diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/rows_headers.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/rows_headers.js index 012f2b6061ee4..c19e033ccb72d 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/rows_headers.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/doc_table/lib/rows_headers.js @@ -24,7 +24,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import { getFakeRow, getFakeRowVals } from 'fixtures/fake_row'; import $ from 'jquery'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import FixturesStubbedLogstashIndexPatternProvider from 'fixtures/stubbed_logstash_index_pattern'; describe('Doc Table', function() { diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_add_filter.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_add_filter.js index 90614cf3c132c..f2acbf363d825 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_add_filter.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_add_filter.js @@ -22,7 +22,7 @@ import ngMock from 'ng_mock'; import { createStateStub } from './_utils'; import { getQueryParameterActions } from '../../np_ready/angular/context/query_parameters/actions'; import { createIndexPatternsStub } from '../../np_ready/angular/context/api/__tests__/_stubs'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { npStart } from 'ui/new_platform'; describe('context app', function() { diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_predecessor_count.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_predecessor_count.js index 1ad4bdbea210d..9ba425bb0e489 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_predecessor_count.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_predecessor_count.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { createStateStub } from './_utils'; import { getQueryParameterActions } from '../../np_ready/angular/context/query_parameters/actions'; diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_query_parameters.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_query_parameters.js index e9ec2c300faa1..39dde2d8bb7cf 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_query_parameters.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_query_parameters.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { createStateStub } from './_utils'; import { getQueryParameterActions } from '../../np_ready/angular/context/query_parameters/actions'; diff --git a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_successor_count.js b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_successor_count.js index 15f3eefac3fd1..c05f5b4aff3bc 100644 --- a/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_successor_count.js +++ b/src/legacy/core_plugins/kibana/public/discover/__tests__/query_parameters/action_set_successor_count.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { createStateStub } from './_utils'; import { getQueryParameterActions } from '../../np_ready/angular/context/query_parameters/actions'; diff --git a/src/legacy/core_plugins/kibana/public/discover/index.ts b/src/legacy/core_plugins/kibana/public/discover/index.ts index 7bde30e0d0f0e..d851cb96a18c4 100644 --- a/src/legacy/core_plugins/kibana/public/discover/index.ts +++ b/src/legacy/core_plugins/kibana/public/discover/index.ts @@ -16,24 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import chrome from 'ui/chrome'; + import { PluginInitializerContext } from 'kibana/public'; -import { npSetup, npStart } from 'ui/new_platform'; import { DiscoverPlugin } from './plugin'; +export { createSavedSearchesService } from './saved_searches/saved_searches'; + // Core will be looking for this when loading our plugin in the new platform export const plugin = (context: PluginInitializerContext) => { return new DiscoverPlugin(); }; - -// Legacy compatiblity part - to be removed at cutover, replaced by a kibana.json file -export const pluginInstance = plugin({} as PluginInitializerContext); -export const setup = pluginInstance.setup(npSetup.core, { - ...npSetup.plugins, - __LEGACY: { - chrome, - }, -}); -export const start = pluginInstance.start(npStart.core, npStart.plugins); - -export { createSavedSearchesService } from './saved_searches/saved_searches'; diff --git a/src/legacy/core_plugins/kibana/public/discover/legacy.ts b/src/legacy/core_plugins/kibana/public/discover/legacy.ts new file mode 100644 index 0000000000000..2ec64177156f9 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/discover/legacy.ts @@ -0,0 +1,32 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import chrome from 'ui/chrome'; +import { PluginInitializerContext } from 'kibana/public'; +import { npSetup, npStart } from 'ui/new_platform'; +import { plugin } from './index'; + +// Legacy compatiblity part - to be removed at cutover, replaced by a kibana.json file +export const pluginInstance = plugin({} as PluginInitializerContext); +export const setup = pluginInstance.setup(npSetup.core, { + ...npSetup.plugins, + __LEGACY: { + chrome, + }, +}); +export const start = pluginInstance.start(npStart.core, npStart.plugins); diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/anchor.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/anchor.js index 45ce6cc9d0af2..debcccebbd11c 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/anchor.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/anchor.js @@ -19,7 +19,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { createIndexPatternsStub, createSearchSourceStub } from './_stubs'; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/predecessors.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/predecessors.js index 266a505f6be14..c24b6ac6307ff 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/predecessors.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/predecessors.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import moment from 'moment'; import * as _ from 'lodash'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; diff --git a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/successors.js b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/successors.js index e06d414ba260c..d4c00930c9383 100644 --- a/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/successors.js +++ b/src/legacy/core_plugins/kibana/public/discover/np_ready/angular/context/api/__tests__/successors.js @@ -21,7 +21,7 @@ import expect from '@kbn/expect'; import ngMock from 'ng_mock'; import moment from 'moment'; import * as _ from 'lodash'; -import { pluginInstance } from 'plugins/kibana/discover/index'; +import { pluginInstance } from 'plugins/kibana/discover/legacy'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; diff --git a/src/legacy/core_plugins/kibana/public/home/index.ts b/src/legacy/core_plugins/kibana/public/home/index.ts index b2d90f1444654..27d09a53ba20d 100644 --- a/src/legacy/core_plugins/kibana/public/home/index.ts +++ b/src/legacy/core_plugins/kibana/public/home/index.ts @@ -22,11 +22,8 @@ import { npSetup, npStart } from 'ui/new_platform'; import chrome from 'ui/chrome'; import { IPrivate } from 'ui/private'; import { HomePlugin, LegacyAngularInjectedDependencies } from './plugin'; -import { createUiStatsReporter, METRIC_TYPE } from '../../../ui_metric/public'; import { TelemetryOptInProvider } from '../../../telemetry/public/services'; -export const trackUiMetric = createUiStatsReporter('Kibana_home'); - /** * Get dependencies relying on the global angular context. * They also have to get resolved together with the legacy imports above @@ -54,9 +51,7 @@ let copiedLegacyCatalogue = false; instance.setup(npSetup.core, { ...npSetup.plugins, __LEGACY: { - trackUiMetric, metadata: npStart.core.injectedMetadata.getLegacyMetadata(), - METRIC_TYPE, getFeatureCatalogueEntries: async () => { if (!copiedLegacyCatalogue) { const injector = await chrome.dangerouslyGetActiveInjector(); diff --git a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts index 0eb55a3902eda..4d9177735556d 100644 --- a/src/legacy/core_plugins/kibana/public/home/kibana_services.ts +++ b/src/legacy/core_plugins/kibana/public/home/kibana_services.ts @@ -55,7 +55,6 @@ export interface HomeKibanaServices { savedObjectsClient: SavedObjectsClientContract; toastNotifications: NotificationsSetup['toasts']; banners: OverlayStart['banners']; - METRIC_TYPE: any; trackUiMetric: (type: UiStatsMetricType, eventNames: string | string[], count?: number) => void; getBasePath: () => string; shouldShowTelemetryOptIn: boolean; diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx index 28bdab14193c4..55c469fa58fc6 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.test.tsx @@ -25,10 +25,6 @@ jest.mock('../../kibana_services', () => ({ getServices: () => ({ addBasePath: (path: string) => `root${path}`, trackUiMetric: () => {}, - METRIC_TYPE: { - LOADED: 'loaded', - CLICK: 'click', - }, }), })); diff --git a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx index 9bbb7aaceb915..1b7761d068d2f 100644 --- a/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx +++ b/src/legacy/core_plugins/kibana/public/home/np_ready/components/welcome.tsx @@ -35,6 +35,7 @@ import { EuiIcon, EuiPortal, } from '@elastic/eui'; +import { METRIC_TYPE } from '@kbn/analytics'; import { FormattedMessage } from '@kbn/i18n/react'; import { getServices } from '../../kibana_services'; @@ -64,17 +65,17 @@ export class Welcome extends React.Component { } private onSampleDataDecline = () => { - this.services.trackUiMetric(this.services.METRIC_TYPE.CLICK, 'sampleDataDecline'); + this.services.trackUiMetric(METRIC_TYPE.CLICK, 'sampleDataDecline'); this.props.onSkip(); }; private onSampleDataConfirm = () => { - this.services.trackUiMetric(this.services.METRIC_TYPE.CLICK, 'sampleDataConfirm'); + this.services.trackUiMetric(METRIC_TYPE.CLICK, 'sampleDataConfirm'); this.redirecToSampleData(); }; componentDidMount() { - this.services.trackUiMetric(this.services.METRIC_TYPE.LOADED, 'welcomeScreenMount'); + this.services.trackUiMetric(METRIC_TYPE.LOADED, 'welcomeScreenMount'); this.props.onOptInSeen(); document.addEventListener('keydown', this.hideOnEsc); } diff --git a/src/legacy/core_plugins/kibana/public/home/plugin.ts b/src/legacy/core_plugins/kibana/public/home/plugin.ts index 42ab049eb5b3a..502c8f45646cf 100644 --- a/src/legacy/core_plugins/kibana/public/home/plugin.ts +++ b/src/legacy/core_plugins/kibana/public/home/plugin.ts @@ -18,11 +18,11 @@ */ import { CoreSetup, CoreStart, LegacyNavLink, Plugin, UiSettingsState } from 'kibana/public'; -import { UiStatsMetricType } from '@kbn/analytics'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { setServices } from './kibana_services'; import { KibanaLegacySetup } from '../../../../../plugins/kibana_legacy/public'; +import { UsageCollectionSetup } from '../../../../../plugins/usage_collection/public'; import { Environment, FeatureCatalogueEntry, @@ -41,8 +41,6 @@ export interface HomePluginStartDependencies { export interface HomePluginSetupDependencies { __LEGACY: { - trackUiMetric: (type: UiStatsMetricType, eventNames: string | string[], count?: number) => void; - METRIC_TYPE: any; metadata: { app: unknown; bundleId: string; @@ -59,6 +57,7 @@ export interface HomePluginSetupDependencies { getFeatureCatalogueEntries: () => Promise; getAngularDependencies: () => Promise; }; + usageCollection: UsageCollectionSetup; kibana_legacy: KibanaLegacySetup; } @@ -71,6 +70,7 @@ export class HomePlugin implements Plugin { core: CoreSetup, { kibana_legacy, + usageCollection, __LEGACY: { getAngularDependencies, ...legacyServices }, }: HomePluginSetupDependencies ) { @@ -78,9 +78,11 @@ export class HomePlugin implements Plugin { id: 'home', title: 'Home', mount: async ({ core: contextCore }, params) => { + const trackUiMetric = usageCollection.reportUiStats.bind(usageCollection, 'Kibana_home'); const angularDependencies = await getAngularDependencies(); setServices({ ...legacyServices, + trackUiMetric, http: contextCore.http, toastNotifications: core.notifications.toasts, banners: contextCore.overlays.banners, diff --git a/src/legacy/core_plugins/kibana/public/kibana.js b/src/legacy/core_plugins/kibana/public/kibana.js index 4100ae7205869..bd947b9cb9d7f 100644 --- a/src/legacy/core_plugins/kibana/public/kibana.js +++ b/src/legacy/core_plugins/kibana/public/kibana.js @@ -47,9 +47,9 @@ import 'uiExports/interpreter'; import 'ui/autoload/all'; import 'ui/kbn_top_nav'; import './home'; -import './discover'; -import './visualize'; -import './dashboard'; +import './discover/legacy'; +import './visualize/legacy'; +import './dashboard/legacy'; import './management'; import './dev_tools'; import 'ui/color_maps'; diff --git a/src/legacy/core_plugins/kibana/public/visualize/index.ts b/src/legacy/core_plugins/kibana/public/visualize/index.ts index f113c81256f8e..a39779792b83a 100644 --- a/src/legacy/core_plugins/kibana/public/visualize/index.ts +++ b/src/legacy/core_plugins/kibana/public/visualize/index.ts @@ -17,50 +17,15 @@ * under the License. */ -import { - IPrivate, - legacyChrome, - npSetup, - npStart, - VisEditorTypesRegistryProvider, -} from './legacy_imports'; -import { VisualizePlugin, LegacyAngularInjectedDependencies } from './plugin'; -import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy'; -import { start as visualizations } from '../../../visualizations/public/np_ready/public/legacy'; +import { PluginInitializerContext } from 'kibana/public'; +import { VisualizePlugin } from './plugin'; export * from './np_ready/visualize_constants'; export { showNewVisModal } from './np_ready/wizard'; -/** - * Get dependencies relying on the global angular context. - * They also have to get resolved together with the legacy imports above - */ -async function getAngularDependencies(): Promise { - const injector = await legacyChrome.dangerouslyGetActiveInjector(); - - const Private = injector.get('Private'); - - const editorTypes = Private(VisEditorTypesRegistryProvider); - - return { - legacyChrome, - editorTypes, - }; -} - -(() => { - const instance = new VisualizePlugin(); - instance.setup(npSetup.core, { - ...npSetup.plugins, - __LEGACY: { - getAngularDependencies, - }, - }); - instance.start(npStart.core, { - ...npStart.plugins, - embeddables, - visualizations, - }); -})(); - export { createSavedVisLoader } from './saved_visualizations/saved_visualizations'; + +// Core will be looking for this when loading our plugin in the new platform +export const plugin = (context: PluginInitializerContext) => { + return new VisualizePlugin(); +}; diff --git a/src/legacy/core_plugins/kibana/public/visualize/legacy.ts b/src/legacy/core_plugins/kibana/public/visualize/legacy.ts new file mode 100644 index 0000000000000..2a1b6130fac89 --- /dev/null +++ b/src/legacy/core_plugins/kibana/public/visualize/legacy.ts @@ -0,0 +1,65 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import 'ui/collapsible_sidebar'; // used in default editor + +import { PluginInitializerContext } from 'kibana/public'; +import { + IPrivate, + legacyChrome, + npSetup, + npStart, + VisEditorTypesRegistryProvider, +} from './legacy_imports'; +import { LegacyAngularInjectedDependencies } from './plugin'; +import { start as embeddables } from '../../../embeddable_api/public/np_ready/public/legacy'; +import { start as visualizations } from '../../../visualizations/public/np_ready/public/legacy'; +import { plugin } from './index'; + +/** + * Get dependencies relying on the global angular context. + * They also have to get resolved together with the legacy imports above + */ +async function getAngularDependencies(): Promise { + const injector = await legacyChrome.dangerouslyGetActiveInjector(); + + const Private = injector.get('Private'); + + const editorTypes = Private(VisEditorTypesRegistryProvider); + + return { + legacyChrome, + editorTypes, + }; +} + +(() => { + const instance = plugin({} as PluginInitializerContext); + instance.setup(npSetup.core, { + ...npSetup.plugins, + __LEGACY: { + getAngularDependencies, + }, + }); + instance.start(npStart.core, { + ...npStart.plugins, + embeddables, + visualizations, + }); +})(); diff --git a/src/legacy/ui/public/directives/__tests__/css_truncate.js b/src/legacy/ui/public/directives/__tests__/css_truncate.js index 9d470c10358cc..bf102f5a29fdb 100644 --- a/src/legacy/ui/public/directives/__tests__/css_truncate.js +++ b/src/legacy/ui/public/directives/__tests__/css_truncate.js @@ -20,7 +20,7 @@ import angular from 'angular'; import expect from '@kbn/expect'; import ngMock from 'ng_mock'; -import 'plugins/kibana/discover/index'; +import 'plugins/kibana/discover/legacy'; let $parentScope; diff --git a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js index 391973f6d909b..fbf917054edbf 100644 --- a/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js +++ b/x-pack/legacy/plugins/dashboard_mode/public/dashboard_viewer.js @@ -34,6 +34,7 @@ import 'ui/color_maps'; import 'ui/agg_response'; import 'ui/agg_types'; import 'leaflet'; +import 'plugins/kibana/dashboard/legacy'; import { npStart } from 'ui/new_platform'; import { localApplicationService } from 'plugins/kibana/local_application_service'; From 6b02ed804fa858447f1e69627ccf6c121358dcab Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Tue, 21 Jan 2020 14:52:32 +0100 Subject: [PATCH 33/39] Migrate session storage short url handling (#55021) --- .../state_session_storage_redirect/index.js | 32 ------- .../package.json | 5 -- src/legacy/server/kbn_server.js | 4 - src/legacy/server/url_shortening/index.js | 20 ----- .../routes/lib/short_url_assert_valid.js | 41 --------- .../routes/lib/short_url_assert_valid.test.js | 63 -------------- .../routes/lib/short_url_error.js | 26 ------ .../routes/lib/short_url_error.test.js | 67 --------------- .../routes/lib/short_url_lookup.js | 43 ---------- .../routes/lib/short_url_lookup.test.js | 84 ------------------- .../url_shortening/url_shortening_mixin.js | 23 ----- src/plugins/newsfeed/public/plugin.tsx | 12 ++- .../share/common/short_url_routes.ts} | 16 ++-- src/plugins/share/public/lib/url_shortener.ts | 5 +- src/plugins/share/public/plugin.test.ts | 17 +++- src/plugins/share/public/plugin.ts | 6 +- .../services/short_url_redirect_app.test.ts | 46 ++++++++++ .../services/short_url_redirect_app.ts} | 39 +++++---- .../share/server/routes/create_routes.ts | 2 + .../share/server/routes/get.ts} | 47 +++++++---- src/plugins/share/server/routes/goto.ts | 10 ++- .../share/server/routes/shorten_url.ts | 3 +- 22 files changed, 151 insertions(+), 460 deletions(-) delete mode 100644 src/legacy/core_plugins/state_session_storage_redirect/index.js delete mode 100644 src/legacy/core_plugins/state_session_storage_redirect/package.json delete mode 100644 src/legacy/server/url_shortening/index.js delete mode 100644 src/legacy/server/url_shortening/routes/lib/short_url_assert_valid.js delete mode 100644 src/legacy/server/url_shortening/routes/lib/short_url_assert_valid.test.js delete mode 100644 src/legacy/server/url_shortening/routes/lib/short_url_error.js delete mode 100644 src/legacy/server/url_shortening/routes/lib/short_url_error.test.js delete mode 100644 src/legacy/server/url_shortening/routes/lib/short_url_lookup.js delete mode 100644 src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js delete mode 100644 src/legacy/server/url_shortening/url_shortening_mixin.js rename src/{legacy/server/url_shortening/routes/create_routes.js => plugins/share/common/short_url_routes.ts} (67%) create mode 100644 src/plugins/share/public/services/short_url_redirect_app.test.ts rename src/{legacy/core_plugins/state_session_storage_redirect/public/index.js => plugins/share/public/services/short_url_redirect_app.ts} (50%) rename src/{legacy/server/url_shortening/routes/goto.js => plugins/share/server/routes/get.ts} (55%) diff --git a/src/legacy/core_plugins/state_session_storage_redirect/index.js b/src/legacy/core_plugins/state_session_storage_redirect/index.js deleted file mode 100644 index 2d4d7c97232c0..0000000000000 --- a/src/legacy/core_plugins/state_session_storage_redirect/index.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export default function(kibana) { - return new kibana.Plugin({ - uiExports: { - app: { - require: ['kibana'], - title: 'Redirecting', - id: 'stateSessionStorageRedirect', - main: 'plugins/state_session_storage_redirect', - hidden: true, - }, - }, - }); -} diff --git a/src/legacy/core_plugins/state_session_storage_redirect/package.json b/src/legacy/core_plugins/state_session_storage_redirect/package.json deleted file mode 100644 index 21956e5d76d5b..0000000000000 --- a/src/legacy/core_plugins/state_session_storage_redirect/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "state_session_storage_redirect", - "version": "kibana", - "description": "When using the state:storeInSessionStorage setting with the short-urls, we need some way to get the full URL's hashed states into sessionStorage, this app will grab the URL from the injected state and and put the URL hashed states into sessionStorage before redirecting the user." -} diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 8ab55d4c517b5..6991527a9503c 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -36,7 +36,6 @@ import * as Plugins from './plugins'; import { indexPatternsMixin } from './index_patterns'; import { savedObjectsMixin } from './saved_objects/saved_objects_mixin'; import { capabilitiesMixin } from './capabilities'; -import { urlShorteningMixin } from './url_shortening'; import { serverExtensionsMixin } from './server_extensions'; import { uiMixin } from '../ui'; import { sassMixin } from './sass'; @@ -123,9 +122,6 @@ export default class KbnServer { // setup capabilities routes capabilitiesMixin, - // setup routes for short urls - urlShorteningMixin, - // ensure that all bundles are built, or that the // watch bundle server is running optimizeMixin, diff --git a/src/legacy/server/url_shortening/index.js b/src/legacy/server/url_shortening/index.js deleted file mode 100644 index 031af0618d7bc..0000000000000 --- a/src/legacy/server/url_shortening/index.js +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { urlShorteningMixin } from './url_shortening_mixin'; diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_assert_valid.js b/src/legacy/server/url_shortening/routes/lib/short_url_assert_valid.js deleted file mode 100644 index ff2b0f214e5ee..0000000000000 --- a/src/legacy/server/url_shortening/routes/lib/short_url_assert_valid.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { parse } from 'url'; -import { trim } from 'lodash'; -import Boom from 'boom'; - -export function shortUrlAssertValid(url) { - const { protocol, hostname, pathname } = parse(url); - - if (protocol) { - throw Boom.notAcceptable(`Short url targets cannot have a protocol, found "${protocol}"`); - } - - if (hostname) { - throw Boom.notAcceptable(`Short url targets cannot have a hostname, found "${hostname}"`); - } - - const pathnameParts = trim(pathname, '/').split('/'); - if (pathnameParts.length !== 2) { - throw Boom.notAcceptable( - `Short url target path must be in the format "/app/{{appId}}", found "${pathname}"` - ); - } -} diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_assert_valid.test.js b/src/legacy/server/url_shortening/routes/lib/short_url_assert_valid.test.js deleted file mode 100644 index f83073e6aefe9..0000000000000 --- a/src/legacy/server/url_shortening/routes/lib/short_url_assert_valid.test.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { shortUrlAssertValid } from './short_url_assert_valid'; - -describe('shortUrlAssertValid()', () => { - const invalid = [ - ['protocol', 'http://localhost:5601/app/kibana'], - ['protocol', 'https://localhost:5601/app/kibana'], - ['protocol', 'mailto:foo@bar.net'], - ['protocol', 'javascript:alert("hi")'], // eslint-disable-line no-script-url - ['hostname', 'localhost/app/kibana'], - ['hostname and port', 'local.host:5601/app/kibana'], - ['hostname and auth', 'user:pass@localhost.net/app/kibana'], - ['path traversal', '/app/../../not-kibana'], - ['deep path', '/app/kibana/foo'], - ['deep path', '/app/kibana/foo/bar'], - ['base path', '/base/app/kibana'], - ]; - - invalid.forEach(([desc, url]) => { - it(`fails when url has ${desc}`, () => { - try { - shortUrlAssertValid(url); - throw new Error(`expected assertion to throw`); - } catch (err) { - if (!err || !err.isBoom) { - throw err; - } - } - }); - }); - - const valid = [ - '/app/kibana', - '/app/monitoring#angular/route', - '/app/text#document-id', - '/app/some?with=query', - '/app/some?with=query#and-a-hash', - ]; - - valid.forEach(url => { - it(`allows ${url}`, () => { - shortUrlAssertValid(url); - }); - }); -}); diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_error.js b/src/legacy/server/url_shortening/routes/lib/short_url_error.js deleted file mode 100644 index ed44ba21aa4c4..0000000000000 --- a/src/legacy/server/url_shortening/routes/lib/short_url_error.js +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import Boom from 'boom'; - -export function handleShortUrlError(error) { - return Boom.boomify(error, { - statusCode: error.statusCode || error.status || 500, - }); -} diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_error.test.js b/src/legacy/server/url_shortening/routes/lib/short_url_error.test.js deleted file mode 100644 index 4eca6320ec834..0000000000000 --- a/src/legacy/server/url_shortening/routes/lib/short_url_error.test.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import _ from 'lodash'; -import { handleShortUrlError } from './short_url_error'; - -function createErrorWithStatus(status) { - const error = new Error(); - error.status = status; - return error; -} - -function createErrorWithStatusCode(statusCode) { - const error = new Error(); - error.statusCode = statusCode; - return error; -} - -describe('handleShortUrlError()', () => { - const caughtErrorsWithStatus = [ - createErrorWithStatus(401), - createErrorWithStatus(403), - createErrorWithStatus(404), - ]; - - const caughtErrorsWithStatusCode = [ - createErrorWithStatusCode(401), - createErrorWithStatusCode(403), - createErrorWithStatusCode(404), - ]; - - const uncaughtErrors = [new Error(), createErrorWithStatus(500), createErrorWithStatusCode(500)]; - - caughtErrorsWithStatus.forEach(err => { - it(`should handle errors with status of ${err.status}`, function() { - expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(err.status); - }); - }); - - caughtErrorsWithStatusCode.forEach(err => { - it(`should handle errors with statusCode of ${err.statusCode}`, function() { - expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(err.statusCode); - }); - }); - - uncaughtErrors.forEach(err => { - it(`should not handle unknown errors`, function() { - expect(_.get(handleShortUrlError(err), 'output.statusCode')).toBe(500); - }); - }); -}); diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js b/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js deleted file mode 100644 index a8a01d1433a7a..0000000000000 --- a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { get } from 'lodash'; - -export function shortUrlLookupProvider(server) { - async function updateMetadata(doc, req) { - try { - await req.getSavedObjectsClient().update('url', doc.id, { - accessDate: new Date(), - accessCount: get(doc, 'attributes.accessCount', 0) + 1, - }); - } catch (err) { - server.log('Warning: Error updating url metadata', err); - //swallow errors. It isn't critical if there is no update. - } - } - - return { - async getUrl(id, req) { - const doc = await req.getSavedObjectsClient().get('url', id); - updateMetadata(doc, req); - - return doc.attributes.url; - }, - }; -} diff --git a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js b/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js deleted file mode 100644 index e8bf72a142d11..0000000000000 --- a/src/legacy/server/url_shortening/routes/lib/short_url_lookup.test.js +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import sinon from 'sinon'; -import { shortUrlLookupProvider } from './short_url_lookup'; -import { SavedObjectsClient } from '../../../../../core/server'; - -describe('shortUrlLookupProvider', () => { - const ID = 'bf00ad16941fc51420f91a93428b27a0'; - const TYPE = 'url'; - const URL = 'http://elastic.co'; - const server = { log: sinon.stub() }; - const sandbox = sinon.createSandbox(); - - let savedObjectsClient; - let req; - let shortUrl; - - beforeEach(() => { - savedObjectsClient = { - get: sandbox.stub(), - create: sandbox.stub().returns(Promise.resolve({ id: ID })), - update: sandbox.stub(), - errors: SavedObjectsClient.errors, - }; - - req = { getSavedObjectsClient: () => savedObjectsClient }; - shortUrl = shortUrlLookupProvider(server); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe('getUrl', () => { - beforeEach(() => { - const attributes = { accessCount: 2, url: URL }; - savedObjectsClient.get.returns({ id: ID, attributes }); - }); - - it('provides the ID to savedObjectsClient', async () => { - await shortUrl.getUrl(ID, req); - - sinon.assert.calledOnce(savedObjectsClient.get); - const [type, id] = savedObjectsClient.get.getCall(0).args; - - expect(type).toEqual(TYPE); - expect(id).toEqual(ID); - }); - - it('returns the url', async () => { - const response = await shortUrl.getUrl(ID, req); - expect(response).toEqual(URL); - }); - - it('increments accessCount', async () => { - await shortUrl.getUrl(ID, req); - - sinon.assert.calledOnce(savedObjectsClient.update); - const [type, id, attributes] = savedObjectsClient.update.getCall(0).args; - - expect(type).toEqual(TYPE); - expect(id).toEqual(ID); - expect(Object.keys(attributes).sort()).toEqual(['accessCount', 'accessDate']); - expect(attributes.accessCount).toEqual(3); - }); - }); -}); diff --git a/src/legacy/server/url_shortening/url_shortening_mixin.js b/src/legacy/server/url_shortening/url_shortening_mixin.js deleted file mode 100644 index 867898cac845a..0000000000000 --- a/src/legacy/server/url_shortening/url_shortening_mixin.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import { createRoutes } from './routes/create_routes'; - -export function urlShorteningMixin(kbnServer, server) { - createRoutes(server); -} diff --git a/src/plugins/newsfeed/public/plugin.tsx b/src/plugins/newsfeed/public/plugin.tsx index 5ea5e5b324717..c4e042fe452f9 100644 --- a/src/plugins/newsfeed/public/plugin.tsx +++ b/src/plugins/newsfeed/public/plugin.tsx @@ -23,7 +23,7 @@ import ReactDOM from 'react-dom'; import React from 'react'; import { I18nProvider } from '@kbn/i18n/react'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; -import { NewsfeedPluginInjectedConfig } from '../types'; +import { FetchResult, NewsfeedPluginInjectedConfig } from '../types'; import { NewsfeedNavButton, NewsfeedApiFetchResult } from './components/newsfeed_header_nav_button'; import { getApi } from './lib/api'; @@ -54,10 +54,14 @@ export class NewsfeedPublicPlugin implements Plugin { private fetchNewsfeed(core: CoreStart) { const { http, injectedMetadata } = core; - const config = injectedMetadata.getInjectedVar( - 'newsfeed' - ) as NewsfeedPluginInjectedConfig['newsfeed']; + const config = injectedMetadata.getInjectedVar('newsfeed') as + | NewsfeedPluginInjectedConfig['newsfeed'] + | undefined; + if (!config) { + // running in new platform, injected metadata not available + return new Rx.Observable(); + } return getApi(http, config, this.kibanaVersion).pipe( takeUntil(this.stop$), // stop the interval when stop method is called catchError(() => Rx.of(null)) // do not throw error diff --git a/src/legacy/server/url_shortening/routes/create_routes.js b/src/plugins/share/common/short_url_routes.ts similarity index 67% rename from src/legacy/server/url_shortening/routes/create_routes.js rename to src/plugins/share/common/short_url_routes.ts index 9540e7441a268..7b42534de2ab1 100644 --- a/src/legacy/server/url_shortening/routes/create_routes.js +++ b/src/plugins/share/common/short_url_routes.ts @@ -17,11 +17,15 @@ * under the License. */ -import { shortUrlLookupProvider } from './lib/short_url_lookup'; -import { createGotoRoute } from './goto'; +export const GOTO_PREFIX = '/goto'; -export function createRoutes(server) { - const shortUrlLookup = shortUrlLookupProvider(server); +export const getUrlIdFromGotoRoute = (path: string) => + path.match(new RegExp(`${GOTO_PREFIX}/(.*)$`))?.[1]; - server.route(createGotoRoute({ server, shortUrlLookup })); -} +export const getGotoPath = (urlId: string) => `${GOTO_PREFIX}/${urlId}`; + +export const GETTER_PREFIX = '/api/short_url'; + +export const getUrlPath = (urlId: string) => `${GETTER_PREFIX}/${urlId}`; + +export const CREATE_PATH = '/api/shorten_url'; diff --git a/src/plugins/share/public/lib/url_shortener.ts b/src/plugins/share/public/lib/url_shortener.ts index 29d91bdb1aae6..f2259f1fabf7d 100644 --- a/src/plugins/share/public/lib/url_shortener.ts +++ b/src/plugins/share/public/lib/url_shortener.ts @@ -19,6 +19,7 @@ import url from 'url'; import { HttpStart } from 'kibana/public'; +import { CREATE_PATH, getGotoPath } from '../../common/short_url_routes'; export async function shortenUrl( absoluteUrl: string, @@ -34,10 +35,10 @@ export async function shortenUrl( const body = JSON.stringify({ url: relativeUrl }); - const resp = await post('/api/shorten_url', { body }); + const resp = await post(CREATE_PATH, { body }); return url.format({ protocol: parsedUrl.protocol, host: parsedUrl.host, - pathname: `${basePath}/goto/${resp.urlId}`, + pathname: `${basePath}${getGotoPath(resp.urlId)}`, }); } diff --git a/src/plugins/share/public/plugin.test.ts b/src/plugins/share/public/plugin.test.ts index 5610490be33b3..730814fe9ed23 100644 --- a/src/plugins/share/public/plugin.test.ts +++ b/src/plugins/share/public/plugin.test.ts @@ -20,6 +20,7 @@ import { registryMock, managerMock } from './plugin.test.mocks'; import { SharePlugin } from './plugin'; import { CoreStart } from 'kibana/public'; +import { coreMock } from '../../../core/public/mocks'; describe('SharePlugin', () => { beforeEach(() => { @@ -30,16 +31,28 @@ describe('SharePlugin', () => { describe('setup', () => { test('wires up and returns registry', async () => { - const setup = await new SharePlugin().setup(); + const coreSetup = coreMock.createSetup(); + const setup = await new SharePlugin().setup(coreSetup); expect(registryMock.setup).toHaveBeenCalledWith(); expect(setup.register).toBeDefined(); }); + + test('registers redirect app', async () => { + const coreSetup = coreMock.createSetup(); + await new SharePlugin().setup(coreSetup); + expect(coreSetup.application.register).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'short_url_redirect', + }) + ); + }); }); describe('start', () => { test('wires up and returns show function, but not registry', async () => { + const coreSetup = coreMock.createSetup(); const service = new SharePlugin(); - await service.setup(); + await service.setup(coreSetup); const start = await service.start({} as CoreStart); expect(registryMock.start).toHaveBeenCalled(); expect(managerMock.start).toHaveBeenCalledWith( diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts index 6d78211cf9954..01c248624950a 100644 --- a/src/plugins/share/public/plugin.ts +++ b/src/plugins/share/public/plugin.ts @@ -17,15 +17,17 @@ * under the License. */ -import { CoreStart, Plugin } from 'src/core/public'; +import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { ShareMenuManager, ShareMenuManagerStart } from './services'; import { ShareMenuRegistry, ShareMenuRegistrySetup } from './services'; +import { createShortUrlRedirectApp } from './services/short_url_redirect_app'; export class SharePlugin implements Plugin { private readonly shareMenuRegistry = new ShareMenuRegistry(); private readonly shareContextMenu = new ShareMenuManager(); - public async setup() { + public async setup(core: CoreSetup) { + core.application.register(createShortUrlRedirectApp(core, window.location)); return { ...this.shareMenuRegistry.setup(), }; diff --git a/src/plugins/share/public/services/short_url_redirect_app.test.ts b/src/plugins/share/public/services/short_url_redirect_app.test.ts new file mode 100644 index 0000000000000..206e637451ec0 --- /dev/null +++ b/src/plugins/share/public/services/short_url_redirect_app.test.ts @@ -0,0 +1,46 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { createShortUrlRedirectApp } from './short_url_redirect_app'; +import { coreMock } from '../../../../core/public/mocks'; +import { hashUrl } from '../../../kibana_utils/public'; + +jest.mock('../../../kibana_utils/public', () => ({ hashUrl: jest.fn(x => `${x}/hashed`) })); + +describe('short_url_redirect_app', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should fetch url and redirect to hashed version', async () => { + const coreSetup = coreMock.createSetup({ basePath: 'base' }); + coreSetup.http.get.mockResolvedValueOnce({ url: '/app/abc' }); + const locationMock = { pathname: '/base/goto/12345', href: '' } as Location; + + const { mount } = createShortUrlRedirectApp(coreSetup, locationMock); + await mount(); + + // check for fetching the complete URL + expect(coreSetup.http.get).toHaveBeenCalledWith('/api/short_url/12345'); + // check for hashing the URL returned from the server + expect(hashUrl).toHaveBeenCalledWith('/app/abc'); + // check for redirecting to the prepended path + expect(locationMock.href).toEqual('base/app/abc/hashed'); + }); +}); diff --git a/src/legacy/core_plugins/state_session_storage_redirect/public/index.js b/src/plugins/share/public/services/short_url_redirect_app.ts similarity index 50% rename from src/legacy/core_plugins/state_session_storage_redirect/public/index.js rename to src/plugins/share/public/services/short_url_redirect_app.ts index 701a5736c7d3b..6f72b711f6602 100644 --- a/src/legacy/core_plugins/state_session_storage_redirect/public/index.js +++ b/src/plugins/share/public/services/short_url_redirect_app.ts @@ -17,24 +17,29 @@ * under the License. */ -import chrome from 'ui/chrome'; -import { hashUrl } from '../../../../plugins/kibana_utils/public'; -import uiRoutes from 'ui/routes'; -import { fatalError } from 'ui/notify'; +import { CoreSetup } from 'kibana/public'; +import { getUrlIdFromGotoRoute, getUrlPath, GOTO_PREFIX } from '../../common/short_url_routes'; +import { hashUrl } from '../../../kibana_utils/public'; -uiRoutes.enable(); -uiRoutes.when('/', { - resolve: { - url: function(AppState, globalState, $window) { - const redirectUrl = chrome.getInjected('redirectUrl'); - try { - const hashedUrl = hashUrl(redirectUrl); - const url = chrome.addBasePath(hashedUrl); +export const createShortUrlRedirectApp = (core: CoreSetup, location: Location) => ({ + id: 'short_url_redirect', + appRoute: GOTO_PREFIX, + chromeless: true, + title: 'Short URL Redirect', + async mount() { + const urlId = getUrlIdFromGotoRoute(location.pathname); - $window.location = url; - } catch (e) { - fatalError(e); - } - }, + if (!urlId) { + throw new Error('Url id not present in path'); + } + + const response = await core.http.get<{ url: string }>(getUrlPath(urlId)); + const redirectUrl = response.url; + const hashedUrl = hashUrl(redirectUrl); + const url = core.http.basePath.prepend(hashedUrl); + + location.href = url; + + return () => {}; }, }); diff --git a/src/plugins/share/server/routes/create_routes.ts b/src/plugins/share/server/routes/create_routes.ts index bd4b6fdb08791..22d10c25197c9 100644 --- a/src/plugins/share/server/routes/create_routes.ts +++ b/src/plugins/share/server/routes/create_routes.ts @@ -22,11 +22,13 @@ import { CoreSetup, Logger } from 'kibana/server'; import { shortUrlLookupProvider } from './lib/short_url_lookup'; import { createGotoRoute } from './goto'; import { createShortenUrlRoute } from './shorten_url'; +import { createGetterRoute } from './get'; export function createRoutes({ http }: CoreSetup, logger: Logger) { const shortUrlLookup = shortUrlLookupProvider({ logger }); const router = http.createRouter(); createGotoRoute({ router, shortUrlLookup, http }); + createGetterRoute({ router, shortUrlLookup, http }); createShortenUrlRoute({ router, shortUrlLookup }); } diff --git a/src/legacy/server/url_shortening/routes/goto.js b/src/plugins/share/server/routes/get.ts similarity index 55% rename from src/legacy/server/url_shortening/routes/goto.js rename to src/plugins/share/server/routes/get.ts index 7a874786423d8..d6b191341dbe1 100644 --- a/src/legacy/server/url_shortening/routes/goto.js +++ b/src/plugins/share/server/routes/get.ts @@ -17,23 +17,40 @@ * under the License. */ -import { handleShortUrlError } from './lib/short_url_error'; +import { CoreSetup, IRouter } from 'kibana/server'; +import { schema } from '@kbn/config-schema'; + import { shortUrlAssertValid } from './lib/short_url_assert_valid'; +import { ShortUrlLookupService } from './lib/short_url_lookup'; +import { getUrlPath } from '../../common/short_url_routes'; -export const createGotoRoute = ({ server, shortUrlLookup }) => ({ - method: 'GET', - path: '/goto_LP/{urlId}', - handler: async function(request, h) { - try { - const url = await shortUrlLookup.getUrl(request.params.urlId, request); +export const createGetterRoute = ({ + router, + shortUrlLookup, + http, +}: { + router: IRouter; + shortUrlLookup: ShortUrlLookupService; + http: CoreSetup['http']; +}) => { + router.get( + { + path: getUrlPath('{urlId}'), + validate: { + params: schema.object({ urlId: schema.string() }), + }, + }, + router.handleLegacyErrors(async function(context, request, response) { + const url = await shortUrlLookup.getUrl(request.params.urlId, { + savedObjects: context.core.savedObjects.client, + }); shortUrlAssertValid(url); - const app = server.getHiddenUiAppById('stateSessionStorageRedirect'); - return h.renderApp(app, { - redirectUrl: url, + return response.ok({ + body: { + url, + }, }); - } catch (err) { - throw handleShortUrlError(err); - } - }, -}); + }) + ); +}; diff --git a/src/plugins/share/server/routes/goto.ts b/src/plugins/share/server/routes/goto.ts index 7343dc1bd34a2..5c3a4e441099f 100644 --- a/src/plugins/share/server/routes/goto.ts +++ b/src/plugins/share/server/routes/goto.ts @@ -22,6 +22,7 @@ import { schema } from '@kbn/config-schema'; import { shortUrlAssertValid } from './lib/short_url_assert_valid'; import { ShortUrlLookupService } from './lib/short_url_lookup'; +import { getGotoPath } from '../../common/short_url_routes'; export const createGotoRoute = ({ router, @@ -34,7 +35,7 @@ export const createGotoRoute = ({ }) => { router.get( { - path: '/goto/{urlId}', + path: getGotoPath('{urlId}'), validate: { params: schema.object({ urlId: schema.string() }), }, @@ -54,10 +55,13 @@ export const createGotoRoute = ({ }, }); } - return response.redirected({ + const body = await context.core.rendering.render(); + + return response.ok({ headers: { - location: http.basePath.prepend('/goto_LP/' + request.params.urlId), + 'content-security-policy': http.csp.header, }, + body, }); }) ); diff --git a/src/plugins/share/server/routes/shorten_url.ts b/src/plugins/share/server/routes/shorten_url.ts index 116b90c6971c5..41570f8a5f453 100644 --- a/src/plugins/share/server/routes/shorten_url.ts +++ b/src/plugins/share/server/routes/shorten_url.ts @@ -22,6 +22,7 @@ import { schema } from '@kbn/config-schema'; import { shortUrlAssertValid } from './lib/short_url_assert_valid'; import { ShortUrlLookupService } from './lib/short_url_lookup'; +import { CREATE_PATH } from '../../common/short_url_routes'; export const createShortenUrlRoute = ({ shortUrlLookup, @@ -32,7 +33,7 @@ export const createShortenUrlRoute = ({ }) => { router.post( { - path: '/api/shorten_url', + path: CREATE_PATH, validate: { body: schema.object({ url: schema.string() }), }, From d3cef4791ec88da7d364596051e6f86310a800dd Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 21 Jan 2020 08:54:42 -0500 Subject: [PATCH 34/39] [Maps] fix warning about missing key in react element (#55372) --- .../legacy/plugins/maps/public/layers/styles/color_utils.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js index df212f23cd894..b04f8ff56e5ee 100644 --- a/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js +++ b/x-pack/legacy/plugins/maps/public/layers/styles/color_utils.js @@ -113,7 +113,11 @@ export const COLOR_PALETTES = COLOR_PALETTES_CONFIGS.map(palette => { height: '100%', display: 'inline-block', }; - return
 
; + return ( +
+   +
+ ); }); return { value: palette.id, From 6feabcd53342482d403247487dfc7bb6ca6e74d9 Mon Sep 17 00:00:00 2001 From: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com> Date: Tue, 21 Jan 2020 17:38:28 +0300 Subject: [PATCH 35/39] Generate a static parser, move tests to vis_type_timelion (#55299) * Use generated parser, move tests to vis_type_timelion * Remove legacy tests * Create a grunt task for generating a parser --- .eslintignore | 1 + .../core_plugins/vis_type_timelion/README.md | 10 + .../public/_generated_/chain.js | 1780 +++++++++++++++++ ...timelion_expression_input_helpers.test.ts} | 172 +- .../timelion_expression_input_helpers.ts | 13 +- tasks/config/peg.js | 4 + 6 files changed, 1843 insertions(+), 137 deletions(-) create mode 100644 src/legacy/core_plugins/vis_type_timelion/README.md create mode 100644 src/legacy/core_plugins/vis_type_timelion/public/_generated_/chain.js rename src/legacy/core_plugins/{timelion/public/directives/__tests__/timelion_expression_input_helpers.js => vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts} (70%) diff --git a/.eslintignore b/.eslintignore index c4fb806b6d394..86a01b68ecab1 100644 --- a/.eslintignore +++ b/.eslintignore @@ -9,6 +9,7 @@ bower_components /built_assets /html_docs /src/plugins/data/common/es_query/kuery/ast/_generated_/** +/src/legacy/core_plugins/vis_type_timelion/public/_generated_/** src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/mock_data /src/legacy/ui/public/angular-bootstrap /src/legacy/ui/public/flot-charts diff --git a/src/legacy/core_plugins/vis_type_timelion/README.md b/src/legacy/core_plugins/vis_type_timelion/README.md new file mode 100644 index 0000000000000..c306e03abf2c6 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_timelion/README.md @@ -0,0 +1,10 @@ +# Vis type Timelion + +# Generate a parser +If your grammar was changed in `public/chain.peg` you need to re-generate the static parser. You could use a grunt task: + +``` +grunt peg:timelion_chain +``` + +The generated parser will be appeared at `public/_generated_` folder, which is included in `.eslintignore` \ No newline at end of file diff --git a/src/legacy/core_plugins/vis_type_timelion/public/_generated_/chain.js b/src/legacy/core_plugins/vis_type_timelion/public/_generated_/chain.js new file mode 100644 index 0000000000000..f812b94238d43 --- /dev/null +++ b/src/legacy/core_plugins/vis_type_timelion/public/_generated_/chain.js @@ -0,0 +1,1780 @@ +module.exports = (function() { + "use strict"; + + /* + * Generated by PEG.js 0.9.0. + * + * http://pegjs.org/ + */ + + function peg$subclass(child, parent) { + function ctor() { this.constructor = child; } + ctor.prototype = parent.prototype; + child.prototype = new ctor(); + } + + function peg$SyntaxError(message, expected, found, location) { + this.message = message; + this.expected = expected; + this.found = found; + this.location = location; + this.name = "SyntaxError"; + + if (typeof Error.captureStackTrace === "function") { + Error.captureStackTrace(this, peg$SyntaxError); + } + } + + peg$subclass(peg$SyntaxError, Error); + + function peg$parse(input) { + var options = arguments.length > 1 ? arguments[1] : {}, + parser = this, + + peg$FAILED = {}, + + peg$startRuleFunctions = { start: peg$parsestart }, + peg$startRuleFunction = peg$parsestart, + + peg$c0 = function(tree) { + return { + tree: tree.filter(function (o) {return o != null}), + functions: functions, + args: args, + variables: variables + } + }, + peg$c1 = ",", + peg$c2 = { type: "literal", value: ",", description: "\",\"" }, + peg$c3 = function(first, arg) {return arg}, + peg$c4 = function(first, rest) { + return [first].concat(rest); + }, + peg$c5 = "=", + peg$c6 = { type: "literal", value: "=", description: "\"=\"" }, + peg$c7 = function(name, value) { + var arg = { + type: 'namedArg', + name: name, + value: value, + location: simpleLocation(location()), + text: text() + }; + currentArgs.push(arg); + return arg; + }, + peg$c8 = function(value) { + var exception = { + type: 'incompleteArgument', + currentArgs: currentArgs, + currentFunction: currentFunction, + location: simpleLocation(location()), + text: text() + } + error(JSON.stringify(exception)); + }, + peg$c9 = function(name) { + var exception = { + type: 'incompleteArgumentValue', + currentArgs: currentArgs, + currentFunction: currentFunction, + name: name, + location: simpleLocation(location()), + text: text() + } + error(JSON.stringify(exception)); + }, + peg$c10 = function(element) {return element}, + peg$c11 = function(literal) { + var result = ltoo(literal); + result.location = simpleLocation(location()), + result.text = text(); + return result; + }, + peg$c12 = "$", + peg$c13 = { type: "literal", value: "$", description: "\"$\"" }, + peg$c14 = function(name) { + if (variables[name]) { + return variables[name]; + } else { + error('$' + name + ' is not defined') + } + }, + peg$c15 = function(name, value) { + variables[name] = value; + }, + peg$c16 = function(first, series) {return series}, + peg$c17 = function(first, rest) { + return [first].concat(rest) + }, + peg$c18 = /^[a-zA-Z]/, + peg$c19 = { type: "class", value: "[a-zA-Z]", description: "[a-zA-Z]" }, + peg$c20 = /^[.a-zA-Z0-9_\-]/, + peg$c21 = { type: "class", value: "[.a-zA-Z0-9_-]", description: "[.a-zA-Z0-9_-]" }, + peg$c22 = function(first, rest) { + currentFunction = first.join('') + rest.join(''); + currentArgs = []; + return currentFunction; + }, + peg$c23 = function(first, rest) { return first.join('') + rest.join('') }, + peg$c24 = { type: "other", description: "function" }, + peg$c25 = ".", + peg$c26 = { type: "literal", value: ".", description: "\".\"" }, + peg$c27 = "(", + peg$c28 = { type: "literal", value: "(", description: "\"(\"" }, + peg$c29 = ")", + peg$c30 = { type: "literal", value: ")", description: "\")\"" }, + peg$c31 = function(name, arg_list) { + var result = { + type: 'function', + function: name, + arguments: arg_list || [], + location: simpleLocation(location()), + text: text() + } + + result.arguments.forEach(function (arg) { + arg.function = name; + args.push(arg); + }) + + functions.push(result) + return result; + }, + peg$c32 = function(func) { + var exception = { + type: 'incompleteFunction', + function: func, + location: simpleLocation(location()), + text: text() + } + error(JSON.stringify(exception)); + }, + peg$c33 = "@", + peg$c34 = { type: "literal", value: "@", description: "\"@\"" }, + peg$c35 = ":", + peg$c36 = { type: "literal", value: ":", description: "\":\"" }, + peg$c37 = function(plot, series) { + return { + type: 'reference', + plot: plot, + series: series + } + }, + peg$c38 = function(plot) { + return { + type: 'reference', + plot: plot + } + }, + peg$c39 = function(func, rest) {return {type: 'chain', chain: [func].concat(rest)}}, + peg$c40 = function(grouped, functions) { + var first = { + type: 'chainList', + list: grouped + } + first.label = text(); + + return {type: "chain", chain: [first].concat(functions)}; + }, + peg$c41 = { type: "other", description: "literal" }, + peg$c42 = "\"", + peg$c43 = { type: "literal", value: "\"", description: "\"\\\"\"" }, + peg$c44 = function(chars) { return chars.join(''); }, + peg$c45 = "'", + peg$c46 = { type: "literal", value: "'", description: "\"'\"" }, + peg$c47 = "true", + peg$c48 = { type: "literal", value: "true", description: "\"true\"" }, + peg$c49 = function() { return true; }, + peg$c50 = "false", + peg$c51 = { type: "literal", value: "false", description: "\"false\"" }, + peg$c52 = function() { return false; }, + peg$c53 = "null", + peg$c54 = { type: "literal", value: "null", description: "\"null\"" }, + peg$c55 = function() { return null; }, + peg$c56 = /^[^()"',= \t]/, + peg$c57 = { type: "class", value: "[^()\"',=\\ \\t]", description: "[^()\"',=\\ \\t]" }, + peg$c58 = function(string) { // this also matches numbers via Number() + var result = string.join(''); + // Sort of hacky, but PEG doesn't have backtracking so + // a number rule is hard to read, and performs worse + if (isNaN(Number(result))) return result; + return Number(result) + }, + peg$c59 = /^[ \t\r\n]/, + peg$c60 = { type: "class", value: "[\\ \\t\\r\\n]", description: "[\\ \\t\\r\\n]" }, + peg$c61 = "\\", + peg$c62 = { type: "literal", value: "\\", description: "\"\\\\\"" }, + peg$c63 = function(sequence) { return sequence; }, + peg$c64 = /^[^"]/, + peg$c65 = { type: "class", value: "[^\"]", description: "[^\"]" }, + peg$c66 = /^[^']/, + peg$c67 = { type: "class", value: "[^']", description: "[^']" }, + peg$c68 = /^[0-9]/, + peg$c69 = { type: "class", value: "[0-9]", description: "[0-9]" }, + peg$c70 = function(digits) {return parseInt(digits.join(''))}, + + peg$currPos = 0, + peg$savedPos = 0, + peg$posDetailsCache = [{ line: 1, column: 1, seenCR: false }], + peg$maxFailPos = 0, + peg$maxFailExpected = [], + peg$silentFails = 0, + + peg$result; + + if ("startRule" in options) { + if (!(options.startRule in peg$startRuleFunctions)) { + throw new Error("Can't start parsing from rule \"" + options.startRule + "\"."); + } + + peg$startRuleFunction = peg$startRuleFunctions[options.startRule]; + } + + function text() { + return input.substring(peg$savedPos, peg$currPos); + } + + function location() { + return peg$computeLocation(peg$savedPos, peg$currPos); + } + + function expected(description) { + throw peg$buildException( + null, + [{ type: "other", description: description }], + input.substring(peg$savedPos, peg$currPos), + peg$computeLocation(peg$savedPos, peg$currPos) + ); + } + + function error(message) { + throw peg$buildException( + message, + null, + input.substring(peg$savedPos, peg$currPos), + peg$computeLocation(peg$savedPos, peg$currPos) + ); + } + + function peg$computePosDetails(pos) { + var details = peg$posDetailsCache[pos], + p, ch; + + if (details) { + return details; + } else { + p = pos - 1; + while (!peg$posDetailsCache[p]) { + p--; + } + + details = peg$posDetailsCache[p]; + details = { + line: details.line, + column: details.column, + seenCR: details.seenCR + }; + + while (p < pos) { + ch = input.charAt(p); + if (ch === "\n") { + if (!details.seenCR) { details.line++; } + details.column = 1; + details.seenCR = false; + } else if (ch === "\r" || ch === "\u2028" || ch === "\u2029") { + details.line++; + details.column = 1; + details.seenCR = true; + } else { + details.column++; + details.seenCR = false; + } + + p++; + } + + peg$posDetailsCache[pos] = details; + return details; + } + } + + function peg$computeLocation(startPos, endPos) { + var startPosDetails = peg$computePosDetails(startPos), + endPosDetails = peg$computePosDetails(endPos); + + return { + start: { + offset: startPos, + line: startPosDetails.line, + column: startPosDetails.column + }, + end: { + offset: endPos, + line: endPosDetails.line, + column: endPosDetails.column + } + }; + } + + function peg$fail(expected) { + if (peg$currPos < peg$maxFailPos) { return; } + + if (peg$currPos > peg$maxFailPos) { + peg$maxFailPos = peg$currPos; + peg$maxFailExpected = []; + } + + peg$maxFailExpected.push(expected); + } + + function peg$buildException(message, expected, found, location) { + function cleanupExpected(expected) { + var i = 1; + + expected.sort(function(a, b) { + if (a.description < b.description) { + return -1; + } else if (a.description > b.description) { + return 1; + } else { + return 0; + } + }); + + while (i < expected.length) { + if (expected[i - 1] === expected[i]) { + expected.splice(i, 1); + } else { + i++; + } + } + } + + function buildMessage(expected, found) { + function stringEscape(s) { + function hex(ch) { return ch.charCodeAt(0).toString(16).toUpperCase(); } + + return s + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\x08/g, '\\b') + .replace(/\t/g, '\\t') + .replace(/\n/g, '\\n') + .replace(/\f/g, '\\f') + .replace(/\r/g, '\\r') + .replace(/[\x00-\x07\x0B\x0E\x0F]/g, function(ch) { return '\\x0' + hex(ch); }) + .replace(/[\x10-\x1F\x80-\xFF]/g, function(ch) { return '\\x' + hex(ch); }) + .replace(/[\u0100-\u0FFF]/g, function(ch) { return '\\u0' + hex(ch); }) + .replace(/[\u1000-\uFFFF]/g, function(ch) { return '\\u' + hex(ch); }); + } + + var expectedDescs = new Array(expected.length), + expectedDesc, foundDesc, i; + + for (i = 0; i < expected.length; i++) { + expectedDescs[i] = expected[i].description; + } + + expectedDesc = expected.length > 1 + ? expectedDescs.slice(0, -1).join(", ") + + " or " + + expectedDescs[expected.length - 1] + : expectedDescs[0]; + + foundDesc = found ? "\"" + stringEscape(found) + "\"" : "end of input"; + + return "Expected " + expectedDesc + " but " + foundDesc + " found."; + } + + if (expected !== null) { + cleanupExpected(expected); + } + + return new peg$SyntaxError( + message !== null ? message : buildMessage(expected, found), + expected, + found, + location + ); + } + + function peg$parsestart() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = peg$parsespace(); + if (s1 === peg$FAILED) { + s1 = null; + } + if (s1 !== peg$FAILED) { + s2 = peg$parseseries(); + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c0(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsearg_list() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parseargument(); + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parsespace(); + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s5 = peg$c1; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c2); } + } + if (s5 !== peg$FAILED) { + s6 = peg$parsespace(); + if (s6 === peg$FAILED) { + s6 = null; + } + if (s6 !== peg$FAILED) { + s7 = peg$parseargument(); + if (s7 !== peg$FAILED) { + peg$savedPos = s3; + s4 = peg$c3(s1, s7); + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parsespace(); + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s5 = peg$c1; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c2); } + } + if (s5 !== peg$FAILED) { + s6 = peg$parsespace(); + if (s6 === peg$FAILED) { + s6 = null; + } + if (s6 !== peg$FAILED) { + s7 = peg$parseargument(); + if (s7 !== peg$FAILED) { + peg$savedPos = s3; + s4 = peg$c3(s1, s7); + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } + if (s2 !== peg$FAILED) { + s3 = peg$parsespace(); + if (s3 === peg$FAILED) { + s3 = null; + } + if (s3 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s4 = peg$c1; + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c2); } + } + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c4(s1, s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseargument() { + var s0, s1, s2, s3, s4, s5; + + s0 = peg$currPos; + s1 = peg$parseargument_name(); + if (s1 !== peg$FAILED) { + s2 = peg$parsespace(); + if (s2 === peg$FAILED) { + s2 = null; + } + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 61) { + s3 = peg$c5; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c6); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parsespace(); + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + s5 = peg$parsearg_type(); + if (s5 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c7(s1, s5); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$parsespace(); + if (s1 === peg$FAILED) { + s1 = null; + } + if (s1 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 61) { + s2 = peg$c5; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c6); } + } + if (s2 !== peg$FAILED) { + s3 = peg$parsespace(); + if (s3 === peg$FAILED) { + s3 = null; + } + if (s3 !== peg$FAILED) { + s4 = peg$parsearg_type(); + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c8(s4); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$parseargument_name(); + if (s1 !== peg$FAILED) { + s2 = peg$parsespace(); + if (s2 === peg$FAILED) { + s2 = null; + } + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 61) { + s3 = peg$c5; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c6); } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c9(s1); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$parsearg_type(); + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c10(s1); + } + s0 = s1; + } + } + } + + return s0; + } + + function peg$parsearg_type() { + var s0, s1; + + s0 = peg$parsevariable_get(); + if (s0 === peg$FAILED) { + s0 = peg$parseseries_type(); + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = peg$parseliteral(); + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c11(s1); + } + s0 = s1; + } + } + + return s0; + } + + function peg$parsevariable_get() { + var s0, s1, s2; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 36) { + s1 = peg$c12; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c13); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseargument_name(); + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c14(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsevariable_set() { + var s0, s1, s2, s3, s4, s5, s6; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 36) { + s1 = peg$c12; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c13); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseargument_name(); + if (s2 !== peg$FAILED) { + s3 = peg$parsespace(); + if (s3 === peg$FAILED) { + s3 = null; + } + if (s3 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 61) { + s4 = peg$c5; + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c6); } + } + if (s4 !== peg$FAILED) { + s5 = peg$parsespace(); + if (s5 === peg$FAILED) { + s5 = null; + } + if (s5 !== peg$FAILED) { + s6 = peg$parsearg_type(); + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c15(s2, s6); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseseries_type() { + var s0; + + s0 = peg$parsevariable_set(); + if (s0 === peg$FAILED) { + s0 = peg$parsevariable_get(); + if (s0 === peg$FAILED) { + s0 = peg$parsegroup(); + if (s0 === peg$FAILED) { + s0 = peg$parsechain(); + if (s0 === peg$FAILED) { + s0 = peg$parsereference(); + } + } + } + } + + return s0; + } + + function peg$parseseries() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + s1 = peg$parseseries_type(); + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$currPos; + s4 = peg$parsespace(); + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s5 = peg$c1; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c2); } + } + if (s5 !== peg$FAILED) { + s6 = peg$parsespace(); + if (s6 === peg$FAILED) { + s6 = null; + } + if (s6 !== peg$FAILED) { + s7 = peg$parseseries_type(); + if (s7 !== peg$FAILED) { + peg$savedPos = s3; + s4 = peg$c16(s1, s7); + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$currPos; + s4 = peg$parsespace(); + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s5 = peg$c1; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c2); } + } + if (s5 !== peg$FAILED) { + s6 = peg$parsespace(); + if (s6 === peg$FAILED) { + s6 = null; + } + if (s6 !== peg$FAILED) { + s7 = peg$parseseries_type(); + if (s7 !== peg$FAILED) { + peg$savedPos = s3; + s4 = peg$c16(s1, s7); + s3 = s4; + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } else { + peg$currPos = s3; + s3 = peg$FAILED; + } + } + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 44) { + s3 = peg$c1; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c2); } + } + if (s3 === peg$FAILED) { + s3 = null; + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c17(s1, s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsefunction_name() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = []; + if (peg$c18.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c19); } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + if (peg$c18.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c19); } + } + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + s2 = []; + if (peg$c20.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c21); } + } + while (s3 !== peg$FAILED) { + s2.push(s3); + if (peg$c20.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c21); } + } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c22(s1, s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseargument_name() { + var s0, s1, s2, s3; + + s0 = peg$currPos; + s1 = []; + if (peg$c18.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c19); } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + if (peg$c18.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c19); } + } + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + s2 = []; + if (peg$c20.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c21); } + } + while (s3 !== peg$FAILED) { + s2.push(s3); + if (peg$c20.test(input.charAt(peg$currPos))) { + s3 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c21); } + } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c23(s1, s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsefunction() { + var s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10; + + peg$silentFails++; + s0 = peg$currPos; + s1 = peg$parsespace(); + if (s1 === peg$FAILED) { + s1 = null; + } + if (s1 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 46) { + s2 = peg$c25; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c26); } + } + if (s2 !== peg$FAILED) { + s3 = peg$parsefunction_name(); + if (s3 !== peg$FAILED) { + s4 = peg$parsespace(); + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 40) { + s5 = peg$c27; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c28); } + } + if (s5 !== peg$FAILED) { + s6 = peg$parsespace(); + if (s6 === peg$FAILED) { + s6 = null; + } + if (s6 !== peg$FAILED) { + s7 = peg$parsearg_list(); + if (s7 === peg$FAILED) { + s7 = null; + } + if (s7 !== peg$FAILED) { + s8 = peg$parsespace(); + if (s8 === peg$FAILED) { + s8 = null; + } + if (s8 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 41) { + s9 = peg$c29; + peg$currPos++; + } else { + s9 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c30); } + } + if (s9 !== peg$FAILED) { + s10 = peg$parsespace(); + if (s10 === peg$FAILED) { + s10 = null; + } + if (s10 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c31(s3, s7); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 46) { + s1 = peg$c25; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c26); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parsefunction_name(); + if (s2 === peg$FAILED) { + s2 = null; + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c32(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c24); } + } + + return s0; + } + + function peg$parsereference() { + var s0, s1, s2, s3, s4; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 64) { + s1 = peg$c33; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c34); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseinteger(); + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 58) { + s3 = peg$c35; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c36); } + } + if (s3 !== peg$FAILED) { + s4 = peg$parseinteger(); + if (s4 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c37(s2, s4); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 64) { + s1 = peg$c33; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c34); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parseinteger(); + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c38(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } + + return s0; + } + + function peg$parsechain() { + var s0, s1, s2, s3, s4; + + s0 = peg$currPos; + s1 = peg$parsefunction(); + if (s1 !== peg$FAILED) { + s2 = peg$parsespace(); + if (s2 === peg$FAILED) { + s2 = null; + } + if (s2 !== peg$FAILED) { + s3 = []; + s4 = peg$parsefunction(); + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = peg$parsefunction(); + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c39(s1, s3); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsegroup() { + var s0, s1, s2, s3, s4, s5, s6, s7; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 40) { + s1 = peg$c27; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c28); } + } + if (s1 !== peg$FAILED) { + s2 = peg$parsespace(); + if (s2 === peg$FAILED) { + s2 = null; + } + if (s2 !== peg$FAILED) { + s3 = peg$parseseries(); + if (s3 !== peg$FAILED) { + s4 = peg$parsespace(); + if (s4 === peg$FAILED) { + s4 = null; + } + if (s4 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 41) { + s5 = peg$c29; + peg$currPos++; + } else { + s5 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c30); } + } + if (s5 !== peg$FAILED) { + s6 = []; + s7 = peg$parsefunction(); + while (s7 !== peg$FAILED) { + s6.push(s7); + s7 = peg$parsefunction(); + } + if (s6 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c40(s3, s6); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parseliteral() { + var s0, s1, s2, s3; + + peg$silentFails++; + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 34) { + s1 = peg$c42; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c43); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsedq_char(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsedq_char(); + } + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 34) { + s3 = peg$c42; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c43); } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c44(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 39) { + s1 = peg$c45; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c46); } + } + if (s1 !== peg$FAILED) { + s2 = []; + s3 = peg$parsesq_char(); + while (s3 !== peg$FAILED) { + s2.push(s3); + s3 = peg$parsesq_char(); + } + if (s2 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 39) { + s3 = peg$c45; + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c46); } + } + if (s3 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c44(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c47) { + s1 = peg$c47; + peg$currPos += 4; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c48); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c49(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 5) === peg$c50) { + s1 = peg$c50; + peg$currPos += 5; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c51); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c52(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + if (input.substr(peg$currPos, 4) === peg$c53) { + s1 = peg$c53; + peg$currPos += 4; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c54); } + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c55(); + } + s0 = s1; + if (s0 === peg$FAILED) { + s0 = peg$currPos; + s1 = []; + if (peg$c56.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c57); } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + if (peg$c56.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c57); } + } + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c58(s1); + } + s0 = s1; + } + } + } + } + } + peg$silentFails--; + if (s0 === peg$FAILED) { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c41); } + } + + return s0; + } + + function peg$parsespace() { + var s0, s1; + + s0 = []; + if (peg$c59.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c60); } + } + if (s1 !== peg$FAILED) { + while (s1 !== peg$FAILED) { + s0.push(s1); + if (peg$c59.test(input.charAt(peg$currPos))) { + s1 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c60); } + } + } + } else { + s0 = peg$FAILED; + } + + return s0; + } + + function peg$parsedq_char() { + var s0, s1, s2; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 92) { + s1 = peg$c61; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c62); } + } + if (s1 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 34) { + s2 = peg$c42; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c43); } + } + if (s2 === peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 92) { + s2 = peg$c61; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c62); } + } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c63(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + if (peg$c64.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c65); } + } + } + + return s0; + } + + function peg$parsesq_char() { + var s0, s1, s2; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 92) { + s1 = peg$c61; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c62); } + } + if (s1 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 39) { + s2 = peg$c45; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c46); } + } + if (s2 === peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 92) { + s2 = peg$c61; + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c62); } + } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c63(s2); + s0 = s1; + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + if (s0 === peg$FAILED) { + if (peg$c66.test(input.charAt(peg$currPos))) { + s0 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s0 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c67); } + } + } + + return s0; + } + + function peg$parseinteger() { + var s0, s1, s2; + + s0 = peg$currPos; + s1 = []; + if (peg$c68.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c69); } + } + if (s2 !== peg$FAILED) { + while (s2 !== peg$FAILED) { + s1.push(s2); + if (peg$c68.test(input.charAt(peg$currPos))) { + s2 = input.charAt(peg$currPos); + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$c69); } + } + } + } else { + s1 = peg$FAILED; + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$c70(s1); + } + s0 = s1; + + return s0; + } + + + function ltoo (literal) { + return {type: 'literal', value: literal} + } + + function simpleLocation (location) { + // Returns an object representing the position of the function within the expression, + // demarcated by the position of its first character and last character. We calculate these values + // using the offset because the expression could span multiple lines, and we don't want to deal + // with column and line values. + return { + min: location.start.offset, + max: location.end.offset + } + } + + var currentFunction; + var currentArgs = []; + + var functions = []; + var args = []; + var variables = {}; + + + + peg$result = peg$startRuleFunction(); + + if (peg$result !== peg$FAILED && peg$currPos === input.length) { + return peg$result; + } else { + if (peg$result !== peg$FAILED && peg$currPos < input.length) { + peg$fail({ type: "end", description: "end of input" }); + } + + throw peg$buildException( + null, + peg$maxFailExpected, + peg$maxFailPos < input.length ? input.charAt(peg$maxFailPos) : null, + peg$maxFailPos < input.length + ? peg$computeLocation(peg$maxFailPos, peg$maxFailPos + 1) + : peg$computeLocation(peg$maxFailPos, peg$maxFailPos) + ); + } + } + + return { + SyntaxError: peg$SyntaxError, + parse: peg$parse + }; +})(); \ No newline at end of file diff --git a/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js b/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts similarity index 70% rename from src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js rename to src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts index ea2d44bcaefe0..18a0c0872dc03 100644 --- a/src/legacy/core_plugins/timelion/public/directives/__tests__/timelion_expression_input_helpers.js +++ b/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.test.ts @@ -17,19 +17,16 @@ * under the License. */ -import expect from '@kbn/expect'; -import PEG from 'pegjs'; -import grammar from 'raw-loader!../../../../vis_type_timelion/public/chain.peg'; -import { SUGGESTION_TYPE, suggest } from '../timelion_expression_input_helpers'; -import { getArgValueSuggestions } from '../../../../vis_type_timelion/public/helpers/arg_value_suggestions'; -import { - setIndexPatterns, - setSavedObjectsClient, -} from '../../../../vis_type_timelion/public/helpers/plugin_services'; +import { SUGGESTION_TYPE, suggest } from './timelion_expression_input_helpers'; +import { getArgValueSuggestions } from '../helpers/arg_value_suggestions'; +import { setIndexPatterns, setSavedObjectsClient } from '../helpers/plugin_services'; +import { IndexPatterns } from 'src/plugins/data/public'; +import { SavedObjectsClient } from 'kibana/public'; +import { ITimelionFunction } from '../../common/types'; describe('Timelion expression suggestions', () => { - setIndexPatterns({}); - setSavedObjectsClient({}); + setIndexPatterns({} as IndexPatterns); + setSavedObjectsClient({} as SavedObjectsClient); const argValueSuggestions = getArgValueSuggestions(); @@ -45,17 +42,13 @@ describe('Timelion expression suggestions', () => { suggestions: [{ name: 'value1' }], }, ], - }; + } as ITimelionFunction; const myFunc2 = { name: 'myFunc2', chainable: false, args: [{ name: 'argA' }, { name: 'argAB' }, { name: 'argABC' }], - }; + } as ITimelionFunction; const functionList = [func1, myFunc2]; - let Parser; - beforeEach(function() { - Parser = PEG.generate(grammar); - }); describe('parse exception', () => { describe('incompleteFunction', () => { @@ -65,17 +58,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [func1, myFunc2], - location: { - min: 0, - max: 1, - }, - type: 'functions', + type: SUGGESTION_TYPE.FUNCTIONS, }); }); it('should filter function suggestions by function name', async () => { @@ -84,17 +72,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [myFunc2], - location: { - min: 0, - max: 4, - }, - type: 'functions', + type: SUGGESTION_TYPE.FUNCTIONS, }); }); }); @@ -106,17 +89,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [], - location: { - min: 11, - max: 12, - }, - type: 'arguments', + type: SUGGESTION_TYPE.ARGUMENTS, }); }); @@ -126,17 +104,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: myFunc2.args, - location: { - min: 9, - max: 10, - }, - type: 'arguments', + type: SUGGESTION_TYPE.ARGUMENTS, }); }); @@ -146,17 +119,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: myFunc2.args, - location: { - min: 9, - max: 25, - }, - type: 'arguments', + type: SUGGESTION_TYPE.ARGUMENTS, }); }); @@ -166,17 +134,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [{ name: 'argA' }, { name: 'argAB', suggestions: [{ name: 'value1' }] }], - location: { - min: 7, - max: 8, - }, - type: 'arguments', + type: SUGGESTION_TYPE.ARGUMENTS, }); }); @@ -186,17 +149,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [{ name: 'argA' }, { name: 'argABC' }], - location: { - min: 24, - max: 25, - }, - type: 'arguments', + type: SUGGESTION_TYPE.ARGUMENTS, }); }); }); @@ -208,17 +166,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [], - location: { - min: 11, - max: 11, - }, - type: 'argument_value', + type: SUGGESTION_TYPE.ARGUMENT_VALUE, }); }); @@ -228,17 +181,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [{ name: 'value1' }], - location: { - min: 11, - max: 11, - }, - type: 'argument_value', + type: SUGGESTION_TYPE.ARGUMENT_VALUE, }); }); }); @@ -252,17 +200,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [func1], - location: { - min: 0, - max: 8, - }, - type: 'functions', + type: SUGGESTION_TYPE.FUNCTIONS, }); }); }); @@ -275,17 +218,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: myFunc2.args, - location: { - min: 9, - max: 9, - }, - type: 'arguments', + type: SUGGESTION_TYPE.ARGUMENTS, }); }); it('should not provide argument suggestions for argument that is all ready set in function def', async () => { @@ -294,18 +232,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions.type).to.equal(SUGGESTION_TYPE.ARGUMENTS); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [{ name: 'argA' }, { name: 'argABC' }], - location: { - min: 24, - max: 24, - }, - type: 'arguments', + type: SUGGESTION_TYPE.ARGUMENTS, }); }); it('should filter argument suggestions by argument name', async () => { @@ -314,17 +246,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [{ name: 'argAB' }, { name: 'argABC' }], - location: { - min: 9, - max: 14, - }, - type: 'arguments', + type: SUGGESTION_TYPE.ARGUMENTS, }); }); it('should not show first argument for chainable functions', async () => { @@ -333,17 +260,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [{ name: 'argA' }, { name: 'argAB', suggestions: [{ name: 'value1' }] }], - location: { - min: 7, - max: 7, - }, - type: 'arguments', + type: SUGGESTION_TYPE.ARGUMENTS, }); }); }); @@ -354,17 +276,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [], - location: { - min: 14, - max: 16, - }, - type: 'argument_value', + type: SUGGESTION_TYPE.ARGUMENT_VALUE, }); }); @@ -374,17 +291,12 @@ describe('Timelion expression suggestions', () => { const suggestions = await suggest( expression, functionList, - Parser, cursorPosition, argValueSuggestions ); - expect(suggestions).to.eql({ + expect(suggestions).toEqual({ list: [{ name: 'value1' }], - location: { - min: 13, - max: 16, - }, - type: 'argument_value', + type: SUGGESTION_TYPE.ARGUMENT_VALUE, }); }); }); diff --git a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts b/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts index 5aa05fb16466b..d7818680c9543 100644 --- a/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts +++ b/src/legacy/core_plugins/vis_type_timelion/public/components/timelion_expression_input_helpers.ts @@ -18,18 +18,17 @@ */ import { get, startsWith } from 'lodash'; -import PEG from 'pegjs'; import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'; +import { i18n } from '@kbn/i18n'; +import { Parser } from 'pegjs'; + // @ts-ignore -import grammar from 'raw-loader!../chain.peg'; +import { parse } from '../_generated_/chain'; -import { i18n } from '@kbn/i18n'; import { ITimelionFunction, TimelionFunctionArgs } from '../../common/types'; import { ArgValueSuggestions, FunctionArg, Location } from '../helpers/arg_value_suggestions'; -const Parser = PEG.generate(grammar); - export enum SUGGESTION_TYPE { ARGUMENTS = 'arguments', ARGUMENT_VALUE = 'argument_value', @@ -57,7 +56,7 @@ function getArgumentsHelp( } async function extractSuggestionsFromParsedResult( - result: ReturnType, + result: ReturnType, cursorPosition: number, functionList: ITimelionFunction[], argValueSuggestions: ArgValueSuggestions @@ -141,7 +140,7 @@ export async function suggest( argValueSuggestions: ArgValueSuggestions ) { try { - const result = await Parser.parse(expression); + const result = await parse(expression); return await extractSuggestionsFromParsedResult( result, diff --git a/tasks/config/peg.js b/tasks/config/peg.js index 0c57c6139d298..1d8667840faba 100644 --- a/tasks/config/peg.js +++ b/tasks/config/peg.js @@ -25,4 +25,8 @@ module.exports = { allowedStartRules: ['start', 'Literal'], }, }, + timelion_chain: { + src: 'src/legacy/core_plugins/vis_type_timelion/public/chain.peg', + dest: 'src/legacy/core_plugins/vis_type_timelion/public/_generated_/chain.js', + }, }; From be31198d21c1693c6d67a4cc47726325ca192a45 Mon Sep 17 00:00:00 2001 From: Aleh Zasypkin Date: Tue, 21 Jan 2020 15:55:54 +0100 Subject: [PATCH 36/39] Re-enable Kerberos + anonymous access test. (#55377) --- .../kerberos_api_integration/apis/security/kerberos_login.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts index bd35f21d8f428..0346da334d2f2 100644 --- a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts +++ b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts @@ -344,8 +344,7 @@ export default function({ getService }: FtrProviderContext) { }); }); - // FAILING: https://github.com/elastic/kibana/issues/52969 - describe.skip('API access with missing access token document or expired refresh token.', () => { + describe('API access with missing access token document or expired refresh token.', () => { let sessionCookie: Cookie; beforeEach(async () => { From ce286f543e4bd4d72ec7ffefac3a410cac2eaf0f Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 21 Jan 2020 15:25:58 +0000 Subject: [PATCH 37/39] [ML] Adding missing job groups to recognizer wizard (#55392) --- .../application/jobs/new_job/recognize/page.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx index 141ed5d1bbb8f..c4a96d9e373c8 100644 --- a/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx +++ b/x-pack/legacy/plugins/ml/public/application/jobs/new_job/recognize/page.tsx @@ -77,6 +77,7 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { const [kibanaObjects, setKibanaObjects] = useState({}); const [saveState, setSaveState] = useState(SAVE_STATE.NOT_SAVED); const [resultsUrl, setResultsUrl] = useState(''); + const [existingGroups, setExistingGroups] = useState(existingGroupIds); // #endregion const { @@ -109,6 +110,10 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { setKibanaObjects(kibanaObjectsResult); setSaveState(SAVE_STATE.NOT_SAVED); + + // mix existing groups from the server with the groups used across all jobs in the module. + const moduleGroups = [...response.jobs.map(j => j.config.groups || [])].flat(); + setExistingGroups([...new Set([...existingGroups, ...moduleGroups])]); } catch (e) { // eslint-disable-next-line no-console console.error(e); @@ -222,6 +227,12 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { ...jobOverrides, [job.job_id as string]: job, }); + if (job.groups !== undefined) { + // add newly added jobs to the list of existing groups + // for use when editing other jobs in the module + const groups = [...new Set([...existingGroups, ...job.groups])]; + setExistingGroups(groups); + } }; const isFormVisible = [SAVE_STATE.NOT_SAVED, SAVE_STATE.SAVING].includes(saveState); @@ -304,7 +315,7 @@ export const Page: FC = ({ moduleId, existingGroupIds }) => { jobs={jobs} jobPrefix={jobPrefix} saveState={saveState} - existingGroupIds={existingGroupIds} + existingGroupIds={existingGroups} jobOverrides={jobOverrides} onJobOverridesChange={onJobOverridesChange} /> From 2bf111c50fc211454a6f658b488323e22a8ad4e3 Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Tue, 21 Jan 2020 10:58:57 -0500 Subject: [PATCH 38/39] [Uptime] Fix flaky uptime overview page test (#54767) * Fix flaky uptime overview page test. * Increase timeout for url checks. * Prefer standard `retry.try` to custom retry implementation. * Remove unneeded symbol. * Remove unnecessary type annotation. Co-authored-by: Elastic Machine --- x-pack/test/functional/apps/uptime/overview.ts | 9 +++------ x-pack/test/functional/page_objects/uptime_page.ts | 8 ++++++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/x-pack/test/functional/apps/uptime/overview.ts b/x-pack/test/functional/apps/uptime/overview.ts index ba701da6e517d..2ef6a381a6a30 100644 --- a/x-pack/test/functional/apps/uptime/overview.ts +++ b/x-pack/test/functional/apps/uptime/overview.ts @@ -50,13 +50,11 @@ export default ({ getPageObjects }: FtrProviderContext) => { ]); }); - // flakey see https://github.com/elastic/kibana/issues/54527 - it.skip('pagination is cleared when filter criteria changes', async () => { + it('pagination is cleared when filter criteria changes', async () => { await pageObjects.uptime.goToUptimePageAndSetDateRange(DEFAULT_DATE_START, DEFAULT_DATE_END); await pageObjects.uptime.changePage('next'); // there should now be pagination data in the URL - const contains = await pageObjects.uptime.pageUrlContains('pagination'); - expect(contains).to.be(true); + await pageObjects.uptime.pageUrlContains('pagination'); await pageObjects.uptime.pageHasExpectedIds([ '0010-down', '0011-up', @@ -71,8 +69,7 @@ export default ({ getPageObjects }: FtrProviderContext) => { ]); await pageObjects.uptime.setStatusFilter('up'); // ensure that pagination is removed from the URL - const doesNotContain = await pageObjects.uptime.pageUrlContains('pagination'); - expect(doesNotContain).to.be(false); + await pageObjects.uptime.pageUrlContains('pagination', false); await pageObjects.uptime.pageHasExpectedIds([ '0000-intermittent', '0001-up', diff --git a/x-pack/test/functional/page_objects/uptime_page.ts b/x-pack/test/functional/page_objects/uptime_page.ts index f04f96148583f..2ae0ea38c957b 100644 --- a/x-pack/test/functional/page_objects/uptime_page.ts +++ b/x-pack/test/functional/page_objects/uptime_page.ts @@ -4,11 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import expect from '@kbn/expect'; import { FtrProviderContext } from '../ftr_provider_context'; export function UptimePageProvider({ getPageObjects, getService }: FtrProviderContext) { const pageObjects = getPageObjects(['common', 'timePicker']); const uptimeService = getService('uptime'); + const retry = getService('retry'); return new (class UptimePage { public async goToUptimePageAndSetDateRange( @@ -51,8 +53,10 @@ export function UptimePageProvider({ getPageObjects, getService }: FtrProviderCo await Promise.all(monitorIdsToCheck.map(id => uptimeService.monitorPageLinkExists(id))); } - public async pageUrlContains(value: string) { - return await uptimeService.urlContains(value); + public async pageUrlContains(value: string, expected: boolean = true) { + retry.try(async () => { + expect(await uptimeService.urlContains(value)).to.eql(expected); + }); } public async changePage(direction: 'next' | 'prev') { From b0af1bf95c26a03135a382c0c758b54782e514fa Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Tue, 21 Jan 2020 09:19:08 -0700 Subject: [PATCH 39/39] Clear core loading indicator just before UI is rendered (#55242) Co-authored-by: Elastic Machine --- src/core/public/core_system.test.ts | 7 +++---- src/core/public/core_system.ts | 15 ++++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/core/public/core_system.test.ts b/src/core/public/core_system.test.ts index 1ee41fe64418e..94fa74f4bd861 100644 --- a/src/core/public/core_system.test.ts +++ b/src/core/public/core_system.test.ts @@ -429,15 +429,14 @@ describe('Notifications targetDomElement', () => { rootDomElement, }); - let targetDomElementParentInStart: HTMLElement | null; + let targetDomElementInStart: HTMLElement | null; MockNotificationsService.start.mockImplementation(({ targetDomElement }): any => { - expect(targetDomElement.parentElement).not.toBeNull(); - targetDomElementParentInStart = targetDomElement.parentElement; + targetDomElementInStart = targetDomElement; }); // Starting the core system should pass the targetDomElement as a child of the rootDomElement await core.setup(); await core.start(); - expect(targetDomElementParentInStart!).toBe(rootDomElement); + expect(targetDomElementInStart!.parentElement).toBe(rootDomElement); }); }); diff --git a/src/core/public/core_system.ts b/src/core/public/core_system.ts index 97bd49416f705..be69cacdd271a 100644 --- a/src/core/public/core_system.ts +++ b/src/core/public/core_system.ts @@ -222,13 +222,6 @@ export class CoreSystem { const notificationsTargetDomElement = document.createElement('div'); const overlayTargetDomElement = document.createElement('div'); - // ensure the rootDomElement is empty - this.rootDomElement.textContent = ''; - this.rootDomElement.classList.add('coreSystemRootDomElement'); - this.rootDomElement.appendChild(coreUiTargetDomElement); - this.rootDomElement.appendChild(notificationsTargetDomElement); - this.rootDomElement.appendChild(overlayTargetDomElement); - const overlays = this.overlay.start({ i18n, targetDomElement: overlayTargetDomElement, @@ -276,6 +269,14 @@ export class CoreSystem { }; const plugins = await this.plugins.start(core); + + // ensure the rootDomElement is empty + this.rootDomElement.textContent = ''; + this.rootDomElement.classList.add('coreSystemRootDomElement'); + this.rootDomElement.appendChild(coreUiTargetDomElement); + this.rootDomElement.appendChild(notificationsTargetDomElement); + this.rootDomElement.appendChild(overlayTargetDomElement); + const rendering = this.rendering.start({ application, chrome,