diff --git a/addons/rose/addon/components/rose/dropdown.js b/addons/rose/addon/components/rose/dropdown.js index 4e6ffea81..8ae6f56e6 100644 --- a/addons/rose/addon/components/rose/dropdown.js +++ b/addons/rose/addon/components/rose/dropdown.js @@ -2,6 +2,9 @@ import Component from '@ember/component'; import layout from '../../templates/components/rose/dropdown'; import { action } from '@ember/object'; +/** + * A dropdown component composed of HTML disclosure elements: details and summary + */ export default Component.extend({ // =attributes @@ -13,7 +16,7 @@ export default Component.extend({ /** * Closes the details element by setting its `open` attribute to `false`. * While the details element handles open/close functionality when clicking - * directly on the summar elemenet without JavaScript, we include this helper + * directly on the summary element without JavaScript, we include this helper * action to ensure that when the user clicks _outside_ of the element, * or _inside_ the content, it still closes. * @param {HTMLElement} element diff --git a/addons/rose/addon/components/rose/dropdown.stories.mdx b/addons/rose/addon/components/rose/dropdown.stories.mdx index 87c50f1f1..c2f71d82f 100644 --- a/addons/rose/addon/components/rose/dropdown.stories.mdx +++ b/addons/rose/addon/components/rose/dropdown.stories.mdx @@ -1,7 +1,5 @@ import { Meta, Story, Preview } from '@storybook/addon-docs/blocks'; import { hbs } from 'ember-cli-htmlbars'; -import { action } from '@storybook/addon-actions'; -import { text, boolean, select } from '@storybook/addon-knobs'; @@ -15,10 +13,10 @@ A simple dropdown component with support for links and buttons. Menu item Menu item + Button menu item `, - context: {} }} @@ -28,10 +26,10 @@ A simple dropdown component with support for links and buttons. Menu item Menu item + Button menu item `, - context: {} }} @@ -43,3 +41,23 @@ A simple dropdown component with support for links and buttons. Button menu item ``` + +Dropdown content can be aligned to right edge of the component instead of the default left edge, which is the default setting. The following example floats the component to right edge of the page inorder to provide enough space to display right aligned dropdown content. + +```htmlbars + + Menu item + Menu item + + Button menu item + +``` + +Dropdown supports `danger` styling through `style` attribute. + +```htmlbars + + Link - danger + Button - danger + +``` diff --git a/addons/rose/addon/components/rose/dropdown/button.js b/addons/rose/addon/components/rose/dropdown/button.js new file mode 100644 index 000000000..6f776e15d --- /dev/null +++ b/addons/rose/addon/components/rose/dropdown/button.js @@ -0,0 +1,10 @@ +import Component from '@ember/component'; +import layout from '../../../templates/components/rose/dropdown/button'; + +/** + * A dropdown button component based on simplified rose/button + */ +export default Component.extend({ + layout, + tagName: '', +}); diff --git a/addons/rose/addon/components/rose/dropdown/link.js b/addons/rose/addon/components/rose/dropdown/link.js new file mode 100644 index 000000000..c6609fe05 --- /dev/null +++ b/addons/rose/addon/components/rose/dropdown/link.js @@ -0,0 +1,10 @@ +import Component from '@ember/component'; +import layout from '../../../templates/components/rose/dropdown/link'; + +/** + * A dropdown link wrapper for LinkTo component. + */ +export default Component.extend({ + layout, + tagName: '', +}); diff --git a/addons/rose/addon/components/rose/header/dropdown.js b/addons/rose/addon/components/rose/header/dropdown.js deleted file mode 100644 index 4a5897084..000000000 --- a/addons/rose/addon/components/rose/header/dropdown.js +++ /dev/null @@ -1,16 +0,0 @@ -import Component from '@ember/component'; -import layout from '../../../templates/components/rose/header/dropdown'; -import { action } from '@ember/object'; - -export default Component.extend({ - layout, - tagName: '', - - // Uses details widget state to close dropdown - @action - close(element) { - if (element.open) { - element.open = false; - } - }, -}); diff --git a/addons/rose/addon/styles/rose/components/dropdown/_button.scss b/addons/rose/addon/styles/rose/components/dropdown/_button.scss new file mode 100644 index 000000000..58b78e4a1 --- /dev/null +++ b/addons/rose/addon/styles/rose/components/dropdown/_button.scss @@ -0,0 +1,36 @@ +@use '../../variables/sizing'; +@use '../../utilities/type'; + +.rose-dropdown-button { + @include type.type(s); + display: block; + box-sizing: border-box; + width: 100%; + // Targeting min-width of ~200 + min-width: (sizing.rems(m) + sizing.rems(xs)) * 10; + border: 0; + padding: 0 sizing.rems(s); + text-decoration: none; + text-align: left; + $line-height: 36 / 14; + line-height: $line-height; + + --background-color: transparent; + --color: var(--stark); + color: var(--color); + background-color: var(--background-color); + + &:hover { + --background-color: var(--ui-gray-subtler-5); + } + + &.rose-dropdown-button-danger { + --background-color: var(--white); + --color: var(--failure); + + &:hover { + --color: var(--white); + --background-color: var(--failure); + } + } +} diff --git a/addons/rose/addon/styles/rose/components/_dropdown.scss b/addons/rose/addon/styles/rose/components/dropdown/_dropdown.scss similarity index 54% rename from addons/rose/addon/styles/rose/components/_dropdown.scss rename to addons/rose/addon/styles/rose/components/dropdown/_dropdown.scss index bd87f3acb..34b2753b0 100644 --- a/addons/rose/addon/styles/rose/components/_dropdown.scss +++ b/addons/rose/addon/styles/rose/components/dropdown/_dropdown.scss @@ -1,6 +1,9 @@ -@use '../variables/sizing'; -@use '../utilities/type'; -@use '../utilities/dropdown'; +@use '../../variables/sizing'; +@use '../../utilities/type'; + +$xs: sizing.rems(xs); // 8 +$xxs: sizing.rems(xxs); // 4 +$chevron: "data:image/svg+xml;utf-8,"; .rose-dropdown { position: relative; @@ -12,18 +15,27 @@ overflow: hidden; display: flex; align-items: center; - margin-right: sizing.rems(s); - padding: 0 sizing.rems(xl) 0 sizing.rems(l); - $lineHeight: 36 / 13; - line-height: $lineHeight; + $yPadding: sizing.rems(xxs) + sizing.rems(xxxs); + padding: $yPadding sizing.rems(xl) $yPadding sizing.rems(l); + color: var(--black); + background: url($chevron) no-repeat right sizing.rems(m) center; + border-radius: sizing.rems(xxxs); + + // Hide default twistie triangle + // Webkit / Chromium + &::-webkit-details-marker { + display: none; + } + // Firefox + list-style-type: none; + &::marker { + display: none; + } - //color: var(--subtle); - background: url("data:image/svg+xml;utf-8,") - no-repeat right sizing.rems(s) center; background-size: sizing.rems(m); &:hover { - background-color: var(--ui-gray-subtler-5); + background-color: var(--ui-gray-subtler-4); } &:not(.has-icon-only) .rose-icon:first-child { @@ -58,14 +70,11 @@ } } - .rose-dropdown-trigger:active, &[open] > .rose-dropdown-trigger { background-color: var(--ui-gray-subtler-4); } .rose-dropdown-content { - $xs: sizing.rems(xs); // 8 - $xxs: sizing.rems(xxs); // 4 position: absolute; padding: $xxs 0; border: 1px solid var(--ui-gray-subtler-2); @@ -76,36 +85,16 @@ z-index: 99; } - &.dropdown-right { + &.rose-dropdown-right { .rose-dropdown-content { right: 0; } } - .rose-dropdown-link, - .rose-button-dropdown { - @include type.type(m, normal); - display: block; - // Targeting min-width of 200 - min-width: (sizing.rems(m) + sizing.rems(xxs)) * 10; - padding: 0 sizing.rems(s); - margin: 0; - border: 0; - color: var(--stark); - background-color: transparent; - // target line height is 36, which is not a standard line height for this - // font size - $lineHeight: 36 / 16; - line-height: $lineHeight; - text-align: left; - text-decoration: none; - - &:hover { - background-color: var(--ui-gray-subtler-4); - } - } - - .rose-button-dropdown { - width: 100%; + .rose-dropdown-separator { + background-color: var(--ui-gray-subtler-3); + height: sizing.rems(xxxxs); + border: none; + margin: $xxs 0; } } diff --git a/addons/rose/addon/styles/rose/components/dropdown/_index.scss b/addons/rose/addon/styles/rose/components/dropdown/_index.scss new file mode 100644 index 000000000..665c33f0f --- /dev/null +++ b/addons/rose/addon/styles/rose/components/dropdown/_index.scss @@ -0,0 +1,3 @@ +@import 'dropdown'; +@import 'button'; +@import 'link'; \ No newline at end of file diff --git a/addons/rose/addon/styles/rose/components/dropdown/_link.scss b/addons/rose/addon/styles/rose/components/dropdown/_link.scss new file mode 100644 index 000000000..38e9d8cb9 --- /dev/null +++ b/addons/rose/addon/styles/rose/components/dropdown/_link.scss @@ -0,0 +1,36 @@ +@use '../../variables/sizing'; +@use '../../utilities/type'; + +.rose-dropdown-link { + @include type.type(s); + display: block; + box-sizing: border-box; + width: 100%; + // Targeting min-width of ~200 + min-width: (sizing.rems(m) + sizing.rems(xs)) * 10; + border: 0; + padding: 0 sizing.rems(s); + text-decoration: none; + text-align: left; + $line-height: 36 / 14; + line-height: $line-height; + + --background-color: transparent; + --color: var(--stark); + color: var(--color); + background-color: var(--background-color); + + &:hover { + --background-color: var(--ui-gray-subtler-5); + } + + &.rose-dropdown-link-danger { + --background-color: var(--white); + --color: var(--failure); + + &:hover { + --color: var(--white); + --background-color: var(--failure); + } + } +} diff --git a/addons/rose/addon/styles/rose/components/header/_dropdown.scss b/addons/rose/addon/styles/rose/components/header/_dropdown.scss index 5436c1b57..8dfe39547 100644 --- a/addons/rose/addon/styles/rose/components/header/_dropdown.scss +++ b/addons/rose/addon/styles/rose/components/header/_dropdown.scss @@ -1,116 +1,42 @@ @use '../../variables/sizing'; @use '../../utilities/type'; -@use '../../utilities/dropdown'; -@include dropdown.dropdown-style( - 'light', - ( - color: var(--white), - chevron: 'light', - ) -); +$chevron: "data:image/svg+xml;utf-8,"; -@include dropdown.dropdown-style( - 'dark', - ( - color: var(--black), - chevron: 'dark', - ) -); +.rose-header { + .rose-dropdown { + &, + &[open] { + .rose-dropdown-trigger { + color: var(--white); + background: url($chevron) no-repeat right sizing.rems(s) center; + background-size: sizing.rems(m); -.rose-header-dropdown { - position: relative; - display: inline-block; + $yPadding: sizing.rems(xxxs); + $rightPadding: sizing.rems(xs) + sizing.rems(l); + padding: $yPadding $rightPadding $yPadding sizing.rems(s); - .rose-header-dropdown-trigger { - cursor: default; - overflow: hidden; - display: flex; - align-items: center; - background-size: sizing.rems(m); - margin-right: sizing.rems(s); - padding-right: sizing.rems(l) - sizing.rems(xxs); - $lineHeight: 48 / 16; - line-height: $lineHeight; + &.has-icon-only { + padding-left: sizing.rems(xs); + } - &.icon-only { - padding-right: sizing.rems(m); + .rose-icon { + --size: #{sizing.rems(l)}; + } - .rose-header-dropdown-trigger-text { - // TODO: factor out into visually-hidden mixin utility - position: absolute; - left: -1000px; - top: -1000px; - width: 1px; - height: 1px; - opacity: 0; + .rose-icon { + display: inline-block; + height: var(--size); + + > svg { + vertical-align: top; + } + } + + &:hover, &:focus { + background-color: transparent; + } } } - - // Hide default twistie triangle - // Webkit / Chromium - &::-webkit-details-marker { - display: none; - } - // Firefox - list-style-type: none; - &::marker { - display: none; - } - - .rose-icon { - display: inline-block; - height: var(--size); - - > svg { - vertical-align: top; - } - } - } - - .rose-header-dropdown-content { - $xs: sizing.rems(xs); // 8 - $xxs: sizing.rems(xxs); // 4 - position: absolute; - padding: $xxs 0; - border: 1px solid var(--ui-gray-subtler-2); - border-radius: sizing.rems(xxxs); - background: var(--subtle); - box-shadow: 0 $xxs $xxs rgba(var(--stark-components), var(--opacity-3)), - 0 $xxs $xs rgba(var(--stark-components), var(--opacity-4)); - z-index: 99; - } - - &.dropdown-right { - .rose-header-dropdown-content { - right: 0; - } - } - - .rose-header-dropdown-link, - .rose-button-header-dropdown { - @include type.type(m, normal); - display: block; - // Targeting min-width of 200 - min-width: (sizing.rems(m) + sizing.rems(xxs)) * 10; - padding: 0 sizing.rems(s); - margin: 0; - border: 0; - color: var(--stark); - background-color: transparent; - // target line height is 36, which is not a standard line height for this - // font size - $lineHeight: 36 / 16; - line-height: $lineHeight; - text-align: left; - text-decoration: none; - - &:hover { - background-color: var(--ui-gray-subtler-4); - } - } - - .rose-button-header-dropdown { - width: 100%; } } diff --git a/addons/rose/addon/styles/rose/components/header/_nav.scss b/addons/rose/addon/styles/rose/components/header/_nav.scss index 6e50ec714..1eee38086 100644 --- a/addons/rose/addon/styles/rose/components/header/_nav.scss +++ b/addons/rose/addon/styles/rose/components/header/_nav.scss @@ -6,6 +6,7 @@ display: flex; flex-wrap: wrap; align-items: center; + margin-right: sizing.rems(s); @media (max-width: media.width(small)) { padding: 0 20px; diff --git a/addons/rose/addon/styles/rose/components/header/_utilities.scss b/addons/rose/addon/styles/rose/components/header/_utilities.scss index ad28137ff..6632a2597 100644 --- a/addons/rose/addon/styles/rose/components/header/_utilities.scss +++ b/addons/rose/addon/styles/rose/components/header/_utilities.scss @@ -6,4 +6,5 @@ flex-wrap: wrap; flex-direction: row-reverse; align-items: center; + margin-right: sizing.rems(s); } diff --git a/addons/rose/addon/styles/rose/components/table/_index.scss b/addons/rose/addon/styles/rose/components/table/_index.scss index ef81e56e7..a801a8478 100644 --- a/addons/rose/addon/styles/rose/components/table/_index.scss +++ b/addons/rose/addon/styles/rose/components/table/_index.scss @@ -1,17 +1 @@ -@use '../../variables/sizing'; -@use '../../utilities/type'; - -.rose-table { - @include type.type(s, normal); - width: 100%; - - .rose-table-header { - background-color: var(--ui-gray-subtler-5); - text-align: left; - } - - .rose-table-cell { - border: sizing.rems(xxxxs) solid var(--ui-gray-subtler-3); - padding: sizing.rems(xs) sizing.rems(xxs); - } -} +@import 'table'; \ No newline at end of file diff --git a/addons/rose/addon/styles/rose/components/table/_table.scss b/addons/rose/addon/styles/rose/components/table/_table.scss new file mode 100644 index 000000000..ba63d37aa --- /dev/null +++ b/addons/rose/addon/styles/rose/components/table/_table.scss @@ -0,0 +1,37 @@ +@use '../../variables/sizing'; +@use '../../utilities/type'; + +.rose-table { + @include type.type(s, normal); + width: 100%; + + .rose-table-row { + border-bottom: sizing.rems(xxxxs) solid var(--ui-gray-subtler-3); + text-align: left; + } + + .rose-table-cell, + .rose-table-header-cell { + vertical-align: middle; + } + + .rose-table-header-cell { + @include type.type(s, semibold); + color: var(--ui-gray); + } + + .rose-table-header, + .rose-table-footer { + .rose-table-cell, + .rose-table-header-cell { + padding: sizing.rems(s); + } + } + + .rose-table-body { + .rose-table-cell, + .rose-table-header-cell { + padding: sizing.rems(l) sizing.rems(s); + } + } +} diff --git a/addons/rose/addon/styles/rose/utilities/_dropdown.scss b/addons/rose/addon/styles/rose/utilities/_dropdown.scss deleted file mode 100644 index 059d2d56d..000000000 --- a/addons/rose/addon/styles/rose/utilities/_dropdown.scss +++ /dev/null @@ -1,18 +0,0 @@ -@use 'sass:map'; - -$dropdown-chevron: ( - 'light': - "data:image/svg+xml;utf-8,", - 'dark': - "data:image/svg+xml;utf-8,", -); - -@mixin dropdown-style($name, $default) { - .rose-header-dropdown-#{$name} { - .rose-header-dropdown-trigger { - color: map.get($default, color); - background: url(map.get($dropdown-chevron, map.get($default, chevron))) - no-repeat right center; - } - } -} diff --git a/addons/rose/addon/templates/components/rose/button.hbs b/addons/rose/addon/templates/components/rose/button.hbs index a8ac2acbd..cd87dd8d8 100644 --- a/addons/rose/addon/templates/components/rose/button.hbs +++ b/addons/rose/addon/templates/components/rose/button.hbs @@ -2,7 +2,7 @@ ...attributes type={{if @submit "submit" "button"}} disabled={{if @disabled "disabled"}} - class="{{if @style (concat "rose-button-" @style)}} {{if @iconLeft "has-icon-left"}} {{if @iconRight "has-icon-right"}} {{if @iconOnly "has-icon-only"}}" + class="{{@classNames}} {{if @style (concat "rose-button-" @style)}} {{if @iconLeft "has-icon-left"}} {{if @iconRight "has-icon-right"}} {{if @iconOnly "has-icon-only"}}" > diff --git a/addons/rose/addon/templates/components/rose/dropdown.hbs b/addons/rose/addon/templates/components/rose/dropdown.hbs index c65c640ba..991bdf085 100644 --- a/addons/rose/addon/templates/components/rose/dropdown.hbs +++ b/addons/rose/addon/templates/components/rose/dropdown.hbs @@ -1,11 +1,11 @@
@@ -24,8 +24,9 @@
{{yield (hash - link=(component "link-to" class="rose-dropdown-link") - button=(component "rose/button" style="dropdown") + link=(component "rose/dropdown/link" style=this.style) + button=(component "rose/dropdown/button" style=this.style) + separator=(component "rose/anonymous" tagName="hr" class="rose-dropdown-separator") )}}
diff --git a/addons/rose/addon/templates/components/rose/dropdown/button.hbs b/addons/rose/addon/templates/components/rose/dropdown/button.hbs new file mode 100644 index 000000000..364a2ef18 --- /dev/null +++ b/addons/rose/addon/templates/components/rose/dropdown/button.hbs @@ -0,0 +1,19 @@ + diff --git a/addons/rose/addon/templates/components/rose/dropdown/link.hbs b/addons/rose/addon/templates/components/rose/dropdown/link.hbs new file mode 100644 index 000000000..6b3bd413a --- /dev/null +++ b/addons/rose/addon/templates/components/rose/dropdown/link.hbs @@ -0,0 +1,8 @@ + + {{yield}} + \ No newline at end of file diff --git a/addons/rose/addon/templates/components/rose/header/dropdown.hbs b/addons/rose/addon/templates/components/rose/header/dropdown.hbs deleted file mode 100644 index cc874c284..000000000 --- a/addons/rose/addon/templates/components/rose/header/dropdown.hbs +++ /dev/null @@ -1,32 +0,0 @@ -
- - - - {{#if @icon}} - - {{/if}} - - - {{@text}} - - - - -
- {{yield (hash - link=(component "link-to" class="rose-header-dropdown-link") - button=(component "rose/button" style="header-dropdown") - )}} -
- -
diff --git a/addons/rose/addon/templates/components/rose/header/nav.hbs b/addons/rose/addon/templates/components/rose/header/nav.hbs index 2b2978e00..a0858cc1a 100644 --- a/addons/rose/addon/templates/components/rose/header/nav.hbs +++ b/addons/rose/addon/templates/components/rose/header/nav.hbs @@ -1,6 +1,6 @@ diff --git a/addons/rose/addon/templates/components/rose/header/utilities.hbs b/addons/rose/addon/templates/components/rose/header/utilities.hbs index 6ee49e33f..e00d45e75 100644 --- a/addons/rose/addon/templates/components/rose/header/utilities.hbs +++ b/addons/rose/addon/templates/components/rose/header/utilities.hbs @@ -1,5 +1,5 @@
{{yield (hash - dropdown=(component 'rose/header/dropdown' alignRight=true) + dropdown=(component 'rose/dropdown' dropdownRight=true) )}}
diff --git a/addons/rose/addon/templates/components/rose/table.hbs b/addons/rose/addon/templates/components/rose/table.hbs index d50ec970e..655e7dbd4 100644 --- a/addons/rose/addon/templates/components/rose/table.hbs +++ b/addons/rose/addon/templates/components/rose/table.hbs @@ -12,6 +12,6 @@ classNames='rose-table-body') footer=(component 'rose/table/section' tagName='tfoot' - classNames='rose-table-foot') + classNames='rose-table-footer') )}} diff --git a/addons/rose/app/components/rose/dropdown/button.js b/addons/rose/app/components/rose/dropdown/button.js new file mode 100644 index 000000000..0def7a1c0 --- /dev/null +++ b/addons/rose/app/components/rose/dropdown/button.js @@ -0,0 +1 @@ +export { default } from 'rose/components/rose/dropdown/button'; \ No newline at end of file diff --git a/addons/rose/app/components/rose/dropdown/link.js b/addons/rose/app/components/rose/dropdown/link.js new file mode 100644 index 000000000..99ec355d3 --- /dev/null +++ b/addons/rose/app/components/rose/dropdown/link.js @@ -0,0 +1 @@ +export { default } from 'rose/components/rose/dropdown/link'; \ No newline at end of file diff --git a/addons/rose/app/components/rose/header/dropdown.js b/addons/rose/app/components/rose/header/dropdown.js deleted file mode 100644 index 16d89ac10..000000000 --- a/addons/rose/app/components/rose/header/dropdown.js +++ /dev/null @@ -1 +0,0 @@ -export { default } from 'rose/components/rose/header/dropdown'; diff --git a/addons/rose/tests/dummy/app/templates/index.hbs b/addons/rose/tests/dummy/app/templates/index.hbs index 5c6efcf85..529b6c7c5 100644 --- a/addons/rose/tests/dummy/app/templates/index.hbs +++ b/addons/rose/tests/dummy/app/templates/index.hbs @@ -1,23 +1,38 @@ - - - - Address - Host Set - Name - Description - - - - - 1.1.1.1 - host-set-1, host-set-2 - optional name - description - - - - - Footer - - - \ No newline at end of file + + + + + + Menu item + Menu item - danger + + Button menu item + + + Section + Section + + + + + Menu item + Menu item - danger + Button - default + + Button - danger + + + + + + Menu item + Menu item - danger + + + Button menu item + Button - danger + diff --git a/addons/rose/tests/integration/components/rose/dropdown-test.js b/addons/rose/tests/integration/components/rose/dropdown-test.js index 3d4597c95..0c0c22695 100644 --- a/addons/rose/tests/integration/components/rose/dropdown-test.js +++ b/addons/rose/tests/integration/components/rose/dropdown-test.js @@ -9,6 +9,7 @@ module('Integration | Component | rose/dropdown', function (hooks) { test('it renders', async function (assert) { await render(hbs``); assert.ok(find('.rose-dropdown')); + assert.notOk(find('.rose-dropdown-right')); }); test('it renders with html attributes', async function (assert) { @@ -32,6 +33,13 @@ module('Integration | Component | rose/dropdown', function (hooks) { assert.ok(find('.has-icon-only')); }); + test('it supports right aligned content', async function (assert) { + await render(hbs` + + `); + assert.ok(find('.rose-dropdown-right')); + }); + test('it renders with content', async function (assert) { await render(hbs` diff --git a/addons/rose/tests/integration/components/rose/dropdown/button-test.js b/addons/rose/tests/integration/components/rose/dropdown/button-test.js new file mode 100644 index 000000000..11fc945a6 --- /dev/null +++ b/addons/rose/tests/integration/components/rose/dropdown/button-test.js @@ -0,0 +1,38 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, find } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; + +module('Integration | Component | rose/dropdown/button', function(hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(hbs``); + assert.ok(find('.rose-dropdown-button')); + }); + + test('it is type="button" by default', async function (assert) { + await render(hbs``); + assert.equal(find('button').type, 'button'); + }); + + test('it is type="submit" when @submit={{true}}', async function (assert) { + await render(hbs``); + assert.equal(find('button').type, 'submit'); + }); + + test('it is disabled when @disabled={{true}}', async function (assert) { + await render(hbs``); + assert.equal(find('button').disabled, true); + }); + + test('it adds a style class', async function (assert) { + await render(hbs``); + assert.ok(find('.rose-dropdown-button-warning')); + }); + + test('it supports an icon-only type', async function (assert) { + await render(hbs``); + assert.ok(find('.has-icon-only .rose-icon')); + }); +}); diff --git a/addons/rose/tests/integration/components/rose/dropdown/link-test.js b/addons/rose/tests/integration/components/rose/dropdown/link-test.js new file mode 100644 index 000000000..a64b51bf8 --- /dev/null +++ b/addons/rose/tests/integration/components/rose/dropdown/link-test.js @@ -0,0 +1,31 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, find } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; + +module('Integration | Component | rose/dropdown/link', function(hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + await render(hbs``); + assert.ok(find('a')); + assert.ok(find('.rose-dropdown-link')); + }); + + test('it renders with attributes', async function (assert) { + await render(hbs``); + assert.ok(find('#dropdown')); + }); + + test('it renders with content', async function (assert) { + await render(hbs` + content + `); + assert.equal(find('.rose-dropdown-link').textContent.trim(), 'content'); + }); + + test('it supports style', async function (assert) { + await render(hbs``); + assert.ok('.rose-dropdown-link-warning'); + }); +}); diff --git a/addons/rose/tests/integration/components/rose/header-test.js b/addons/rose/tests/integration/components/rose/header-test.js index 7e0f57da7..2feb23438 100644 --- a/addons/rose/tests/integration/components/rose/header-test.js +++ b/addons/rose/tests/integration/components/rose/header-test.js @@ -53,6 +53,6 @@ module('Integration | Component | rose/header', function (hooks) { `); - assert.ok(find('.rose-header-dropdown')); + assert.ok(find('.rose-dropdown')); }); }); diff --git a/addons/rose/tests/integration/components/rose/header/dropdown-test.js b/addons/rose/tests/integration/components/rose/header/dropdown-test.js deleted file mode 100644 index 96806b98b..000000000 --- a/addons/rose/tests/integration/components/rose/header/dropdown-test.js +++ /dev/null @@ -1,80 +0,0 @@ -import { module, test } from 'qunit'; -import { setupRenderingTest } from 'ember-qunit'; -import { render, click, find, findAll } from '@ember/test-helpers'; -import hbs from 'htmlbars-inline-precompile'; - -module('Integration | Component | rose/header/dropdown', function (hooks) { - setupRenderingTest(hooks); - - test('it renders', async function (assert) { - await render(hbs``); - assert.ok(find('.rose-header-dropdown')); - }); - - test('it renders with html attributes', async function (assert) { - await render( - hbs`` - ); - assert.ok(find('#custom-id')); - assert.ok(find('.custom-class')); - }); - - test('it renders with trigger', async function (assert) { - await render(hbs``); - assert.equal( - find('.rose-header-dropdown-trigger').textContent.trim(), - 'Click me' - ); - }); - - test('it supports an icon', async function (assert) { - await render(hbs``); - assert.ok(find('svg')); - }); - - test('it supports icon-only display', async function (assert) { - await render(hbs``); - assert.ok(find('.icon-only')); - }); - - test('it supports right-aligned content', async function (assert) { - await render(hbs``); - assert.ok(find('.dropdown-right')); - }); - - test('it renders with content', async function (assert) { - await render(hbs` - - - - - - `); - assert.ok(find('.rose-header-dropdown-content')); - assert.equal(findAll('a').length, 2); - assert.equal(findAll('button').length, 3); - }); - - test('it is toggled on click on trigger content', async function (assert) { - await render(hbs``); - - assert.notOk(find('#dropdown').open); - await click('summary'); - assert.ok(find('#dropdown').open); - await click('summary'); - assert.notOk(find('#dropdown').open); - }); - - test('it is closed with outside click is triggered', async function (assert) { - await render(hbs` -
- -
- `); - - await click('summary'); - assert.ok(find('#dropdown').open); - await click('#wrapper'); - assert.notOk(find('#dropdown').open); - }); -}); diff --git a/addons/rose/tests/integration/components/rose/header/nav-test.js b/addons/rose/tests/integration/components/rose/header/nav-test.js index d1c5a688d..fd713c0db 100644 --- a/addons/rose/tests/integration/components/rose/header/nav-test.js +++ b/addons/rose/tests/integration/components/rose/header/nav-test.js @@ -24,6 +24,6 @@ module('Integration | Component | rose/header/nav', function (hooks) { `); - assert.equal(findAll('.rose-header-dropdown').length, 2); + assert.equal(findAll('.rose-dropdown').length, 2); }); }); diff --git a/addons/rose/tests/integration/components/rose/header/utilities-test.js b/addons/rose/tests/integration/components/rose/header/utilities-test.js index 884266d8f..1bb44de8e 100644 --- a/addons/rose/tests/integration/components/rose/header/utilities-test.js +++ b/addons/rose/tests/integration/components/rose/header/utilities-test.js @@ -16,6 +16,13 @@ module('Integration | Component | rose/header/utilities', function (hooks) { `); - assert.equal(findAll('.rose-header-dropdown').length, 2); + assert.equal(findAll('.rose-dropdown').length, 2); + }); + + test('it renders nav dropdown elements as right aligned', async function (assert) { + await render(hbs` + + `); + assert.ok(find('.rose-dropdown.rose-dropdown-right')); }); }); diff --git a/ui/core/app/authenticators/password.js b/ui/core/app/authenticators/password.js index a70c89ac7..88bf63c0a 100644 --- a/ui/core/app/authenticators/password.js +++ b/ui/core/app/authenticators/password.js @@ -37,9 +37,52 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator { * @param {string} scopeID * @return {string} */ - buildDeauthEndpointURL({ org_id: scopeID }) { + buildDeauthEndpointURL({ scope: { id: scopeID } }) { const adapter = this.store.adapterFor('application'); const options = { adapterOptions: { method: 'deauthenticate' } }; return adapter.buildURL('scope', scopeID, options, 'findRecord'); } + + /** + * Intercepts the authenticate response, if any, and assigns the returned + * token to all future requests via `addTokenToAuthorization`. + * Returns the response data as normal. + * @override + */ + authenticate() { + return super.authenticate(...arguments) + .then(data => { + const token = data?.token; + if (token) this.addTokenToAuthorization(token); + return data; + }); + } + + /** + * When restoring the session (say, on load), assigns the session token + * to all future requests via `addTokenToAuthorization`. + * @override + * @param {object} data + * @return {object} + */ + restore(data) { + const token = data?.token; + if (token) this.addTokenToAuthorization(token); + return super.restore(data); + } + + /** + * Assigns a token string all future requests via the `Authorization` header + * on the application adapter prototype. Only application and adapters that + * extend the application adapter receive this header (which is most). + * @param {string} token + */ + addTokenToAuthorization(token) { + const adapterPrototype = + this.store.adapterFor('application').constructor.prototype; + const headers = adapterPrototype?.headers; + if (!headers) adapterPrototype.headers = {}; + adapterPrototype.headers.Authorization = null; + if (token) adapterPrototype.headers.Authorization = `Bearer ${token}`; + } } diff --git a/ui/core/app/components/form/org/index.hbs b/ui/core/app/components/form/org/index.hbs new file mode 100644 index 000000000..45adc26d0 --- /dev/null +++ b/ui/core/app/components/form/org/index.hbs @@ -0,0 +1,49 @@ + + + + {{#if @model.errors.name}} + + {{#each @model.errors.name as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + {{#if @model.errors.description}} + + {{#each @model.errors.description as |error|}} + {{error.message}} + {{/each}} + + {{/if}} + + + + + diff --git a/ui/core/app/controllers/scopes/scope/authenticate.js b/ui/core/app/controllers/scopes/scope/authenticate.js new file mode 100644 index 000000000..b982a3c1c --- /dev/null +++ b/ui/core/app/controllers/scopes/scope/authenticate.js @@ -0,0 +1,21 @@ +import Controller from '@ember/controller'; +import { sort } from '@ember/object/computed'; + +export default class ScopesScopeAuthenticateController extends Controller { + + // =attributes + + /** + * @type {Array} + */ + @sort('model.scopes', (a, b) => { + if (a.isGlobal) { + return -1; + } else if (b.isGlobal) { + return 1; + } + return 0; + }) + sortedScopes; + +} diff --git a/ui/core/app/controllers/scopes/scope/orgs.js b/ui/core/app/controllers/scopes/scope/orgs.js new file mode 100644 index 000000000..9e553d5a3 --- /dev/null +++ b/ui/core/app/controllers/scopes/scope/orgs.js @@ -0,0 +1,17 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; + +export default class ScopesScopeOrgsController extends Controller { + // =services + + @service intl; + + // =attributes + + /** + * @type {string} + */ + get breadCrumb() { + return this.intl.t('resources.orgs'); + } +} diff --git a/ui/core/app/controllers/scopes/scope/orgs/new.js b/ui/core/app/controllers/scopes/scope/orgs/new.js new file mode 100644 index 000000000..d41b81a96 --- /dev/null +++ b/ui/core/app/controllers/scopes/scope/orgs/new.js @@ -0,0 +1,17 @@ +import Controller from '@ember/controller'; +import { inject as service } from '@ember/service'; + +export default class ScopesScopeOrgsNewController extends Controller { + // =services + + @service intl; + + // =attributes + + /** + * @type {string} + */ + get breadCrumb() { + return this.intl.t('titles.new'); + } +} diff --git a/ui/core/app/router.js b/ui/core/app/router.js index 91a8aceca..b0ef0cbf6 100644 --- a/ui/core/app/router.js +++ b/ui/core/app/router.js @@ -9,17 +9,35 @@ export default class Router extends EmberRouter { Router.map(function () { this.route('scopes', function () { this.route('scope', { path: ':scope_id' }, function () { + this.route('authenticate', function () { this.route('method', { path: ':auth_method_id' }); }); + + this.route('orgs', function() { + this.route('new'); + }); + + this.route('projects', function() { + this.route('project', { path: ':project_id' }, function() { + this.route('host-catalogs', function() { + this.route('new'); + this.route('host-catalog', { path: ':host_catalog_id' }, function() {}); + }); + }); + this.route('new'); + }); + this.route('users', function () { this.route('user', { path: ':user_id' }, function () {}); this.route('new'); }); + this.route('groups', function () { this.route('group', { path: ':group_id' }, function () {}); this.route('new'); }); + this.route('roles', function () { this.route('role', { path: ':role_id' }, function () { this.route('grants'); @@ -27,6 +45,7 @@ Router.map(function () { }); this.route('new'); }); + this.route('projects', function() { this.route('project', { path: ':project_id' }, function() { this.route('host-catalogs', function() { @@ -42,6 +61,7 @@ Router.map(function () { this.route('auth-method', { path: ':auth_method_id' }, function() {}); this.route('new'); }); + }); }); }); diff --git a/ui/core/app/routes/scopes/index.js b/ui/core/app/routes/scopes/index.js index 562a27a15..b1ec77f7f 100644 --- a/ui/core/app/routes/scopes/index.js +++ b/ui/core/app/routes/scopes/index.js @@ -15,15 +15,15 @@ export default class ScopesIndexRoute extends Route { * @param {[ScopeModel]} model * @param {?ScopeModel} model.firstObject */ - redirect({ firstObject }) { + redirect() { const authenticatedScopeID = get( this.session, 'data.authenticated.scope.id' ); if (authenticatedScopeID) { this.transitionTo('scopes.scope', authenticatedScopeID); - } else if (firstObject) { - this.transitionTo('scopes.scope', firstObject.id); + } else { + this.transitionTo('scopes.scope', 'global'); } } } diff --git a/ui/core/app/routes/scopes/scope.js b/ui/core/app/routes/scopes/scope.js index 2707a238b..08cd2d207 100644 --- a/ui/core/app/routes/scopes/scope.js +++ b/ui/core/app/routes/scopes/scope.js @@ -1,6 +1,12 @@ import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; export default class ScopesScopeRoute extends Route { + + // =services + + @service intl; + // =methods /** @@ -14,11 +20,18 @@ export default class ScopesScopeRoute extends Route { * @return {Promise{ScopeModel}} */ model({ scope_id: id }) { + // Since only global and org scopes are authenticatable, we can infer type + // from ID because global has a fixed ID. + const type = id === 'global' ? 'global' : 'org'; return this.store.findRecord('scope', id).catch(() => { const maybeExistingScope = this.store.peekRecord('scope', id); - // TODO it's unclear if creating a temporary unsaved scope is reliable - // we may need to revert to a POJO. - return maybeExistingScope || this.store.createRecord('scope', { id }); + const scopeOptions = { id, type }; + if (type === 'global') { + scopeOptions.name = this.intl.t('titles.global'); + } + return maybeExistingScope || + this.store.createRecord('scope', scopeOptions); }); } + } diff --git a/ui/core/app/routes/scopes/scope/auth-methods.js b/ui/core/app/routes/scopes/scope/auth-methods.js index ffad801fa..0e35b2c28 100644 --- a/ui/core/app/routes/scopes/scope/auth-methods.js +++ b/ui/core/app/routes/scopes/scope/auth-methods.js @@ -7,9 +7,17 @@ export default class ScopesScopeAuthMethodsRoute extends Route { @service intl; @service notify; + @service session; // =methods + /** + * If arriving here unauthenticated, redirect to index for further processing. + */ + beforeModel() { + if (!this.session.isAuthenticated) this.transitionTo('index'); + } + /** * Load all auth-methods under current scope * @return {Promise[AuthMethodModel]} diff --git a/ui/core/app/routes/scopes/scope/authenticate.js b/ui/core/app/routes/scopes/scope/authenticate.js index d7dae560d..ef08a1566 100644 --- a/ui/core/app/routes/scopes/scope/authenticate.js +++ b/ui/core/app/routes/scopes/scope/authenticate.js @@ -7,12 +7,6 @@ export default class ScopesScopeAuthenticateRoute extends Route { @service session; - // =attributes - - // TODO: this assumes we're in an org scope, but the "landing" - // for global scope is probably different. - routeIfAlreadyAuthenticated = 'scopes.scope.projects'; - // =methods /** @@ -37,7 +31,12 @@ export default class ScopesScopeAuthenticateRoute extends Route { redirect() { if (this.session.isAuthenticated) { - this.transitionTo(this.routeIfAlreadyAuthenticated); + const scope = this.modelFor('scopes.scope'); + if (scope.isGlobal) { + this.transitionTo('scopes.scope.orgs'); + } else { + this.transitionTo('scopes.scope.projects'); + } } } } diff --git a/ui/core/app/routes/scopes/scope/authenticate/method.js b/ui/core/app/routes/scopes/scope/authenticate/method.js index 5208b152d..ed1ebc026 100644 --- a/ui/core/app/routes/scopes/scope/authenticate/method.js +++ b/ui/core/app/routes/scopes/scope/authenticate/method.js @@ -9,10 +9,6 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route { @service notify; @service intl; - // =attributes - - routeAfterAuthentication = 'scopes.scope.projects'; - // =methods /** @@ -55,7 +51,11 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route { requestCookies, { scope, authMethod } ); - this.transitionTo(this.routeAfterAuthentication); + if (scope.isGlobal) { + this.transitionTo('scopes.scope.orgs'); + } else { + this.transitionTo('scopes.scope.projects'); + } } catch (e) { const errorMessage = this.intl.t('errors.titles.authentication-failed'); this.notify.error(errorMessage, { closeAfter: null }); diff --git a/ui/core/app/routes/scopes/scope/groups.js b/ui/core/app/routes/scopes/scope/groups.js index 7eed4406e..c2c75c8fb 100644 --- a/ui/core/app/routes/scopes/scope/groups.js +++ b/ui/core/app/routes/scopes/scope/groups.js @@ -7,9 +7,17 @@ export default class ScopesScopeGroupsRoute extends Route { @service intl; @service notify; + @service session; // =methods + /** + * If arriving here unauthenticated, redirect to index for further processing. + */ + beforeModel() { + if (!this.session.isAuthenticated) this.transitionTo('index'); + } + /** * Load all groups under current scope * @return {Promise[GroupModel]} diff --git a/ui/core/app/routes/scopes/scope/orgs.js b/ui/core/app/routes/scopes/scope/orgs.js new file mode 100644 index 000000000..c2fa56ffc --- /dev/null +++ b/ui/core/app/routes/scopes/scope/orgs.js @@ -0,0 +1,64 @@ +import Route from '@ember/routing/route'; +import { inject as service } from '@ember/service'; +import { action } from '@ember/object'; + +export default class ScopesScopeOrgsRoute extends Route { + + // =services + + @service intl; + @service notify; + @service session; + + // =methods + + /** + * If arriving here unauthenticated, redirect to index for further processing. + */ + beforeModel() { + if (!this.session.isAuthenticated) this.transitionTo('index'); + } + + /** + * Loads all scopes under the global scope. + * @return {Promise{[ScopeModel]}} + */ + async model() { + return this.store.query('scope', { scope_id: 'global' }); + } + + // =actions + + /** + * Rollback changes on org. + * @param {ScopeModel} org + */ + @action + cancel(org) { + const { isNew } = org; + org.rollbackAttributes(); + if (isNew) this.transitionTo('scopes.scope.orgs'); + } + + /** + * Handle save org. + * @param {ScopeModel} org + * @param {Event} e + */ + @action + async save(org) { + const { isNew } = org; + try { + await org.save(); + this.refresh(); + this.notify.success( + this.intl.t(isNew ? 'notify.create-success' : 'notify.save-success') + ); + this.transitionTo('scopes.scope', org); + } catch (error) { + // TODO: replace with translated strings + this.notify.error(error.message, { closeAfter: null }); + } + } + +} diff --git a/ui/core/app/routes/scopes/scope/orgs/index.js b/ui/core/app/routes/scopes/scope/orgs/index.js new file mode 100644 index 000000000..4a344e13a --- /dev/null +++ b/ui/core/app/routes/scopes/scope/orgs/index.js @@ -0,0 +1,4 @@ +import Route from '@ember/routing/route'; + +export default class ScopesScopeOrgsIndexRoute extends Route { +} diff --git a/ui/core/app/routes/scopes/scope/orgs/new.js b/ui/core/app/routes/scopes/scope/orgs/new.js new file mode 100644 index 000000000..0337f2dce --- /dev/null +++ b/ui/core/app/routes/scopes/scope/orgs/new.js @@ -0,0 +1,18 @@ +import Route from '@ember/routing/route'; + +export default class ScopesScopeOrgsNewRoute extends Route { + + // =methods + + /** + * Creates a new unsaved org scope belonging to the global scope. + * @return {ScopeModel} + */ + model() { + return this.store.createRecord('scope', { + type: 'org', + scopeID: 'global', + }); + } + +} diff --git a/ui/core/app/routes/scopes/scope/roles.js b/ui/core/app/routes/scopes/scope/roles.js index 84cf2ae0f..b0418e399 100644 --- a/ui/core/app/routes/scopes/scope/roles.js +++ b/ui/core/app/routes/scopes/scope/roles.js @@ -7,9 +7,17 @@ export default class ScopesScopeRolesRoute extends Route { @service intl; @service notify; + @service session; // =methods + /** + * If arriving here unauthenticated, redirect to index for further processing. + */ + beforeModel() { + if (!this.session.isAuthenticated) this.transitionTo('index'); + } + /** * Load all roles under current scope. * @return {Promise{[RoleModel]}} diff --git a/ui/core/app/routes/scopes/scope/users.js b/ui/core/app/routes/scopes/scope/users.js index f1982b769..20731c90f 100644 --- a/ui/core/app/routes/scopes/scope/users.js +++ b/ui/core/app/routes/scopes/scope/users.js @@ -7,9 +7,17 @@ export default class ScopesScopeUsersRoute extends Route { @service intl; @service notify; + @service session; // =methods + /** + * If arriving here unauthenticated, redirect to index for further processing. + */ + beforeModel() { + if (!this.session.isAuthenticated) this.transitionTo('index'); + } + /** * Load all users under current scope. * @return {Promise{[UserModel]}} diff --git a/ui/core/app/session-stores/application.js b/ui/core/app/session-stores/application.js index 401a98b44..681b7b12e 100644 --- a/ui/core/app/session-stores/application.js +++ b/ui/core/app/session-stores/application.js @@ -1,38 +1,6 @@ import LocalStorageSessionStore from 'auth/session-stores/local-storage'; -import { inject as service } from '@ember/service'; -import { get } from '@ember/object'; -//import config from '../config/environment'; -export default class ApplicationSessionStore extends LocalStorageSessionStore { - @service store; - - /** - * If the auth sessionData includes a token, apply it as a bearer token in the - * application adapter's Authorization header. Otherwise clear the header. - * @param {object} sessionData - * @return {object} - */ - persist(sessionData) { - this.addTokenToAuthorization(sessionData); - //this.addScopeToSession(sessionData); - return super.persist(...arguments); - } - - /** - * If the session has a full token, it is added as a bearer token to the - * `Authorization` header for all API requests. - * @param {object} sessionData - */ - addTokenToAuthorization(sessionData) { - const token = get(sessionData, 'authenticated.token'); - const adapter = this.store.adapterFor('application'); - const headers = get(adapter, 'headers'); - if (!headers) adapter.headers = {}; - adapter.headers.Authorization = null; - if (token) adapter.headers.Authorization = `Bearer ${token}`; - } - -} +export default class ApplicationSessionStore extends LocalStorageSessionStore {} // import CookieSessionStore from 'auth/session-stores/cookie'; // import config from '../config/environment'; diff --git a/ui/core/app/templates/application.hbs b/ui/core/app/templates/application.hbs index 15d146e9a..2422a624e 100644 --- a/ui/core/app/templates/application.hbs +++ b/ui/core/app/templates/application.hbs @@ -14,7 +14,9 @@ @iconOnly={{true}} @icon="user-square-fill" as |dropdown| > - + {{t 'actions.deauthenticate'}} diff --git a/ui/core/app/templates/scopes/scope.hbs b/ui/core/app/templates/scopes/scope.hbs index 37530455c..5f60d2607 100644 --- a/ui/core/app/templates/scopes/scope.hbs +++ b/ui/core/app/templates/scopes/scope.hbs @@ -1,6 +1,7 @@ {{#if this.session.isAuthenticated}} + {{#unless @model.isGlobal}} @@ -26,6 +27,7 @@ {{outlet "sidebar"}} + {{/unless}} {{/if}} diff --git a/ui/core/app/templates/scopes/scope/authenticate.hbs b/ui/core/app/templates/scopes/scope/authenticate.hbs index 43b329188..75a60893f 100644 --- a/ui/core/app/templates/scopes/scope/authenticate.hbs +++ b/ui/core/app/templates/scopes/scope/authenticate.hbs @@ -14,7 +14,7 @@ @text={{t "actions.choose-different-org"}} @icon="user-organization" as |dropdown|> - {{#each @model.scopes as |scope|}} + {{#each this.sortedScopes as |scope|}} {{scope.displayName}} diff --git a/ui/core/app/templates/scopes/scope/groups/group/index.hbs b/ui/core/app/templates/scopes/scope/groups/group/index.hbs index ac8915f59..e78a0c548 100644 --- a/ui/core/app/templates/scopes/scope/groups/group/index.hbs +++ b/ui/core/app/templates/scopes/scope/groups/group/index.hbs @@ -16,20 +16,24 @@ + + + + {{t 'actions.delete-group'}} + + + + - - - {{t 'actions.delete'}} - diff --git a/ui/core/app/templates/scopes/scope/orgs.hbs b/ui/core/app/templates/scopes/scope/orgs.hbs new file mode 100644 index 000000000..35fbae66b --- /dev/null +++ b/ui/core/app/templates/scopes/scope/orgs.hbs @@ -0,0 +1,3 @@ +{{title (t "resources.orgs")}} + +{{outlet}} diff --git a/ui/core/app/templates/scopes/scope/orgs/index.hbs b/ui/core/app/templates/scopes/scope/orgs/index.hbs new file mode 100644 index 000000000..9abc5722e --- /dev/null +++ b/ui/core/app/templates/scopes/scope/orgs/index.hbs @@ -0,0 +1,56 @@ + + + + + + + +

{{t "resources.orgs"}}

+
+ + + {{#if @model}} + + {{t 'titles.new'}} + + {{/if}} + + + + + {{#if @model}} + + + {{#each @model as |org|}} + + {{/each}} + + + {{else}} + + + + + {{t 'descriptions.orgs-welcome'}} + + + + {{t 'titles.new'}} + + + + + {{/if}} + + + +
diff --git a/ui/core/app/templates/scopes/scope/orgs/new.hbs b/ui/core/app/templates/scopes/scope/orgs/new.hbs new file mode 100644 index 000000000..d9fe8b361 --- /dev/null +++ b/ui/core/app/templates/scopes/scope/orgs/new.hbs @@ -0,0 +1,21 @@ +{{title (t 'titles.new')}} + + + + + + + + +

{{t "titles.new"}}

+
+ + + + + +
diff --git a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/index.hbs b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/index.hbs index 0a979afcf..1055f4ea2 100644 --- a/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/index.hbs +++ b/ui/core/app/templates/scopes/scope/projects/project/host-catalogs/host-catalog/index.hbs @@ -10,4 +10,4 @@ {{on "click" (route-action "delete" @model)}} > {{t 'actions.delete'}} - \ No newline at end of file + diff --git a/ui/core/app/templates/scopes/scope/projects/project/index.hbs b/ui/core/app/templates/scopes/scope/projects/project/index.hbs index 5dccae834..c3bec3790 100644 --- a/ui/core/app/templates/scopes/scope/projects/project/index.hbs +++ b/ui/core/app/templates/scopes/scope/projects/project/index.hbs @@ -16,20 +16,24 @@ + + + + {{t 'actions.delete-project'}} + + + + - - - {{t 'actions.delete'}} - diff --git a/ui/core/app/templates/scopes/scope/roles/role.hbs b/ui/core/app/templates/scopes/scope/roles/role.hbs index 0cd189b0a..397e9f504 100644 --- a/ui/core/app/templates/scopes/scope/roles/role.hbs +++ b/ui/core/app/templates/scopes/scope/roles/role.hbs @@ -10,6 +10,18 @@

{{t "resources.role"}}

+ + + + {{t 'actions.delete-role'}} + + + + diff --git a/ui/core/app/templates/scopes/scope/roles/role/index.hbs b/ui/core/app/templates/scopes/scope/roles/role/index.hbs index bbb671abe..e72610ec6 100644 --- a/ui/core/app/templates/scopes/scope/roles/role/index.hbs +++ b/ui/core/app/templates/scopes/scope/roles/role/index.hbs @@ -3,11 +3,4 @@ @submit={{route-action "save" @model}} @cancel={{route-action "cancel" @model}} /> - - - {{t 'actions.delete'}} - + diff --git a/ui/core/app/templates/scopes/scope/users/index.hbs b/ui/core/app/templates/scopes/scope/users/index.hbs index 1ee0f3ac2..fb0a06a09 100644 --- a/ui/core/app/templates/scopes/scope/users/index.hbs +++ b/ui/core/app/templates/scopes/scope/users/index.hbs @@ -21,21 +21,21 @@ - {{t 'form.label.name'}} - {{t 'form.label.description'}} + {{t 'form.label.name'}} + {{t 'form.label.description'}} {{#each @model as |user|}} - + {{user.name}} - + {{user.description}} {{/each}} diff --git a/ui/core/app/templates/scopes/scope/users/user/index.hbs b/ui/core/app/templates/scopes/scope/users/user/index.hbs index d80978336..6392e84f7 100644 --- a/ui/core/app/templates/scopes/scope/users/user/index.hbs +++ b/ui/core/app/templates/scopes/scope/users/user/index.hbs @@ -8,6 +8,18 @@

{{t "resources.user"}}

+ + + + {{t 'actions.delete-user'}} + + + + @@ -17,19 +29,11 @@ - - - - {{t 'actions.delete'}} - diff --git a/ui/core/mirage/scenarios/default.js b/ui/core/mirage/scenarios/default.js index 2e830c4ce..5a87be352 100644 --- a/ui/core/mirage/scenarios/default.js +++ b/ui/core/mirage/scenarios/default.js @@ -2,7 +2,12 @@ export default function(server) { // Scope resources - const globalScope = server.create('scope', { id: 'global' }); // creates a global scope + // creates a global scope + const globalScope = server.create('scope', { + id: 'global', + type: 'global', + name: 'Global' + }); const orgScope = server.createList('scope', 3, { type: 'org', scope: { id: globalScope.id, type: globalScope.type } diff --git a/ui/core/tests/acceptance/authentication-test.js b/ui/core/tests/acceptance/authentication-test.js index 478e2f1dd..fa43f2247 100644 --- a/ui/core/tests/acceptance/authentication-test.js +++ b/ui/core/tests/acceptance/authentication-test.js @@ -1,5 +1,5 @@ import { module, test } from 'qunit'; -import { visit, currentURL, fillIn, click, find } from '@ember/test-helpers'; +import { visit, currentURL, fillIn, click, find, findAll } from '@ember/test-helpers'; import { setupApplicationTest } from 'ember-qunit'; import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; import { Response } from 'miragejs'; @@ -23,8 +23,11 @@ module('Acceptance | authentication', function (hooks) { let scope; let authMethod; let authMethodID; + let globalAuthenticateURL; let authenticateURL; let authMethodAuthenticateURL; + let authMethodGlobalAuthenticateURL; + let orgsURL; let projectsURL; hooks.beforeEach(function () { @@ -45,21 +48,14 @@ module('Acceptance | authentication', function (hooks) { authMethodID = authMethod.id; scopesURL = `/scopes`; orgScopeURL = `/scopes/${orgScopeID}`; + globalAuthenticateURL = `/scopes/global/authenticate`; authenticateURL = `/scopes/${orgScopeID}/authenticate`; + authMethodGlobalAuthenticateURL = `/scopes/global/authenticate/${authMethodID}`; authMethodAuthenticateURL = `/scopes/${orgScopeID}/authenticate/${authMethodID}`; + orgsURL = `/scopes/global/orgs`; projectsURL = `/scopes/${orgScopeID}/projects`; }); - - test('visiting scopes when there are none shows a message', async function (assert) { - assert.expect(2); - this.server.get('/scopes', () => new Response(404)); - await visit(scopesURL); - await a11yAudit(); - assert.equal(currentURL(), scopesURL); - assert.ok(find('.rose-message')); - }); - test('visiting auth methods authenticate route redirects to first auth method', async function (assert) { assert.expect(1); await visit(authenticateURL); @@ -96,19 +92,19 @@ module('Acceptance | authentication', function (hooks) { assert.equal(currentURL(), authMethodAuthenticateURL); }); - test('visiting any authentication parent route while unauthenticated redirects to first authenticate method', async function (assert) { + test('visiting any authentication parent route while unauthenticated redirects to first global authenticate method', async function (assert) { assert.expect(3); await visit(indexURL); - assert.equal(currentURL(), authMethodAuthenticateURL); + assert.equal(currentURL(), authMethodGlobalAuthenticateURL); await visit(scopesURL); - assert.equal(currentURL(), authMethodAuthenticateURL); + assert.equal(currentURL(), authMethodGlobalAuthenticateURL); assert.notOk(currentSession().isAuthenticated); }); - test('visiting projects while unauthenticated redirects to first authenticate method', async function (assert) { + test('visiting projects while unauthenticated redirects to first global authenticate method', async function (assert) { assert.expect(2); await visit(projectsURL); - assert.equal(currentURL(), authMethodAuthenticateURL); + assert.equal(currentURL(), authMethodGlobalAuthenticateURL); assert.notOk(currentSession().isAuthenticated); }); @@ -119,7 +115,7 @@ module('Acceptance | authentication', function (hooks) { assert.equal(currentURL(), authMethodAuthenticateURL); }); - test('visiting any authentication parent route while already authenticated redirects to projects', async function (assert) { + test('visiting any authentication parent route while already authenticated with an org redirects to projects', async function (assert) { assert.expect(6); authenticateSession({ scope }); await visit(indexURL); @@ -135,6 +131,18 @@ module('Acceptance | authentication', function (hooks) { assert.ok(currentSession().isAuthenticated); }); + test('visiting index or scopes routes while already authenticated with global redirects to orgs', async function (assert) { + assert.expect(4); + authenticateSession({ scope: { id: globalScope.id, type: globalScope.type } }); + await visit(indexURL); + assert.equal(currentURL(), orgsURL); + await visit(scopesURL); + assert.equal(currentURL(), orgsURL); + await visit(globalAuthenticateURL); + assert.equal(currentURL(), orgsURL); + assert.ok(currentSession().isAuthenticated); + }); + test('failed authentication shows a notification message', async function (assert) { assert.expect(3); await visit(authMethodAuthenticateURL); @@ -157,15 +165,19 @@ module('Acceptance | authentication', function (hooks) { assert.ok(currentSession().isAuthenticated); }); - test('deauthentication redirects to first authenticate method', async function (assert) { + test('deauthentication redirects to first global authenticate method', async function (assert) { assert.expect(3); await visit(authMethodAuthenticateURL); await fillIn('[name="identification"]', 'test'); await fillIn('[name="password"]', 'test'); await click('[type="submit"]'); assert.ok(currentSession().isAuthenticated); - await click('.rose-button-header-dropdown'); + // Open header utilities dropdown + await click('.rose-header-utilities .rose-dropdown summary'); + // Find and click on last element in dropdown - should be deauthenticate button + const menu = findAll('.rose-header-utilities .rose-dropdown .rose-dropdown-content button'); + await click(menu[menu.length - 1]); assert.notOk(currentSession().isAuthenticated); - assert.equal(currentURL(), authMethodAuthenticateURL); + assert.equal(currentURL(), authMethodGlobalAuthenticateURL); }); }); diff --git a/ui/core/tests/acceptance/groups-test.js b/ui/core/tests/acceptance/groups-test.js index b619d09ce..0d622f1f0 100644 --- a/ui/core/tests/acceptance/groups-test.js +++ b/ui/core/tests/acceptance/groups-test.js @@ -133,7 +133,7 @@ module('Acceptance | groups', function (hooks) { assert.expect(1); const groupsCount = this.server.db.groups.length; await visit(groupURL); - await click('.rose-button-warning'); + await click('.rose-layout-page-actions .rose-dropdown-button-danger'); assert.equal(this.server.db.groups.length, groupsCount - 1); }); @@ -210,7 +210,7 @@ module('Acceptance | groups', function (hooks) { ); }); await visit(groupURL); - await click('.rose-button-warning'); + await click('.rose-layout-page-actions .rose-dropdown-button-danger'); assert.ok( find('[role="alert"]').textContent.trim(), 'Oops.', diff --git a/ui/core/tests/acceptance/host-catalogs-test.js b/ui/core/tests/acceptance/host-catalogs-test.js index 59bc08dcc..fada1730e 100644 --- a/ui/core/tests/acceptance/host-catalogs-test.js +++ b/ui/core/tests/acceptance/host-catalogs-test.js @@ -80,7 +80,7 @@ module('Acceptance | host catalogs', function (hooks) { authenticateSession({}); await visit(urls.hostCatalog); assert.equal(currentURL(), urls.hostCatalog); - await click('.rose-button-warning'); + await click('.rose-layout-page-actions .rose-dropdown-button-danger'); assert.equal(this.server.db.hostCatalogs.length, hostCatalogsCount - 1); }); @@ -162,7 +162,7 @@ module('Acceptance | host catalogs', function (hooks) { }); authenticateSession({}); await visit(urls.hostCatalog); - await click('.rose-button-warning'); + await click('.rose-layout-page-actions .rose-dropdown-button-danger'); await a11yAudit(); assert.ok( find('[role="alert"]').textContent.trim(), diff --git a/ui/core/tests/acceptance/orgs-test.js b/ui/core/tests/acceptance/orgs-test.js new file mode 100644 index 000000000..5871fd404 --- /dev/null +++ b/ui/core/tests/acceptance/orgs-test.js @@ -0,0 +1,76 @@ +import { module, test } from 'qunit'; +//import { visit, currentURL, click, fillIn, find } from '@ember/test-helpers'; +import { setupApplicationTest } from 'ember-qunit'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; +//import a11yAudit from 'ember-a11y-testing/test-support/audit'; +//import { Response } from 'miragejs'; +import { + //authenticateSession, + // These are left here intentionally for future reference. + //currentSession, + //invalidateSession, +} from 'ember-simple-auth/test-support'; + +module('Acceptance | org', function (hooks) { + setupApplicationTest(hooks); + setupMirage(hooks); + + // let org; + // let orgID; + // let orgsURL; + // let newOrgURL; + // let existingOrg; + // let existingOrgURL; + // let getOrgScopesCount; + // let getFirstOrgScope; + // let initialOrgScopesCount; + // + // hooks.beforeEach(function () { + // authenticateSession({}); + // + // }); + + test('visiting orgs', async function (assert) { + assert.expect(0); + }); + + test('visiting orgs within a org scope redirects to the parent org scope', async function (assert) { + assert.expect(0); + }); + + test('can create new orgs', async function (assert) { + assert.expect(0); + }); + + test('can cancel create new orgs', async function (assert) { + assert.expect(0); + }); + + test('saving a new org with invalid fields displays error messages', async function (assert) { + assert.expect(0); + }); + + test('can save changes to existing org', async function (assert) { + assert.expect(0); + }); + + test('can cancel changes to existing org', async function (assert) { + assert.expect(0); + }); + + test('can delete org', async function (assert) { + assert.expect(0); + }); + + test('saving an existing org with invalid fields displays error messages', async function (assert) { + assert.expect(0); + }); + + test('errors are displayed when save org fails', async function (assert) { + assert.expect(0); + }); + + test('errors are displayed when delete org fails', async function (assert) { + assert.expect(0); + }); +}); diff --git a/ui/core/tests/acceptance/projects-test.js b/ui/core/tests/acceptance/projects-test.js index ced4a8f13..4af813592 100644 --- a/ui/core/tests/acceptance/projects-test.js +++ b/ui/core/tests/acceptance/projects-test.js @@ -135,7 +135,7 @@ module('Acceptance | projects', function (hooks) { test('can delete project', async function (assert) { assert.expect(1); await visit(existingProjectURL); - await click('.rose-button-warning'); + await click('.rose-layout-page-actions .rose-dropdown-button-danger'); assert.equal(getProjectScopesCount(), initialProjectScopesCount - 1); }); @@ -212,7 +212,7 @@ module('Acceptance | projects', function (hooks) { ); }); await visit(existingProjectURL); - await click('.rose-button-warning'); + await click('.rose-layout-page-actions .rose-dropdown-button-danger'); assert.ok( find('[role="alert"]').textContent.trim(), 'Oops.', diff --git a/ui/core/tests/acceptance/roles-test.js b/ui/core/tests/acceptance/roles-test.js index 2aa364f1b..14bd45a24 100644 --- a/ui/core/tests/acceptance/roles-test.js +++ b/ui/core/tests/acceptance/roles-test.js @@ -133,7 +133,7 @@ module('Acceptance | roles', function (hooks) { assert.expect(1); const rolesCount = this.server.db.roles.length; await visit(roleURL); - await click('.rose-button-warning'); + await click('.rose-layout-page-actions .rose-dropdown-button-danger'); assert.equal(this.server.db.roles.length, rolesCount - 1); }); @@ -210,7 +210,7 @@ module('Acceptance | roles', function (hooks) { ); }); await visit(roleURL); - await click('.rose-button-warning'); + await click('.rose-layout-page-actions .rose-dropdown-button-danger'); assert.ok( find('[role="alert"]').textContent.trim(), 'Oops.', diff --git a/ui/core/tests/acceptance/users-test.js b/ui/core/tests/acceptance/users-test.js index 31fa5c98e..55e4ac108 100644 --- a/ui/core/tests/acceptance/users-test.js +++ b/ui/core/tests/acceptance/users-test.js @@ -133,7 +133,7 @@ module('Acceptance | users', function (hooks) { assert.expect(1); const usersCount = this.server.db.users.length; await visit(userURL); - await click('.rose-button-warning'); + await click('.rose-layout-page-actions .rose-dropdown-button-danger'); assert.equal(this.server.db.users.length, usersCount - 1); }); @@ -210,7 +210,7 @@ module('Acceptance | users', function (hooks) { ); }); await visit(userURL); - await click('.rose-button-warning'); + await click('.rose-layout-page-actions .rose-dropdown-button-danger'); assert.ok( find('[role="alert"]').textContent.trim(), 'Oops.', diff --git a/ui/core/tests/integration/components/form/org-test.js b/ui/core/tests/integration/components/form/org-test.js new file mode 100644 index 000000000..76c3d9b8f --- /dev/null +++ b/ui/core/tests/integration/components/form/org-test.js @@ -0,0 +1,23 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render, find } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +module('Integration | Component | form/org', function(hooks) { + setupRenderingTest(hooks); + + test('it renders', async function (assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.set('myAction', function(val) { ... }); + this.submit = () => {}; + this.cancel = () => {}; + + await render(hbs` + `); + + assert.ok(find('form')); + }); +}); diff --git a/ui/core/tests/unit/authenticators/password-test.js b/ui/core/tests/unit/authenticators/password-test.js new file mode 100644 index 000000000..8bf59c9ee --- /dev/null +++ b/ui/core/tests/unit/authenticators/password-test.js @@ -0,0 +1,44 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; +import setupMirage from 'ember-cli-mirage/test-support/setup-mirage'; + +module('Unit | Authenticator | password', function (hooks) { + setupTest(hooks); + setupMirage(hooks); + + let store; + let applicationAdapter; + let session; + let authenticator; + + hooks.beforeEach(function () { + store = this.owner.lookup('service:store'); + applicationAdapter = store.adapterFor('application'); + session = this.owner.lookup('service:session'); + authenticator = this.owner.lookup('authenticator:password'); + session.invalidate(); + }); + + hooks.afterEach(function () { + session.invalidate(); + }); + + test('it adds an authorization header to application adapter on restore', async function (assert) { + assert.expect(1); + authenticator.restore({ token: 'token1234' }); + assert.equal(applicationAdapter.headers.Authorization, 'Bearer token1234'); + }); + + test('it adds an authorization header to application adapter on authenticate', async function (assert) { + assert.expect(1); + const creds = { identification: 'foo', password: 'bar' }; + const scope = { id: 'global' }; + const authMethod = { id: 'paum_1234' }; + await authenticator.authenticate( + creds, + null, + { scope, authMethod } + ); + assert.equal(applicationAdapter.headers.Authorization, 'Bearer thetokenstring'); + }); +}); diff --git a/ui/core/tests/unit/controllers/scopes/scope/authenticate-test.js b/ui/core/tests/unit/controllers/scopes/scope/authenticate-test.js new file mode 100644 index 000000000..41bd58b67 --- /dev/null +++ b/ui/core/tests/unit/controllers/scopes/scope/authenticate-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Controller | scopes/scope/authenticate', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function(assert) { + let controller = this.owner.lookup('controller:scopes/scope/authenticate'); + assert.ok(controller); + }); +}); diff --git a/ui/core/tests/unit/controllers/scopes/scope/orgs-test.js b/ui/core/tests/unit/controllers/scopes/scope/orgs-test.js new file mode 100644 index 000000000..f89afd0fc --- /dev/null +++ b/ui/core/tests/unit/controllers/scopes/scope/orgs-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Controller | scopes/scope/orgs', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function(assert) { + let controller = this.owner.lookup('controller:scopes/scope/orgs'); + assert.ok(controller); + }); +}); diff --git a/ui/core/tests/unit/controllers/scopes/scope/orgs/new-test.js b/ui/core/tests/unit/controllers/scopes/scope/orgs/new-test.js new file mode 100644 index 000000000..c6631a1ab --- /dev/null +++ b/ui/core/tests/unit/controllers/scopes/scope/orgs/new-test.js @@ -0,0 +1,12 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Controller | scopes/scope/orgs/new', function(hooks) { + setupTest(hooks); + + // Replace this with your real tests. + test('it exists', function(assert) { + let controller = this.owner.lookup('controller:scopes/scope/orgs/new'); + assert.ok(controller); + }); +}); diff --git a/ui/core/tests/unit/routes/scopes/scope/orgs-test.js b/ui/core/tests/unit/routes/scopes/scope/orgs-test.js new file mode 100644 index 000000000..4b9d477c3 --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope/orgs-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/orgs', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/orgs'); + assert.ok(route); + }); +}); diff --git a/ui/core/tests/unit/routes/scopes/scope/orgs/index-test.js b/ui/core/tests/unit/routes/scopes/scope/orgs/index-test.js new file mode 100644 index 000000000..c62bbc625 --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope/orgs/index-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/orgs/index', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/orgs/index'); + assert.ok(route); + }); +}); diff --git a/ui/core/tests/unit/routes/scopes/scope/orgs/new-test.js b/ui/core/tests/unit/routes/scopes/scope/orgs/new-test.js new file mode 100644 index 000000000..3cfdb8522 --- /dev/null +++ b/ui/core/tests/unit/routes/scopes/scope/orgs/new-test.js @@ -0,0 +1,11 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Route | scopes/scope/orgs/new', function(hooks) { + setupTest(hooks); + + test('it exists', function(assert) { + let route = this.owner.lookup('route:scopes/scope/orgs/new'); + assert.ok(route); + }); +}); diff --git a/ui/core/tests/unit/session-stores/application-test.js b/ui/core/tests/unit/session-stores/application-test.js deleted file mode 100644 index 6a3e908bc..000000000 --- a/ui/core/tests/unit/session-stores/application-test.js +++ /dev/null @@ -1,41 +0,0 @@ -import { module, test } from 'qunit'; -import { setupTest } from 'ember-qunit'; - -module('Unit | Session Store | application', function (hooks) { - setupTest(hooks); - - let store; - let applicationAdapter; - let sessionStore; - - hooks.beforeEach(function () { - store = this.owner.lookup('service:store'); - applicationAdapter = store.adapterFor('application'); - sessionStore = this.owner.lookup('session-store:application'); - sessionStore.clear(); - }); - - hooks.afterEach(function () { - sessionStore.clear(); - }); - - test('it does not add an authorization header to application adapter on persist if no token is present', async function (assert) { - assert.expect(1); - sessionStore.persist({ authenticated: { token: null } }); - assert.notOk(applicationAdapter.headers.Authorization); - }); - - test('it adds an authorization header to application adapter on persist', async function (assert) { - assert.expect(1); - sessionStore.persist({ authenticated: { token: 'token1234' } }); - assert.equal(applicationAdapter.headers.Authorization, 'Bearer token1234'); - }); - - test('it adds an authorization header to application adapter which already has headers on persist', async function (assert) { - assert.expect(2); - applicationAdapter.headers = { foo: 'bar' }; - sessionStore.persist({ authenticated: { token: 'token1234' } }); - assert.equal(applicationAdapter.headers.foo, 'bar'); - assert.equal(applicationAdapter.headers.Authorization, 'Bearer token1234'); - }); -}); diff --git a/ui/core/translations/en-us.yaml b/ui/core/translations/en-us.yaml index 2f2e4df01..31e60a02d 100644 --- a/ui/core/translations/en-us.yaml +++ b/ui/core/translations/en-us.yaml @@ -9,6 +9,11 @@ actions: deauthenticate: Deauthenticate choose-org: Choose Scope choose-different-org: Choose a Different Scope + delete-user: Delete user + delete-group: Delete group + delete-role: Delete role + delete-project: Delete project + delete-host-catalog: Delete host catalog auth: titles: no-scopes: No Scopes Available @@ -27,6 +32,8 @@ resources: roles: Roles group: Group groups: Groups + orgs: Orgs + org: Org projects: Projects project: Project host-catalog: Host Catalog @@ -53,6 +60,7 @@ titles: new-grant: New Grant principals: Principals host-sets: Host Sets + global: Global descriptions: host-catalogs: Overview of collections of hosts and host sets. empty-set: There are no items to display yet. You may be able to add items or try back later. @@ -75,6 +83,7 @@ form: type: Type identification: Username password: Password + manage: Manage grant: Grant principal: Principal errors: