diff --git a/ui/app/components/app-footer.hbs b/ui/app/components/app-footer.hbs index 00bbee18486a..cdf2f1244d63 100644 --- a/ui/app/components/app-footer.hbs +++ b/ui/app/components/app-footer.hbs @@ -13,6 +13,7 @@ + {{/if}} diff --git a/ui/app/controllers/vault/cluster/showcase.js b/ui/app/controllers/vault/cluster/showcase.js new file mode 100644 index 000000000000..a048cfd1889a --- /dev/null +++ b/ui/app/controllers/vault/cluster/showcase.js @@ -0,0 +1,304 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +import Controller from '@ember/controller'; +import { action } from '@ember/object'; +// import { tracked } from '@glimmer/tracking'; + +export default class ShowcaseController extends Controller { + // ########################################### + // FORMFIELD + // ########################################### + + jsonExample = JSON.stringify( + { type: 'JSON', text: 'Lorem ipsum', value: 1234, object: { a: 'b', c: [9, 8, 7] } }, + null, + 2 + ); + + @action + dynamicFormFieldModel(editType, variant) { + const model = { + // emulate model's setter + set: () => {}, + }; + if (editType === 'boolean') { + if (variant === 'checked' || variant === 'disabled') { + model.boolean = true; + } + } else if (editType === 'checkboxList') { + model.checkboxList = 'Ipsum'; + } else if (editType === 'dateTimeLocal') { + if (variant === 'with value' || variant === 'with validation error') { + model.dateTimeLocal = new Date(); + } + } else if (editType === 'input') { + if (variant === 'with value' || variant === 'with validation error') { + model.input = 'Lorem ipsum dolor sit amet'; + } else if (variant === 'with character limit') { + model.input = '123456789'; + } else if (variant === 'readonly' || variant === 'disabled') { + model.input = 'Lorem ipsum'; + } + } else if (editType === 'json') { + model.json = '{}'; + if (variant === 'with value' || variant === 'with value + restore') { + model.json = this.jsonExample; + } + } else if (editType === 'kv') { + if (variant === 'with value' || variant === 'with validation error') { + model.kv = { + 'my-key': 'This is the value for `my-key`', + }; + } else if (variant === 'with whitespace in key') { + model.kv = { + 'my key with space': 'You need to set `@allowWhiteSpace` to avoid this warning', + }; + } + } else if (editType === 'mountAccessor') { + if (variant === 'with value') { + // TODO! not sure what to use here, it's too connected with the `authMethods` task (see the `mount-accessor-select` controller) + model.mountAccessor = '???'; + } + } else if (editType === 'object') { + model.object = '{}'; + if (variant === 'with value' || variant === 'with value + restore') { + model.object = { type: 'object', text: 'Hello world', number: 4567, array: ['a', 'b', 'c'] }; + } + } else if (editType === 'optionalText') { + if (variant === 'with value' || variant === 'with value + subText + docLink') { + model.optionalText = 'Lorem ipsum'; + } + } else if (editType === 'password') { + if (variant === 'with value') { + model.password = '123abc'; + } + } else if (editType === 'radio') { + model.radio = 2; + } else if (editType === 'regex') { + if (variant === 'with value') { + model.regex = '/^lorem .*$/i'; + } + } else if (editType === 'stringArray') { + if (variant === 'with value') { + model.stringArray = ['Lorem', 'Ipsum']; + } + } else if (editType === 'searchSelect') { + if (variant === 'with value') { + model.searchSelect = ['Lorem', 'Ipsum']; + } + } else if (editType === 'select') { + if (variant === 'with value') { + model.select = 'Ipsum'; + } + } else if (editType === 'sensitive') { + if (variant === 'with value' || variant === 'with copy') { + model.sensitive = 'Lorem ipsum dolor'; + } + } else if (editType === 'textarea') { + if (variant === 'with value' || variant === 'with validation error') { + model.textarea = 'Lorem\nipsum\ndolor'; + } + } else if (editType === 'ttl') { + if (variant === 'with value' || variant === 'with validation error') { + model.ttl = 123; + } else if (variant === 'with value 0s') { + model.ttl = '0s'; + } else if (variant === 'with value 1h') { + model.ttl = '1h'; + } + } + return model; + } + + @action + dynamicFormFieldModelValidations(editType, variant) { + const modelValidations = {}; + if (editType === 'checkboxList') { + if (variant === 'with validation error') { + modelValidations.checkboxList = { + isValid: false, + errors: ['This is the validation error message #1', 'This is the validation error message #2'], + }; + } + } else if (editType === 'dateTimeLocal') { + if (variant === 'with validation error') { + modelValidations.dateTimeLocal = { + isValid: false, + errors: ['This is the validation error message #1', 'This is the validation error message #2'], + }; + } + } else if (editType === 'file') { + if (variant === 'with validation error') { + // NOTICE! this generates a double error message, it's a bug in the code (error is already output by the `FormField`, see line 374, but is also output by `TextFile` via the argument `@validationError`) + modelValidations.file = { + isValid: false, + errors: ['This is the validation error message #1', 'This is the validation error message #2'], + }; + } + } else if (editType === 'kv') { + if (variant === 'with validation error') { + modelValidations.kv = { + isValid: false, + errors: ['This is the validation error message #1', 'This is the validation error message #2'], + }; + } + } else if (editType === 'stringArray') { + if (variant === 'with validation error') { + modelValidations.stringArray = { + isValid: false, + errors: ['This is the validation error message #1', 'This is the validation error message #2'], + }; + } + } else if (editType === 'select') { + if (variant === 'with validation error') { + modelValidations.select = { + isValid: false, + errors: ['This is the validation error message #1', 'This is the validation error message #2'], + }; + } + } else if (editType === 'textarea') { + if (variant === 'with validation error') { + modelValidations.textarea = { + isValid: false, + errors: ['This is the validation error message #1', 'This is the validation error message #2'], + }; + } + } else if (editType === 'ttl') { + if (variant === 'with validation error') { + // NOTICE: there is a bug in the CSS for the class "ttl-picker-form-field-error" because the border color is applied to the `input` child, but such element is hidden so the red border is not visible! + modelValidations.ttl = { + isValid: false, + errors: ['This is the validation error message #1', 'This is the validation error message #2'], + }; + } + } + return modelValidations; + } + + @action + dynamicFormFieldOptionsModels(editType) { + let optionsModels = []; + if (editType === 'searchSelect') { + // TODO! find which is the right format for this + optionsModels = []; + } + return optionsModels; + } + + @action + dynamicFormFieldOptionsPossibleValues(editType, variant) { + let possibleValues = []; + if (editType === 'checkboxList') { + possibleValues = ['Lorem', 'Ipsum', 'Dolor']; + } else if (editType === 'radio') { + possibleValues = [ + { + label: 'One', + value: 1, + }, + { + label: 'Two', + value: 2, + }, + { + label: 'Three', + value: 3, + }, + ]; + if (variant === 'with item subText') { + possibleValues.forEach((i) => (i.subText = `Subtext for ${i.label.toLowerCase()}`)); + } + if (variant === 'with item helpText') { + possibleValues.forEach((i) => (i.helpText = `Helptext for ${i.label.toLowerCase()}`)); + } + } else if (editType === 'select') { + possibleValues = ['Lorem', 'Ipsum', 'Dolor']; + } + return possibleValues; + } + + @action + dynamicFormFieldOptionsFieldValue(editType) { + let fieldValue; + if (editType === 'checkboxList') { + fieldValue = 'checkboxList'; + } else if (editType === 'input') { + fieldValue = 'input'; + } else if (editType === 'optionalText') { + fieldValue = 'optionalText'; + } else if (editType === 'radio') { + fieldValue = 'radio'; + } else if (editType === 'regex') { + fieldValue = 'regex'; + } + return fieldValue; + } + + // ########################################### + // READONLY-FORMFIELD + // ########################################### + + @action + dynamicReadonlyFormFieldValue(attrType, variant) { + let value; + // if (attrType === 'select') { + // } else { + // } + if (variant === 'with value' || variant === 'with value + helpText + subText') { + value = 'Lorem ipsum'; + } + return value; + } + + // ########################################### + // OBJECT-LIST-INPUT + // ########################################### + + @action + dynamicObjectListInputObjectKeys(variant) { + // any variant needs the definition of the object keys + if (variant) { + return [ + { label: 'Label for input A', key: 'A', placeholder: 'Placeholder for A' }, + { label: 'Label for input B', key: 'B', placeholder: 'Placeholder for B' }, + { label: 'Label for input C', key: 'C' }, + ]; + } + } + + @action + dynamicObjectListInputInputValue(variant) { + let inputValue = []; + if (variant === 'with single set of values') { + inputValue = [{ A: 'First value for A', B: 'First value for B', C: '' }]; + } else if (variant === 'with multiple sets of values' || variant === 'with validation error') { + inputValue = [ + { A: 'First value for A', B: 'First value for B', C: '' }, + { A: 'Second value for A', B: 'Second value for B', C: '' }, + ]; + } + return inputValue; + } + + @action + dynamicObjectListInputValidationErrors(variant) { + let validationErrors = []; + if (variant === 'with validation error') { + validationErrors = [ + { A: { errors: ['Error message for first A'], isValid: false } }, + { B: { errors: ['Error message for second B'], isValid: false } }, + ]; + } + return validationErrors; + } + + // ########################################### + // OTHER + // ########################################### + + @action + noop() {} +} diff --git a/ui/app/router.js b/ui/app/router.js index 0c597e1192e7..d38071c40e4e 100644 --- a/ui/app/router.js +++ b/ui/app/router.js @@ -14,6 +14,13 @@ export default class Router extends EmberRouter { Router.map(function () { this.route('vault', { path: '/' }, function () { this.route('cluster', { path: '/:cluster_name' }, function () { + // ---------------------- + // ---------------------- + // Form Elements - Showcase + // ---------------------- + this.route('showcase'); + // ---------------------- + // ---------------------- this.route('dashboard'); this.mount('config-ui'); this.mount('sync'); diff --git a/ui/app/styles/app.scss b/ui/app/styles/app.scss index 8a0a248c7d45..f14072db44b6 100644 --- a/ui/app/styles/app.scss +++ b/ui/app/styles/app.scss @@ -8,6 +8,7 @@ @import 'ember-power-select'; @import './core'; @import './docs'; +@import './showcase'; @mixin font-face($name) { @font-face { diff --git a/ui/app/styles/showcase.scss b/ui/app/styles/showcase.scss new file mode 100644 index 000000000000..865f18bd6ba0 --- /dev/null +++ b/ui/app/styles/showcase.scss @@ -0,0 +1,87 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +/* SHOWCASE OF COMPONENTS */ + +.shw-page-wrapper { + padding: 24px 48px; + + /* ------------------ */ + + /* local styling for showcase text elements [intentionally scrappy but simple to use] */ + + h1:not([class]) { + font-family: $family-sans; + font-size: 48px; + line-height: 1.2em; + font-weight: 900; + color: #000; + margin: 24px 0 8px; + } + + h2:not([class]) { + font-family: $family-sans; + font-size: 32px; + line-height: 1.2em; + font-weight: 700; + color: #000; + margin: 24px 0 8px; + } + + h3:not([class]) { + font-family: $family-sans; + font-size: 24px; + line-height: 1.2em; + font-weight: bold; + color: #000; + margin: 24px 0 8px; + } + + h4:not([class]) { + font-family: $family-sans; + font-size: 16px; + line-height: 1.2em; + font-weight: bold; + color: #000; + margin: 24px 0 8px; + } + + pre:not([class]) { + font-family: $family-monospace; + font-size: 11px; + line-height: 1.2em; + font-weight: bold; + color: #999; + margin: 24px 0 4px; + } + + blockquote:not([class]) { + display: block; + font-family: $family-monospace; + font-size: 12px; + line-height: 1.4em; + color: #666; + font-style: italic; + margin: 24px 0 32px; + padding-left: 12px; + border-left: 2px solid #ccc; + } + + hr:not([class]) { + border: none; + background: none; + border-top: 1px solid #bac1cc; + margin: 48px 0; + } + + hr:not([class])[level='2'] { + border: none; + background: none; + border-top: 1px dotted #bac1cc; + margin: 36px 0; + } + + /* ------------------ */ +} diff --git a/ui/app/templates/vault/cluster/showcase.hbs b/ui/app/templates/vault/cluster/showcase.hbs new file mode 100644 index 000000000000..4e3eda38ef47 --- /dev/null +++ b/ui/app/templates/vault/cluster/showcase.hbs @@ -0,0 +1,618 @@ +{{! + Copyright (c) HashiCorp, Inc. + SPDX-License-Identifier: BUSL-1.1 +~}} + +
+ +

