Merge remote-tracking branch 'origin/develop' into ICU-294-list-host-sets
This commit is contained in:
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…" @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…" @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…" 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
-40
@@ -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';
|
||||
@@ -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'));
|
||||
});
|
||||
});
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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]}
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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]}}
|
||||
|
||||
@@ -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,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';
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
+1
-1
@@ -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>
|
||||
@@ -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 }
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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(),
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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.',
|
||||
|
||||
@@ -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.',
|
||||
|
||||
@@ -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,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:
|
||||
|
||||
Reference in new issue
Block a user