Skip to content

Commit

Permalink
Closes #703 - Visual editor of the configuration UI shows possible va…
Browse files Browse the repository at this point in the history
…lues of Enum properties (#878)

Co-authored-by: Marius Oehler <[email protected]>
  • Loading branch information
MariusBrill and mariusoe authored Feb 25, 2021
1 parent 8e770de commit 36c6c59
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { Menubar } from 'primereact/menubar';
import { Message } from 'primereact/message';
import { Row } from 'primereact/row';
import { TreeTable } from 'primereact/treetable';
import { Dropdown } from 'primereact/dropdown';
import PropTypes from 'prop-types';
import React from 'react';
import axios from '../../lib/axios-api';

// helper for a schema property type constants
const schemaType = {
Expand Down Expand Up @@ -49,6 +51,8 @@ class TreeTableEditor extends React.Component {
data: undefined,
expandedKeys: DEFAULT_EXPANDED_KEYS,
showAll: true,
enumOptions: [],
oldNode: '',
};
}

Expand Down Expand Up @@ -329,6 +333,21 @@ class TreeTableEditor extends React.Component {
wrapWithExtras={this.wrapWithExtras}
/>
);
case schemaType.ENUM:
if (this.state.oldNode.key != node.key) {
this.fetchDropdownOptions(node.key, type);
this.setState({ oldNode: node, enumOptions: [] });
}
return (
<EnumEditor
node={node}
options={this.state.enumOptions}
loadingMessage={'loading...'}
onPropValueChange={this.onPropValueChange}
onPropValueRemove={this.onPropValueRemove}
wrapWithExtras={this.wrapWithExtras}
/>
);
case schemaType.INTEGER:
return (
<NumberEditor
Expand Down Expand Up @@ -362,6 +381,31 @@ class TreeTableEditor extends React.Component {
}
};

fetchDropdownOptions = (configPath, schemaType) => {
if (schemaType == 'ENUM') {
axios
.post('/autocomplete', {
path: configPath,
})
.then((response) => {
const suggestions = response.data;
if (suggestions) {
const result = suggestions.map((suggestion) => {
return {
key: suggestion,
value: suggestion,
};
});

this.setState({ enumOptions: result });
}
})
.catch((error) => {
console.warn('Could not fetch autocompletion results.', error);
});
}
};

render() {
const { loading } = this.props;

Expand Down Expand Up @@ -464,6 +508,55 @@ var StringEditor = ({ node, onPropValueChange, onPropValueRemove, wrapWithExtras
}
};

/** Editor for Enums */
var EnumEditor = ({ node, options, loadingMessage, onPropValueChange, onPropValueRemove, wrapWithExtras }) => {
const defaultValue = loadingMessage;
let disable = !options.length;
let currentValue = node.value;
if (disable) {
options.push({
key: loadingMessage,
value: loadingMessage,
});
currentValue = loadingMessage;
}

const onChange = (e) => {
onPropValueChange(node.key, e.value);
};
// component to render
const component = () => (
<Dropdown
value={currentValue}
options={options}
optionLabel={'key'}
optionValue={'value'}
onChange={onChange}
disabled={
disable ||
JSON.stringify(options) ===
JSON.stringify([
{
key: loadingMessage,
value: loadingMessage,
},
])
}
/>
);

if (!wrapWithExtras) {
return component();
} else {
return wrapWithExtras(component, {
node,
defaultSupplier: () => (defaultValue && defaultValue) || 0,
onPropValueChange,
onPropValueRemove,
});
}
};

/** Editor for numbers */
var NumberEditor = ({ node, integer, onPropValueChange, onPropValueRemove, wrapWithExtras }) => {
const defaultValue = node.value;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,19 @@ public List<String> getSuggestions(List<String> path) {
* Returns the names of the properties in a given path
*
* @param propertyPath The path to a property one wants to recieve the properties of
*
* @return The names of the properties of the given path as list
*/
private List<String> collectProperties(List<String> propertyPath) {
Type endType = PropertyPathHelper.getPathEndType(propertyPath, InspectitConfig.class);
if (CollectionUtils.isEmpty(propertyPath) || ((propertyPath.size() == 1) && propertyPath.get(0).equals(""))) {
return getProperties(InspectitConfig.class);
}
if (endType instanceof Class<?> && ((Class<?>) endType).isEnum()) {
return Arrays.stream(((Class<?>) endType).getEnumConstants())
.map(Object::toString)
.collect(Collectors.toList());
}
if (endType == null || PropertyPathHelper.isTerminal(endType) || PropertyPathHelper.isListOfTerminalTypes(endType) || !(endType instanceof Class<?>)) {
return Collections.emptyList();
}
Expand All @@ -50,6 +56,7 @@ private List<String> collectProperties(List<String> propertyPath) {
* Return the properties of a given class
*
* @param beanClass the class one wants the properties of
*
* @return the properties of the given class
*/
@VisibleForTesting
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,7 @@ void checkFirstLevel() {

List<String> result = completer.getSuggestions(input);

assertThat(result).containsExactlyInAnyOrder(
"actions",
"data",
"exclude-lambdas",
"ignored-bootstrap-packages",
"ignored-packages",
"internal",
"rules",
"scopes",
"special");
assertThat(result).containsExactlyInAnyOrder("actions", "data", "exclude-lambdas", "ignored-bootstrap-packages", "ignored-packages", "internal", "rules", "scopes", "special");
}

@Test
Expand All @@ -61,21 +52,7 @@ void pastList() {

List<String> result = completer.getSuggestions(input);

assertThat(result).containsExactlyInAnyOrder(
"config",
"env",
"exporters",
"instrumentation",
"logging",
"metrics",
"plugins",
"privacy",
"publish-open-census-to-bootstrap",
"self-monitoring",
"service-name",
"tags",
"thread-pool-size",
"tracing");
assertThat(result).containsExactlyInAnyOrder("config", "env", "exporters", "instrumentation", "logging", "metrics", "plugins", "privacy", "publish-open-census-to-bootstrap", "self-monitoring", "service-name", "tags", "thread-pool-size", "tracing");
}

@Test
Expand All @@ -87,6 +64,19 @@ void endsInWildcard() {
assertThat(result).isEmpty();
}

@Test
void endsInEnum() {
List<String> input = Arrays.asList("inspectit", "tracing", "add-common-tags");

List<String> result = completer.getSuggestions(input);

assertThat(result).hasSize(4);
assertThat(result).contains("NEVER");
assertThat(result).contains("ON_GLOBAL_ROOT");
assertThat(result).contains("ON_LOCAL_ROOT");
assertThat(result).contains("ALWAYS");
}

@Test
void propertyIsPresentAndReadMethodIsNull() {
List<String> input = Arrays.asList("inspectit", "instrumentation", "data", "method_duration", "is-tag");
Expand Down Expand Up @@ -149,21 +139,7 @@ public class GetProperties {
void getPropertiesInspectit() {
List<String> result = completer.getProperties(InspectitConfig.class);

assertThat(result).containsExactlyInAnyOrder(
"config",
"env",
"exporters",
"instrumentation",
"logging",
"metrics",
"plugins",
"privacy",
"publish-open-census-to-bootstrap",
"self-monitoring",
"service-name",
"tags",
"thread-pool-size",
"tracing");
assertThat(result).containsExactlyInAnyOrder("config", "env", "exporters", "instrumentation", "logging", "metrics", "plugins", "privacy", "publish-open-census-to-bootstrap", "self-monitoring", "service-name", "tags", "thread-pool-size", "tracing");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,14 @@ public class PropertyPathHelper {
* A HashSet of classes which are used as wildcards in the search for properties. If a found class matches one of these
* classes, the end of the property path is reached. Mainly used in the search of maps
*/
private static final HashSet<Class<?>> TERMINAL_TYPES = new HashSet<>(Arrays.asList(Object.class, String.class, Integer.class, Long.class,
Float.class, Double.class, Character.class, Void.class,
Boolean.class, Byte.class, Short.class, Duration.class, Path.class, URL.class, FileSystemResource.class));
private static final HashSet<Class<?>> TERMINAL_TYPES = new HashSet<>(Arrays.asList(Object.class, String.class, Integer.class, Long.class, Float.class, Double.class, Character.class, Void.class, Boolean.class, Byte.class, Short.class, Duration.class, Path.class, URL.class, FileSystemResource.class));

/**
* Returns the type which can be found at the end of the path. Returns null if the path does not exist
*
* @param propertyNames The list of properties one wants to check
* @param type The type in which the current top-level properties should be found
*
* @return The type which can be found at the end of the path. Returns null if the path does not exist
*/
public Type getPathEndType(List<String> propertyNames, Type type) {
Expand All @@ -57,6 +56,7 @@ public Type getPathEndType(List<String> propertyNames, Type type) {
*
* @param propertyNames List of property names
* @param mapValueType The type which is given as value type of a map
*
* @return True: The type exists <br> False: the type does not exists
*/
Type getTypeInMap(List<String> propertyNames, Type mapValueType) {
Expand All @@ -72,6 +72,7 @@ Type getTypeInMap(List<String> propertyNames, Type mapValueType) {
*
* @param propertyNames List of property names
* @param listValueType The type which is given as value type of a list
*
* @return True: The type exists <br> False: the type does not exists
*/
Type getTypeInList(List<String> propertyNames, Type listValueType) {
Expand All @@ -83,14 +84,14 @@ Type getTypeInList(List<String> propertyNames, Type listValueType) {
*
* @param propertyNames List of property names
* @param beanType The bean through which should be searched
*
* @return True: the property and all other properties exists <br> False: At least one of the properties does not exist
*/
private Type getTypeInBean(List<String> propertyNames, Class<?> beanType) {
String propertyName = CaseUtils.kebabCaseToCamelCase(propertyNames.get(0));
Optional<PropertyDescriptor> foundProperty =
Arrays.stream(BeanUtils.getPropertyDescriptors(beanType))
.filter(descriptor -> CaseUtils.compareIgnoreCamelOrKebabCase(propertyName, descriptor.getName()))
.findFirst();
Optional<PropertyDescriptor> foundProperty = Arrays.stream(BeanUtils.getPropertyDescriptors(beanType))
.filter(descriptor -> CaseUtils.compareIgnoreCamelOrKebabCase(propertyName, descriptor.getName()))
.findFirst();
if (foundProperty.isPresent()) {
Type propertyType;
Method writeMethod = foundProperty.get().getWriteMethod();
Expand All @@ -108,16 +109,16 @@ private Type getTypeInBean(List<String> propertyNames, Class<?> beanType) {
* Checks if a given type is a terminal type or an enum.
*
* @param type The type to be checked.
*
* @return True: the given type is a terminal or an enum. False: the given type is neither a terminal type nor an enum.
*/
public boolean isTerminal(Type type) {
if (TERMINAL_TYPES.contains(type)) {
return true;
} else if (type instanceof Class) {
return ((Class<?>) type).isEnum()
|| ((Class<?>) type).isPrimitive()
|| ApplicationConversionService.getSharedInstance().canConvert(String.class, (Class<?>) type)
|| ApplicationConversionService.getSharedInstance().canConvert(Number.class, (Class<?>) type);
return ((Class<?>) type).isEnum() || ((Class<?>) type).isPrimitive() || ApplicationConversionService.getSharedInstance()
.canConvert(String.class, (Class<?>) type) || (ApplicationConversionService.getSharedInstance()
.canConvert(Number.class, (Class<?>) type));
}
return false;
}
Expand All @@ -127,6 +128,7 @@ public boolean isTerminal(Type type) {
* Every class which is no Collection, Map or terminal (see {@link #isTerminal(Type)} is classified as POJO.
*
* @param type the type to check
*
* @return true if the given type is a pojo.
*/
public boolean isBean(Type type) {
Expand All @@ -149,6 +151,7 @@ public boolean isBean(Type type) {
* Checks if a given type is a list of terminal types
*
* @param type
*
* @return True: the given type is a list of a terminal type. False: either the given type is not a list or not a list of terminal types
*/
public boolean isListOfTerminalTypes(Type type) {
Expand All @@ -161,13 +164,13 @@ public boolean isListOfTerminalTypes(Type type) {
return false;
}


/**
* This method takes an array of strings and returns each entry as ArrayList containing the parts of each element.
* <p>
* 'inspectit.hello-i-am-testing' would be returned as {'inspectit', 'helloIAmTesting'}
*
* @param propertyName A String containing the property path
*
* @return a List containing containing the parts of the property path as String
*/
public List<String> parse(String propertyName) {
Expand All @@ -190,6 +193,7 @@ public List<String> parse(String propertyName) {
*
* @param propertyName A String with the path of a property
* @param result Reference to the list in which the extracted expressions should be saved in
*
* @return the remaining expression
*/
private String extractExpression(String propertyName, List<String> result) {
Expand Down Expand Up @@ -237,13 +241,13 @@ private String removeLeadingDot(String string) {
}
}


/**
* Checks if two paths are the same. If one path uses the Wildcard "*", the check with the corresponding literal in the
* other path return true.
*
* @param pathA the first path to be compared
* @param pathB the second path to be compared
*
* @return
*/
public boolean comparePaths(List<String> pathA, List<String> pathB) {
Expand All @@ -265,6 +269,7 @@ public boolean comparePaths(List<String> pathA, List<String> pathB) {
*
* @param a the first path to be compared
* @param a the second path to be compared
*
* @return Returns true if each String in the two paths is equal.
*/
public boolean comparePathsIgnoreCamelOrKebabCase(List<String> a, List<String> b) {
Expand All @@ -279,7 +284,6 @@ public boolean comparePathsIgnoreCamelOrKebabCase(List<String> a, List<String> b
return true;
}


/**
* Checks if the first given path starts with the second given full path
* <p>
Expand All @@ -289,6 +293,7 @@ public boolean comparePathsIgnoreCamelOrKebabCase(List<String> a, List<String> b
*
* @param path The path you want to check
* @param prefix The prefix the other path should begin with
*
* @return
*/
public boolean hasPathPrefix(List<String> path, List<String> prefix) {
Expand Down

0 comments on commit 36c6c59

Please sign in to comment.