diff --git a/ui/app/styles/core/buttons.scss b/ui/app/styles/core/buttons.scss index c1db822743..4bb693b59f 100644 --- a/ui/app/styles/core/buttons.scss +++ b/ui/app/styles/core/buttons.scss @@ -383,6 +383,7 @@ a.button.disabled { // Existing class on component, modifying to match existing UI Structure buttons .hds-button { font-weight: $font-weight-semibold; // TODO consult design on font weight after button class audit + display: inline-flex; // temporarily fixes existing button alignment until we adopt Hds::ButtonSet // for toolbar-button must pass arg @color="secondary" &.toolbar-button { color: $black; diff --git a/ui/lib/kmip/addon/templates/components/edit-form-kmip-role.hbs b/ui/lib/kmip/addon/templates/components/edit-form-kmip-role.hbs index bdd27c500d..e3dbc291b4 100644 --- a/ui/lib/kmip/addon/templates/components/edit-form-kmip-role.hbs +++ b/ui/lib/kmip/addon/templates/components/edit-form-kmip-role.hbs @@ -86,14 +86,13 @@
- + />
{{#if this.cancelLink}}
diff --git a/ui/lib/kubernetes/addon/components/page/configure.hbs b/ui/lib/kubernetes/addon/components/page/configure.hbs index 658f4d96b5..b623c2a2a9 100644 --- a/ui/lib/kubernetes/addon/components/page/configure.hbs +++ b/ui/lib/kubernetes/addon/components/page/configure.hbs @@ -64,14 +64,14 @@ Configuration values can be inferred from the pod and your local environment variables.

- + />
{{/if}}
@@ -79,24 +79,15 @@
- - + /> {{#if this.alert}} {{/if}} diff --git a/ui/lib/kubernetes/addon/components/page/credentials.hbs b/ui/lib/kubernetes/addon/components/page/credentials.hbs index fc6e64110d..f6156f7edb 100644 --- a/ui/lib/kubernetes/addon/components/page/credentials.hbs +++ b/ui/lib/kubernetes/addon/components/page/credentials.hbs @@ -46,9 +46,7 @@
- +
{{else}}
@@ -99,23 +97,21 @@ />
- - + />
diff --git a/ui/lib/kubernetes/addon/components/page/overview.hbs b/ui/lib/kubernetes/addon/components/page/overview.hbs index 8bf1d365b5..0fb2bb0ad2 100644 --- a/ui/lib/kubernetes/addon/components/page/overview.hbs +++ b/ui/lib/kubernetes/addon/components/page/overview.hbs @@ -33,15 +33,14 @@ @fallbackComponent="input-search" @onChange={{this.selectRole}} /> - + />
diff --git a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs index c978239cd6..5d0d0bb6b6 100644 --- a/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs +++ b/ui/lib/kubernetes/addon/components/page/role/create-and-edit.hbs @@ -116,10 +116,14 @@ @valueUpdated={{fn (mut template.rules)}} @helpText={{sanitized-html this.roleRulesHelpText}} > - + {{/let}} @@ -141,16 +145,13 @@
- - + /> +
\ No newline at end of file diff --git a/ui/lib/kubernetes/addon/components/page/roles.hbs b/ui/lib/kubernetes/addon/components/page/roles.hbs index a00a699bb7..826e769fff 100644 --- a/ui/lib/kubernetes/addon/components/page/roles.hbs +++ b/ui/lib/kubernetes/addon/components/page/roles.hbs @@ -43,9 +43,7 @@ {{#if role.rolesPath.isLoading}}
  • - +
  • {{else}}
  • diff --git a/ui/lib/kv/addon/components/kv-version-dropdown.hbs b/ui/lib/kv/addon/components/kv-version-dropdown.hbs index 00494bc43b..2dd0203711 100644 --- a/ui/lib/kv/addon/components/kv-version-dropdown.hbs +++ b/ui/lib/kv/addon/components/kv-version-dropdown.hbs @@ -10,6 +10,7 @@ {{#each @metadata.sortedVersions as |versionData|}}
  • {{#if @onSelect}} + {{! TODO Hds::Button manual update }} + {{#if @failedDirectoryQuery}} diff --git a/ui/lib/kv/addon/components/page/secret/details.hbs b/ui/lib/kv/addon/components/page/secret/details.hbs index 5974f1072a..b1b11cd663 100644 --- a/ui/lib/kv/addon/components/page/secret/details.hbs +++ b/ui/lib/kv/addon/components/page/secret/details.hbs @@ -23,9 +23,13 @@ <:toolbarActions> {{#if this.showUndelete}} - + {{/if}} {{#if this.showDelete}}
    - - + />
    {{#if this.invalidFormAlert}}
    - - + /> {{#if this.invalidFormAlert}}
    - - + />
    {{#if this.invalidFormAlert}} {{Body.data.library}} {{/if}} - + /> diff --git a/ui/lib/ldap/addon/components/page/configure.hbs b/ui/lib/ldap/addon/components/page/configure.hbs index 4e1db67531..9bd25991ad 100644 --- a/ui/lib/ldap/addon/components/page/configure.hbs +++ b/ui/lib/ldap/addon/components/page/configure.hbs @@ -47,24 +47,21 @@
    - - + /> {{#if this.invalidFormMessage}} {{#if library.libraryPath.isLoading}}
  • - +
  • {{else}}
  • diff --git a/ui/lib/ldap/addon/components/page/library/check-out.hbs b/ui/lib/ldap/addon/components/page/library/check-out.hbs index 407f44d2cd..92fbf6064f 100644 --- a/ui/lib/ldap/addon/components/page/library/check-out.hbs +++ b/ui/lib/ldap/addon/components/page/library/check-out.hbs @@ -39,12 +39,9 @@
    - + />
    \ No newline at end of file diff --git a/ui/lib/ldap/addon/components/page/library/create-and-edit.hbs b/ui/lib/ldap/addon/components/page/library/create-and-edit.hbs index 734f7c5bcb..ea03e3aa93 100644 --- a/ui/lib/ldap/addon/components/page/library/create-and-edit.hbs +++ b/ui/lib/ldap/addon/components/page/library/create-and-edit.hbs @@ -21,18 +21,20 @@
    - - + /> {{#if this.invalidFormMessage}}

    All accounts

    {{#if @library.canCheckOut}} - + /> {{/if}}
    diff --git a/ui/lib/ldap/addon/components/page/overview.hbs b/ui/lib/ldap/addon/components/page/overview.hbs index c719e2ec73..73e313d88d 100644 --- a/ui/lib/ldap/addon/components/page/overview.hbs +++ b/ui/lib/ldap/addon/components/page/overview.hbs @@ -53,15 +53,14 @@ @fallbackComponent="input-search" @onChange={{this.selectRole}} /> - + /> diff --git a/ui/lib/ldap/addon/components/page/role/create-and-edit.hbs b/ui/lib/ldap/addon/components/page/role/create-and-edit.hbs index 1a013afe73..99928ed1dc 100644 --- a/ui/lib/ldap/addon/components/page/role/create-and-edit.hbs +++ b/ui/lib/ldap/addon/components/page/role/create-and-edit.hbs @@ -45,18 +45,20 @@
    - - + /> {{#if this.invalidFormMessage}}
    - + />
    {{/if}} \ No newline at end of file diff --git a/ui/lib/ldap/addon/components/page/roles.hbs b/ui/lib/ldap/addon/components/page/roles.hbs index e9861413b3..d61c93da0a 100644 --- a/ui/lib/ldap/addon/components/page/roles.hbs +++ b/ui/lib/ldap/addon/components/page/roles.hbs @@ -51,9 +51,7 @@ {{#if role.rolePath.isLoading}}
  • - +
  • {{else}}
  • diff --git a/ui/scripts/codemods/hds/button.js b/ui/scripts/codemods/hds/button.js new file mode 100755 index 0000000000..ff5b2e8cec --- /dev/null +++ b/ui/scripts/codemods/hds/button.js @@ -0,0 +1,284 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: BUSL-1.1 + */ + +/* eslint-env node */ + +/** + * codemod to transform button html element to Hds::Button component + * transformation is skipped if is-ghost or is-transparent is found in class list + * if loading or is-loading is found to be a conditionally applied class the loading icon will be conditionally applied instead + * if the text arg cannot be built from the child nodes (chained if block or multiple nodes that cannot be easily combined) the transformation will be skipped + * classes relevant to the legacy button will be removed (see classesToRemove array) + * html onclick event handler will be replaced with the {{on "click"}} modifier + * + * example execution from ui directory: + ** -> npx ember-template-recast ./app/templates -t ./scripts/codemods/hds/button.js + * for best results run prettier after: + ** -> npx ember-template-recast ./app/templates -t ./scripts/codemods/hds/button.js && npx prettier --config .prettierrc.js --write ./app/templates + */ + +class Transforms { + // button classes that will be removed from attribute + classesToRemove = [ + 'button', + 'is-compact', + 'is-danger', + 'is-danger-outlined', + 'is-flat', + 'is-icon', + 'is-loading', + 'is-link', + 'is-primary', + 'tool-tip-trigger', + 'is-secondary', + ]; + classesToTransform = [{ current: 'toolbar-link', updated: 'toolbar-button' }]; + + constructor(node, builders) { + this.node = node; + this.attrs = []; + this.modifiers = [...node.modifiers]; + this.builders = builders; + this.hasIcon = false; + this.hasText = false; + } + + shouldTransform() { + // buttons that have the is-ghost and/or is-transparent class will not be transformed + // these usages have unclear mappings to tertiary buttons and in some cases will be replaced with Hds::Interactive + const classAttr = this.node.attributes.find((attr) => attr.name === 'class'); + if (classAttr) { + const shouldTransform = (chars) => { + return chars.includes('is-ghost') || chars.includes('is-transparent') ? false : true; + }; + if (classAttr.value.type === 'ConcatStatement') { + for (const part of classAttr.value.parts) { + if (part.type === 'TextNode' && !shouldTransform(part.chars)) { + return false; + } + } + } else { + return shouldTransform(classAttr.value.chars); + } + } + return true; + } + + addAttr(name, value) { + this.attrs.push(this.builders.attr(name, value)); + } + + filterClassTextNode(value) { + // map color related classes to @color args + let color = 'secondary'; // currently the default for .button class + for (const colorClass of ['is-primary', 'is-danger', 'is-danger-outlined']) { + if (value.chars.includes(colorClass)) { + color = colorClass === 'is-primary' ? null : 'critical'; + break; + } + } + if (color) { + this.addAttr('@color', this.builders.text(color)); + } + // remove button related classes no longer needed + // map unused classes to new ones + const classArray = value.chars.split(' '); + const chars = classArray + .filter((className) => !this.classesToRemove.includes(className)) + .map((className) => { + const transform = this.classesToTransform.find((classHash) => classHash.current === className); + return transform?.updated || className; + }) + .join(' '); + return chars ? { ...value, chars } : null; + } + + convertIsLoadingMustache(part, filteredParts) { + let isLoading = false; + const filteredParams = part.params.map((param) => { + if (param.type === 'StringLiteral' && param.value.includes('loading')) { + // rebuild param since icon name is loading and class name could be is-loading + isLoading = true; + return this.builders.string('loading'); + } + return param; + }); + if (isLoading) { + this.addAttr('@icon', this.builders.mustache('if', filteredParams)); + } else { + filteredParts.push(part); + } + } + + filterClassConcatStatement(attr) { + const filteredParts = []; + attr.value.parts.forEach((part) => { + if (part.type === 'TextNode') { + const value = this.filterClassTextNode(part); + if (value) { + filteredParts.push(value); + } + } else if (part.type === 'MustacheStatement') { + this.convertIsLoadingMustache(part, filteredParts); + } else { + filteredParts.push(part); + } + }); + if (filteredParts.length) { + return filteredParts.length === 1 ? filteredParts[0] : { ...attr.value, parts: filteredParts }; + } + } + + filterClasses(attr) { + if (attr.name === 'class') { + let attrValue = attr.value; + const { type } = attrValue; + if (type === 'ConcatStatement') { + attrValue = this.filterClassConcatStatement(attr); + } else if (type === 'TextNode') { + attrValue = this.filterClassTextNode(attr.value); + } + if (attrValue) { + this.addAttr('class', attrValue); + } + } + } + + convertOnClick(attr) { + const params = [this.builders.string('click')]; + if (!attr.value.params.length) { + params.push(attr.value.path); + } else { + params.push(this.builders.sexpr(attr.value.path, attr.value.params)); + } + const onClickModifier = this.builders.elementModifier('on', params); + this.modifiers.push(onClickModifier); + } + + filterAttributes() { + this.node.attributes.forEach((attr) => { + if (attr.name === 'class') { + return this.filterClasses(attr); + } else if (attr.name === 'onclick') { + return this.convertOnClick(attr); + } else if (attr.name === 'type' && attr.value.chars === 'button') { + // remove type="button" attribute since it is default + return; + } + this.attrs.push(attr); + }); + } + + textToString(node) { + // filter out escape charaters like \n and whitespace from TextNode and rebuild as StringLiteral + const text = decodeURI(node.chars).trim(); + if (text) { + return this.builders.string(text); + } + } + + filterTextNode(node, parts) { + if (node.type === 'TextNode') { + const text = this.textToString(node); + if (text) { + parts.push(text); + } + } + } + + convertBlockStatementNode(node, parts) { + // convert if/else block statement to inline if mustache + if (node.type === 'BlockStatement' && node.path.original === 'if' && !node.inverse.chained) { + // only deal with text nodes -- more complex expressions should be converted to getter on component + const program = node.program.body; + const ifValueNode = program.length === 1 && program[0].type === 'TextNode' ? program[0] : null; + const inverse = node.inverse.body; + const elseValueNode = inverse.length === 1 && inverse[0].type === 'TextNode' ? inverse[0] : null; + + if (ifValueNode && elseValueNode) { + const params = [...node.params, this.textToString(ifValueNode), this.textToString(elseValueNode)]; + parts.push(this.builders.mustache(node.path, params)); + } + } + } + + convertIconNode(node) { + if (node.tag === 'Icon') { + const nameAttr = node.attributes.find((attr) => attr.name === '@name'); + this.addAttr('@icon', this.builders.string(nameAttr.value.chars)); + // Hds::Button has @iconPosition arg when used with text + // it seems most usages with button are leading which is default and recommended + this.hasIcon = true; + } + } + + pushAcceptedNodes(node, parts) { + // some nodes may not need conversion and can be added to the @text assembly as is + const acceptedNodes = ['MustacheStatement']; + if (acceptedNodes.includes(node.type)) { + parts.push(node); + } + } + + childNodesToArgs() { + // convert child nodes to a format supported by an attr value for @text arg + const parts = []; + this.node.children.forEach((node) => { + // following methods are used to build the @text arg + this.filterTextNode(node, parts); + this.convertBlockStatementNode(node, parts); + this.pushAcceptedNodes(node, parts); + // we also need to set the icon related args + this.convertIconNode(node); + }); + + // filter out ignored text nodes (\n) and compare with out compiled parts + // if the lengths do not match then we were unable to transform a part and we must abort text build + const relevantParts = this.node.children.filter((node) => { + if (node.type === 'TextNode' && !this.textToString(node)) { + return false; + } + return true; + }); + if (parts.length && relevantParts.length === parts.length) { + const value = parts.length === 1 ? parts[0] : this.builders.concat(parts); + this.addAttr('@text', value); + this.hasText = true; + } else if (this.hasIcon) { + // if there was an icon node but no text we need to add the @isIconOnly arg + this.addAttr('@isIconOnly', this.builders.mustache(this.builders.boolean(true))); + } + } + + buildElement() { + if (this.hasText || this.hasIcon) { + return this.builders.element( + { name: 'Hds::Button', selfClosing: true }, + { attrs: this.attrs, modifiers: this.modifiers } + ); + } + } +} + +module.exports = (env) => { + const { builders } = env.syntax; + + return { + ElementNode(node) { + if (node.tag === 'button') { + try { + const transforms = new Transforms(node, builders); + if (transforms.shouldTransform()) { + transforms.childNodesToArgs(); + transforms.filterAttributes(); + return transforms.buildElement(); + } + } catch (error) { + console.log(`\nError caught transforming button in ${env.filePath}\n`, error); // eslint-disable-line + } + } + }, + }; +};