Vault Components Showcase

+ +
+ +

Form Components

+ +

FormFieldLabel

+ +
FormFieldLabel components add labels and descriptions to inputs
+ +
Only Label
+ + +
Label with HelpText
+ + +
Only SubText
+ + +
SubText with DocLink
+ + +
Label + SubText
+ + +
Label with HelpText + SubText with DocLink
+ + +
+ +

FormField

+ +
FormField components are field elements associated with a particular model.
+ + {{#let + (array + (hash editType="boolean" variants=(array "base" "with label" "with helpText + subText + docLink" "checked" "disabled")) + (hash + editType="checkboxList" + variants=(array "base" "with label" "with helpText + subText + docLink" "with validation error") + ) + (hash + editType="dateTimeLocal" + variants=(array "base" "with label" "with helpText + subText + docLink" "with value" "with validation error") + ) + (hash editType="file" variants=(array "base" "with label" "with helpText + subText + docLink" "with validation error")) + (hash + editType="input" + attrType="string" + variants=(array + "base" + "with label" + "with helpText + subText + docLink" + "with placeholder" + "with value" + "with character limit" + "with validation error" + "readonly" + "disabled" + ) + ) + (hash + editType="json" + attrType="string" + variants=(array "base" "with label" "with helpText" "with value" "with value + restore") + ) + (hash + editType="kv" + variants=(array + "base" + "with label" + "with label as section header" + "with helpText + subText" + "with key/value placeholders" + "single row" + "with value" + "with validation error" + "with whitespace in key" + ) + ) + (hash editType="mountAccessor" variants=(array "base" "with label" "with helpText" "with value")) + (hash editType="object" attrType="object" variants=(array "base" "with label" "with helpText" "with value")) + (hash + editType="optionalText" + variants=(array + "base" "with label" "with subText + docLink" "with default subText" "with value" "with value + subText + docLink" + ) + ) + (hash + editType="password" + attrType="string" + variants=(array "base" "with label" "with helpText + subText + docLink" "with value") + ) + (hash + editType="radio" + variants=(array + "base" "with label" "with helpText + subText + docLink" "with item subText" "with item helpText" "disabled" + ) + ) + (hash editType="regex" variants=(array "base" "with label" "with helpText + subText + docLink" "with value")) + (hash + editType="searchSelect" + variants=(array "base" "with label" "with label as section header" "with helpText + subText" "with value") + ) + (hash + editType="select" + variants=(array "base" "with label" "with helpText + subText" "with no default" "with value" "with validation error") + ) + (hash editType="stringArray" variants=(array "base" "with label" "with helpText + subText" "with value")) + (hash editType="sensitive" variants=(array "base" "with label" "with value" "with copy")) + (hash + editType="textarea" + attrType="string" + variants=(array "base" "with label" "with helpText + subText + docLink" "with value" "with validation error") + ) + (hash + editType="ttl" + variants=(array + "base" + "with helpText" + "with custom helper texts" + "with value" + "with value 0s" + "with value 1h" + "with validation error" + "with hidden toggle" + ) + ) + (hash editType="yield" variants=(array "yielded content")) + ) + as |FormFieldTypes| + }} + {{#each FormFieldTypes as |FormFieldType index|}} + {{#unless (eq index 0)}} +
+ {{/unless}} + {{#let FormFieldType.editType FormFieldType.attrType FormFieldType.variants as |editType attrType variants|}} +

Type "{{editType}}"

+ {{#each variants as |variant|}} + {{#let + (fn this.dynamicFormFieldOptionsModels editType) + (fn this.dynamicFormFieldOptionsPossibleValues editType variant) + (fn this.dynamicFormFieldOptionsFieldValue editType) + as |dynamicFormFieldOptionsModels dynamicFormFieldOptionsPossibleValues dynamicFormFieldOptionsFieldValue| + }} +
{{capitalize variant}}
+ + {{/let}} + {{/each}} + {{/let}} + {{/each}} + {{/let}} + +
+ +

FormError

+ +
Yielded text
+ This is the error text + +
+ +

FormSaveButtons

+ +
Save
+ + +
Save / Loading state (isSaving=true)
+ + +
Save + Cancel (onCancel)
+ + +
Save + Cancel (cancelLinkParams)
+ + +
Save (isSaving=true / saveButtonText="Loading") + Cancel
+ + +
Without top border (includeBox=false)
+ + +
+ +

ReadonlyFormField

+ + {{#let + (array + (hash + attrType="text" + variants=(array "base" "with label" "with helpText + subText" "with value" "with value + helpText + subText") + ) + (hash + attrType="password" + variants=(array "base" "with label" "with helpText + subText" "with value" "with value + helpText + subText") + ) + (hash + attrType="date" + variants=(array "base" "with label" "with helpText + subText" "with value" "with value + helpText + subText") + ) + (hash + attrType="select" + variants=(array "base" "with label" "with helpText + subText" "with value" "with value + helpText + subText") + ) + ) + as |ReadonlyFormFieldTypes| + }} + {{#each ReadonlyFormFieldTypes as |ReadonlyFormFieldType index|}} + {{#unless (eq index 0)}} +
+ {{/unless}} + {{#let ReadonlyFormFieldType.attrType ReadonlyFormFieldType.variants as |attrType variants|}} +

Input type="{{attrType}}"

+ {{#each variants as |variant|}} +
{{capitalize variant}}
+ + {{/each}} + {{/let}} + {{/each}} + {{/let}} + +
+ +

FormFieldGroups

+ +

TODO

+ +
+ +

FormFieldGroupsLoop

+ +

TODO

+ +
+ +

FormFieldGroupFromModel

+ +

TODO

+ +
+ +

AutocompleteInput

+ + {{#let (array "base" "with label" "with subText" "with placeholder" "with value") as |variants index|}} + {{#each variants as |variant|}} + {{#unless (eq index 0)}} +
+ {{/unless}} +
{{capitalize variant}}
+ + {{/each}} + {{/let}} + +
+ +

CommandInput

+ +

Not used in the codebase

+ +
+ +

EnableInput

+ +
EnableInput components render a disabled input with a hardcoded masked value beside an "Edit" button to + "enable" the input. Clicking "Edit" hides the disabled input and renders the yielded component. This way any data + management is handled by the parent. These are useful for editing inputs of sensitive values not returned by the API. The + extra click ensures the user is intentionally editing the field.
+ +
With generic input / with Input placeholder (@label)
+
+ + + +
+ +
With generic input / with ReadonlyFormField placeholder (@attr)
+
+ + + +
+ +
+ +

FilterInput

+ +
Base
+ + +
With value
+ + +
With icon hidden
+ + +
+ +

InputSearch

+ +
This component renders an input that fires a callback on "keyup" containing the input's value
+ +
Base
+ + +
With label
+ + +
With subText
+ + +
With placeholder
+ + +
With value
+ + +
+ +

KvSuggestionInput

+ +
Input component that fetches secrets at a provided mount path and displays them as suggestions in a dropdown. + As the user types the result set will be filtered providing suggestions for the user to select. After the input debounce + wait time (500ms), if the value ends in a slash, secrets will be fetched at that path. The new result set will then be + displayed in the dropdown as suggestions for the newly inputted path. Selecting a suggestion will append it to the input + value. This allows the user to build a full path to a secret for the provided mount. This is useful for helping the user + find deeply nested secrets given the path based policy system. If the user does not have list permission they are still + able to enter a path to a secret but will not see suggestions. Input is disabled when mount path is not provided
+ + {{#let (array "base" "with label" "with subText" "no mountpath") as |variants index|}} + {{#each variants as |variant|}} + {{#unless (eq index 0)}} +
+ {{/unless}} +
{{capitalize variant}}
+ + {{/each}} + {{/let}} + +
+ +

MaskedInput

+ +
MaskedInput components are textarea inputs where the input is hidden. They are used to enter sensitive + information like passwords.
+ + {{#let + (array + "base" + "with label" + "with value" + "allow download" + "allow copy + download" + "display only" + "display only with value" + "display only with empty string as value" + ) + as |variants index| + }} + {{#each variants as |variant|}} + {{#unless (eq index 0)}} +
+ {{/unless}} +
{{capitalize variant}}
+ + {{/each}} + {{/let}} + +
+ +

NavigateInput

+ +
NavigateInput components are used to filter list data.
+ + {{#let (array "base" "with placeholder" "with value") as |variants index|}} + {{#each variants as |variant|}} + {{#unless (eq index 0)}} +
+ {{/unless}} +
{{capitalize variant}}
+ + {{/each}} + {{/let}} + +
+ +

ObjectListInput

+ +
ObjectListInput components are used to render a variable number of text inputs in a single row with an "Add" + button at the end of the row. Clicking 'add' generates a new row of empty inputs. Each input field is generated by an + object in the @objectKeys array. Labels render above each column.
+ + {{#let + (array "base" "with single set of values" "with multiple sets of values" "with validation error") + as |variants index| + }} + {{#each variants as |variant|}} + {{#unless (eq index 0)}} +
+ {{/unless}} +
{{capitalize variant}}
+ + {{/each}} + {{/let}} + +
+ +

Select

+ + {{#let (array "base" "with label" "with no default" "with selected value" "inline" "full width") as |variants index|}} + {{#each variants as |variant|}} + {{#unless (eq index 0)}} +
+ {{/unless}} +
{{capitalize variant}}
+