[uiSettings] support overriding uiSettings from the config file (#21628)

This PR implements the `uiSettings.overrides` setting which [when stored in kibana.yml or passed as config args when starting Kibana] allows forcing some uiSettings to always have a specific value. This setting accepts a map of uiSetting keys to values that will always be used to override whatever is stored in the config saved object.

![image](https://user-images.githubusercontent.com/1329312/43619094-feded1ae-9680-11e8-9ec3-c12d4d949c46.png)

When users view the settings in the advanced settings UI they are disabled and describe why they can't be changed.

![image](https://user-images.githubusercontent.com/1329312/43618938-2cdee0f4-9680-11e8-9ed6-f384d4ee78f6.png)

Attempting to change these values from the uiSettings client/service/api is also prevented, causing a 400 error to be thrown and/or sent as the response.
This commit is contained in:
Spencer 2018-08-14 21:52:35 -07:00 committed by GitHub
parent 53a69f6a29
commit e49d5f3b10
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1506 additions and 62 deletions

View file

@ -42,8 +42,7 @@ export async function rebuildCache(settings, logger) {
'--env.name=production',
'--optimize.useBundleCache=false',
'--server.autoListen=false',
'--plugins.initialize=false',
'--uiSettings.enabled=false'
'--plugins.initialize=false'
];
const proc = execa(process.execPath, kibanaArgs, {

View file

@ -76,7 +76,7 @@ exports[`AdvancedSettings should render normally 1`] = `
categoryCounts={
Object {
"elasticsearch": 2,
"general": 7,
"general": 11,
}
}
clear={[Function]}
@ -96,6 +96,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"description": "Description for Test array setting",
"displayName": "Test array setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:array:setting",
"options": undefined,
"readonly": false,
@ -111,6 +112,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"description": "Description for Test boolean setting",
"displayName": "Test boolean setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:boolean:setting",
"options": undefined,
"readonly": false,
@ -128,6 +130,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"description": "Description for Test custom string setting",
"displayName": "Test custom string setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:customstring:setting",
"options": undefined,
"readonly": false,
@ -143,12 +146,83 @@ exports[`AdvancedSettings should render normally 1`] = `
"description": "Description for Test image setting",
"displayName": "Test image setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:image:setting",
"options": undefined,
"readonly": false,
"type": "image",
"value": undefined,
},
Object {
"ariaName": "test is overridden json",
"category": Array [
"general",
],
"defVal": "{
\\"foo\\": \\"bar\\"
}",
"description": "Description for overridden json",
"displayName": "An overridden json",
"isCustom": undefined,
"isOverridden": true,
"name": "test:isOverridden:json",
"options": undefined,
"readonly": false,
"type": "json",
"value": undefined,
},
Object {
"ariaName": "test is overridden number",
"category": Array [
"general",
],
"defVal": 1234,
"description": "Description for overridden number",
"displayName": "An overridden number",
"isCustom": undefined,
"isOverridden": true,
"name": "test:isOverridden:number",
"options": undefined,
"readonly": false,
"type": "number",
"value": undefined,
},
Object {
"ariaName": "test is overridden select",
"category": Array [
"general",
],
"defVal": "orange",
"description": "Description for overridden select setting",
"displayName": "Test overridden select setting",
"isCustom": undefined,
"isOverridden": true,
"name": "test:isOverridden:select",
"options": Array [
"apple",
"orange",
"banana",
],
"readonly": false,
"type": "select",
"value": undefined,
},
Object {
"ariaName": "test is overridden string",
"category": Array [
"general",
],
"defVal": "foo",
"description": "Description for overridden string",
"displayName": "An overridden string",
"isCustom": undefined,
"isOverridden": true,
"name": "test:isOverridden:string",
"options": undefined,
"readonly": false,
"type": "string",
"value": undefined,
},
Object {
"ariaName": "test json setting",
"category": Array [
@ -158,6 +232,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"description": "Description for Test json setting",
"displayName": "Test json setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:json:setting",
"options": undefined,
"readonly": false,
@ -173,6 +248,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"description": "Description for Test markdown setting",
"displayName": "Test markdown setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:markdown:setting",
"options": undefined,
"readonly": false,
@ -188,6 +264,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"description": "Description for Test number setting",
"displayName": "Test number setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:number:setting",
"options": undefined,
"readonly": false,
@ -203,6 +280,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"description": "Description for Test select setting",
"displayName": "Test select setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:select:setting",
"options": Array [
"apple",
@ -222,6 +300,7 @@ exports[`AdvancedSettings should render normally 1`] = `
"description": "Description for Test string setting",
"displayName": "Test string setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:string:setting",
"options": undefined,
"readonly": false,
@ -329,7 +408,7 @@ exports[`AdvancedSettings should render specific setting if given setting key 1`
categoryCounts={
Object {
"elasticsearch": 2,
"general": 7,
"general": 11,
}
}
clear={[Function]}
@ -347,6 +426,7 @@ exports[`AdvancedSettings should render specific setting if given setting key 1`
"description": "Description for Test string setting",
"displayName": "Test string setting",
"isCustom": undefined,
"isOverridden": false,
"name": "test:string:setting",
"options": undefined,
"readonly": false,

View file

@ -90,6 +90,7 @@ export class AdvancedSettings extends Component {
name: setting[0],
value: setting[1].userValue,
isCustom: config.isCustom(setting[0]),
isOverridden: config.isOverridden(setting[0]),
});
})
.filter((c) => !c.readonly)

View file

@ -19,6 +19,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import dedent from 'dedent';
import { AdvancedSettings } from './advanced_settings';
@ -44,6 +45,7 @@ const config = {
set: () => {},
remove: () => {},
isCustom: (setting) => setting.isCustom,
isOverridden: (key) => Boolean(config.getAll()[key].isOverridden),
getAll: () => {
return {
'test:array:setting': {
@ -109,6 +111,39 @@ const config = {
type: 'string',
isCustom: true,
},
'test:isOverridden:string': {
isOverridden: true,
value: 'foo',
name: 'An overridden string',
description: 'Description for overridden string',
type: 'string',
},
'test:isOverridden:number': {
isOverridden: true,
value: 1234,
name: 'An overridden number',
description: 'Description for overridden number',
type: 'number',
},
'test:isOverridden:json': {
isOverridden: true,
value: dedent`
{
"foo": "bar"
}
`,
name: 'An overridden json',
description: 'Description for overridden json',
type: 'json',
},
'test:isOverridden:select': {
isOverridden: true,
value: 'orange',
name: 'Test overridden select setting',
description: 'Description for overridden select setting',
type: 'select',
options: ['apple', 'orange', 'banana'],
},
};
}
};

View file

@ -1,5 +1,106 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Field for array setting should render as read only with help text if overridden 1`] = `
<EuiFlexGroup
alignItems="stretch"
className="advancedSettings__field"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiDescribedFormGroup
className="advancedSettings__field__wrapper"
description={
<UNDEFINED>
<div
dangerouslySetInnerHTML={
Object {
"__html": "Description for Array test setting",
}
}
/>
<UNDEFINED>
<EuiSpacer
size="s"
/>
<EuiText
grow={true}
size="xs"
>
<UNDEFINED>
Default:
<EuiCode>
default_value
</EuiCode>
</UNDEFINED>
</EuiText>
</UNDEFINED>
</UNDEFINED>
}
fullWidth={false}
gutterSize="l"
idAria="array:test:setting-aria"
title={
<h3>
Array test setting
</h3>
}
titleSize="xs"
>
<EuiFormRow
describedByIds={
Array [
"array:test:setting-aria",
]
}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<EuiText
grow={true}
size="xs"
>
This setting is overriden by the Kibana server and can not be changed.
</EuiText>
}
isInvalid={false}
label={
<span
aria-label="array test setting"
>
array:test:setting
</span>
}
>
<EuiFieldText
compressed={false}
data-test-subj="advancedSetting-editField-array:test:setting"
disabled={true}
fullWidth={false}
isLoading={false}
onChange={[Function]}
onKeyDown={[Function]}
value="user, value"
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
/>
</EuiFlexGroup>
`;
exports[`Field for array setting should render custom setting icon if it is custom 1`] = `
<EuiFlexGroup
alignItems="stretch"
@ -270,6 +371,105 @@ exports[`Field for array setting should render user value if there is user value
</EuiFlexGroup>
`;
exports[`Field for boolean setting should render as read only with help text if overridden 1`] = `
<EuiFlexGroup
alignItems="stretch"
className="advancedSettings__field"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiDescribedFormGroup
className="advancedSettings__field__wrapper"
description={
<UNDEFINED>
<div
dangerouslySetInnerHTML={
Object {
"__html": "Description for Boolean test setting",
}
}
/>
<UNDEFINED>
<EuiSpacer
size="s"
/>
<EuiText
grow={true}
size="xs"
>
<UNDEFINED>
Default:
<EuiCode>
true
</EuiCode>
</UNDEFINED>
</EuiText>
</UNDEFINED>
</UNDEFINED>
}
fullWidth={false}
gutterSize="l"
idAria="boolean:test:setting-aria"
title={
<h3>
Boolean test setting
</h3>
}
titleSize="xs"
>
<EuiFormRow
describedByIds={
Array [
"boolean:test:setting-aria",
]
}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<EuiText
grow={true}
size="xs"
>
This setting is overriden by the Kibana server and can not be changed.
</EuiText>
}
isInvalid={false}
label={
<span
aria-label="boolean test setting"
>
boolean:test:setting
</span>
}
>
<EuiSwitch
checked={false}
data-test-subj="advancedSetting-editField-boolean:test:setting"
disabled={true}
label="Off"
onChange={[Function]}
onKeyDown={[Function]}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
/>
</EuiFlexGroup>
`;
exports[`Field for boolean setting should render custom setting icon if it is custom 1`] = `
<EuiFlexGroup
alignItems="stretch"
@ -534,6 +734,104 @@ exports[`Field for boolean setting should render user value if there is user val
</EuiFlexGroup>
`;
exports[`Field for image setting should render as read only with help text if overridden 1`] = `
<EuiFlexGroup
alignItems="stretch"
className="advancedSettings__field"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiDescribedFormGroup
className="advancedSettings__field__wrapper"
description={
<UNDEFINED>
<div
dangerouslySetInnerHTML={
Object {
"__html": "Description for Image test setting",
}
}
/>
<UNDEFINED>
<EuiSpacer
size="s"
/>
<EuiText
grow={true}
size="xs"
>
<UNDEFINED>
Default:
<EuiCode>
null
</EuiCode>
</UNDEFINED>
</EuiText>
</UNDEFINED>
</UNDEFINED>
}
fullWidth={false}
gutterSize="l"
idAria="image:test:setting-aria"
title={
<h3>
Image test setting
</h3>
}
titleSize="xs"
>
<EuiFormRow
describedByIds={
Array [
"image:test:setting-aria",
]
}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<EuiText
grow={true}
size="xs"
>
This setting is overriden by the Kibana server and can not be changed.
</EuiText>
}
isInvalid={false}
label={
<span
aria-label="image test setting"
>
image:test:setting
</span>
}
>
<EuiImage
allowFullScreen={true}
alt="image:test:setting"
fullScreenIconColor="light"
size="original"
url="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
/>
</EuiFlexGroup>
`;
exports[`Field for image setting should render custom setting icon if it is custom 1`] = `
<EuiFlexGroup
alignItems="stretch"
@ -810,6 +1108,127 @@ exports[`Field for image setting should render user value if there is user value
</EuiFlexGroup>
`;
exports[`Field for json setting should render as read only with help text if overridden 1`] = `
<EuiFlexGroup
alignItems="stretch"
className="advancedSettings__field"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiDescribedFormGroup
className="advancedSettings__field__wrapper"
description={
<UNDEFINED>
<div
dangerouslySetInnerHTML={
Object {
"__html": "Description for Json test setting",
}
}
/>
<UNDEFINED>
<EuiSpacer
size="s"
/>
<EuiText
grow={true}
size="xs"
>
<UNDEFINED>
Default:
<EuiCodeBlock
language="json"
overflowHeight={null}
paddingSize="s"
>
{}
</EuiCodeBlock>
</UNDEFINED>
</EuiText>
</UNDEFINED>
</UNDEFINED>
}
fullWidth={false}
gutterSize="l"
idAria="json:test:setting-aria"
title={
<h3>
Json test setting
</h3>
}
titleSize="xs"
>
<EuiFormRow
describedByIds={
Array [
"json:test:setting-aria",
]
}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<EuiText
grow={true}
size="xs"
>
This setting is overriden by the Kibana server and can not be changed.
</EuiText>
}
isInvalid={false}
label={
<span
aria-label="json test setting"
>
json:test:setting
</span>
}
>
<div
data-test-subj="advancedSetting-editField-json:test:setting"
>
<EuiCodeEditor
editorProps={
Object {
"$blockScrolling": Infinity,
}
}
height="auto"
isReadOnly={true}
maxLines={30}
minLines={6}
mode="json"
onChange={[Function]}
setOptions={
Object {
"showLineNumbers": false,
"tabSize": 2,
}
}
theme="textmate"
value="{\\"hello\\": \\"world\\"}"
width="100%"
/>
</div>
</EuiFormRow>
</EuiDescribedFormGroup>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
/>
</EuiFlexGroup>
`;
exports[`Field for json setting should render custom setting icon if it is custom 1`] = `
<EuiFlexGroup
alignItems="stretch"
@ -883,6 +1302,7 @@ exports[`Field for json setting should render custom setting icon if it is custo
}
}
height="auto"
isReadOnly={false}
maxLines={30}
minLines={6}
mode="json"
@ -1011,6 +1431,7 @@ exports[`Field for json setting should render default value if there is no user
}
}
height="auto"
isReadOnly={false}
maxLines={30}
minLines={6}
mode="json"
@ -1139,6 +1560,7 @@ exports[`Field for json setting should render user value if there is user value
}
}
height="auto"
isReadOnly={false}
maxLines={30}
minLines={6}
mode="json"
@ -1164,6 +1586,123 @@ exports[`Field for json setting should render user value if there is user value
</EuiFlexGroup>
`;
exports[`Field for markdown setting should render as read only with help text if overridden 1`] = `
<EuiFlexGroup
alignItems="stretch"
className="advancedSettings__field"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiDescribedFormGroup
className="advancedSettings__field__wrapper"
description={
<UNDEFINED>
<div
dangerouslySetInnerHTML={
Object {
"__html": "Description for Markdown test setting",
}
}
/>
<UNDEFINED>
<EuiSpacer
size="s"
/>
<EuiText
grow={true}
size="xs"
>
<UNDEFINED>
Default:
<EuiCode>
null
</EuiCode>
</UNDEFINED>
</EuiText>
</UNDEFINED>
</UNDEFINED>
}
fullWidth={false}
gutterSize="l"
idAria="markdown:test:setting-aria"
title={
<h3>
Markdown test setting
</h3>
}
titleSize="xs"
>
<EuiFormRow
describedByIds={
Array [
"markdown:test:setting-aria",
]
}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<EuiText
grow={true}
size="xs"
>
This setting is overriden by the Kibana server and can not be changed.
</EuiText>
}
isInvalid={false}
label={
<span
aria-label="markdown test setting"
>
markdown:test:setting
</span>
}
>
<div
data-test-subj="advancedSetting-editField-markdown:test:setting"
>
<EuiCodeEditor
editorProps={
Object {
"$blockScrolling": Infinity,
}
}
height="auto"
isReadOnly={true}
maxLines={30}
minLines={6}
mode="markdown"
onChange={[Function]}
setOptions={
Object {
"showLineNumbers": false,
"tabSize": 2,
}
}
theme="textmate"
value="**bold**"
width="100%"
/>
</div>
</EuiFormRow>
</EuiDescribedFormGroup>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
/>
</EuiFlexGroup>
`;
exports[`Field for markdown setting should render custom setting icon if it is custom 1`] = `
<EuiFlexGroup
alignItems="stretch"
@ -1237,6 +1776,7 @@ exports[`Field for markdown setting should render custom setting icon if it is c
}
}
height="auto"
isReadOnly={false}
maxLines={30}
minLines={6}
mode="markdown"
@ -1330,6 +1870,7 @@ exports[`Field for markdown setting should render default value if there is no u
}
}
height="auto"
isReadOnly={false}
maxLines={30}
minLines={6}
mode="markdown"
@ -1454,6 +1995,7 @@ exports[`Field for markdown setting should render user value if there is user va
}
}
height="auto"
isReadOnly={false}
maxLines={30}
minLines={6}
mode="markdown"
@ -1479,6 +2021,107 @@ exports[`Field for markdown setting should render user value if there is user va
</EuiFlexGroup>
`;
exports[`Field for number setting should render as read only with help text if overridden 1`] = `
<EuiFlexGroup
alignItems="stretch"
className="advancedSettings__field"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiDescribedFormGroup
className="advancedSettings__field__wrapper"
description={
<UNDEFINED>
<div
dangerouslySetInnerHTML={
Object {
"__html": "Description for Number test setting",
}
}
/>
<UNDEFINED>
<EuiSpacer
size="s"
/>
<EuiText
grow={true}
size="xs"
>
<UNDEFINED>
Default:
<EuiCode>
5
</EuiCode>
</UNDEFINED>
</EuiText>
</UNDEFINED>
</UNDEFINED>
}
fullWidth={false}
gutterSize="l"
idAria="number:test:setting-aria"
title={
<h3>
Number test setting
</h3>
}
titleSize="xs"
>
<EuiFormRow
describedByIds={
Array [
"number:test:setting-aria",
]
}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<EuiText
grow={true}
size="xs"
>
This setting is overriden by the Kibana server and can not be changed.
</EuiText>
}
isInvalid={false}
label={
<span
aria-label="number test setting"
>
number:test:setting
</span>
}
>
<EuiFieldNumber
compressed={false}
data-test-subj="advancedSetting-editField-number:test:setting"
disabled={true}
fullWidth={false}
isLoading={false}
onChange={[Function]}
onKeyDown={[Function]}
value={10}
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
/>
</EuiFlexGroup>
`;
exports[`Field for number setting should render custom setting icon if it is custom 1`] = `
<EuiFlexGroup
alignItems="stretch"
@ -1749,6 +2392,124 @@ exports[`Field for number setting should render user value if there is user valu
</EuiFlexGroup>
`;
exports[`Field for select setting should render as read only with help text if overridden 1`] = `
<EuiFlexGroup
alignItems="stretch"
className="advancedSettings__field"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiDescribedFormGroup
className="advancedSettings__field__wrapper"
description={
<UNDEFINED>
<div
dangerouslySetInnerHTML={
Object {
"__html": "Description for Select test setting",
}
}
/>
<UNDEFINED>
<EuiSpacer
size="s"
/>
<EuiText
grow={true}
size="xs"
>
<UNDEFINED>
Default:
<EuiCode>
orange
</EuiCode>
</UNDEFINED>
</EuiText>
</UNDEFINED>
</UNDEFINED>
}
fullWidth={false}
gutterSize="l"
idAria="select:test:setting-aria"
title={
<h3>
Select test setting
</h3>
}
titleSize="xs"
>
<EuiFormRow
describedByIds={
Array [
"select:test:setting-aria",
]
}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<EuiText
grow={true}
size="xs"
>
This setting is overriden by the Kibana server and can not be changed.
</EuiText>
}
isInvalid={false}
label={
<span
aria-label="select test setting"
>
select:test:setting
</span>
}
>
<EuiSelect
compressed={false}
data-test-subj="advancedSetting-editField-select:test:setting"
disabled={true}
fullWidth={false}
hasNoInitialSelection={false}
isLoading={false}
onChange={[Function]}
onKeyDown={[Function]}
options={
Array [
Object {
"text": "apple",
"value": "apple",
},
Object {
"text": "orange",
"value": "orange",
},
Object {
"text": "banana",
"value": "banana",
},
]
}
value="banana"
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
/>
</EuiFlexGroup>
`;
exports[`Field for select setting should render custom setting icon if it is custom 1`] = `
<EuiFlexGroup
alignItems="stretch"
@ -2070,6 +2831,107 @@ exports[`Field for select setting should render user value if there is user valu
</EuiFlexGroup>
`;
exports[`Field for string setting should render as read only with help text if overridden 1`] = `
<EuiFlexGroup
alignItems="stretch"
className="advancedSettings__field"
component="div"
direction="row"
gutterSize="l"
justifyContent="flexStart"
responsive={true}
wrap={false}
>
<EuiFlexItem
component="div"
grow={false}
>
<EuiDescribedFormGroup
className="advancedSettings__field__wrapper"
description={
<UNDEFINED>
<div
dangerouslySetInnerHTML={
Object {
"__html": "Description for String test setting",
}
}
/>
<UNDEFINED>
<EuiSpacer
size="s"
/>
<EuiText
grow={true}
size="xs"
>
<UNDEFINED>
Default:
<EuiCode>
null
</EuiCode>
</UNDEFINED>
</EuiText>
</UNDEFINED>
</UNDEFINED>
}
fullWidth={false}
gutterSize="l"
idAria="string:test:setting-aria"
title={
<h3>
String test setting
</h3>
}
titleSize="xs"
>
<EuiFormRow
describedByIds={
Array [
"string:test:setting-aria",
]
}
error={null}
fullWidth={false}
hasEmptyLabelSpace={false}
helpText={
<EuiText
grow={true}
size="xs"
>
This setting is overriden by the Kibana server and can not be changed.
</EuiText>
}
isInvalid={false}
label={
<span
aria-label="string test setting"
>
string:test:setting
</span>
}
>
<EuiFieldText
compressed={false}
data-test-subj="advancedSetting-editField-string:test:setting"
disabled={true}
fullWidth={false}
isLoading={false}
onChange={[Function]}
onKeyDown={[Function]}
value="foo"
/>
</EuiFormRow>
</EuiDescribedFormGroup>
</EuiFlexItem>
<EuiFlexItem
component="div"
grow={false}
/>
</EuiFlexGroup>
`;
exports[`Field for string setting should render custom setting icon if it is custom 1`] = `
<EuiFlexGroup
alignItems="stretch"

View file

@ -319,7 +319,7 @@ export class Field extends PureComponent {
renderField(setting) {
const { loading, changeImage, unsavedValue } = this.state;
const { name, value, type, options } = setting;
const { name, value, type, options, isOverridden } = setting;
switch(type) {
case 'boolean':
@ -328,7 +328,7 @@ export class Field extends PureComponent {
label={!!unsavedValue ? 'On' : 'Off'}
checked={!!unsavedValue}
onChange={this.onFieldChange}
disabled={loading}
disabled={loading || isOverridden}
onKeyDown={this.onFieldKeyDown}
data-test-subj={`advancedSetting-editField-${name}`}
/>
@ -346,6 +346,7 @@ export class Field extends PureComponent {
height="auto"
minLines={6}
maxLines={30}
isReadOnly={isOverridden}
setOptions={{
showLineNumbers: false,
tabSize: 2,
@ -369,7 +370,7 @@ export class Field extends PureComponent {
} else {
return (
<EuiFilePicker
disabled={loading}
disabled={loading || isOverridden}
onChange={this.onImageChange}
accept=".jpg,.jpeg,.png"
ref={(input) => { this.changeImageForm = input; }}
@ -387,7 +388,7 @@ export class Field extends PureComponent {
})}
onChange={this.onFieldChange}
isLoading={loading}
disabled={loading}
disabled={loading || isOverridden}
onKeyDown={this.onFieldKeyDown}
data-test-subj={`advancedSetting-editField-${name}`}
/>
@ -398,7 +399,7 @@ export class Field extends PureComponent {
value={unsavedValue}
onChange={this.onFieldChange}
isLoading={loading}
disabled={loading}
disabled={loading || isOverridden}
onKeyDown={this.onFieldKeyDown}
data-test-subj={`advancedSetting-editField-${name}`}
/>
@ -409,7 +410,7 @@ export class Field extends PureComponent {
value={unsavedValue}
onChange={this.onFieldChange}
isLoading={loading}
disabled={loading}
disabled={loading || isOverridden}
onKeyDown={this.onFieldKeyDown}
data-test-subj={`advancedSetting-editField-${name}`}
/>
@ -426,6 +427,14 @@ export class Field extends PureComponent {
}
renderHelpText(setting) {
if (setting.isOverridden) {
return (
<EuiText size="xs">
This setting is overriden by the Kibana server and can not be changed.
</EuiText>
);
}
const defaultLink = this.renderResetToDefaultLink(setting);
const imageLink = this.renderChangeImageLink(setting);
@ -538,9 +547,12 @@ export class Field extends PureComponent {
renderActions(setting) {
const { ariaName, name } = setting;
const { loading, isInvalid, changeImage, savedValue, unsavedValue } = this.state;
if(savedValue === unsavedValue && !changeImage) {
const isDisabled = loading || setting.isOverridden;
if (savedValue === unsavedValue && !changeImage) {
return;
}
return (
<EuiFormRow className="advancedSettings__field__actions" hasEmptyLabelSpace>
<EuiFlexGroup>
@ -549,7 +561,7 @@ export class Field extends PureComponent {
fill
aria-label={`Save ${ariaName}`}
onClick={this.saveEdit}
disabled={loading || isInvalid}
disabled={isDisabled || isInvalid}
data-test-subj={`advancedSetting-saveEditField-${name}`}
>
Save
@ -559,7 +571,7 @@ export class Field extends PureComponent {
<EuiButtonEmpty
aria-label={`Cancel editing ${ariaName}`}
onClick={() => changeImage ? this.cancelChangeImage() : this.cancelEdit()}
disabled={loading}
disabled={isDisabled}
data-test-subj={`advancedSetting-cancelEditField-${name}`}
>
Cancel

View file

@ -42,6 +42,7 @@ const settings = {
value: undefined,
defVal: ['default_value'],
isCustom: false,
isOverridden: false,
options: null,
},
boolean: {
@ -53,6 +54,7 @@ const settings = {
value: undefined,
defVal: true,
isCustom: false,
isOverridden: false,
options: null,
},
image: {
@ -64,6 +66,7 @@ const settings = {
value: undefined,
defVal: null,
isCustom: false,
isOverridden: false,
options: {
maxSize: {
length: 1000,
@ -81,6 +84,7 @@ const settings = {
value: '{"foo": "bar"}',
defVal: '{}',
isCustom: false,
isOverridden: false,
options: null,
},
markdown: {
@ -92,6 +96,7 @@ const settings = {
value: undefined,
defVal: '',
isCustom: false,
isOverridden: false,
options: null,
},
number: {
@ -103,6 +108,7 @@ const settings = {
value: undefined,
defVal: 5,
isCustom: false,
isOverridden: false,
options: null,
},
select: {
@ -114,6 +120,7 @@ const settings = {
value: undefined,
defVal: 'orange',
isCustom: false,
isOverridden: false,
options: ['apple', 'orange', 'banana'],
},
string: {
@ -125,6 +132,7 @@ const settings = {
value: undefined,
defVal: null,
isCustom: false,
isOverridden: false,
options: null,
},
};
@ -158,6 +166,22 @@ describe('Field', () => {
expect(component).toMatchSnapshot();
});
it('should render as read only with help text if overridden', async () => {
const component = shallow(
<Field
setting={{
...setting,
value: userValues[type],
isOverridden: true,
}}
save={save}
clear={clear}
/>
);
expect(component).toMatchSnapshot();
});
it('should render user value if there is user value is set', async () => {
const component = shallow(
<Field

View file

@ -27,7 +27,7 @@ import { DEFAULT_CATEGORY } from './default_category';
* @param {object} current value of setting
* @returns {object} the editable config object
*/
export function toEditableConfig({ def, name, value, isCustom }) {
export function toEditableConfig({ def, name, value, isCustom, isOverridden }) {
if (!def) {
def = {};
}
@ -38,6 +38,7 @@ export function toEditableConfig({ def, name, value, isCustom }) {
value,
category: def.category && def.category.length ? def.category : [DEFAULT_CATEGORY],
isCustom,
isOverridden,
readonly: !!def.readonly,
defVal: def.value,
type: getValType(def, value),

View file

@ -110,6 +110,10 @@ export default async () => Joi.object({
}).default(),
}).default(),
uiSettings: Joi.object().keys({
overrides: Joi.object().unknown(true).default()
}).default(),
logging: Joi.object().keys({
silent: Joi.boolean().default(false),

View file

@ -38,6 +38,7 @@ module.service(`config`, function ($rootScope, Promise) {
this.isDeclared = (...args) => uiSettings.isDeclared(...args);
this.isDefault = (...args) => uiSettings.isDefault(...args);
this.isCustom = (...args) => uiSettings.isCustom(...args);
this.isOverridden = (...args) => uiSettings.isOverridden(...args);
// modify remove() to use angular Promises
this.remove = (key) => (

View file

@ -34,13 +34,13 @@ import { uiSettingsMixin } from '../ui_settings_mixin';
describe('uiSettingsMixin()', () => {
const sandbox = sinon.createSandbox();
async function setup(options = {}) {
const {
enabled = true
} = options;
async function setup() {
const config = await Config.withDefaultSchema({
uiSettings: { enabled }
uiSettings: {
overrides: {
foo: 'bar'
}
}
});
// maps of decorations passed to `server.decorate()`
@ -111,6 +111,9 @@ describe('uiSettingsMixin()', () => {
sinon.assert.calledOnce(uiSettingsServiceFactory);
sinon.assert.calledWithExactly(uiSettingsServiceFactory, server, {
foo: 'bar',
overrides: {
foo: 'bar'
},
getDefaults: sinon.match.func,
});
});

View file

@ -17,7 +17,6 @@
* under the License.
*/
import { isEqual } from 'lodash';
import expect from 'expect.js';
import { errors as esErrors } from 'elasticsearch';
import Chance from 'chance';
@ -42,6 +41,7 @@ describe('ui settings', () => {
const {
getDefaults,
defaults = {},
overrides,
esDocSource = {},
savedObjectsClient = createObjectsClientStub(TYPE, ID, esDocSource)
} = options;
@ -52,6 +52,7 @@ describe('ui settings', () => {
buildNum: BUILD_NUM,
getDefaults: getDefaults || (() => defaults),
savedObjectsClient,
overrides,
});
const createOrUpgradeSavedConfig = sandbox.stub(createOrUpgradeSavedConfigNS, 'createOrUpgradeSavedConfig');
@ -67,21 +68,6 @@ describe('ui settings', () => {
afterEach(() => sandbox.restore());
describe('overview', () => {
it('has expected api surface', () => {
const { uiSettings } = setup();
expect(uiSettings).to.have.property('get').a('function');
expect(uiSettings).to.have.property('getAll').a('function');
expect(uiSettings).to.have.property('getDefaults').a('function');
expect(uiSettings).to.have.property('getRaw').a('function');
expect(uiSettings).to.have.property('getUserProvided').a('function');
expect(uiSettings).to.have.property('remove').a('function');
expect(uiSettings).to.have.property('removeMany').a('function');
expect(uiSettings).to.have.property('set').a('function');
expect(uiSettings).to.have.property('setMany').a('function');
});
});
describe('#setMany()', () => {
it('returns a promise', () => {
const { uiSettings } = setup();
@ -127,6 +113,23 @@ describe('ui settings', () => {
sinon.assert.calledTwice(savedObjectsClient.update);
sinon.assert.calledOnce(createOrUpgradeSavedConfig);
});
it('throws an error if any key is overridden', async () => {
const { uiSettings } = setup({
overrides: {
foo: 'bar'
}
});
try {
await uiSettings.setMany({
bar: 'box',
foo: 'baz'
});
} catch (error) {
expect(error.message).to.be('Unable to update "foo" because it is overridden');
}
});
});
describe('#set()', () => {
@ -140,6 +143,20 @@ describe('ui settings', () => {
await uiSettings.set('one', 'value');
savedObjectsClient.assertUpdateQuery({ one: 'value' });
});
it('throws an error if the key is overridden', async () => {
const { uiSettings } = setup({
overrides: {
foo: 'bar'
}
});
try {
await uiSettings.set('foo', 'baz');
} catch (error) {
expect(error.message).to.be('Unable to update "foo" because it is overridden');
}
});
});
describe('#remove()', () => {
@ -153,6 +170,20 @@ describe('ui settings', () => {
await uiSettings.remove('one');
savedObjectsClient.assertUpdateQuery({ one: null });
});
it('throws an error if the key is overridden', async () => {
const { uiSettings } = setup({
overrides: {
foo: 'bar'
}
});
try {
await uiSettings.remove('foo');
} catch (error) {
expect(error.message).to.be('Unable to update "foo" because it is overridden');
}
});
});
describe('#removeMany()', () => {
@ -172,6 +203,20 @@ describe('ui settings', () => {
await uiSettings.removeMany(['one', 'two', 'three']);
savedObjectsClient.assertUpdateQuery({ one: null, two: null, three: null });
});
it('throws an error if any key is overridden', async () => {
const { uiSettings } = setup({
overrides: {
foo: 'bar'
}
});
try {
await uiSettings.setMany(['bar', 'foo']);
} catch (error) {
expect(error.message).to.be('Unable to update "foo" because it is overridden');
}
});
});
describe('#getDefaults()', () => {
@ -210,18 +255,25 @@ describe('ui settings', () => {
const esDocSource = { user: 'customized' };
const { uiSettings } = setup({ esDocSource });
const result = await uiSettings.getUserProvided();
expect(isEqual(result, {
user: { userValue: 'customized' }
})).to.equal(true);
expect(result).to.eql({
user: {
userValue: 'customized',
}
});
});
it('ignores null user configuration (because default values)', async () => {
const esDocSource = { user: 'customized', usingDefault: null, something: 'else' };
const { uiSettings } = setup({ esDocSource });
const result = await uiSettings.getUserProvided();
expect(isEqual(result, {
user: { userValue: 'customized' }, something: { userValue: 'else' }
})).to.equal(true);
expect(result).to.eql({
user: {
userValue: 'customized'
},
something: {
userValue: 'else'
}
});
});
it('returns an empty object on 404 responses', async () => {
@ -291,6 +343,27 @@ describe('ui settings', () => {
expect(err).to.be(expectedUnexpectedError);
}
});
it('includes overridden values for overridden keys', async () => {
const esDocSource = {
user: 'customized'
};
const overrides = {
foo: 'bar'
};
const { uiSettings } = setup({ esDocSource, overrides });
expect(await uiSettings.getUserProvided()).to.eql({
user: {
userValue: 'customized',
},
foo: {
userValue: 'bar',
isOverridden: true,
},
});
});
});
describe('#getRaw()', () => {
@ -324,6 +397,24 @@ describe('ui settings', () => {
},
});
});
it('includes the values for overridden keys', async () => {
const esDocSource = { foo: 'bar' };
const defaults = { key: { value: chance.word() } };
const overrides = { foo: true };
const { uiSettings } = setup({ esDocSource, defaults, overrides });
const result = await uiSettings.getRaw();
expect(result).to.eql({
foo: {
userValue: true,
isOverridden: true,
},
key: {
value: defaults.key.value,
},
});
});
});
describe('#getAll()', () => {
@ -361,6 +452,29 @@ describe('ui settings', () => {
bar: 'user-provided',
});
});
it('includes the values for overridden keys', async () => {
const esDocSource = {
foo: 'user-override',
bar: 'user-provided',
};
const defaults = {
foo: {
value: 'default'
},
};
const overrides = {
foo: 'bax'
};
const { uiSettings } = setup({ esDocSource, defaults, overrides });
expect(await uiSettings.getAll()).to.eql({
foo: 'bax',
bar: 'user-provided',
});
});
});
describe('#get()', () => {
@ -392,5 +506,61 @@ describe('ui settings', () => {
const result = await uiSettings.get('dateFormat');
expect(result).to.equal('YYYY-MM-DD');
});
it('returns the overridden value for an overrided key', async () => {
const esDocSource = { dateFormat: 'YYYY-MM-DD' };
const overrides = { dateFormat: 'foo' };
const { uiSettings } = setup({ esDocSource, overrides });
expect(await uiSettings.get('dateFormat')).to.be('foo');
});
it('returns the default value for an override with value null', async () => {
const esDocSource = { dateFormat: 'YYYY-MM-DD' };
const overrides = { dateFormat: null };
const defaults = { dateFormat: { value: 'foo' } };
const { uiSettings } = setup({ esDocSource, overrides, defaults });
expect(await uiSettings.get('dateFormat')).to.be('foo');
});
it('returns the overridden value if the document does not exist', async () => {
const overrides = { dateFormat: 'foo' };
const { uiSettings, savedObjectsClient } = setup({ overrides });
savedObjectsClient.get.throws(savedObjectsClientErrors.createGenericNotFoundError());
expect(await uiSettings.get('dateFormat')).to.be('foo');
});
});
describe('#isOverridden()', () => {
it('returns false if no overrides defined', () => {
const { uiSettings } = setup();
expect(uiSettings.isOverridden('foo')).to.be(false);
});
it('returns false if overrides defined but key is not included', () => {
const { uiSettings } = setup({ overrides: { foo: true, bar: true } });
expect(uiSettings.isOverridden('baz')).to.be(false);
});
it('returns false for object prototype properties', () => {
const { uiSettings } = setup({ overrides: { foo: true, bar: true } });
expect(uiSettings.isOverridden('hasOwnProperty')).to.be(false);
});
it('returns true if overrides defined and key is overridden', () => {
const { uiSettings } = setup({ overrides: { foo: true, bar: true } });
expect(uiSettings.isOverridden('bar')).to.be(true);
});
});
describe('#assertUpdateAllowed()', () => {
it('returns false if no overrides defined', () => {
const { uiSettings } = setup();
expect(uiSettings.assertUpdateAllowed('foo')).to.be(undefined);
});
it('throws 400 Boom error when keys is overridden', () => {
const { uiSettings } = setup({ overrides: { foo: true } });
expect(() => uiSettings.assertUpdateAllowed('foo')).to.throwError(error => {
expect(error).to.have.property('message', 'Unable to update "foo" because it is overridden');
expect(error).to.have.property('isBoom', true);
expect(error.output).to.have.property('statusCode', 400);
});
});
});
});

View file

@ -20,6 +20,8 @@ You can use \`config.get(\\"throwableProperty\\", defaultValue)\`, which will ju
\`defaultValue\` when the key is unrecognized."
`;
exports[`#overrideLocalDefault #assertUpdateAllowed() throws error when keys is overridden 1`] = `"Unable to update \\"foo\\" because its value is overridden by the Kibana server"`;
exports[`#overrideLocalDefault key has no user value calls subscriber with new and previous value: single subscriber call 1`] = `
Array [
Array [
@ -95,6 +97,10 @@ Object {
}
`;
exports[`#remove throws an error if key is overridden 1`] = `"Unable to update \\"bar\\" because its value is overridden by the Kibana server"`;
exports[`#set throws an error if key is overridden 1`] = `"Unable to update \\"foo\\" because its value is overridden by the Kibana server"`;
exports[`#subscribe calls handler with { key, newValue, oldValue } when config changes 1`] = `
Array [
Array [

View file

@ -102,6 +102,16 @@ You can use \`config.get("${key}", defaultValue)\`, which will just return
return this.isDeclared(key) && !('value' in this._cache[key]);
}
isOverridden(key) {
return this.isDeclared(key) && Boolean(this._cache[key].isOverridden);
}
assertUpdateAllowed(key) {
if (this.isOverridden(key)) {
throw new Error(`Unable to update "${key}" because its value is overridden by the Kibana server`);
}
}
overrideLocalDefault(key, newDefault) {
// capture the previous value
const prevDefault = this._defaults[key]
@ -137,6 +147,8 @@ You can use \`config.get("${key}", defaultValue)\`, which will just return
}
async _update(key, value) {
this.assertUpdateAllowed(key);
const declared = this.isDeclared(key);
const defaults = this._defaults;
@ -151,7 +163,7 @@ You can use \`config.get("${key}", defaultValue)\`, which will just return
}
const initialVal = declared ? this.get(key) : undefined;
this._setLocally(key, newVal, initialVal);
this._setLocally(key, newVal);
try {
const { settings } = await this._api.batchSet(key, newVal);
@ -165,6 +177,8 @@ You can use \`config.get("${key}", defaultValue)\`, which will just return
}
_setLocally(key, newValue) {
this.assertUpdateAllowed(key);
if (!this.isDeclared(key)) {
this._cache[key] = {};
}

View file

@ -123,6 +123,18 @@ describe('#set', () => {
await expect(config.set('foo', 'bar')).resolves.toBe(false);
});
it('throws an error if key is overridden', async () => {
const { config } = setup({
initialSettings: {
foo: {
isOverridden: true,
value: 'bar'
}
}
});
await expect(config.set('foo', true)).rejects.toThrowErrorMatchingSnapshot();
});
});
describe('#remove', () => {
@ -140,6 +152,18 @@ describe('#remove', () => {
await expect(config.remove('dateFormat')).resolves.toBe(false);
});
it('throws an error if key is overridden', async () => {
const { config } = setup({
initialSettings: {
bar: {
isOverridden: true,
userValue: true
}
}
});
await expect(config.remove('bar')).rejects.toThrowErrorMatchingSnapshot();
});
});
describe('#isDeclared', () => {
@ -293,4 +317,61 @@ describe('#overrideLocalDefault', () => {
expect(config.getAll()).toMatchSnapshot('getAll after override');
});
});
describe('#isOverridden()', () => {
it('returns false if key is unknown', () => {
const { config } = setup();
expect(config.isOverridden('foo')).toBe(false);
});
it('returns false if key is no overridden', () => {
const { config } = setup({
initialSettings: {
foo: {
userValue: 1
},
bar: {
isOverridden: true,
userValue: 2
}
}
});
expect(config.isOverridden('foo')).toBe(false);
});
it('returns true when key is overridden', () => {
const { config } = setup({
initialSettings: {
foo: {
userValue: 1
},
bar: {
isOverridden: true,
userValue: 2
},
}
});
expect(config.isOverridden('bar')).toBe(true);
});
it('returns false for object prototype properties', () => {
const { config } = setup();
expect(config.isOverridden('hasOwnProperty')).toBe(false);
});
});
describe('#assertUpdateAllowed()', () => {
it('returns false if no settings defined', () => {
const { config } = setup();
expect(config.assertUpdateAllowed('foo')).toBe(undefined);
});
it('throws error when keys is overridden', () => {
const { config } = setup({
initialSettings: {
foo: {
isOverridden: true,
userValue: 'bar'
}
}
});
expect(() => config.assertUpdateAllowed('foo')).toThrowErrorMatchingSnapshot();
});
});
});

View file

@ -82,7 +82,11 @@ export function docExistsSuite() {
},
defaultIndex: {
userValue: defaultIndex
}
},
foo: {
userValue: 'bar',
isOverridden: true
},
}
});
});
@ -109,10 +113,33 @@ export function docExistsSuite() {
},
defaultIndex: {
userValue: defaultIndex
}
},
foo: {
userValue: 'bar',
isOverridden: true
},
}
});
});
it('returns a 400 if trying to set overridden value', async () => {
const { kbnServer } = await setup();
const { statusCode, result } = await kbnServer.inject({
method: 'POST',
url: '/api/kibana/settings/foo',
payload: {
value: 'baz'
}
});
expect(statusCode).to.be(400);
assertSinonMatch(result, {
error: 'Bad Request',
message: 'Unable to update "foo" because it is overridden',
statusCode: 400
});
});
});
describe('setMany route', () => {
@ -138,9 +165,34 @@ export function docExistsSuite() {
},
defaultIndex: {
userValue: defaultIndex
},
foo: {
userValue: 'bar',
isOverridden: true
},
}
});
});
it('returns a 400 if trying to set overridden value', async () => {
const { kbnServer } = await setup();
const { statusCode, result } = await kbnServer.inject({
method: 'POST',
url: '/api/kibana/settings',
payload: {
changes: {
foo: 'baz'
}
}
});
expect(statusCode).to.be(400);
assertSinonMatch(result, {
error: 'Bad Request',
message: 'Unable to update "foo" because it is overridden',
statusCode: 400
});
});
});
@ -164,9 +216,28 @@ export function docExistsSuite() {
settings: {
buildNum: {
userValue: sinon.match.number
}
},
foo: {
userValue: 'bar',
isOverridden: true
},
}
});
});
it('returns a 400 if deleting overridden value', async () => {
const { kbnServer } = await setup();
const { statusCode, result } = await kbnServer.inject({
method: 'DELETE',
url: '/api/kibana/settings/foo'
});
expect(statusCode).to.be(400);
assertSinonMatch(result, {
error: 'Bad Request',
message: 'Unable to update "foo" because it is overridden',
statusCode: 400
});
});
});
}

View file

@ -48,7 +48,7 @@ export function docMissingSuite() {
});
describe('get route', () => {
it('creates doc, returns a 200 with no settings', async () => {
it('creates doc, returns a 200 with only overridden settings', async () => {
const { kbnServer } = getServices();
const { statusCode, result } = await kbnServer.inject({
@ -58,7 +58,12 @@ export function docMissingSuite() {
expect(statusCode).to.be(200);
assertSinonMatch(result, {
settings: {}
settings: {
foo: {
userValue: 'bar',
isOverridden: true
}
}
});
});
});
@ -82,6 +87,10 @@ export function docMissingSuite() {
},
defaultIndex: {
userValue: defaultIndex
},
foo: {
userValue: 'bar',
isOverridden: true
}
}
});
@ -109,6 +118,10 @@ export function docMissingSuite() {
},
defaultIndex: {
userValue: defaultIndex
},
foo: {
userValue: 'bar',
isOverridden: true
}
}
});
@ -129,6 +142,10 @@ export function docMissingSuite() {
settings: {
buildNum: {
userValue: sinon.match.number
},
foo: {
userValue: 'bar',
isOverridden: true
}
}
});

View file

@ -27,7 +27,6 @@ import { docMissingSuite } from './doc_missing';
import { indexMissingSuite } from './index_missing';
describe('uiSettings/routes', function () {
/**
* The "doc missing" and "index missing" tests verify how the uiSettings
* API behaves in between healthChecks, so they interact with the healthCheck

View file

@ -59,7 +59,7 @@ export function indexMissingSuite() {
}
describe('get route', () => {
it('returns a 200 and with empty values', async () => {
it('returns a 200 and with just overridden values', async () => {
const { kbnServer } = await setup();
const { statusCode, result } = await kbnServer.inject({
@ -68,7 +68,14 @@ export function indexMissingSuite() {
});
expect(statusCode).to.be(200);
expect(result).to.eql({ settings: {} });
expect(result).to.eql({
settings: {
foo: {
userValue: 'bar',
isOverridden: true
}
}
});
});
});
@ -93,6 +100,10 @@ export function indexMissingSuite() {
},
defaultIndex: {
userValue: defaultIndex
},
foo: {
userValue: 'bar',
isOverridden: true
}
}
});
@ -122,6 +133,10 @@ export function indexMissingSuite() {
},
defaultIndex: {
userValue: defaultIndex
},
foo: {
userValue: 'bar',
isOverridden: true
}
}
});
@ -144,6 +159,10 @@ export function indexMissingSuite() {
settings: {
buildNum: {
userValue: sinon.match.number
},
foo: {
userValue: 'bar',
isOverridden: true
}
}
});

View file

@ -39,7 +39,13 @@ export async function startServers() {
log.indent(-4);
await es.start();
kbnServer = kbnTestServer.createServerWithCorePlugins();
kbnServer = kbnTestServer.createServerWithCorePlugins({
uiSettings: {
overrides: {
foo: 'bar',
}
}
});
await kbnServer.ready();
await kbnServer.server.plugins.elasticsearch.waitUntilReady();
}

View file

@ -30,10 +30,12 @@ export function uiSettingsMixin(kbnServer, server) {
const getDefaults = () => (
kbnServer.uiExports.uiSettingDefaults
);
const overrides = kbnServer.config.get('uiSettings.overrides');
server.decorate('server', 'uiSettingsServiceFactory', (options = {}) => {
return uiSettingsServiceFactory(server, {
getDefaults,
overrides,
...options
});
});
@ -41,6 +43,7 @@ export function uiSettingsMixin(kbnServer, server) {
server.addMemoizedFactoryToRequest('getUiSettingsService', request => {
return getUiSettingsServiceForRequest(server, request, {
getDefaults,
overrides,
});
});

View file

@ -18,14 +18,9 @@
*/
import { defaultsDeep } from 'lodash';
import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config';
import Boom from 'boom';
function hydrateUserSettings(userSettings) {
return Object.keys(userSettings)
.map(key => ({ key, userValue: userSettings[key] }))
.filter(({ userValue }) => userValue !== null)
.reduce((acc, { key, userValue }) => ({ ...acc, [key]: { userValue } }), {});
}
import { createOrUpgradeSavedConfig } from './create_or_upgrade_saved_config';
/**
* Service that provides access to the UiSettings stored in elasticsearch.
@ -53,6 +48,7 @@ export class UiSettingsService {
getDefaults = () => ({}),
// function that accepts log messages in the same format as server.log
log = () => {},
overrides = {},
} = options;
this._type = type;
@ -60,6 +56,7 @@ export class UiSettingsService {
this._buildNum = buildNum;
this._savedObjectsClient = savedObjectsClient;
this._getDefaults = getDefaults;
this._overrides = overrides;
this._log = log;
}
@ -91,7 +88,26 @@ export class UiSettingsService {
}
async getUserProvided(options) {
return hydrateUserSettings(await this._read(options));
const userProvided = {};
// write the userValue for each key stored in the saved object that is not overridden
for (const [key, userValue] of Object.entries(await this._read(options))) {
if (userValue !== null && !this.isOverridden(key)) {
userProvided[key] = {
userValue
};
}
}
// write all overridden keys, dropping the userValue is override is null and
// adding keys for overrides that are not in saved object
for (const [key, userValue] of Object.entries(this._overrides)) {
userProvided[key] = userValue === null
? { isOverridden: true }
: { isOverridden: true, userValue };
}
return userProvided;
}
async setMany(changes) {
@ -114,7 +130,21 @@ export class UiSettingsService {
await this.setMany(changes);
}
isOverridden(key) {
return this._overrides.hasOwnProperty(key);
}
assertUpdateAllowed(key) {
if (this.isOverridden(key)) {
throw Boom.badRequest(`Unable to update "${key}" because it is overridden`);
}
}
async _write({ changes, autoCreateOrUpgradeIfMissing = true }) {
for (const key of Object.keys(changes)) {
this.assertUpdateAllowed(key);
}
try {
await this._savedObjectsClient.update(this._type, this._id, changes);
} catch (error) {

View file

@ -37,6 +37,7 @@ export function uiSettingsServiceFactory(server, options) {
const {
savedObjectsClient,
getDefaults,
overrides,
} = options;
return new UiSettingsService({
@ -45,6 +46,7 @@ export function uiSettingsServiceFactory(server, options) {
buildNum: config.get('pkg.buildNum'),
savedObjectsClient,
getDefaults,
overrides,
log: (...args) => server.log(...args),
});
}

View file

@ -34,11 +34,13 @@ import { uiSettingsServiceFactory } from './ui_settings_service_factory';
*/
export function getUiSettingsServiceForRequest(server, request, options = {}) {
const {
getDefaults
getDefaults,
overrides,
} = options;
const uiSettingsService = uiSettingsServiceFactory(server, {
getDefaults,
overrides,
savedObjectsClient: request.getSavedObjectsClient()
});

View file

@ -66,7 +66,9 @@ export class KibanaServerUiSettings {
const { payload } = await this._wreck.get('/api/kibana/settings');
for (const key of Object.keys(payload.settings)) {
await this._wreck.delete(`/api/kibana/settings/${key}`);
if (!payload.settings[key].isOverridden) {
await this._wreck.delete(`/api/kibana/settings/${key}`);
}
}
this._log.debug('replacing kibana config doc: %j', doc);