Merge remote-tracking branch 'origin/develop' into ICU-294-list-host-sets

This commit is contained in:
Susmitha Girumala committed 2020-08-19 14:31:38 -04:00
commit a1b4adfcd8
83 files changed
+1110 -511

No files matched your search

@@ -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
@@ -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';
<Meta title="Rose|Dropdown" component="Dropdown" />
@@ -15,10 +13,10 @@ A simple dropdown component with support for links and buttons.
<Rose::Dropdown @text="Choose One&hellip;" @icon="user-organization" as |dropdown|>
<dropdown.link @route="index">Menu item</dropdown.link>
<dropdown.link @route="about">Menu item</dropdown.link>
<dropdown.separator />
<dropdown.button>Button menu item</dropdown.button>
</Rose::Dropdown>
`,
context: {}
}}</Story>
</Preview>
@@ -28,10 +26,10 @@ A simple dropdown component with support for links and buttons.
<Rose::Dropdown @text="Choose One&hellip;" @icon="user-organization" @iconOnly={{true}} as |dropdown|>
<dropdown.link @route="index">Menu item</dropdown.link>
<dropdown.link @route="about">Menu item</dropdown.link>
<dropdown.separator />
<dropdown.button>Button menu item</dropdown.button>
</Rose::Dropdown>
`,
context: {}
}}</Story>
</Preview>
@@ -43,3 +41,23 @@ A simple dropdown component with support for links and buttons.
<dropdown.button>Button menu item</dropdown.button>
</Rose::Dropdown>
```
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
<Rose::Dropdown @text="Choose One&hellip;" style="float: right" @dropdownRight={{true}} as |dropdown|>
<dropdown.link @route="index">Menu item</dropdown.link>
<dropdown.link @route="about">Menu item</dropdown.link>
<dropdown.separator />
<dropdown.button>Button menu item</dropdown.button>
</Rose::Dropdown>
```
Dropdown supports `danger` styling through `style` attribute.
```htmlbars
<Rose::Dropdown @text="Dropdown" as |dropdown|>
<dropdown.link @style="danger">Link - danger</dropdown.link>
<dropdown.button @style="danger">Button - danger</dropdown.button>
</Rose::Dropdown>
```
@@ -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: '',
});
@@ -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: '',
});
@@ -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;
}
},
});
@@ -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);
}
}
}
@@ -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,<svg viewBox='0 0 24 24' fill='%231b1f26' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6-1.41-1.41z'></path></svg>";
.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,<svg viewBox='0 0 24 24' fill='%231b1f26' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6-1.41-1.41z'></path></svg>")
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;
}
}
@@ -0,0 +1,3 @@
@import 'dropdown';
@import 'button';
@import 'link';
@@ -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);
}
}
}
@@ -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,<svg viewBox='0 0 24 24' fill='%23ebeef2' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6-1.41-1.41z'></path></svg>";
@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%;
}
}
@@ -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;
@@ -6,4 +6,5 @@
flex-wrap: wrap;
flex-direction: row-reverse;
align-items: center;
margin-right: sizing.rems(s);
}
@@ -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';
@@ -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);
}
}
}
@@ -1,18 +0,0 @@
@use 'sass:map';
$dropdown-chevron: (
'light':
"data:image/svg+xml;utf-8,<svg viewBox='0 0 24 24' fill='%23ebeef2' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6-1.41-1.41z'></path></svg>",
'dark':
"data:image/svg+xml;utf-8,<svg viewBox='0 0 24 24' fill='%231b1f26' xmlns='http://www.w3.org/2000/svg'><path fill-rule='evenodd' clip-rule='evenodd' d='M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6-1.41-1.41z'></path></svg>",
);
@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;
}
}
}
@@ -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"}}"
>
<span class="rose-button-wrapper">
@@ -1,11 +1,11 @@
<details
...attributes
class="rose-dropdown"
class="rose-dropdown {{if @dropdownRight "rose-dropdown-right"}}"
{{on-click-outside this.close}}
>
<summary
class="rose-dropdown-trigger{{if @iconOnly " has-icon-only"}}"
class="rose-dropdown-trigger {{if @iconOnly "has-icon-only"}}"
title={{@text}}
>
@@ -24,8 +24,9 @@
<div class="rose-dropdown-content" {{on-click-inside this.close}}>
{{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")
)}}
</div>
@@ -0,0 +1,19 @@
<button
...attributes
type={{if @submit "submit" "button"}}
disabled={{if @disabled "disabled"}}
class="rose-dropdown-button {{if @style (concat "rose-dropdown-button-" @style)}} {{if @iconOnly "has-icon-only"}}"
>
<span class="rose-dropdown-button-wrapper">
<span class="rose-dropdown-button-body">
{{yield}}
</span>
{{#if @iconOnly}}
<Rose::Icon
@name={{@iconOnly}}
@size="medium"
/>
{{/if}}
</span>
</button>
@@ -0,0 +1,8 @@
<LinkTo
...attributes
@route={{if @route @route "index"}}
@model={{@model}}
class="rose-dropdown-link {{if @style (concat "rose-dropdown-link-" @style)}}"
>
{{yield}}
</LinkTo>
@@ -1,32 +0,0 @@
<details
...attributes
class="rose-header-dropdown{{if @alignRight " dropdown-right"}} {{if @style (concat "rose-header-dropdown-" @style) "rose-header-dropdown-light"}}"
{{on-click-outside this.close}}
>
<summary
class="rose-header-dropdown-trigger{{if @iconOnly " icon-only"}}"
title={{@text}}
>
{{#if @icon}}
<Rose::Icon
@name={{@icon}}
@size="large"
/>
{{/if}}
<span class="rose-header-dropdown-trigger-text">
{{@text}}
</span>
</summary>
<div class="rose-header-dropdown-content">
{{yield (hash
link=(component "link-to" class="rose-header-dropdown-link")
button=(component "rose/button" style="header-dropdown")
)}}
</div>
</details>
@@ -1,6 +1,6 @@
<nav class="rose-header-nav">
{{yield (hash
link=(component 'link-to' class="rose-header-nav-link")
dropdown=(component 'rose/header/dropdown')
dropdown=(component 'rose/dropdown')
)}}
</nav>
@@ -1,5 +1,5 @@
<div class="rose-header-utilities">
{{yield (hash
dropdown=(component 'rose/header/dropdown' alignRight=true)
dropdown=(component 'rose/dropdown' dropdownRight=true)
)}}
</div>
@@ -12,6 +12,6 @@
classNames='rose-table-body')
footer=(component 'rose/table/section'
tagName='tfoot'
classNames='rose-table-foot')
classNames='rose-table-footer')
)}}
</table>
@@ -0,0 +1 @@
export { default } from 'rose/components/rose/dropdown/button';
@@ -0,0 +1 @@
export { default } from 'rose/components/rose/dropdown/link';
@@ -1 +0,0 @@
export { default } from 'rose/components/rose/header/dropdown';
+38 -23
View File
@@ -1,23 +1,38 @@
<Rose::Table as |table|>
<table.header as |header|>
<header.row as |row|>
<row.headerCell>Address</row.headerCell>
<row.headerCell>Host Set</row.headerCell>
<row.headerCell>Name</row.headerCell>
<row.headerCell>Description</row.headerCell>
</header.row>
</table.header>
<table.body as |body|>
<body.row as |row|>
<row.headerCell>1.1.1.1</row.headerCell>
<row.cell>host-set-1, host-set-2</row.cell>
<row.cell>optional name</row.cell>
<row.cell>description</row.cell>
</body.row>
</table.body>
<table.footer as |footer|>
<footer.row as |row|>
<row.cell>Footer</row.cell>
</footer.row>
</table.footer>
</Rose::Table>
<Rose::Header as |header|>
<header.brand @logo="logo" @text="Product Name" />
<header.nav as |nav|>
<nav.dropdown @text="org-name" @icon="user-organization" as |dropdown|>
<dropdown.link @route="index">Menu item</dropdown.link>
<dropdown.link @route="about" @style="danger">Menu item - danger</dropdown.link>
<dropdown.separator />
<dropdown.button>Button menu item</dropdown.button>
</nav.dropdown>
<nav.link @route="index">Section</nav.link>
<nav.link @route="about">Section</nav.link>
</header.nav>
<header.utilities as |utilities|>
<utilities.dropdown
@text="user@example.net"
@iconOnly={{true}}
@icon="user-square-fill" as |dropdown|
>
<dropdown.link @route="index">Menu item</dropdown.link>
<dropdown.link @route="about" @style="danger">Menu item - danger</dropdown.link>
<dropdown.button>Button - default</dropdown.button>
<dropdown.separator />
<dropdown.button @style="danger">Button - danger</dropdown.button>
</utilities.dropdown>
</header.utilities>
</Rose::Header>
<Rose::Dropdown @text="Dropdown" as |dropdown|>
<dropdown.link @route="index">Menu item</dropdown.link>
<dropdown.link @route="about" @style="danger">Menu item - danger</dropdown.link>
<dropdown.separator></dropdown.separator>
<dropdown.button>Button menu item</dropdown.button>
<dropdown.button @style="danger">Button - danger</dropdown.button>
</Rose::Dropdown>
@@ -9,6 +9,7 @@ module('Integration | Component | rose/dropdown', function (hooks) {
test('it renders', async function (assert) {
await render(hbs`<Rose::Dropdown />`);
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`
<Rose::Dropdown @dropdownRight={{true}} />
`);
assert.ok(find('.rose-dropdown-right'));
});
test('it renders with content', async function (assert) {
await render(hbs`<Rose::Dropdown @text="Click me" as |dropdown|>
<dropdown.link @route="about"/>
@@ -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`<Rose::Dropdown::Button />`);
assert.ok(find('.rose-dropdown-button'));
});
test('it is type="button" by default', async function (assert) {
await render(hbs`<Rose::Dropdown::Button />`);
assert.equal(find('button').type, 'button');
});
test('it is type="submit" when @submit={{true}}', async function (assert) {
await render(hbs`<Rose::Dropdown::Button @submit={{true}} />`);
assert.equal(find('button').type, 'submit');
});
test('it is disabled when @disabled={{true}}', async function (assert) {
await render(hbs`<Rose::Dropdown::Button @disabled={{true}} />`);
assert.equal(find('button').disabled, true);
});
test('it adds a style class', async function (assert) {
await render(hbs`<Rose::Dropdown::Button @style="warning" />`);
assert.ok(find('.rose-dropdown-button-warning'));
});
test('it supports an icon-only type', async function (assert) {
await render(hbs`<Rose::Dropdown::Button @iconOnly="chevron-left" />`);
assert.ok(find('.has-icon-only .rose-icon'));
});
});
@@ -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`<Rose::Dropdown::Link />`);
assert.ok(find('a'));
assert.ok(find('.rose-dropdown-link'));
});
test('it renders with attributes', async function (assert) {
await render(hbs`<Rose::Dropdown::Link id="dropdown" />`);
assert.ok(find('#dropdown'));
});
test('it renders with content', async function (assert) {
await render(hbs`<Rose::Dropdown::Link>
content
</Rose::Dropdown::Link>`);
assert.equal(find('.rose-dropdown-link').textContent.trim(), 'content');
});
test('it supports style', async function (assert) {
await render(hbs`<Rose::Dropdown::Link @style='warning' />`);
assert.ok('.rose-dropdown-link-warning');
});
});
@@ -53,6 +53,6 @@ module('Integration | Component | rose/header', function (hooks) {
<utility.dropdown />
</header.utilities>
</Rose::Header>`);
assert.ok(find('.rose-header-dropdown'));
assert.ok(find('.rose-dropdown'));
});
});
@@ -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`<Rose::Header::Dropdown />`);
assert.ok(find('.rose-header-dropdown'));
});
test('it renders with html attributes', async function (assert) {
await render(
hbs`<Rose::Header::Dropdown id="custom-id" class="custom-class"/>`
);
assert.ok(find('#custom-id'));
assert.ok(find('.custom-class'));
});
test('it renders with trigger', async function (assert) {
await render(hbs`<Rose::Header::Dropdown @text="Click me" />`);
assert.equal(
find('.rose-header-dropdown-trigger').textContent.trim(),
'Click me'
);
});
test('it supports an icon', async function (assert) {
await render(hbs`<Rose::Header::Dropdown @icon="user-square-fill" />`);
assert.ok(find('svg'));
});
test('it supports icon-only display', async function (assert) {
await render(hbs`<Rose::Header::Dropdown @iconOnly={{true}} />`);
assert.ok(find('.icon-only'));
});
test('it supports right-aligned content', async function (assert) {
await render(hbs`<Rose::Header::Dropdown @alignRight={{true}} />`);
assert.ok(find('.dropdown-right'));
});
test('it renders with content', async function (assert) {
await render(hbs`<Rose::Header::Dropdown @text="Click me" as |dropdown|>
<dropdown.link @route="about"/>
<dropdown.link @route="about"/>
<dropdown.button />
<dropdown.button />
<dropdown.button />
</Rose::Header::Dropdown>`);
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`<Rose::Header::Dropdown id="dropdown" />`);
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`
<div id="wrapper">
<Rose::Header::Dropdown id="dropdown" />
</div>
`);
await click('summary');
assert.ok(find('#dropdown').open);
await click('#wrapper');
assert.notOk(find('#dropdown').open);
});
});
@@ -24,6 +24,6 @@ module('Integration | Component | rose/header/nav', function (hooks) {
<nav.dropdown />
<nav.dropdown />
</Rose::Header::nav>`);
assert.equal(findAll('.rose-header-dropdown').length, 2);
assert.equal(findAll('.rose-dropdown').length, 2);
});
});
@@ -16,6 +16,13 @@ module('Integration | Component | rose/header/utilities', function (hooks) {
<nav.dropdown />
<nav.dropdown />
</Rose::Header::Utilities>`);
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`<Rose::Header::Utilities as |nav| >
<nav.dropdown />
</Rose::Header::Utilities>`);
assert.ok(find('.rose-dropdown.rose-dropdown-right'));
});
});
+44 -1
View File
@@ -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}`;
}
}
+49
View File
@@ -0,0 +1,49 @@
<Rose::Form
@onSubmit={{@submit}}
@cancel={{@cancel}}
@disabled={{@model.isSaving}}
@error={{@model.errors.base}}
as |form|
>
<form.input
@name="name"
@type="name"
@value={{@model.name}}
@label={{t "form.label.name"}}
@error={{@model.errors.name}}
readonly={{false}}
as |field|>
{{#if @model.errors.name}}
<field.errors as |errors|>
{{#each @model.errors.name as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.input>
<form.textarea
@name="description"
@type="description"
@value={{@model.description}}
@label={{t "form.label.description"}}
@error={{@model.errors.description}}
disabled={{@model.isSaving}}
readonly={{false}}
as |field|>
{{#if @model.errors.description}}
<field.errors as |errors|>
{{#each @model.errors.description as |error|}}
<errors.message>{{error.message}}</errors.message>
{{/each}}
</field.errors>
{{/if}}
</form.textarea>
<form.actions
@disabled={{if @model.cannotSave @model.cannotSave}}
@submitText={{t "actions.save"}}
@cancelText={{t "actions.cancel"}} />
</Rose::Form>
@@ -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;
}
@@ -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');
}
}
@@ -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');
}
}
+20
View File
@@ -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');
});
});
});
});
+3 -3
View File
@@ -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');
}
}
}
+16 -3
View File
@@ -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);
});
}
}
@@ -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]}
@@ -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');
}
}
}
}
@@ -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 });
@@ -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]}
+64
View File
@@ -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 });
}
}
}
@@ -0,0 +1,4 @@
import Route from '@ember/routing/route';
export default class ScopesScopeOrgsIndexRoute extends Route {
}
@@ -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',
});
}
}
+8
View File
@@ -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]}}
+8
View File
@@ -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]}}
+1 -33
View File
@@ -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';
+3 -1
View File
@@ -14,7 +14,9 @@
@iconOnly={{true}}
@icon="user-square-fill" as |dropdown|
>
<dropdown.button {{on "click" (route-action "invalidateSession")}}>
<dropdown.button
{{on "click" (route-action "invalidateSession")}}
>
{{t 'actions.deauthenticate'}}
</dropdown.button>
</utilities.dropdown>
+2
View File
@@ -1,6 +1,7 @@
<Rose::Layout::Sidebar as |sidebarLayout|>
{{#if this.session.isAuthenticated}}
{{#unless @model.isGlobal}}
<sidebarLayout.sidebar>
<Rose::Nav::Sidebar @title={{t "titles.general"}} as |nav|>
<nav.link @route="scopes.scope.projects">
@@ -26,6 +27,7 @@
{{outlet "sidebar"}}
</sidebarLayout.sidebar>
{{/unless}}
{{/if}}
<sidebarLayout.body>
@@ -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|}}
<dropdown.link @route="scopes.scope.authenticate" @model={{scope}}>
{{scope.displayName}}
</dropdown.link>
@@ -16,20 +16,24 @@
</Rose::Nav::Tabs>
</page.navigation>
<page.actions>
<Rose::Dropdown @text="{{t 'form.label.manage'}}" @dropdownRight={{true}} as |dropdown|>
<dropdown.button
@style="danger"
@disabled={{@model.canSave}}
{{on "click" (route-action "delete" @model)}}
>
{{t 'actions.delete-group'}}
</dropdown.button>
</Rose::Dropdown>
</page.actions>
<page.body>
<Form::Project
@model={{@model}}
@submit={{route-action "save" @model}}
@cancel={{route-action "cancel" @model}}
/>
<Rose::Separator />
<Rose::Button
@style="warning"
@disabled={{@model.canSave}}
{{on "click" (route-action "delete" @model)}}
>
{{t 'actions.delete'}}
</Rose::Button>
</page.body>
</Rose::Layout::Page>
@@ -0,0 +1,3 @@
{{title (t "resources.orgs")}}
{{outlet}}
@@ -0,0 +1,56 @@
<Rose::Layout::Page as |page|>
<page.breadcrumbs>
<BreadCrumbs />
</page.breadcrumbs>
<page.header>
<h2>{{t "resources.orgs"}}</h2>
</page.header>
<page.actions>
{{#if @model}}
<Rose::LinkButton
@route="scopes.scope.orgs.new"
@style="primary"
>
{{t 'titles.new'}}
</Rose::LinkButton>
{{/if}}
</page.actions>
<page.body>
{{#if @model}}
<Rose::Cards as |cards|>
{{#each @model as |org|}}
<cards.link
@title={{org.name}}
@subtitle={{org.id}}
@description={{org.description}}
@route="scopes.scope"
@model={{org}}
/>
{{/each}}
</Rose::Cards>
{{else}}
<Rose::Layout::Centered>
<Rose::Message @title={{t 'titles.orgs-welcome'}} as |message|>
<message.description>
{{t 'descriptions.orgs-welcome'}}
</message.description>
<message.link @route="scopes.scope.orgs.new">
<Rose::Icon @name="plus-circle-outline" />
{{t 'titles.new'}}
</message.link>
</Rose::Message>
</Rose::Layout::Centered>
{{/if}}
</page.body>
</Rose::Layout::Page>
@@ -0,0 +1,21 @@
{{title (t 'titles.new')}}
<Rose::Layout::Page as |page|>
<page.breadcrumbs>
<BreadCrumbs />
</page.breadcrumbs>
<page.header>
<h2>{{t "titles.new"}}</h2>
</page.header>
<page.body>
<Form::Org
@model={{@model}}
@submit={{route-action "save" @model}}
@cancel={{route-action "cancel" @model}}
/>
</page.body>
</Rose::Layout::Page>
@@ -10,4 +10,4 @@
{{on "click" (route-action "delete" @model)}}
>
{{t 'actions.delete'}}
</Rose::Button>
</Rose::Button>
@@ -16,20 +16,24 @@
</Rose::Nav::Tabs>
</page.navigation>
<page.actions>
<Rose::Dropdown @text="{{t 'form.label.manage'}}" @dropdownRight={{true}} as |dropdown|>
<dropdown.button
@style="danger"
@disabled={{@model.canSave}}
{{on "click" (route-action "delete" @model)}}
>
{{t 'actions.delete-project'}}
</dropdown.button>
</Rose::Dropdown>
</page.actions>
<page.body>
<Form::Project
@model={{@model}}
@submit={{route-action "save" @model}}
@cancel={{route-action "cancel" @model}}
/>
<Rose::Separator />
<Rose::Button
@style="warning"
@disabled={{@model.canSave}}
{{on "click" (route-action "delete" @model)}}
>
{{t 'actions.delete'}}
</Rose::Button>
</page.body>
</Rose::Layout::Page>
@@ -10,6 +10,18 @@
<h2>{{t "resources.role"}}</h2>
</page.header>
<page.actions>
<Rose::Dropdown @text="{{t 'form.label.manage'}}" @dropdownRight={{true}} as |dropdown|>
<dropdown.button
@style="danger"
@disabled={{@model.canSave}}
{{on "click" (route-action "delete" @model)}}
>
{{t 'actions.delete-role'}}
</dropdown.button>
</Rose::Dropdown>
</page.actions>
<page.navigation>
<Rose::Nav::Tabs as |nav|>
<nav.link @route='scopes.scope.roles.role.index'>
@@ -3,11 +3,4 @@
@submit={{route-action "save" @model}}
@cancel={{route-action "cancel" @model}}
/>
<Rose::Separator />
<Rose::Button
@style="warning"
@disabled={{@model.canSave}}
{{on "click" (route-action "delete" @model)}}
>
{{t 'actions.delete'}}
</Rose::Button>
@@ -21,21 +21,21 @@
<Rose::Table as |table|>
<table.header as |header|>
<header.row as |row|>
<row.cell>{{t 'form.label.name'}}</row.cell>
<row.cell>{{t 'form.label.description'}}</row.cell>
<row.headerCell>{{t 'form.label.name'}}</row.headerCell>
<row.headerCell>{{t 'form.label.description'}}</row.headerCell>
</header.row>
</table.header>
<table.body as |body|>
{{#each @model as |user|}}
<body.row as |row|>
<row.cell>
<row.headerCell>
<LinkTo
@route="scopes.scope.users.user"
@model={{user}}
>
{{user.name}}
</LinkTo>
</row.cell>
</row.headerCell>
<row.cell>{{user.description}}</row.cell>
</body.row>
{{/each}}
@@ -8,6 +8,18 @@
<h2>{{t "resources.user"}}</h2>
</page.header>
<page.actions>
<Rose::Dropdown @text="{{t 'form.label.manage'}}" @dropdownRight={{true}} as |dropdown|>
<dropdown.button
@style="danger"
@disabled={{@model.canSave}}
{{on "click" (route-action "delete" @model)}}
>
{{t 'actions.delete-user'}}
</dropdown.button>
</Rose::Dropdown>
</page.actions>
<page.navigation>
<Rose::Nav::Tabs as |nav|>
<nav.link @route='scopes.scope.users.user'>
@@ -17,19 +29,11 @@
</page.navigation>
<page.body>
<Form::Project
<Form::User
@model={{@model}}
@submit={{route-action "save" @model}}
@cancel={{route-action "cancel" @model}}
/>
<Rose::Separator />
<Rose::Button
@style="warning"
@disabled={{@model.canSave}}
{{on "click" (route-action "delete" @model)}}
>
{{t 'actions.delete'}}
</Rose::Button>
</page.body>
</Rose::Layout::Page>
+6 -1
View File
@@ -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 }
+32 -20
View File
@@ -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);
});
});
+2 -2
View File
@@ -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.',
@@ -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(),
+76
View File
@@ -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);
});
});
+2 -2
View File
@@ -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.',
+2 -2
View File
@@ -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.',
+2 -2
View File
@@ -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.',
@@ -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`
<Form::Org
@submit={{this.submit}}
@cancel={{this.cancel}}
/>`);
assert.ok(find('form'));
});
});
@@ -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');
});
});
@@ -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);
});
});
@@ -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);
});
});
@@ -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);
});
});
@@ -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);
});
});
@@ -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);
});
});
@@ -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);
});
});
@@ -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');
});
});
+9
View File
@@ -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: