Skip to content

Advanced Settings - Image Input#15342

Merged
kobelb merged 21 commits intoelastic:masterfrom
kobelb:reporting-logo
Jan 3, 2018
Merged

Advanced Settings - Image Input#15342
kobelb merged 21 commits intoelastic:masterfrom
kobelb:reporting-logo

Conversation

@kobelb
Copy link
Copy Markdown
Contributor

@kobelb kobelb commented Dec 1, 2017

Adds an "image" advanced setting type. The maximum size is currently hard-coded for all images to 200kB, we could potentially make this configurable and allow the uiSetting configuration to specify this value, but I figured 200kB was reasonable for the time being. The inputBaseSixtyFour only supports a single file for the time being, and will throw an Error that results in a "fatal error" if it's given multiple files.

I used RxJs/Observables for the inputBaseSixtyFour directive so that we could abort the FileReader when the change event is fired before the reading has finished.

"Release Note: Added image support to the Advanced Settings"

@kobelb
Copy link
Copy Markdown
Contributor Author

kobelb commented Dec 1, 2017

@spalger I tagged you as a reviewer, since you're the Observables expert, but if you'd like me to defer to someone else, just lemme know.

@jbudz
Copy link
Copy Markdown
Contributor

jbudz commented Dec 1, 2017

other side note, feel free to ignore: you could defer file size to the server's max payload size config. I'll stop intruding on this - sorry!

@kobelb
Copy link
Copy Markdown
Contributor Author

kobelb commented Dec 1, 2017

@jbudz please don't stop intruding! I figured we'd want to stop huge images from getting into the .kibana "config" document, and might want more fine grained control of this, and a more pleasant user experience, do you think 200kb is too small?

@jbudz
Copy link
Copy Markdown
Contributor

jbudz commented Dec 1, 2017

Nope makes sense, I'd expect 200kb to be fine for a logo and if there's other uses we can always bump it up then.

@spalger
Copy link
Copy Markdown
Contributor

spalger commented Dec 1, 2017

you could defer file size to the server's max payload size config.

I think it's important that we enforce this validation on the server too, if we really want there to be a limit

import { Observable } from 'rxjs/Rx';
const module = uiModules.get('kibana');

const $readFileContents = (file) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personal option, createFileContent$ (create file content stream) reads a little better, especially when you're assigning it to a variable:

const fileContent$ = createFileContent$(file)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, I will make it so.

