8. Components
Components are a set of features which are either build-in or something you can re-use or extend for your own needs. Components in question are either built-in commands or UI side components providing higher level features within commands itself.
8.1. Flow
When you use Flow Components to build something that involves
use of a multiple components, your implementation may become a bit cluttered.
To ease these use cases, we added a
ComponentFlow
that can hook multiple component executions together
as a “flow”.
The following listings show examples of flows and their output in a shell:
static class FlowSampleComplex {
@Autowired
private ComponentFlow.Builder componentFlowBuilder;
public void runFlow() {
Map<String, String> single1SelectItems = new HashMap<>();
single1SelectItems.put("key1", "value1");
single1SelectItems.put("key2", "value2");
List<SelectItem> multi1SelectItems = Arrays.asList(SelectItem.of("key1", "value1"),
SelectItem.of("key2", "value2"), SelectItem.of("key3", "value3"));
ComponentFlow flow = componentFlowBuilder.clone().reset()
.withStringInput("field1")
.name("Field1")
.defaultValue("defaultField1Value")
.and()
.withStringInput("field2")
.name("Field2")
.and()
.withConfirmationInput("confirmation1")
.name("Confirmation1")
.and()
.withPathInput("path1")
.name("Path1")
.and()
.withSingleItemSelector("single1")
.name("Single1")
.selectItems(single1SelectItems)
.and()
.withMultiItemSelector("multi1")
.name("Multi1")
.selectItems(multi1SelectItems)
.and()
.build();
flow.run();
}
}
Normal execution order of a components is same as defined with a builder. It’s
possible to conditionally choose where to jump in a flow by using a next
function and returning target component id. If this returned id is aither null
or doesn’t exist flow is essentially stopped right there.
static class FlowSampleConditional {
@Autowired
private ComponentFlow.Builder componentFlowBuilder;
public void runFlow() {
Map<String, String> single1SelectItems = new HashMap<>();
single1SelectItems.put("Field1", "field1");
single1SelectItems.put("Field2", "field2");
ComponentFlow flow = componentFlowBuilder.clone().reset()
.withSingleItemSelector("single1")
.name("Single1")
.selectItems(single1SelectItems)
.next(ctx -> ctx.getResultItem().get().getItem())
.and()
.withStringInput("field1")
.name("Field1")
.defaultValue("defaultField1Value")
.next(ctx -> null)
.and()
.withStringInput("field2")
.name("Field2")
.defaultValue("defaultField2Value")
.next(ctx -> null)
.and()
.build();
flow.run();
}
}
The result from running a flow returns ComponentFlowResult , which you can
use to do further actions.
|
8.2. Flow Components
Starting from version 2.1.x, a new component model provides an easier way to create higher-level user interaction for the usual use cases, such as asking for input in various forms. These usually are just plain text input or choosing something from a list.
Templates for built-in components are in the
org/springframework/shell/component
classpath.
Built-in components generally follow this logic:
-
Enter a run loop for user input.
-
Generate component-related context.
-
Render the runtime status of a component state.
-
Exit.
-
Render the final status of a component state.
Flow gives better interface for defining the flow of components that are better suited for defining interactive command flows. |
8.2.1. Component Render
You can implement component rendering in either of two ways: fully
programmatically or by using a ANTLR Stringtemplate.
Strictly speaking, there is a simple Function
renderer interface
that takes Context
as an input and outputs a list of AttributedString
.
This lets you choose between templating and code.
Templating is a good choice if you do not need to do anything complex or you just want to slightly modify existing component layouts. Rendering through code then gives you flexibility to do whatever you need.
The programmatic way to render is to create a Function
:
class StringInputCustomRenderer implements Function<StringInputContext, List<AttributedString>> {
@Override
public List<AttributedString> apply(StringInputContext context) {
AttributedStringBuilder builder = new AttributedStringBuilder();
builder.append(context.getName());
builder.append(" ");
if (context.getResultValue() != null) {
builder.append(context.getResultValue());
}
else {
String input = context.getInput();
if (StringUtils.hasText(input)) {
builder.append(input);
}
else {
builder.append("[Default " + context.getDefaultValue() + "]");
}
}
return Arrays.asList(builder.toAttributedString());
}
}
Then you can hook it to a component:
@ShellMethod(key = "component stringcustom", value = "String input", group = "Components")
public String stringInputCustom(boolean mask) {
StringInput component = new StringInput(getTerminal(), "Enter value", "myvalue",
new StringInputCustomRenderer());
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
if (mask) {
component.setMaskCharacter('*');
}
StringInputContext context = component.run(StringInputContext.empty());
return "Got value " + context.getResultValue();
}
Components have their own context but usually share some functionality from a parent component types. The following tables show those context variables:
Key | Description |
---|---|
|
The value after a component renders its result. |
|
The name of a component — that is, its title. |
|
The possible message set for a component. |
|
The level of a message — one of |
|
Return |
|
Return |
|
Return |
|
The raw user input. |
Key | Description |
---|---|
|
The name of a component — that is, its title. |
|
The raw user input — mostly used for filtering. |
|
The full list of item states. |
|
The visible list of item states. |
|
Return |
|
The current cursor row in a selector. |
Key | Description |
---|---|
|
The width of terminal, type is Integer and defaults to NULL if not set. |
8.2.2. String Input
The string input component asks a user for simple text input, optionally masking values if the content contains something sensitive. The following listing shows an example:
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component string", value = "String input", group = "Components")
public String stringInput(boolean mask) {
StringInput component = new StringInput(getTerminal(), "Enter value", "myvalue");
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
if (mask) {
component.setMaskCharacter('*');
}
StringInputContext context = component.run(StringInputContext.empty());
return "Got value " + context.getResultValue();
}
}
The following image shows typical output from a string input component:
The context object is StringInputContext
. The following table lists its context variables:
Key | Description |
---|---|
|
The default value, if set. Otherwise, null. |
|
The masked input value |
|
The masked result value |
|
The mask character, if set. Otherwise, null. |
|
|
|
The parent context variables (see TextComponentContext Template Variables). |
8.2.3. Path Input
The path input component asks a user for a Path
and gives additional information about a path itself.
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component path input", value = "Path input", group = "Components")
public String pathInput() {
PathInput component = new PathInput(getTerminal(), "Enter value");
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
PathInputContext context = component.run(PathInputContext.empty());
return "Got value " + context.getResultValue();
}
}
The following image shows typical output from a path input component:
The context object is PathInputContext
. The following table describes its context variables:
Key | Description |
---|---|
|
The parent context variables (see TextComponentContext Template Variables). |
8.2.4. Path Search
The path search component asks base directory for scan and optional search expression.
Results are shown in a single select list where user can pick a path.
PathSearchConfig
can be used to customise component behaviour.
PathSearchConfig config = new PathSearch.PathSearchConfig();
config.setMaxPathsShow(5);
config.setMaxPathsSearch(100);
config.setSearchForward(true);
config.setSearchCaseSensitive(false);
config.setSearchNormalize(false);
PathSearch component = new PathSearch(getTerminal(), "Enter value", config);
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
PathSearchContext context = component.run(PathSearchContext.empty());
return "Got value " + context.getResultValue();
Logic for search is passed as is into algorithms documented in Search Algorithms. |
The following image shows typical output from a path search component:
The context object is PathSearchContext
. The following table describes its context variables:
Key | Description |
---|---|
|
The items available for rendering search results. |
|
The parent context variables (see TextComponentContext Template Variables). |
8.2.5. Confirmation
The confirmation component asks a user for a simple confirmation. It is essentially a yes-or-no question.
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component confirmation", value = "Confirmation input", group = "Components")
public String confirmationInput(boolean no) {
ConfirmationInput component = new ConfirmationInput(getTerminal(), "Enter value", !no);
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
ConfirmationInputContext context = component.run(ConfirmationInputContext.empty());
return "Got value " + context.getResultValue();
}
}
The following image shows the typical output from a confirmation component:
The context object is ConfirmationInputContext
. The following table describes its context variables:
Key | Description |
---|---|
|
The default value — either |
|
The parent context variables (see TextComponentContext Template Variables). |
8.2.6. Single Select
A single select component asks a user to choose one item from a list. It is similar to a simple dropbox implementation. The following listing shows an example:
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component single", value = "Single selector", group = "Components")
public String singleSelector() {
SelectorItem<String> i1 = SelectorItem.of("key1", "value1");
SelectorItem<String> i2 = SelectorItem.of("key2", "value2");
List<SelectorItem<String>> items = Arrays.asList(i1, i2);
SingleItemSelector<String, SelectorItem<String>> component = new SingleItemSelector<>(getTerminal(),
items, "testSimple", null);
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
SingleItemSelectorContext<String, SelectorItem<String>> context = component
.run(SingleItemSelectorContext.empty());
String result = context.getResultItem().flatMap(si -> Optional.ofNullable(si.getItem())).get();
return "Got value " + result;
}
}
The following image shows typical output for a single select component:
The context object is SingleItemSelectorContext
. The following table describes its context variables:
Key | Description |
---|---|
|
The returned value when the component exists. |
|
The visible items, where rows contains maps of name and selected items. |
|
The parent context variables (see SelectorComponentContext Template Variables). |
You can pre-select an item by defining it to get exposed. This is
useful if you know the default and lets the user merely press Enter
to make a choice.
The following listing sets a default:
SelectorItem<String> i1 = SelectorItem.of("key1", "value1");
SelectorItem<String> i2 = SelectorItem.of("key2", "value2");
List<SelectorItem<String>> items = Arrays.asList(i1, i2);
SingleItemSelector<String, SelectorItem<String>> component = new SingleItemSelector<>(getTerminal(),
items, "testSimple", null);
component.setDefaultExpose(i2);
8.2.7. Multi Select
The multi select component asks a user to select multiple items from a list. The following listing shows an example:
@ShellComponent
public class ComponentCommands extends AbstractShellComponent {
@ShellMethod(key = "component multi", value = "Multi selector", group = "Components")
public String multiSelector() {
List<SelectorItem<String>> items = new ArrayList<>();
items.add(SelectorItem.of("key1", "value1"));
items.add(SelectorItem.of("key2", "value2", false, true));
items.add(SelectorItem.of("key3", "value3"));
MultiItemSelector<String, SelectorItem<String>> component = new MultiItemSelector<>(getTerminal(),
items, "testSimple", null);
component.setResourceLoader(getResourceLoader());
component.setTemplateExecutor(getTemplateExecutor());
MultiItemSelectorContext<String, SelectorItem<String>> context = component
.run(MultiItemSelectorContext.empty());
String result = context.getResultItems().stream()
.map(si -> si.getItem())
.collect(Collectors.joining(","));
return "Got value " + result;
}
}
The following image shows a typical multi-select component:
The context object is MultiItemSelectorContext
. The following table describes its context variables:
Key | Description |
---|---|
|
The values returned when the component exists. |
|
The visible items, where rows contain maps of name, selected, on-row, and enabled items. |
|
The parent context variables (see SelectorComponentContext Template Variables). |