-
Notifications
You must be signed in to change notification settings - Fork 8.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[indexPatterns/create] refactor time field options #11996
Changes from 10 commits
942e937
f8caf83
4cb39d3
c3c3829
ca93050
1cf49c0
b3f7d83
8ea9813
8d1df86
b8a7f4f
6ad5ff8
cebb6a6
7e33e0f
4eab34b
54a80f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -29,37 +29,23 @@ uiModules.get('apps/management') | |
nameIsPattern: false, | ||
expandable: false, | ||
nameInterval: _.find(intervals, { name: 'daily' }), | ||
timeField: null, | ||
timeFieldOption: null, | ||
}; | ||
|
||
// UI state. | ||
this.dateFields = null; | ||
this.timeFieldOptions = []; | ||
this.timeFieldOptionsError = null; | ||
this.sampleCount = 5; | ||
this.samples = null; | ||
this.existing = null; | ||
this.nameIntervalOptions = intervals; | ||
this.patternErrors = []; | ||
this.fetchFieldsError = null; | ||
|
||
const TIME_FILTER_FIELD_OPTIONS = { | ||
NO_DATE_FIELD_DESIRED: { | ||
name: $translate.instant('KIBANA-NO_DATE_FIELD_DESIRED') | ||
}, | ||
NO_DATE_FIELDS_IN_INDICES: { | ||
name: $translate.instant('KIBANA-NO_DATE_FIELDS_IN_INDICES') | ||
} | ||
}; | ||
|
||
const fetchFieldList = () => { | ||
this.dateFields = null; | ||
this.formValues.timeField = null; | ||
let fetchFieldsError; | ||
let dateFields; | ||
|
||
const getTimeFieldOptions = () => { | ||
const missingPattern = !this.formValues.name; | ||
const missingInterval = this.formValues.nameIsPattern && !this.formValues.nameInterval; | ||
if (missingPattern || missingInterval) { | ||
return; | ||
return Promise.resolve({ options: [] }); | ||
} | ||
|
||
loadingCount += 1; | ||
|
@@ -69,79 +55,81 @@ uiModules.get('apps/management') | |
|
||
return indexPatterns.mapper.getFieldsForIndexPattern(pattern, { | ||
skipIndexPatternCache: true, | ||
}) | ||
.catch((err) => { | ||
// TODO: we should probably display a message of some kind | ||
}); | ||
}) | ||
.then( | ||
fields => { | ||
const dateFields = fields.filter(field => field.type === 'date'); | ||
|
||
if (dateFields.length === 0) { | ||
return { | ||
options: [ | ||
{ | ||
display: $translate.instant('KIBANA-INDICES_DONT_CONTAIN_TIME_FIELDS') | ||
} | ||
] | ||
}; | ||
} | ||
|
||
return { | ||
options: [ | ||
{ | ||
display: $translate.instant('KIBANA-NO_DATE_FIELD_DESIRED') | ||
}, | ||
...dateFields.map(field => ({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❤️ |
||
display: field.name, | ||
fieldName: field.name | ||
})), | ||
] | ||
}; | ||
}, | ||
err => { | ||
if (err instanceof IndexPatternMissingIndices) { | ||
fetchFieldsError = $translate.instant('KIBANA-INDICES_MATCH_PATTERN'); | ||
return []; | ||
return { | ||
error: $translate.instant('KIBANA-INDICES_MATCH_PATTERN') | ||
}; | ||
} | ||
|
||
throw err; | ||
}); | ||
}) | ||
.then(fields => { | ||
if (fields.length > 0) { | ||
fetchFieldsError = null; | ||
dateFields = fields.filter(field => field.type === 'date'); | ||
} | ||
|
||
return { | ||
fetchFieldsError, | ||
dateFields, | ||
}; | ||
}, notify.fatal) | ||
) | ||
.finally(() => { | ||
loadingCount -= 1; | ||
}); | ||
}; | ||
|
||
const updateFieldList = results => { | ||
this.fetchFieldsError = results.fetchFieldsError; | ||
if (this.fetchFieldsError) { | ||
return; | ||
} | ||
const findTimeFieldOption = match => { | ||
if (!match) return; | ||
|
||
this.dateFields = results.dateFields || []; | ||
this.indexHasDateFields = this.dateFields.length > 0; | ||
const moreThanOneDateField = this.dateFields.length > 1; | ||
if (this.indexHasDateFields) { | ||
this.dateFields.unshift(TIME_FILTER_FIELD_OPTIONS.NO_DATE_FIELD_DESIRED); | ||
} else { | ||
this.dateFields.unshift(TIME_FILTER_FIELD_OPTIONS.NO_DATE_FIELDS_IN_INDICES); | ||
} | ||
|
||
if (!moreThanOneDateField) { | ||
// At this point the `dateFields` array contains the date fields and the "no selection" | ||
// option. When we have less than two date fields we choose the last option, which will | ||
// be the "no date fields available" option if there are zero date fields, or the only | ||
// date field if there is one. | ||
this.formValues.timeField = this.dateFields[this.dateFields.length - 1]; | ||
} | ||
return this.timeFieldOptions.find(option => ( | ||
// comparison is not done with _.isEqual() because options get a unique | ||
// `$$hashKey` tag attached to them by ng-repeat | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ❤️ this comment |
||
option.fieldName === match.fieldName && | ||
option.display === match.display | ||
)); | ||
}; | ||
|
||
const updateFieldListAndSetTimeField = (results, timeFieldName) => { | ||
updateFieldList(results); | ||
const pickDefaultTimeFieldOption = () => { | ||
const noOptions = this.timeFieldOptions.length === 0; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we should try and use pure functions as much as possible, so in this case, can we simply use an argument There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is another thing I tried and reverted, mostly because of the ambiguity caused by the existence of multiple That said, this function intentionally doesn't produce side effects so that it's pure by some definitions. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd actually be in favor of that decision (to move it outside of the controller). Yes it doesn't produce side effects and that might be the more important aspect of pure functions but it still does rely on scoped state when it doesn't need to. I have no desire to hold up the review for this particular topic, as I'm just trying to understand how everyone feels about this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
By this I mean to say that every time Example: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea that makes sense, and I don't think there is any decoupling happening here. There is and will always be a hard dependence, but I don't think the changes I'm suggesting make that coupling any less obvious, as when you're doing a search for But as a pure function, you can:
IMO, it seems like an easy win to strive for pure as much as possible. It's very possible that I'm not seeing a part of this that makes it harder or undesired and open to listening to other perspectives but the more codebases that I've worked in and the longer I've been writing code, the more I'm thinking that pure functions are a win/win across the board. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree with @chrisronline's reasoning here. I did some similar extraction as part of https://github.com/elastic/kibana/pull/11285/files#diff-053c72f0b419832d5edb39bd81422e06R1 and it made the controller logic easier to follow because it was higher level. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm okay with this not going in right now. I think I'm going to open a discussion issue about this in general to get some general thoughts and best practices that others have learned. I do think it's an important conversation but it doesn't have to happen in this PR |
||
const fieldOptions = this.timeFieldOptions.filter(option => !!option.fieldName); | ||
const infoOptions = this.timeFieldOptions.filter(option => !option.fieldName); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are infoOptions? Maybe it's better to describe these as And instead of deriving this state based on const selectableOptions = this.timeFieldOptions.filter(option => option.isSelectable);
const nonSelectableOptions = this.timeFieldOptions.filter(option => !option.isSelectable); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, they're all selectable, they are really just "non-field" options. I could use that as the description rather than "info" if you think that's better. |
||
const tooManyOptions = fieldOptions.length > 1 || infoOptions.length > 1; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What does it mean for there to be too many options? Too many to do what? Wondering if there's a name that can communicate the intent more clearly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Too many options to choose the default. If there are multiple field or nonField options then we can't pick the default, the user has to choose something... open to suggestions for better names. |
||
|
||
if (!results.dateFields.length) { | ||
return; | ||
if (noOptions || tooManyOptions) { | ||
return null; | ||
} | ||
|
||
const matchingTimeField = results.dateFields.find(field => field.name === timeFieldName); | ||
|
||
//assign the field from the results-list | ||
//angular recreates a new timefield instance, each time the list is refreshed. | ||
//This ensures the selected field matches one of the instances in the list. | ||
if (matchingTimeField) { | ||
this.formValues.timeField = matchingTimeField; | ||
if (fieldOptions.length === 1) { | ||
return fieldOptions[0]; | ||
} | ||
|
||
return infoOptions[0]; | ||
}; | ||
|
||
const resetIndex = () => { | ||
this.patternErrors = []; | ||
this.samples = null; | ||
this.existing = null; | ||
this.fetchFieldsError = null; | ||
}; | ||
|
||
function mockIndexPattern(index) { | ||
|
@@ -207,39 +195,54 @@ uiModules.get('apps/management') | |
return loadingCount > 0; | ||
}; | ||
|
||
this.refreshFieldList = () => { | ||
const timeField = this.formValues.timeField; | ||
this.refreshTimeFieldOptions = () => { | ||
const prevOption = this.formValues.timeFieldOption; | ||
|
||
loadingCount += 1; | ||
fetchFieldList().then(results => { | ||
if (timeField) { | ||
updateFieldListAndSetTimeField(results, timeField.name); | ||
} else { | ||
updateFieldList(results); | ||
} | ||
}).finally(() => { | ||
loadingCount -= 1; | ||
}); | ||
this.timeFieldOptions = []; | ||
this.timeFieldOptionsError = null; | ||
this.formValues.timeFieldOption = null; | ||
getTimeFieldOptions() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there a reason we aren't opting for the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Interesting. Why doesn't a simple There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Manually triggering digest cycles is really a last resort option and can lead to all sorts of issues even in simple apps. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's interesting. I'd love to hear more about this. Do you have any resources and/or past github issues around this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I agree that sometimes There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree, we should avoid using $apply where possible. I think the way to do that is to follow a few simple rules:
There are probably a few others we can dig up by Googling but the general theme is to just use whatever tools Angular makes available which will tie into the $digest cycle under the hood. These are situations where $apply is inappropriate. This leaves us with the situations where $apply is appropriate: anywhere these tools can't be used. I think if we can confidently assess a call stack and state that it will never touch any of these tools, and therefore never trigger a $digest cycle on its own, then we can safely use $apply. Thoughts @spalger and @chrisronline ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @cjcenizal by that logic there are no function in this PR that qualify unless we reimplement some core services. All of the asynchronicity in this controller is introduced by the If you're suggesting that we write more code that is completely disconnected from angular, that only inter-ops with angular at the last minute using something like My stance in this discussion so far was against using angular services in a non-angular context (like an This specific pr is even worse in my opinion, converting any of the helpers to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Then I guess we can't use $apply. :)
I see, sorry, I didn't pick up on that earlier. I thought we were just talking about how to use $apply correctly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @spalger I think that's an excellent point and something I wasn't considering fully. You're right in that I'm advising to add a layer from angular that needs to go back into angular and that's probably not a good idea unless there is a really good reason. I'm happy with the discussion around this and I 100% agree that we should try and decouple our business logic away from our angular implementation. With that said, I'm fine with the code as-is and appreciate the thoughts! |
||
.then(results => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Feels like a good place for object destructuring: |
||
this.timeFieldOptions = results.options; | ||
this.timeFieldOptionsError = results.error; | ||
if (!this.timeFieldOptions) { | ||
return; | ||
} | ||
|
||
const restoredOption = findTimeFieldOption(prevOption); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we add a brief comment to describe these 3 lines? // Persist the selected state in the UI. |
||
const defaultOption = pickDefaultTimeFieldOption(); | ||
this.formValues.timeFieldOption = restoredOption || defaultOption; | ||
}) | ||
.catch(notify.error) | ||
.finally(() => { | ||
loadingCount -= 1; | ||
}); | ||
}; | ||
|
||
this.createIndexPattern = () => { | ||
const id = this.formValues.name; | ||
let timeFieldName; | ||
if ((this.formValues.timeField !== TIME_FILTER_FIELD_OPTIONS.NO_DATE_FIELD_DESIRED) | ||
&& (this.formValues.timeField !== TIME_FILTER_FIELD_OPTIONS.NO_DATE_FIELDS_IN_INDICES)) { | ||
timeFieldName = this.formValues.timeField.name; | ||
} | ||
const { | ||
name, | ||
timeFieldOption, | ||
nameIsPattern, | ||
nameInterval, | ||
expandable | ||
} = this.formValues; | ||
|
||
// Only event-time-based index patterns set an intervalName. | ||
const intervalName = | ||
this.formValues.nameIsPattern | ||
? this.formValues.nameInterval.name | ||
: undefined; | ||
const id = name; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can just rename the variable in the above destructing assignment instead?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I had it like that at one point, but found it easier to read when all of the values passed to |
||
|
||
const notExpandable = | ||
!this.formValues.expandable && this.canExpandIndices() | ||
const timeFieldName = timeFieldOption && timeFieldOption.fieldName; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This variable name makes me think it's a string but the code indicates it's a boolean? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It ends up being the field name or false, which isn't what I want... good catch |
||
|
||
// this seems wrong, but it's the original logic... https://git.io/vHYFo | ||
const notExpandable = (!expandable && this.canExpandIndices()) | ||
? true | ||
: undefined; | ||
|
||
// Only event-time-based index patterns set an intervalName. | ||
const intervalName = (nameIsPattern && nameInterval) | ||
? nameInterval.name | ||
: undefined; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In my eyes This is trying to represent "no intervalName", so I don't think |
||
|
||
loadingCount += 1; | ||
sendCreateIndexPatternRequest(indexPatterns, { | ||
id, | ||
|
@@ -287,7 +290,6 @@ uiModules.get('apps/management') | |
|
||
if (!nameIsPattern) { | ||
delete this.formValues.nameInterval; | ||
delete this.formValues.timeField; | ||
} else { | ||
this.formValues.nameInterval = this.formValues.nameInterval || intervals.byName.days; | ||
this.formValues.name = this.formValues.name || getDefaultPatternForInterval(this.formValues.nameInterval); | ||
|
@@ -330,28 +332,23 @@ uiModules.get('apps/management') | |
.finally(() => { | ||
// prevent running when no change happened (ie, first watcher call) | ||
if (!_.isEqual(newVal, oldVal)) { | ||
fetchFieldList().then(results => { | ||
if (lastPromise === samplePromise) { | ||
updateFieldList(results); | ||
samplePromise = null; | ||
} | ||
}); | ||
this.refreshTimeFieldOptions(); | ||
} | ||
}); | ||
}); | ||
|
||
$scope.$watchMulti([ | ||
'controller.sampleCount' | ||
], () => { | ||
this.refreshFieldList(); | ||
this.refreshTimeFieldOptions(); | ||
}); | ||
|
||
$scope.$watchMulti([ | ||
'controller.isLoading()', | ||
'form.name.$error.indexNameInput', | ||
'controller.formValues.timeField' | ||
], ([loading, invalidIndexName, timeField]) => { | ||
const state = { loading, invalidIndexName, timeField }; | ||
'controller.formValues.timeFieldOption' | ||
], ([loading, invalidIndexName, timeFieldOption]) => { | ||
const state = { loading, invalidIndexName, timeFieldOption }; | ||
this.createButtonText = pickCreateButtonText($translate, state); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason this is on a newline and not:
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So it lines up with the errorback below. With this version of the implementation it's not necessary to use a single
.then()
, so I'll switch it to.then().catch()