const reader = new FileReader();
reader.onerror = (err) => {
observer.error(err);
observer.complete();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

observer.error() is like rejecting a promise, calling observer.complete() afterward does nothing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I'll remove the unnecessary completion.

Copy link
Copy Markdown
Contributor

@spalger spalger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, tested by adding a logo to Kibanas uiSettings https://gist.github.com/anonymous/b2c3a92fee83fc7749af8113349258e7

.map(dataUrl => {
const validations = validators.map(validator => validator(dataUrl));

return { dataUrl, validations };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

More opinions: when I need to carry data onto another part of a stream I break up the original stream rather than require the previous stream to pass on it's input value. It might look something like this here:

// produce fileContent$ whenever the $element 'change' event is triggered.
const fileContent$ = Observable
  .fromEvent($elem, 'change')
  .map(e => e.target.files)
  .switchMap(files => {
    if (files.length === 0) {
      return [];
    }
 
    if (files.length > 1) {
      throw new Error('Only one file is supported at this time');
    }
 
    return $readFileContents(files[0]);
  })
  .share();

// validate the content of the files after it is loaded
const validations$ = fileContent$
  .map(fileContent => (
    validators.map(validator => validator(fileContent))
  ))
  .toArray()

// push results from input/validation to the ngModel
const unsubscribe = Observable
  .combineLatest(fileContent$, validations$)
  .subscribe(([ fileContent, validations ]) => {
    $scope.$evalAsync(() => {
      ...
    })
  })

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like it, more readable! I will make it so.

Copy link
Copy Markdown
Contributor

@spalger spalger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, mixed signals, I think we should enforce the size limit on the server too, perhaps the max image size should come from the config value, maybe something like this: https://gist.github.com/anonymous/c5b669c4df1ab0a813670a217b3ea83a

@kobelb
Copy link
Copy Markdown
Contributor Author

kobelb commented Dec 1, 2017

@spalger wrt to the server enforcing the size limit, that makes sense, I'll make those changes.

}

if (files.length > 1) {
throw new Error('Only one file is supported at this time');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't this be a validator rather than a fatal error?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, is it possible to submit multiple files since the input[type=file] requires the multiple attribute to enable multi-selection?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't really want this error to be displayed to the end-user as much as the developer who tries to use it on an input with the multiple attribute, so I don't think it makes much sense to have it as a validator.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could, potentially, but it makes the code more complicated to support multiple files (when we don't really need it yet) so I'm throwing "better" errors more places to make this more obvious.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@kobelb
Copy link
Copy Markdown
Contributor Author

kobelb commented Dec 4, 2017

@spalger I'm now enforcing the maximum size of the setting on the server as well, using a configurable value specified like:

maxSize: {
  length: kbToBase64Length(200),
  description: '200 kB'
}

The UiSettingsService itself is enforcing this and throwing an InvalidValueError when this is violated. Given the way that the uiSettings themselves are implemented, I didn't see a great way to share validation rules between the server/client, because the "uiSettings" are serialized and included as part of the kbn-initial-state.

The "proper" solution to this would likely require a rather significant effort, does the current implementation seem reasonable enough for the time-being, or would you rather I investigate a more unified and robust manner for validating ui settings?

await uiSettings.set(key, value);
} catch (err) {
if (err instanceof InvalidValueError) {
return boom.badRequest(err.message);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw should be appropriate here

* Invalid value for UiSetting
* @class InvalidValudError
*/
export class InvalidValueError extends Error {
Copy link
Copy Markdown
Contributor

@spalger spalger Dec 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have a fairly established convention for handling service-specific errors. See:

Copy link
Copy Markdown
Contributor Author

@kobelb kobelb Dec 5, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this an approach that we intend to support with the new platform? Throwing boom errors, which are http specific, seems like it won't scale that well as we start to support more "background" tasks that aren't attached to a specific http request/response.

const validations = defaults[key].validations;
const value = changes[key];
for (const validation of Object.values(validations)) {
const result = validation.schema.validate(value);
Copy link
Copy Markdown
Contributor

@spalger spalger Dec 4, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's important to capture result.value here, since JoiSchema#validate() will return defaults and such if they are configured. Also, why not have a single joi schema for the value? Joi can merge multiple validations into a single error message and if you use abortEarly: false it will list all errors rather than just the first.

Copy link
Copy Markdown
Contributor

@spalger spalger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I'm a little concerned by the amount of flexibility implemented currently. I'm not opposed to having validation be a feature of the UiSettings service, but the fact that we are supporting the Joi JSON-DSL in the uiSettingDefaults means we're kind of adding a whole new DSL to our API. I wouldn't expect support for Joi to follow us into the new platform so I'm thinking we should narrow the scope of the solution here a bit... Thoughts?

@kobelb
Copy link
Copy Markdown
Contributor Author

kobelb commented Dec 5, 2017

@spalger narrowing the solution seems fine to me, I has hesitant to add such flexible validation support to uiSettings, and at one point I was just hard-coding the maxLength validation here but it felt like this was bolted on, and it really only worked well for strings, since we're checking the "length" of the value.

There's also a commit before switching to joi where I was just returning { isValid: boolean, message: optional<string> }, do either of these seem like a good middle-ground?

@spalger
Copy link
Copy Markdown
Contributor

spalger commented Dec 6, 2017

I personally think that having type-specific validation options is fine, so maxSizeKb would only be supported for images, and UiSettingsService#setMany would just validate step through each value being set, see if it's an image, and then check it's maxSizeKb. Using a function means that we don't have static values to use in the UI, right? It won't scale if we decided to add a ton of validation to the uiSettings, but I don't think we need to worry about that now.

PS: I like maxSizeKb because it means that the UI can easily describe the value as kilobytes, and it limits the amount of customization intentionally.

PPS: Since a setting like maxSizeKb must assume that the data is base64 encoded in order to properly check the size in KB we might as well ensure that the value is base64 encoded too.

Copy link
Copy Markdown
Contributor

@spalger spalger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@kobelb kobelb merged commit 4b0a9fc into elastic:master Jan 3, 2018
@kobelb kobelb deleted the reporting-logo branch January 3, 2018 16:11
kobelb added a commit to kobelb/kibana that referenced this pull request Jan 3, 2018
* Beginning to work on image advanced setting type

* Making the maxSize code and UI more user-friendly (hopefully).

* Getting rid of that border, I was using the wrong class

* Displaying an image for the preview

* Not displaying the image if we don't have a value

* Removing no longer used code

* Adding explicit throw in the subscription to make our intents more clear

* Changing some of the Observable style based on  input from the peer review

* Using the maxSizeKB option to enforce the size

* Adding support for no max

* Throwing better errors

* No longer duplicated the multiple usage error message

* Using the maxSize attribute to control the max size of the settings

* No longer using JSON.stringify, it's already a string and that approach
won't really do anything for other data-types

* Extracting validate to it's own method

* No longer hardcoding the maxLength validation in the uiSettings

* Using a Joi schema instead

* Punting on server-side validation for the time being
kobelb added a commit that referenced this pull request Jan 3, 2018
* Beginning to work on image advanced setting type

* Making the maxSize code and UI more user-friendly (hopefully).

* Getting rid of that border, I was using the wrong class

* Displaying an image for the preview

* Not displaying the image if we don't have a value

* Removing no longer used code

* Adding explicit throw in the subscription to make our intents more clear

* Changing some of the Observable style based on  input from the peer review

* Using the maxSizeKB option to enforce the size

* Adding support for no max

* Throwing better errors

* No longer duplicated the multiple usage error message

* Using the maxSize attribute to control the max size of the settings

* No longer using JSON.stringify, it's already a string and that approach
won't really do anything for other data-types

* Extracting validate to it's own method

* No longer hardcoding the maxLength validation in the uiSettings

* Using a Joi schema instead

* Punting on server-side validation for the time being
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants