Ui: Add contributing pattern doc (#19897)

* format readme to prepare for pattern info

* small text changes

* add markdown files for each section

* readme updates

* routing md draft

* add table of contents

* add oidc pr sample

* update routing

* add decorator section

* serializer docs

* add table of contents

* update readme

* add title

* add decorator section

* models readme

* update comments

* modify examples

* add bullets and more comments

* what the heck fix bullet

* model docs

* form docs

* routing doc

* serializer/adapter

* adds docs for model-validations decorator (#20596)

* UI Docs: Components (#20602)

* Add CSS best practices (#20370)

* wip--saving work

* wip

* friday morning....

* update

* fix exists to exist

* one more change

* UI docs: Add ember engine creation documentation (#20789)

---------

Co-authored-by: Jordan Reimer <zofskeez@gmail.com>
Co-authored-by: Chelsea Shaw <82459713+hashishaw@users.noreply.github.com>
Co-authored-by: Angel Garbarino <Monkeychip@users.noreply.github.com>
Co-authored-by: Kianna <30884335+kiannaquach@users.noreply.github.com>
This commit is contained in:
claire bontempo 2023-05-30 10:24:35 -07:00 committed by GitHub
parent 0615a50674
commit ea292e8142
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 815 additions and 115 deletions

View File

@ -4,15 +4,19 @@
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Vault UI](#vault-ui)
- [Ember CLI Version Matrix](#ember-cli-version-matrix)
- [Ember CLI Version Upgrade Matrix](#ember-cli-version-upgrade-matrix)
- [Prerequisites](#prerequisites)
- [Running a Vault Server](#running-a-vault-server)
- [Running / Development](#running--development)
- [Running the UI locally](#running-the-ui-locally)
- [Mirage](#mirage)
- [Building Vault UI into a Vault Binary](#building-vault-ui-into-a-vault-binary)
- [Development](#development)
- [Quick commands](#quick-commands)
- [Code Generators](#code-generators)
- [Running Tests](#running-tests)
- [Linting](#linting)
- [Building Vault UI into a Vault Binary](#building-vault-ui-into-a-vault-binary)
- [Further Reading / Useful Links](#further-reading--useful-links)
- [Contributing / Best Practices](#contributing--best-practices)
- [Further Reading / Useful Links](#further-reading--useful-links)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
@ -20,7 +24,7 @@
This README outlines the details of collaborating on this Ember application.
## Ember CLI Version Matrix
## Ember CLI Version Upgrade Matrix
| Vault Version | Ember Version |
| ------------- | ------------- |
@ -34,11 +38,11 @@ This README outlines the details of collaborating on this Ember application.
You will need the following things properly installed on your computer.
* [Git](https://git-scm.com/)
* [Node.js](https://nodejs.org/)
* [Yarn](https://yarnpkg.com/)
* [Ember CLI](https://cli.emberjs.com/release/)
* [Google Chrome](https://google.com/chrome/)
- [Git](https://git-scm.com/)
- [Node.js](https://nodejs.org/)
- [Yarn](https://yarnpkg.com/)
- [Ember CLI](https://cli.emberjs.com/release/)
- [Google Chrome](https://google.com/chrome/)
In order to enforce the same version of `yarn` across installs, the `yarn` binary is included in the repo
in the `.yarn/releases` folder. To update to a different version of `yarn`, use the `yarn policies set-version VERSION` command. For more information on this, see the [documentation](https://yarnpkg.com/en/docs/cli/policies).
@ -46,57 +50,87 @@ in the `.yarn/releases` folder. To update to a different version of `yarn`, use
## Running a Vault Server
Before running Vault UI locally, a Vault server must be running. First, ensure
Vault dev is built according the the instructions in `../README.md`. To start a
single local Vault server:
Vault dev is built according the the instructions in `../README.md`.
- `yarn vault`
To start a local Vault cluster:
- `yarn vault:cluster`
- To start a single local Vault server: `yarn vault`
- To start a local Vault cluster: `yarn vault:cluster`
These commands may also be [aliased on your local device](https://github.com/hashicorp/vault-tools/blob/master/users/noelle/vault_aliases).
## Running / Development
## Running the UI locally
To get all of the JavaScript dependencies installed, run this in the `ui` directory:
To spin up the UI, a Vault server must be running (see previous step).
_All of the commands below assume you're in the `ui/` directory._
- `yarn`
> These steps will start an Ember CLI server that proxies requests to port 8200,
> and enable live rebuilding of the application as you change the UI application code.
> Visit your app at [http://localhost:4200](http://localhost:4200).
If you want to run Vault UI and proxy back to a Vault server running
on the default port, 8200, run the following in the `ui` directory:
1. Install dependencies:
- `yarn start`
`yarn`
This will start an Ember CLI server that proxies requests to port 8200,
and enable live rebuilding of the application as you change the UI application code.
Visit your app at [http://localhost:4200](http://localhost:4200).
2. Run Vault UI and proxy back to a Vault server running on the default port, 8200:
If your Vault server is running on a different port you can use the
long-form version of the npm script:
`yarn start`
> If your Vault server is running on a different port you can use the
> long-form version of the npm script:
`ember server --proxy=http://localhost:PORT`
To run yarn with mirage, do:
### Mirage
- `yarn start:mirage handlername`
[Mirage](https://miragejs.com/docs/getting-started/introduction/) can be helpful for mocking backend endpoints.
Look in [mirage/handlers](mirage/handlers/) for existing mocked backends.
Where `handlername` is one of the options exported in `mirage/handlers/index`
Run yarn with mirage: `yarn start:mirage handlername`
Where `handlername` is one of the options exported in [mirage/handlers/index](mirage/handlers/index.js)
## Building Vault UI into a Vault Binary
We use the [embed](https://golang.org/pkg/embed/) package from Go >1.20 to build
the static assets of the Ember application into a Vault binary.
This can be done by running these commands from the root directory:
`make static-dist`
`make dev-ui`
This will result in a Vault binary that has the UI built-in - though in
a non-dev setup it will still need to be enabled via the `ui` config or
setting `VAULT_UI` environment variable.
## Development
### Quick commands
| Command | Description |
| ------------------------------------- | ----------------------------------------------------------------------- |
| `yarn start` | start the app with live reloading |
| `yarn start:mirage <handler>` | start the app with the mocked mirage backend, with handler provided |
| `make static-dist && make dev-ui` | build a Vault binary with UI assets (run from root directory not `/ui`) |
| `ember g component foo -ir core` | generate a component in the /addon engine |
| `yarn test:quick -f='<test name>'` -s | run tests in the browser, filtering by test name |
| `yarn lint:js` | lint javascript files |
### Code Generators
Make use of the many generators for code, try `ember help generate` for more details. If you're using a component that can be widely-used, consider making it an `addon` component instead (see [this PR](https://github.com/hashicorp/vault/pull/6629) for more details)
eg. a reusable component named foo that you'd like in the core engine
eg. a reusable component named foo that you'd like in the core engine (read more about Ember engines [here](https://ember-engines.com/docs)).
- `ember g component foo --in lib/core`
- `echo "export { default } from 'core/components/foo';" > lib/core/app/components/foo.js`
- `ember g component foo -ir core`
The above command creates a template-only component by default. If you'd like to add a backing class, add the `-gc` flag:
- `ember g component foo -gc -ir core`
### Running Tests
Running tests will spin up a Vault dev server on port 9200 via a
Running tests will spin up a Vault dev server on port :9200 via a
pretest script that testem (the test runner) executes. All of the
acceptance tests then run, proxing requests back to that server.
acceptance tests then run, which proxy requests back to that server.
- `yarn run test:oss`
- `yarn run test:oss -s` to keep the test server running after the initial run.
@ -105,26 +139,28 @@ acceptance tests then run, proxing requests back to that server.
### Linting
- `yarn lint`
- `yarn lint:js`
- `yarn lint:hbs`
- `yarn lint:fix`
### Building Vault UI into a Vault Binary
### Contributing / Best Practices
We use the [embed](https://golang.org/pkg/embed/) package from Go 1.16+ to build
the static assets of the Ember application into a Vault binary.
Hello and thank you for contributing to the Vault UI! Below is a list of patterns we follow on the UI team to keep in mind when contributing to the UI codebase. This is an ever-evolving process, so we welcome any comments, questions or general feedback.
This can be done by running these commands from the root directory run:
`make static-dist`
`make dev-ui`
> **Remember** prefixing your branch name with `ui/` will run UI tests and skip the go tests. If your PR includes backend changes, _do not_ prefix your branch, instead add the `ui` label on github. This will trigger the UI test suite to run, in addition to the backend Go tests.
This will result in a Vault binary that has the UI built-in - though in
a non-dev setup it will still need to be enabled via the `ui` config or
setting `VAULT_UI` environment variable.
- [routing](docs/routing.md)
- [serializers/adapters](docs/serializers-adapters.md)
- [models](docs/models.md)
- [components](docs/components.md)
- [forms](docs/forms.md)
- [css](docs/css.md)
- [ember engines](docs/engines.md)
## Further Reading / Useful Links
* [ember.js](https://emberjs.com/)
* [ember-cli](https://cli.emberjs.com/release/)
* Development Browser Extensions
* [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi)
* [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/)
- [ember.js](https://emberjs.com/)
- [ember-cli](https://cli.emberjs.com/release/)
- Development Browser Extensions
- [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi)
- [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/)

View File

@ -7,70 +7,7 @@
import validators from 'vault/utils/validators';
import { get } from '@ember/object';
/**
* used to validate properties on a class
*
* decorator expects validations object with the following shape:
* { [propertyKeyName]: [{ type, options, message, level, validator }] }
* each key in the validations object should refer to the property on the class to apply the validation to
*
* type - string referring to the type of validation to apply -- must be exported from validators util for lookup
*
* options - an optional object for given validator -- min, max, nullable etc. -- see validators in util
*
* message - string added to the errors array and returned from the validate method if validation fails
* function may also be provided with model as single argument that returns a string
*
* level - optional string that defaults to 'error'. Currently the only other accepted value is 'warn'
*
* validator - function that may be used in place of type that is invoked in the validate method
* useful when specific validations are needed (dependent on other class properties etc.)
* must be passed as function that takes the class context (this) as the only argument and returns true or false
* each property supports multiple validations provided as an array -- for example, presence and length for string
*
* validations must be invoked using the validate method which is added directly to the decorated class
* const { isValid, state } = this.model.validate();
* isValid represents the validity of the full class -- if no properties provided in the validations object are invalid this will be true
* state represents the error state of the properties defined in the validations object
* const { isValid, errors } = state[propertyKeyName];
* isValid represents the validity of the property
* errors will be populated with messages defined in the validations object when validations fail. message must be a complete sentence (and include punctuation)
* since a property can have multiple validations, errors is always returned as an array
*
*** basic example
*
* import Model from '@ember-data/model';
* import withModelValidations from 'vault/decorators/model-validations';
*
* Notes: all messages need to have a period at the end of them.
* const validations = { foo: [{ type: 'presence', message: 'foo is a required field.' }] };
* @withModelValidations(validations)
* class SomeModel extends Model { foo = null; }
*
* const model = new SomeModel();
* const { isValid, state } = model.validate();
* -> isValid = false;
* -> state.foo.isValid = false;
* -> state.foo.errors = ['foo is a required field'];
*
*** example using custom validator
*
* const validations = { foo: [{ validator: (model) => model.bar.includes('test') ? model.foo : false, message: 'foo is required if bar includes test.' }] };
* @withModelValidations(validations)
* class SomeModel extends Model { foo = false; bar = ['foo', 'baz']; }
*
* const model = new SomeModel();
* const { isValid, state } = model.validate();
* -> isValid = false;
* -> state.foo.isValid = false;
* -> state.foo.errors = ['foo is required if bar includes test.'];
*
* *** example adding class in hbs file
*
* all form-validations need to have a red border around them. Add this by adding a conditional class 'has-error-border'
* class="input field {{if this.errors.name.errors 'has-error-border'}}"
*/
// see documentation at ui/docs/model-validations.md for detailed usage information
export function withModelValidations(validations) {
return function decorator(SuperClass) {
return class ModelValidations extends SuperClass {

61
ui/docs/components.md Normal file
View File

@ -0,0 +1,61 @@
# Writing and consuming components
Components can range from small, highly reusable "atoms" to large units with lots of business logic specific to one workflow or action. In any scenario, these are things to keep in mind while developing components for the Vault UI.
Please note that these guidelines are aspirational and you will see instances of antipatterns in the codebase. Many of these should be updated as we move forward. As with any ruleset, sometimes it is appropriate to break the rule.
## Page components for every route
Route templates should render a `Page` component, which includes breadcrumbs, page title, and then renders whatever else should be on the page (often another scoped component).
- This component should be named something like `<Page::CreateFoo />` and you can create it like `ember g component page/create-foo -gc`.
- The Route should pass the model hook data into the component in the template. However, if the model hook returns multiple objects they should each be passed into the Page component as separate args. For example: within a route's template whose model hook returns two different data models, the route's template would look like:
```hbs
<Page::CreateFoo @config={{this.model.config}} @foo={{this.model.foo}} />
```
## Conditional rendering
Generally, we want the burden of deciding whether a component should render to live in the parent rather than the child.
- **Readability** - it's easier to tell at a glance that a component will sometimes not render if the `{{#if}}` block is on the parent.
- **Performance** - when a component is in charge of its own rendering logic, the component's lifecycle hooks will fire whether or not the component will render on the page. This can lead to degraded performance, for example if hundreds of the same component are listed on the page.
## Reusable components
When developing components, make sure to:
- Add splattributes to the top level, eg:
```hbs
<div data-test-stuff ...attributes>Stuff!</div>
```
- Consider passing splattributes or yielding something instead of passing a new arg
**Instead of:** passing a new arg that controls a style
```
<Block @title="Example" @hasPadding={{false}} />
```
**Prefer:** passing a class or helper that controls a style
```
<Block @title="Example" class="padding-0" />
```
- Minimize the number of args that must be passed
**Instead of:** Passing in separate args that are both required for icon to render
```
<Block @title="Example" @hasIcon={{true}} @iconName="key" />
```
**Prefer:** One arg that is rendered if present
```
<Block @title="Example" @icon="key" />
```

39
ui/docs/css.md Normal file
View File

@ -0,0 +1,39 @@
# CSS/SCSS
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Guidelines](#guidelines)
- [Helper classes](#helper-classes)
- [Core class styles](#core-class-styles)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Guidelines
- [**Helper classes**](#helper-classes) should be used if a styling block does not already exist and/or a reasonable number of helper classes can cover the desired style.
- [**Core classes**](#core-class-styles) provide styling for any classes not associated with a component. The scope of each file is defined by the file name.
- **Component specific styling** should only be added to, or created when you cannot achieve the styling with a helper class or core class.
- **Utils'** files define mixins, and variables.
> ### Known issues
> The following are known issues that we are working to address in conjunction with the adoption of HDS.
> 1. **Size variables** The UI does not follow a consistent size variable pattern. We use both px and rem to define font-size and we use px, rem, and ems to define margins, heights and widths. For accessibility reasons we _should_ define all font-sizing at the very least by rem, though this is not consistently done in the app.
> 2. **Variable naming** The UI does not have a consistent pattern to variable naming. We use a mix of numbers and words (ex: `ui-gray-050` is the same as `ui-gray-lightest`).
> 3. **Random variables** We have dieing but not dead variables. For example, we have some variables that define box-shadows and we have some variables to define animations, but we are missing many box-shadow definitions and we do not consistently use the animation variables.
> 4. **Missing variables** The UI does not have a variable for all commonly occurring sizes and colors. For example, we do not have a variable that covers the `14px` though it is a commonly used size.
> 5. **!Important** `!important` is sprinkled throughout helper, core and component files. Ideally, the cascading and order of styles would eliminate the need of this keyword. However, because `!important` exist randomly in many of our files, we now have cascading issues inside helper files and core files. In all known cases where these issues exist a comment has been left as to why the order of classes in that particular area matters.
### Helper classes
A good portion of our class definitions have come from Bulma. Bulma has since been removed, but we still rely on many of its class definitions. Bulma class definitions, specifically their helper classes, always end in the keyword `!important`. This use of `important!` and Bulma specific naming patterns has led to a mix of inconsistent helper class names. Moving forward, we have agreed as a team to pursue the following patterns. When it makes sense, please default to these instead of relying on existing helper names for guidance.
- Drop the starting verb. Many of our helper classes start with a verb `has` or `is`. Going forward we prefer to drop the verb. `margin-bottom` instead of `is-margin-bottom`.
- Start your helper class name with what the class controls. `margin-bottom` instead of `bottom-margin`.
- Match your helper class size to a pre-existing size variable. `margin-bottom-large` instead of `margin-bottom-big`.
### Core class styles
All files under `app/styles/core` directory define style for the class of the file name. Think of these as files for the heavily used classes that are not defined as a component. Things like `.box` or `.title`. These classes are used in our app over many files that span multiple components, but they are themselves are _not_ components.
If the core file ends in a `s` (e.g. `lists` or `containers`) the plural indicates that the file defines more than just the style for the class `container`. The `core/containers.scss` file defines classes for all things relating to container-type classes: `page-container`, `section` as well as the `container` class. There are only a few plural core files as they are the exception and not the norm.

175
ui/docs/ember-engines.md Normal file
View File

@ -0,0 +1,175 @@
# [Ember Engines](https://ember-engines.com/docs)
This is a quickstart guide inspired by [ember engine quickstart](https://ember-engines.com/docs/quickstart) on how to set up an ember engine in Vault!
## Create a new in-repo engine:
`ember g in-repo-engine <engine-name>`
_This blueprint in-repo engine command will add a new folder `lib/<engine-name>` and add the engine to our main apps `package.json`_
## Engines package.json:
```json
{
"name": "<engine-name>",
"dependencies": {
"ember-cli-htmlbars": "*",
"ember-cli-babel": "*"
},
"ember-addon": {
"paths": ["../core"]
}
}
```
For our application, we want to include the **ember-addon** path `../core`
By adding this **ember-addon** path, we are able to share elements between your in-repo addon and the Vault application[^1].
## Configure your Engine
In the engines `index.js` file:
```js
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
/* eslint-disable node/no-extraneous-require */
const { buildEngine } = require('ember-engines/lib/engine-addon');
module.exports = buildEngine({
name: '<engine-name>',
lazyLoading: {
enabled: false,
},
isDevelopingAddon() {
return true;
},
});
```
Within your Engines `config/environment.js` file:
```js
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
// config/environment.js
'use strict';
module.exports = function (environment) {
const ENV = {
modulePrefix: '<engine-name>',
environment: environment,
};
return ENV;
};
```
Within your Engines `addon/engine.js` file:
```js
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Engine from '@ember/engine';
import loadInitializers from 'ember-load-initializers';
import Resolver from 'ember-resolver';
import config from './config/environment';
const { modulePrefix } = config;
export default class <EngineName>Engine extends Engine {
modulePrefix = modulePrefix;
Resolver = Resolver;
dependencies = {
services: ['router', 'store', 'secret-mount-path', 'flash-messages'],
externalRoutes: ['secrets'],
};
}
loadInitializers(<EngineName>Engine, modulePrefix);
```
### Service Dependencies:
The services in the example above are common services that we often use in our engines. If your engine requires other services from the main application, add them to the services array.
#### Notes:
- Service dependencies are OPTIONAL. If your engine does not use any external services, you do not need to include a services dependency array.
- Remember to include any dependencies here in the engine's dependencies in app/app.js
### External Route Dependencies:
The external route dependencies allow you to link to a route outside of your engine. In this example, we list 'secrets' in the externalRoute and the route is defined in the `app.js` file.
#### Notes:
- In order to link to the other routes in the main app using the `LinkToExternal` component from your engine, you need to add the route to the `app/app.js` and your engines `addon/engine.js`. More information on [Linking to An External Context.](https://ember-engines.com/docs/link-to-external).
## Additional info about your engine's `application.hbs`:
- Optional step: Add some text in the engines `application.hbs` file (to see if your engine was set up correctly).
- **NOTE: Most of our existing engines do not keep the generated `application.hbs` template file. If nothing will be added to it and it remains as just an `{{outlet}}` it can safely be removed.**
## Register your engine with our main application:
In our `app/app.js` file in the engines object, add your engines name and dependencies.
```js
/**
* Copyright (c) HashiCorp, Inc.
* SPDX-License-Identifier: MPL-2.0
*/
import Application from '@ember/application';
import Resolver from 'ember-resolver';
import loadInitializers from 'ember-load-initializers';
import config from 'vault/config/environment';
export default class App extends Application {
...
engines = {
<engine-name>: {
dependencies: {
services: ['router', 'store', 'secret-mount-path', 'flash-messages', <any-other-dependencies-you-have>],
externalRoutes: {
secrets: 'vault.cluster.secrets.backends',
},
},
},
};
}
loadInitializers(App, config.modulePrefix);
```
If you used `ember g in-repo-engine <engine-name>` to generate the engines blueprint, it should have added `this.mount(<engine-name>)` to the main apps `router.js` file (this adds your engine and its associated routes). \*Move `this.mount(<engine-name>)` to match your engines route structure. For more information about [Routable Engines](https://ember-engines.com/docs/quickstart#routable-engines).
### Important Notes:
- Anytime a new engine is created, you will need to `yarn install` and **RESTART** ember server!
- To add `package.json` **dependencies** or **devDependencies**, you can copy + paste the dependency into corresponding sections. Most of the time, we will want to use "\*" in place of the version number to ensure all the dependencies have the latest version.
### Common blueprint commands:
- **Generating In-repo engines routes:** `ember generate route <route-name> --in-repo <in-repo-name>` - _generates tests and route files and templates_
- **Remove In-repo engines routes:** `ember destroy route <route-name> --in-repo <in-repo-name>` - _removes tests and route files and templates_
- **Generating In-repo engines components:** `ember generate component <component-name> --in-repo <in-repo-name>`- _generates tests and component files and templates_
- **Remove In-repo engines components:** `ember destroy component <component-name> --in-repo <in-repo-name>`- _removes tests and component files and templates_
[^1]: https://ember-engines.com/docs/quickstart#create-as-in-repo-engine

19
ui/docs/forms.md Normal file
View File

@ -0,0 +1,19 @@
# Forms
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Guidelines](#guidelines)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Guidelines
- Render `FlashMessage` on success
- Handling errors/validation messages:
- Render API errors using the `AlertBanner` at the top of forms
- Display validation error messages `onsubmit` (not `onchange` for inputs)
- Render an `<AlertInline>` [beside](../lib/pki/addon/components/pki-role-generate.hbs) form buttons, especially if the error banner is hidden from view (long forms). Message options:
- The `invalidFormMessage` from a model's `validate()` method that includes an error count
- Generic message for API errors or forms without model validations: 'There was an error submitting this form.'
- Add `has-error-border` class to invalid inputs

View File

@ -0,0 +1,159 @@
# Model Validations Decorator
The model-validations decorator provides a method on a model class which may be used for validating properties based on a provided rule set.
## API
The decorator expects a validations object as the only argument with the following shape:
``` js
const validations = {
[propertyKeyName]: [
{ type, options, message, level, validator }
]
};
```
**propertyKeyName** [string] - each key in the validations object should refer to the property on the class to apply the validation to.
**type** [string] - the type of validation to apply. These must be exported from the [validators util](../app/utils/validators.js) for lookup. Type is required if a *validator* function is not provided.
**options** [object] - an optional object for the given validator -- min, max, nullable etc.
**message** [string | function] - string added to the errors array and returned in the state object from the validate method if validation fails. A function may also be provided with the model as the lone argument that returns a string. Since this value is typically displayed to the user it should be a complete sentence with proper punctuation.
**level** [string] *optional* - string that defaults to 'error'. Currently the only other accepted value is 'warn'.
**validator** [function] *optional* - a function that may be used in place of type that is invoked in the validate method. This is useful when specific validations are needed which may be dependent on other class properties.
This function takes the class context (this) as the only argument and returns true or false.
## Usage
Each property defined in the validations object supports multiple validations provided as an array. For example, *presence* and *containsWhiteSpace* can both be added as validations for a string property.
```js
const validations = {
name: [
{ type: 'presence', message: 'Name is required.' },
{
type: 'containsWhiteSpace',
message: 'Name cannot contain whitespace.',
},
],
};
```
Decorate the model class and pass the validations object as the argument
```js
import Model, { attr } from '@ember-data/model';
import withModelValidations from 'vault/decorators/model-validations';
const validations = {
name: [
{ type: 'presence', message: 'Name is required.' },
],
};
@withModelValidations(validations)
class SomeModel extends Model {
@attr name;
}
```
Validations must be invoked using the validate method which is added directly to the decorated class.
```js
const model = await this.store.findRecord('some-model', id);
const { isValid, state, invalidFormMessage } = model.validate();
if (isValid) {
await model.save();
} else {
this.formError = invalidFormMessage;
this.errors = state;
}
```
**isValid** [boolean] - the validity of the full class. If no properties provided in the validations object are invalid this will be true.
**state** [object] - the error state of the properties defined in the validations object. This object is keyed by the property names from the validations object and each property contains an *isValid* and *errors* value. The *errors* array will be populated with messages defined in the validations object when validations fail. Since a property can have multiple validations, errors is always returned as an array.
**invalidFormMessage** [string] - message describing the number of errors currently present on the model class.
```js
const { state } = model.validate();
const { isValid, errors } = state[propertyKeyName];
if (!isValid) {
this.flashMessages.danger(errors.join('. '));
}
```
## Examples
### Basic
```js
const validations = {
foo: [
{ type: 'presence', message: 'foo is a required field.' }
],
};
@withModelValidations(validations)
class SomeModel extends Model { foo = null; }
const model = new SomeModel();
const { isValid, state } = model.validate();
console.log(isValid); // false
console.log(state.foo.isValid); // false
console.log(state.foo.errors); // ['foo is a required field']
```
### Custom validator
```js
const validations = {
foo: [{
validator: (model) => model.bar.includes('test') ? model.foo : false,
message: 'foo is required if bar includes test.'
}],
};
@withModelValidations(validations)
class SomeModel extends Model {
foo = false;
bar = ['foo', 'baz'];
}
const model = new SomeModel();
const { isValid, state } = model.validate();
console.log(isValid); // false
console.log(state.foo.isValid); // false
console.log(state.foo.errors); // ['foo is required if bar includes test.']
model.foo = true;
model.bar.push('test');
console.log(isValid); // true
console.log(state.foo.isValid); // true
console.log(state.foo.errors); // []
```
### Adding class in template based on validation state
All form validation errors must have a red border around them. Add this by adding a conditional class *has-error-border* to the element.
```js
@action
async save() {
const { isValid, state } = this.model.validate();
if (isValid) {
await this.model.save();
} else {
this.isNameInvalid = !state.name.isValid;
}
}
```
```hbs
<input class="input field {{if this.isNameInvalid 'has-error-border'}}" />
```

153
ui/docs/models.md Normal file
View File

@ -0,0 +1,153 @@
# Models
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Models](#models)
- [Capabilities](#capabilities)
- [Decorators](#decorators)
- [@withFormFields()](#withformfields)
- [@withModelValidations()](#withmodelvalidations)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Capabilities
- The API will prevent users from performing disallowed actions, adding capabilities is purely to improve UX
- Always test the capability works as expected (never assume the API path 🙂), the extra string interpolation can lead to sneaky typos and incorrect returns from the getters
```js
import lazyCapabilities, { apiPath } from 'vault/macros/lazy-capabilities';
export default class FooModel extends Model {
@attr backend;
@attr('string') fooId;
// use string interpolation for dynamic parts of API path
// the first arg is the apiPath, and the rest are the model attribute paths for those values
@lazyCapabilities(apiPath`${'backend'}/foo/${'fooId'}`, 'backend', 'fooId') fooPath;
// explicitly check for false because default behavior is showing the thing (i.e. the capability hasn't loaded yet and is undefined)
get canRead() {
return this.fooPath.get('canRead') !== false;
}
get canEdit() {
return this.fooPath.get('canUpdate') !== false;
}
}
```
## Decorators
### [@withFormFields()](../app/decorators/model-form-fields.js)
- Sets `allFields`, `formFields` and/or `formFieldGroups` properties on a model class
- `allFields` includes every model attribute (regardless of args passed to decorator)
- `formFields` and `formFieldGroups` only exist if the relevant arg is passed to the decorator
```js
import { withFormFields } from 'vault/decorators/model-form-fields';
const formFieldAttrs = ['attrName', 'anotherAttr'];
const formGroupObjects = [
// In form-field-groups.hbs form toggle group names are determined by key names
// 'default' attribute fields render before any toggle groups
// additional attribute fields render inside toggle groups
{ default: ['someAttribute'] },
{ 'Additional options': ['anotherAttr'] },
];
@withFormFields(formFieldAttrs, formGroupObjects)
export default class SomeModel extends Model {
@attr('string', { ...options }) someAttribute;
@attr('boolean', { ...options }) anotherAttr;
}
```
- Each model attribute expands into the following object:
```js
{
name: 'someAttribute',
type: 'string',
options: { ...options },
}
```
```js
// only includes attributes passed to the first argument
model.formFields = [
{
name: 'someAttribute',
type: 'string',
options: { ...options },
},
];
// expanded attributes are grouped by key
model.formFieldGroups = [
{
default: [
{
name: 'someAttribute',
type: 'string',
options: { ...options },
},
],
},
{
'Additional options': [
{
name: 'anotherAttr',
type: 'boolean',
options: { ...options },
},
],
},
];
```
### [@withModelValidations()](../app/decorators/model-validations.js)
- Adds `validate()` method on model to check attributes are valid before making an API request
- Option to write a custom validation, or use validation method from the [validators util](../app/utils/validators.js) which is referenced by the `type` key
- Option to add `level: 'warn'` to draw user attention to the input, without preventing form submission
- Component example [here](../lib/pki/addon/components/pki-generate-root.ts)
```js
import { withModelValidations } from 'vault/decorators/model-validations';
const validations = {
// object key is the model's attribute name
password: [{ type: 'presence', message: 'Password is required' }],
keyName: [
{
validator(model) {
return model.keyName === 'default' ? false : true;
},
message: `Key name cannot be the reserved value 'default'`,
},
],
};
@withModelValidations(validations)
export default class FooModel extends Model {}
// calling validate() returns an object:
model.validate() = {
isValid: false,
state: {
password: {
errors: ['Password is required.'],
warnings: [],
isValid: false,
},
keyName: {
errors: ["Key name cannot be the reserved value 'default'"],
warnings: [],
isValid: true,
},
},
invalidFormMessage: 'There are 2 errors with this form.',
};
```

92
ui/docs/routing.md Normal file
View File

@ -0,0 +1,92 @@
# Routing
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Guidelines](#guidelines)
- [File structure](#file-structure)
- [Shared functionality](#shared-functionality)
- [Decorators](#decorators)
- [@withConfirmLeave()](#withconfirmleave)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Guidelines
- Parent route typically serves to group related child resources
- Parent index route typically displays empty state placeholder with call to action or redirects to default child resource
- Child resource names are pluralized
- Child index route represents list view.
- Child singularized name + /details is the read view.
- _Avoid_ extending routes. This can lead to unnecessary inheritance which gets messy quickly. For [shared functionality](#shared-functionality), consider a decorator.
## File structure
The file structure can be leveraged to simplify CRUD actions and passing data. The singular resource route should live at the same level as its folder, this automatically passes its model to any route nested within the folder.
Below, `details.js` and `edit.js` will automatically receive the model returned by the model hook in `resource-foo.js`. Alternately, if defining a custom model hook in those routes, we can use methods like `this.modelFor` instead of re-querying records.
```
├── routes/vault/cluster/access
│   ├── parent/
│   │   ├── index.js
│   │   ├── resource-foos /
│   │   │   ├── resource-foo.js
│   │   │   ├── create.js
│   │   │   ├── index.js
│   │   │   ├── resource-foo/
│   │   │   │   ├── details.js
│   │   │   │   ├── edit.js
```
> For example, [OIDC](../app/routes/vault/cluster/access/oidc/) route structure [_original PR_](https://github.com/hashicorp/vault/pull/16028):
```
├── routes/vault/cluster/access
│   ├── oidc/
│   │   ├── index.js
│   │   ├── clients/
│   │   │   ├── client.js
│   │   │   ├── create.js
│   │   │   ├── index.js
│   │   │   ├── client/
│   │   │   │   ├── details.js
│   │   │   │   ├── edit.js
│   │   │   │   ├── providers.js <- utilizes the modelFor method to get id about parent's clientId
```
## Shared functionality
To guide users, we sometimes have a call to action that depends on a resource's state. For example, if a secret engine hasn't been configured routing to the first step to do so, and otherwise navigating to its overview page.
Instead of extending route classes to share this `isConfigured` state, consider a decorator! [withConfig()](../../ui/lib/kubernetes/addon/decorators/fetch-config.js) is a great example.
## Decorators
### [@withConfirmLeave()](../lib/core/addon/decorators/confirm-leave.js)
- Renders `window.confirm()` alert that a user has unsaved changes if navigaing away from route with the decorator
- Unloads or rolls back Ember data model record
<!-- TODO add withConfig() if we refactor for more general use -->
<!-- ### [withConfig()](../../ui/lib/kubernetes/addon/decorators/fetch-config.js)
We sometimes have a call to action guiding users that depends on a resource's state. For example, if a secret engine hasn't been configured the UI renders an empty state linking to the first configuration step. Otherwise, it routes to the overview page of that engine.
Sample use:
```js
import { withConfig } from '../decorators/fetch-config';
@withConfig()
export default class SomeRouter extends Route {
model() {
// in case of any error other than 404 we want to display that to the user
if (this.configError) {
throw this.configError;
}
return {
config: this.configModel, // configuration data to determine UI state
};
}
}
``` -->

View File

@ -0,0 +1,29 @@
# Serializers & Adapters
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
- [Guidelines](#guidelines)
- [Gotchas](#gotchas)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
## Guidelines
- Prepend internal functions with an underscore to differentiate from Ember methods `_getUrl`
- Consider using the [named-path](../app/adapters/named-path.js) adapter if the model name is part of the request path
- Utilize the serializer to remove sending model attributes that do not correspond to an API parameter. Example in [key serializer](../app/serializers/pki/key.js)
```js
export default class SomeSerializer extends ApplicationSerializer {
attrs = {
attrName: { serialize: false },
};
}
```
> Note: this will remove the attribute when calling `snapshot.serialize()` even if the method is called within the serialize method where custom logic may be written
## Gotchas
- The JSON serializer removes attributes with empty arrays [Example in MFA serializer](https://github.com/hashicorp/vault/blob/e55c18ed1299e0d36b88e603fa9f12adaf8e75dc/ui/app/serializers/mfa-login-enforcement.js#L37-L44)