Merge pull request #357 from hashicorp/feature-role-grant-scope
Feature role grant scope
This commit is contained in:
22 files changed
+1306
-841
No files matched your search
@@ -17,6 +17,11 @@ export default class GeneratedRoleModel extends BaseModel {
|
||||
})
|
||||
description;
|
||||
|
||||
@attr('string', {
|
||||
description: 'The scope to which this role applies. Defaults to the role\'s scope.',
|
||||
})
|
||||
grant_scope_id;
|
||||
|
||||
@attr('date', {
|
||||
description: 'The time this resource was created\nOutput only.',
|
||||
readOnly: true,
|
||||
|
||||
@@ -21,6 +21,7 @@ module('Unit | Serializer | role', function (hooks) {
|
||||
name: 'User',
|
||||
description: 'Description',
|
||||
version: 1,
|
||||
grant_scope_id: null
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
{{yield (hash
|
||||
link=(component "rose/dropdown/link" style=this.style)
|
||||
button=(component "rose/dropdown/button" style=this.style)
|
||||
item=(component "rose/anonymous" class="rose-dropdown-item")
|
||||
separator=(component "rose/anonymous" tagName="hr" class="rose-dropdown-separator")
|
||||
)}}
|
||||
</div>
|
||||
|
||||
@@ -37,6 +37,28 @@ A simple dropdown component with support for links and buttons.
|
||||
}}</Story>
|
||||
</Preview>
|
||||
|
||||
<Preview>
|
||||
<Story name="Dropdown with Radio Buttons" height="13rem">{{
|
||||
template: hbs`
|
||||
<Rose::Form as |form|>
|
||||
<form.radioGroup @name="color" @selectedValue="red" as |radioGroup|>
|
||||
<Rose::Dropdown @text="Color" as |dropdown|>
|
||||
<dropdown.item>
|
||||
<radioGroup.radio @label="Red" @value="red" />
|
||||
</dropdown.item>
|
||||
<dropdown.item>
|
||||
<radioGroup.radio @label="Green" @value="green" />
|
||||
</dropdown.item>
|
||||
<dropdown.item>
|
||||
<radioGroup.radio @label="Blue" @value="blue" />
|
||||
</dropdown.item>
|
||||
</Rose::Dropdown>
|
||||
</form.radioGroup>
|
||||
</Rose::Form>
|
||||
`,
|
||||
}}</Story>
|
||||
</Preview>
|
||||
|
||||
|
||||
```html
|
||||
<Rose::Dropdown @text="Choose One…" as |dropdown|>
|
||||
@@ -73,4 +95,25 @@ Dropdown indicator can be hidden using `showCaret` attribute.
|
||||
<dropdown.link @route="index">Menu item</dropdown.link>
|
||||
<dropdown.button>Button menu item</dropdown.button>
|
||||
</Rose::Dropdown>
|
||||
```
|
||||
```
|
||||
|
||||
Dropdowns also support radio buttons. Currently, this is the only supported
|
||||
form field.
|
||||
|
||||
```html
|
||||
<Rose::Form as |form|>
|
||||
<form.radioGroup @name="color" @selectedValue="red" as |radioGroup|>
|
||||
<Rose::Dropdown @text="Color" as |dropdown|>
|
||||
<dropdown.item>
|
||||
<radioGroup.radio @label="Red" @value="red" />
|
||||
</dropdown.item>
|
||||
<dropdown.item>
|
||||
<radioGroup.radio @label="Green" @value="green" />
|
||||
</dropdown.item>
|
||||
<dropdown.item>
|
||||
<radioGroup.radio @label="Blue" @value="blue" />
|
||||
</dropdown.item>
|
||||
</Rose::Dropdown>
|
||||
</form.radioGroup>
|
||||
</Rose::Form>
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { text, boolean } from '@storybook/addon-knobs';
|
||||
import { text, boolean, select } from '@storybook/addon-knobs';
|
||||
|
||||
<Meta title="Rose|Form/Radio/Group" component="Form/Radio/Group" />
|
||||
|
||||
@@ -41,6 +41,7 @@ Renders a form radiofield group.
|
||||
<radioGroup.radio
|
||||
@label={{firstFieldLabel}}
|
||||
@value={{firstFieldValue}}
|
||||
@icon={{firstFieldIcon}}
|
||||
/>
|
||||
<radioGroup.radio
|
||||
@label={{secondFieldLabel}}
|
||||
@@ -58,6 +59,16 @@ Renders a form radiofield group.
|
||||
selectedValue: text('selectedValue', 'disagree'),
|
||||
firstFieldLabel: text('first label', 'Agree'),
|
||||
firstFieldValue: text('first value', 'agree'),
|
||||
firstFieldIcon: select(
|
||||
'first icon',
|
||||
{
|
||||
'user-organization': 'user-organization',
|
||||
'copy-action': 'copy-action',
|
||||
'unfold-more': 'unfold-more',
|
||||
'unfold-less': 'unfold-less',
|
||||
},
|
||||
'user-organization'
|
||||
),
|
||||
secondFieldLabel: text('second label', 'Maybe'),
|
||||
secondFieldValue: text('second value', 'maybe'),
|
||||
thirdFieldLabel: text('third label', 'Disagree'),
|
||||
|
||||
@@ -12,7 +12,16 @@
|
||||
@disabled={{@disabled}}/>
|
||||
|
||||
<label class="rose-form-radio-label" for={{this.id}}>
|
||||
|
||||
{{#if @icon}}
|
||||
<Rose::Icon
|
||||
@name={{@icon}}
|
||||
@size="medium"
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
{{@label}}
|
||||
|
||||
</label>
|
||||
|
||||
{{yield}}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { text, boolean } from '@storybook/addon-knobs';
|
||||
import { text, boolean, select } from '@storybook/addon-knobs';
|
||||
|
||||
<Meta title="Rose|Form/Radio/Radio" component="Form/Radio/Radio" />
|
||||
|
||||
@@ -27,6 +27,7 @@ Renders a form radiofield.
|
||||
@value={{value}}
|
||||
@selectedValue={{selectedValue}}
|
||||
@disabled={{disabled}}
|
||||
@icon={{icon}}
|
||||
/>
|
||||
`,
|
||||
context: {
|
||||
@@ -38,6 +39,17 @@ Renders a form radiofield.
|
||||
'value and selectedValue must match to select radiofield'
|
||||
),
|
||||
disabled: boolean('disabled', false),
|
||||
icon: select(
|
||||
'icon',
|
||||
{
|
||||
'none': null,
|
||||
'user-organization': 'user-organization',
|
||||
'copy-action': 'copy-action',
|
||||
'unfold-more': 'unfold-more',
|
||||
'unfold-less': 'unfold-less',
|
||||
},
|
||||
null
|
||||
),
|
||||
}
|
||||
}}</Story>
|
||||
</Preview>
|
||||
@@ -1,3 +1,4 @@
|
||||
@import 'dropdown';
|
||||
@import 'button';
|
||||
@import 'link';
|
||||
@import 'item';
|
||||
@@ -0,0 +1,43 @@
|
||||
@use '../../variables/sizing';
|
||||
@use '../../utilities/type';
|
||||
|
||||
.rose-dropdown-item {
|
||||
@include type.type(s);
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: (sizing.rems(m)) * 20;
|
||||
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-form-radio {
|
||||
color: currentColor;
|
||||
margin-bottom: 0;
|
||||
|
||||
.rose-form-radio-label {
|
||||
display: block;
|
||||
padding-top: sizing.rems(xs) + sizing.rems(xxxxs);
|
||||
padding-bottom: sizing.rems(xs) + sizing.rems(xxxxs);
|
||||
font-weight: normal;
|
||||
|
||||
// Now that the target height is 36, the radio image needs to be
|
||||
// repositioned
|
||||
&::after {
|
||||
top: calc(50% - #{sizing.rems(s)});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,11 @@ $data-svg-prefix: 'data:image/svg+xml;utf-8,';
|
||||
color: currentColor;
|
||||
padding-left: sizing.rems(l);
|
||||
|
||||
.rose-icon {
|
||||
margin-right: sizing.rems(xxs);
|
||||
margin-left: sizing.rems(xxs);
|
||||
}
|
||||
|
||||
&::before {
|
||||
position: absolute;
|
||||
top: sizing.rems(xs);
|
||||
|
||||
@@ -41,3 +41,28 @@
|
||||
<dropdown.link @route="index">Menu item</dropdown.link>
|
||||
<dropdown.button>Button menu item</dropdown.button>
|
||||
</Rose::Dropdown>
|
||||
|
||||
<Rose::Form as |form|>
|
||||
<form.radioGroup @name="color" @selectedValue="red" as |radioGroup|>
|
||||
<Rose::Dropdown @text="Color" as |dropdown|>
|
||||
<dropdown.item>
|
||||
<radioGroup.radio
|
||||
@label="Red"
|
||||
@value="red"
|
||||
/>
|
||||
</dropdown.item>
|
||||
<dropdown.item>
|
||||
<radioGroup.radio
|
||||
@label="Green"
|
||||
@value="green"
|
||||
/>
|
||||
</dropdown.item>
|
||||
<dropdown.item>
|
||||
<radioGroup.radio
|
||||
@label="Blue"
|
||||
@value="blue"
|
||||
/>
|
||||
</dropdown.item>
|
||||
</Rose::Dropdown>
|
||||
</form.radioGroup>
|
||||
</Rose::Form>
|
||||
@@ -17,6 +17,11 @@ module('Integration | Component | rose/form/radio/radio', function (hooks) {
|
||||
assert.equal(await find('label').textContent.trim(), 'Label');
|
||||
});
|
||||
|
||||
test('it supports an icon', async function (assert) {
|
||||
await render(hbs`<Rose::Form::Radio::Radio @icon="user-square-fill" />`);
|
||||
assert.ok(find('svg'));
|
||||
});
|
||||
|
||||
test('it is checked when @value and @selectedValue values match', async function (assert) {
|
||||
await render(
|
||||
hbs`<Rose::Form::Radio::Radio @value="tree" @selectedValue="tree"/>`
|
||||
|
||||
@@ -44,6 +44,49 @@
|
||||
{{/if}}
|
||||
</form.textarea>
|
||||
|
||||
<fieldset
|
||||
aria-labelledby="legend-text-grant-scope"
|
||||
aria-describedby="helper-text-grant-scope"
|
||||
>
|
||||
<legend id="legend-text-grant-scope">{{t "form.grant_scope.label"}}</legend>
|
||||
<Rose::Form::HelperText @id="helper-text-grant-scope" @error={{@model.errors.grant_scope_id}}>
|
||||
{{t "form.grant_scope.help"}}
|
||||
</Rose::Form::HelperText>
|
||||
<form.radioGroup @name="grant_scope_id" @selectedValue={{@model.grant_scope_id}} as |radioGroup|>
|
||||
<Rose::Dropdown
|
||||
@text={{this.selectedGrantScope.displayName}}
|
||||
@icon={{if this.selectedGrantScope.isProject "app-icons/project" "app-icons/org"}}
|
||||
as |dropdown|>
|
||||
{{#each @grantScopes as |grantScope|}}
|
||||
<dropdown.item>
|
||||
<radioGroup.radio
|
||||
@label={{grantScope.model.displayName}}
|
||||
@value={{grantScope.model.id}}
|
||||
@icon={{unless grantScope.model.isProject "app-icons/org"}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
{{#each grantScope.subScopes as |subScope|}}
|
||||
<dropdown.item class="indent-1">
|
||||
<radioGroup.radio
|
||||
@label={{subScope.model.displayName}}
|
||||
@value={{subScope.model.id}}
|
||||
@icon={{unless subScope.model.isProject "app-icons/org"}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
{{#each subScope.subScopes as |subSubScope|}}
|
||||
<dropdown.item class="indent-2">
|
||||
<radioGroup.radio
|
||||
@label={{subSubScope.displayName}}
|
||||
@value={{subSubScope.id}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
{{/each}}
|
||||
</Rose::Dropdown>
|
||||
</form.radioGroup>
|
||||
</fieldset>
|
||||
|
||||
<form.actions
|
||||
@disabled={{if @model.cannotSave @model.cannotSave}}
|
||||
@enableEditText={{t "actions.edit-form"}}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { computed } from '@ember/object';
|
||||
|
||||
export default class FormRoleGrantsComponent extends Component {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* Returns the selected grant scope model.
|
||||
* @type {ScopeModel}
|
||||
*/
|
||||
@computed('args.model.{store,grant_scope_id}')
|
||||
get selectedGrantScope() {
|
||||
return this.args.model.grant_scope_id
|
||||
? this.args.model.store
|
||||
.peekRecord('scope', this.args.model.grant_scope_id)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,32 @@
|
||||
import Route from '@ember/routing/route';
|
||||
|
||||
export default class ScopesScopeRolesRoleIndexRoute extends Route {}
|
||||
export default class ScopesScopeRolesRoleIndexRoute extends Route {
|
||||
|
||||
/**
|
||||
* Load the role's scope and sub scopes into a hierarchical data structure.
|
||||
* @param {Model} model
|
||||
*/
|
||||
async afterModel(model) {
|
||||
const defaultScope = await this.store.findRecord('scope', model.scopeID);
|
||||
const subScopes = (await this.store.query('scope', { scope_id: defaultScope.id }))
|
||||
.map(scope => ({
|
||||
model: scope,
|
||||
subScopes: this.store.query('scope', { scope_id: scope.id })
|
||||
}));
|
||||
const grantScopes = [{
|
||||
model: defaultScope,
|
||||
subScopes
|
||||
}];
|
||||
this.set('grantScopes', grantScopes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds `grantScopes` to the context.
|
||||
* @param {Controller} controller
|
||||
*/
|
||||
setupController(controller) {
|
||||
super.setupController(...arguments);
|
||||
controller.set('grantScopes', this.grantScopes);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -42,6 +42,26 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.rose-dropdown {
|
||||
.rose-dropdown-item {
|
||||
&.indent-1 {
|
||||
padding-left: sizing.rems(s) * 2;
|
||||
}
|
||||
|
||||
&.indent-2 {
|
||||
padding-left: sizing.rems(l) + sizing.rems(m);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fieldset {
|
||||
margin-bottom: sizing.rems(m);
|
||||
|
||||
legend {
|
||||
@extend .rose-form-label;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.rose-table {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Form::Role
|
||||
@model={{@model}}
|
||||
@grantScopes={{this.grantScopes}}
|
||||
@submit={{route-action "save" @model}}
|
||||
@cancel={{route-action "cancel" @model}}
|
||||
/>
|
||||
|
||||
@@ -19,6 +19,14 @@ export default factory.extend({
|
||||
const groups = server.createList('group', 2, { scope });
|
||||
role.update({ users, groups });
|
||||
}
|
||||
})
|
||||
}),
|
||||
|
||||
/**
|
||||
* Set the grant scope ID to match the scope ID.
|
||||
*/
|
||||
afterCreate(role) {
|
||||
const { scope: { id } } = role;
|
||||
role.update({ grant_scope_id: id });
|
||||
}
|
||||
|
||||
});
|
||||
@@ -72,6 +72,7 @@
|
||||
"ember-notify": "^6.0.0",
|
||||
"ember-page-title": "^5.2.1",
|
||||
"ember-qunit": "^4.6.0",
|
||||
"ember-radio-button": "^2.0.1",
|
||||
"ember-resolver": "^8.0.0",
|
||||
"ember-route-action-helper": "^2.0.8",
|
||||
"ember-simple-auth": "^3.0.0",
|
||||
|
||||
@@ -19,6 +19,7 @@ module('Acceptance | roles', function (hooks) {
|
||||
scopes: {
|
||||
global: null,
|
||||
org: null,
|
||||
project: null,
|
||||
},
|
||||
role: null,
|
||||
};
|
||||
@@ -36,6 +37,13 @@ module('Acceptance | roles', function (hooks) {
|
||||
type: 'org',
|
||||
scope: { id: 'global', type: 'global' },
|
||||
});
|
||||
// The project scope is not yet used for role tests (though it will be
|
||||
// in the future). This is created simply to test the grant scope loading
|
||||
// mechanism.
|
||||
instances.scopes.project = this.server.create('scope', {
|
||||
type: 'project',
|
||||
scope: { id: instances.scopes.org.id, type: instances.scopes.org.type },
|
||||
});
|
||||
instances.role = this.server.create('role', {
|
||||
scope: instances.scopes.org,
|
||||
}, 'withPrincipals');
|
||||
|
||||
@@ -43,3 +43,6 @@ session_connection_limit:
|
||||
help: The maximum number of connections allowed per session. For unlimited, specify "-1".
|
||||
host_set:
|
||||
label: Host Set
|
||||
grant_scope:
|
||||
label: Grant Scope
|
||||
help: Specify the scope to which this role applies.
|
||||
Reference in new issue
Block a user