Merge pull request #357 from hashicorp/feature-role-grant-scope

Feature role grant scope
This commit is contained in:
Susmitha Girumala authored and GitHub committed 2020-11-04 15:45:11 -05:00
commit 4e361cb732
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&hellip;" 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"}}
+18
View File
@@ -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);
}
}
+20
View File
@@ -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}}
/>
+9 -1
View File
@@ -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 });
}
});
+1
View File
@@ -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",
+8
View File
@@ -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');
+3
View File
@@ -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.
+1009 -835
View File
File diff suppressed because it is too large. Load diff