Compare commits
280
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4fa5b924c9 | ||
|
|
e86a4fc27d | ||
|
|
d72b02ad29 | ||
|
|
545e7d5a7f | ||
|
|
4055f3a06e | ||
|
|
4fed7f3d94 | ||
|
|
ee29f3c599 | ||
|
|
a1b0481db9 | ||
|
|
685ddeac7e | ||
|
|
f2f5a46cdf | ||
|
|
1a0650ff7c | ||
|
|
82a0c7c17c | ||
|
|
1397df807f | ||
|
|
cc01bddea3 | ||
|
|
9f32b8cdc3 | ||
|
|
ae8e20b8e1 | ||
|
|
aab980a7dc | ||
|
|
13146975e2 | ||
|
|
542b469ad8 | ||
|
|
c2c63294f4 | ||
|
|
56936f78b8 | ||
|
|
dcc77153bf | ||
|
|
4b587d35fa | ||
|
|
ce7cdba0bc | ||
|
|
4e13ea56c3 | ||
|
|
3ef7436f3f | ||
|
|
ee748de5c9 | ||
|
|
74501a440f | ||
|
|
828faf2be4 | ||
|
|
dfb9d6e686 | ||
|
|
2be18bcf6f | ||
|
|
3bef3e4bc5 | ||
|
|
81447e65c8 | ||
|
|
2e71beee81 | ||
|
|
dfbb54a1ee | ||
|
|
0d67b512a2 | ||
|
|
e551ec4008 | ||
|
|
49a85f1a7f | ||
|
|
15d3520408 | ||
|
|
5d7c0180d5 | ||
|
|
053e16dca8 | ||
|
|
3e2ed2dc27 | ||
|
|
faaf7352c1 | ||
|
|
58252ee6f5 | ||
|
|
85ddd7fc4b | ||
|
|
863e81bc01 | ||
|
|
4b212e1ae0 | ||
|
|
343f73bc21 | ||
|
|
cf5f41acbd | ||
|
|
2f12cc91f2 | ||
|
|
99125dbb77 | ||
|
|
3bfae4b33d | ||
|
|
da6b34b337 | ||
|
|
9a909c0ec1 | ||
|
|
42c01100ad | ||
|
|
fe56acd57f | ||
|
|
e4fb2e7c4d | ||
|
|
b1361bcb83 | ||
|
|
30090d82e3 | ||
|
|
fbe8aefcb4 | ||
|
|
fa02fd3f89 | ||
|
|
f021719d8b | ||
|
|
be9ee8f672 | ||
|
|
48ca4c5ac0 | ||
|
|
ac7db55be2 | ||
|
|
dea79d785b | ||
|
|
f597901140 | ||
|
|
0c1b5bf879 | ||
|
|
bab68f4507 | ||
|
|
c6e4a24dab | ||
|
|
0bcb91ede0 | ||
|
|
fbeb764c76 | ||
|
|
67959d7a58 | ||
|
|
c4bb5173d2 | ||
|
|
ea011592a7 | ||
|
|
e761e0e4af | ||
|
|
fa36f9008b | ||
|
|
15d360abc5 | ||
|
|
9914954d08 | ||
|
|
ec184ff4f1 | ||
|
|
bb2d0420d9 | ||
|
|
e3cabe3102 | ||
|
|
7065f462cd | ||
|
|
6e4ab7d881 | ||
|
|
859a3fdd83 | ||
|
|
b1568aa30f | ||
|
|
a46b44b13f | ||
|
|
59dee87141 | ||
|
|
f1a20ea33d | ||
|
|
4a5e72d77b | ||
|
|
87a6d839e7 | ||
|
|
1b449bdea4 | ||
|
|
1ba0c873e9 | ||
|
|
f8e7ec7f5f | ||
|
|
4d4affe6d7 | ||
|
|
b13befaff2 | ||
|
|
3836656042 | ||
|
|
55e3284e3f | ||
|
|
27a416001b | ||
|
|
43a41d6818 | ||
|
|
b17088803f | ||
|
|
5eb9d01b23 | ||
|
|
288a38f6ea | ||
|
|
e7724ab8a5 | ||
|
|
e7c7d2b280 | ||
|
|
80abe240b4 | ||
|
|
6960aaeffc | ||
|
|
deb8871060 | ||
|
|
ba671b4ab3 | ||
|
|
bd3a4935a4 | ||
|
|
13ce61218c | ||
|
|
fa2e1021c5 | ||
|
|
c213ee18b2 | ||
|
|
43c667729f | ||
|
|
26e6cc8a9b | ||
|
|
5d57c8dd25 | ||
|
|
f31a93085a | ||
|
|
1927ef7832 | ||
|
|
fd53f7b282 | ||
|
|
eaaeed6d1b | ||
|
|
8f8566548a | ||
|
|
40e12366e6 | ||
|
|
39ea49ccdf | ||
|
|
a69dfa251a | ||
|
|
906a336acb | ||
|
|
9c9988a305 | ||
|
|
1a901b15d2 | ||
|
|
e894dbc4e8 | ||
|
|
18e95435f5 | ||
|
|
e2154f9698 | ||
|
|
d27ddb1eee | ||
|
|
8d3aea2e23 | ||
|
|
4d47b3cf51 | ||
|
|
b741d3af16 | ||
|
|
92cf57a542 | ||
|
|
aa6d369514 | ||
|
|
1ed685d81f | ||
|
|
586a4dbe23 | ||
|
|
f858da88f2 | ||
|
|
5f342a0ee8 | ||
|
|
3468acce39 | ||
|
|
b0a93e2e2d | ||
|
|
c2b66b74eb | ||
|
|
32ec1f0f70 | ||
|
|
e539a86600 | ||
|
|
efcdd0701e | ||
|
|
9ab9800f7e | ||
|
|
4336fde21b | ||
|
|
4cb7985ac1 | ||
|
|
04726a61da | ||
|
|
53270519cb | ||
|
|
5105d80a9d | ||
|
|
53423739d3 | ||
|
|
f782d90a7e | ||
|
|
721d09899a | ||
|
|
d1303c7bca | ||
|
|
31682ffd85 | ||
|
|
b5a791d618 | ||
|
|
e110f33e40 | ||
|
|
68bf02ee25 | ||
|
|
063d643d76 | ||
|
|
b6818871fc | ||
|
|
1effa79c93 | ||
|
|
6fd075f7e8 | ||
|
|
0d8508cd4e | ||
|
|
f3bd8d0b6f | ||
|
|
8889e86cec | ||
|
|
6500d56273 | ||
|
|
80cffeb48b | ||
|
|
263c8c01e9 | ||
|
|
74531f5d85 | ||
|
|
e0eed2b4ea | ||
|
|
c9df20184a | ||
|
|
afa1deef94 | ||
|
|
5d04dca21e | ||
|
|
746f45b6da | ||
|
|
a62d36af92 | ||
|
|
a41572302a | ||
|
|
2138aa8b01 | ||
|
|
4ea53c9cc7 | ||
|
|
c4e5a4c121 | ||
|
|
e5544415b0 | ||
|
|
ce84d2405c | ||
|
|
b07c90ee53 | ||
|
|
27a28c6db2 | ||
|
|
82eed3c5fe | ||
|
|
3d13d8f412 | ||
|
|
6927008041 | ||
|
|
c978958df2 | ||
|
|
e8b2e1aa6f | ||
|
|
43528c64c2 | ||
|
|
d7f090f348 | ||
|
|
93a9efbd1b | ||
|
|
a50a0735bb | ||
|
|
838567c2d2 | ||
|
|
95e6144e44 | ||
|
|
58c9df272f | ||
|
|
3696e45275 | ||
|
|
73503f98dc | ||
|
|
15f6c408d4 | ||
|
|
0ccc6e7f78 | ||
|
|
69a10f891a | ||
|
|
18d5b60c6d | ||
|
|
f279ac44ca | ||
|
|
29f21e0dfe | ||
|
|
9e588212a4 | ||
|
|
3361aa1800 | ||
|
|
5c3b44b2f6 | ||
|
|
59d345a912 | ||
|
|
2adfdf6ef6 | ||
|
|
8aa9a51c88 | ||
|
|
60b7d0134a | ||
|
|
ae26d588a9 | ||
|
|
abc1827280 | ||
|
|
682bb14037 | ||
|
|
28ec1008a0 | ||
|
|
83ab375829 | ||
|
|
e960c34542 | ||
|
|
4d2cb86a62 | ||
|
|
22131864f8 | ||
|
|
ac8a6054b0 | ||
|
|
22d578620d | ||
|
|
82506d11c8 | ||
|
|
84818a54f5 | ||
|
|
a0b2c6883a | ||
|
|
180b74cc98 | ||
|
|
12f2042f87 | ||
|
|
f096eaf7d7 | ||
|
|
e2ff75f435 | ||
|
|
96ef767e65 | ||
|
|
f387416d6d | ||
|
|
933ade8a16 | ||
|
|
bf4d43cee2 | ||
|
|
6b3b6c4d58 | ||
|
|
4630e130d4 | ||
|
|
42ace44716 | ||
|
|
888b106780 | ||
|
|
6bd994c86b | ||
|
|
6ce09162d7 | ||
|
|
f487c4f0d9 | ||
|
|
2d0afd7b9c | ||
|
|
f406fc3861 | ||
|
|
760710ae23 | ||
|
|
0219b5c99a | ||
|
|
6056b50939 | ||
|
|
9e57f838d5 | ||
|
|
896671ceb1 | ||
|
|
ca9485053e | ||
|
|
7e6c46de2b | ||
|
|
c68c1ba0fb | ||
|
|
5f83751db2 | ||
|
|
8179ab7dec | ||
|
|
d93d48197f | ||
|
|
d010d25934 | ||
|
|
3bb9d66377 | ||
|
|
42fe8f4eca | ||
|
|
45bf901d9e | ||
|
|
a35aacdf5e | ||
|
|
feddb2efad | ||
|
|
47c664eda9 | ||
|
|
9d847348a0 | ||
|
|
9fc2b7a409 | ||
|
|
02642bc5e4 | ||
|
|
8f8e257523 | ||
|
|
e50db68abe | ||
|
|
ec23e3706b | ||
|
|
e9c297b1cf | ||
|
|
4ae5e73def | ||
|
|
78505c720f | ||
|
|
09c8680ff2 | ||
|
|
811d1dbe32 | ||
|
|
dbe7003068 | ||
|
|
05709f7ee5 | ||
|
|
3084d6cf06 | ||
|
|
ac92575b36 | ||
|
|
900f342894 | ||
|
|
2f3478d043 | ||
|
|
af8947dc62 | ||
|
|
a3c12b36e7 | ||
|
|
92640b96aa |
No files matched your search
@@ -1,5 +1,5 @@
|
||||
name: Validate Monorepo
|
||||
on: [push]
|
||||
on: pull_request
|
||||
jobs:
|
||||
|
||||
compliance:
|
||||
@@ -39,6 +39,7 @@ jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
@@ -56,6 +57,7 @@ jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/cache@v2
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
name: "Trigger Test Boundary UI release"
|
||||
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: Please use tag-like format n.n.n-someword (1.111.11-beta)
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
trigger-circleci-release:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Trigger Release job
|
||||
if: github.event_name == 'workflow_dispatch'
|
||||
run: |
|
||||
echo "Triggering Test release using BRANCH and TEST_TAG ${{ github.event.inputs.branch }}"
|
||||
|
||||
curl -u ${{ secrets.CIRCLE_TOKEN }}: -X POST --header "Content-Type: application/json" \
|
||||
-d '{ "branch": "main", "parameters": {"TAG": "'${{ github.event.inputs.branch }}'" }}' \
|
||||
https://circleci.com/api/v2/project/github/hashicorp/boundary-ui-releases/pipeline
|
||||
@@ -0,0 +1,6 @@
|
||||
# Contributing to Boundary
|
||||
|
||||
Thank you for contributing to Boundary! Here you can find common questions around reporting issues and opening
|
||||
pull requests to our project.
|
||||
|
||||
When contributing in any way to the Boundary project (new issue, PR, etc), please be aware that our team identifies with many gender pronouns and to use non-binary pronouns when addressing our team. For more reading on our code of conduct, please see the [HashiCorp community guidelines](https://www.hashicorp.com/community-guidelines).
|
||||
@@ -24,6 +24,9 @@ subproject, e.g. running or testing.
|
||||
- [Building Admin UI for Production](#building-admin-ui-for-production)
|
||||
- [Building with a Container](#building-with-a-container)
|
||||
- [Building Desktop UI for Production](#building-desktop-ui-for-production)
|
||||
- [Connect Boundary UI to Boundary local instance](#connect-boundary-ui-to-boundary-local-instance)
|
||||
- [For admin](#for-admin)
|
||||
- [For desktop](#for-desktop)
|
||||
- [Committing](#committing)
|
||||
- [License Checking](#license-checking)
|
||||
|
||||
@@ -35,6 +38,9 @@ You will need the following things properly installed on your computer.
|
||||
|
||||
* [Git][git]
|
||||
* [Node.js][node]
|
||||
* v10–v15
|
||||
* **Apple Silicon Users**: if you experience problems building or running
|
||||
Boundary UIs, try Node v15.
|
||||
* [Yarn][yarn] installed globally
|
||||
* [Google Chrome][chrome]
|
||||
* [Firefox][firefox]
|
||||
@@ -122,6 +128,31 @@ environment variable to set signing certificate name (e.g Developer ID Applicati
|
||||
Assets are saved to `ui/desktop/electron-app/out/make/`.
|
||||
DMG packaged desktop UI is available at asset location as `boundary.dmg`.
|
||||
|
||||
### Connect Boundary UI to Boundary local instance
|
||||
|
||||
This describes how to connect local Boundary UI to your local instance of Boundary.
|
||||
|
||||
This assumes you already have up and running a Boundary dev instance in your local environment with a listener `127.0.0.1:9200` (default behaviour). [Learn how to start a local Boundary dev instance](https://learn.hashicorp.com/tutorials/boundary/getting-started-dev?in=boundary/getting-started#start-a-dev-environment).
|
||||
#### For admin
|
||||
|
||||
This assumes you are within `boundary-ui/ui/admin`.
|
||||
|
||||
You will need to turn `off` mirage, tell the UI where to find Boundary and run it: `$ ENABLE_MIRAGE=false API_HOST=http://localhost:9200 yarn start`.
|
||||
|
||||
Following terminal instructions, open in your browser: `http://localhost:4200/`.
|
||||
|
||||
Once you open the UI you will see the login screen, authenticate using generated admin login name and password.
|
||||
|
||||
#### For desktop
|
||||
|
||||
This assumes you are within `boundary-ui/ui/desktop`.
|
||||
|
||||
You will need to turn `off` mirage and run it: `$ ENABLE_MIRAGE=false yarn start:desktop`.
|
||||
|
||||
In login screen, authenticate using generated admin login name and password.
|
||||
|
||||
Enter the origin of your Boundary dev instance, by default is: `http://localhost:9200`.
|
||||
|
||||
### Committing
|
||||
|
||||
Instead of the default `git commit`, we prescribe a custom command that ensures
|
||||
|
||||
@@ -3,7 +3,7 @@ import { serializeIntoHash } from '@ember-data/adapter/-private';
|
||||
/* eslint-disable-next-line ember/no-mixins */
|
||||
import AdapterBuildURLMixin from '../mixins/adapter-build-url';
|
||||
import config from 'ember-get-config';
|
||||
import { get, getWithDefault } from '@ember/object';
|
||||
import { get } from '@ember/object';
|
||||
import { InvalidError } from '@ember-data/adapter/error';
|
||||
import { dasherize } from '@ember/string';
|
||||
import { pluralize } from 'ember-inflector';
|
||||
@@ -69,7 +69,7 @@ export default class ApplicationAdapter extends RESTAdapter.extend(
|
||||
* @return {string}
|
||||
*/
|
||||
urlSuffix(modelName, id, snapshot = {}) {
|
||||
const method = getWithDefault(snapshot, 'adapterOptions.method', '');
|
||||
const method = get(snapshot, 'adapterOptions.method') || '';
|
||||
return method ? `:${method}` : '';
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,12 @@ import { attr } from '@ember-data/model';
|
||||
export default class GeneratedAuthMethodModel extends BaseModel {
|
||||
// =attributes
|
||||
|
||||
@attr('boolean', {
|
||||
description: 'Specifies if this is the primary auth method for its scope.',
|
||||
readOnly: true,
|
||||
})
|
||||
is_primary;
|
||||
|
||||
@attr('string', {
|
||||
description: 'The type of the resource, to help differentiate schemas',
|
||||
})
|
||||
|
||||
@@ -7,6 +7,12 @@ import { attr } from '@ember-data/model';
|
||||
export default class GeneratedScopeModel extends BaseModel {
|
||||
// =attributes
|
||||
|
||||
@attr('string', {
|
||||
description:
|
||||
'The ID of the primary auth method for this scope. A primary auth method\nis allowed to vivify users when new accounts are created and is the source for the users account info.',
|
||||
})
|
||||
primary_auth_method_id;
|
||||
|
||||
@attr('string', {
|
||||
description: 'The type of the resource',
|
||||
})
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
import GeneratedAuthMethodModel from '../generated/models/auth-method';
|
||||
import { equal } from '@ember/object/computed';
|
||||
|
||||
export default class AuthMethodModel extends GeneratedAuthMethodModel {}
|
||||
export default class AuthMethodModel extends GeneratedAuthMethodModel {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
@equal('is_primary', true) isPrimary;
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isPassword() {
|
||||
return this.type === 'password';
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {boolean}
|
||||
*/
|
||||
get isOIDC() {
|
||||
return this.type === 'oidc';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import ApplicationSerializer from './application';
|
||||
|
||||
export default class AuthMethodSerializer extends ApplicationSerializer {
|
||||
/**
|
||||
* Sorts the array in order of "is_primary" such that the primary method
|
||||
* appears first. The "natural" order of auth methods places primary first.
|
||||
*
|
||||
* @override
|
||||
* @return {object}
|
||||
*/
|
||||
normalizeArrayResponse() {
|
||||
const normalized = super.normalizeArrayResponse(...arguments);
|
||||
normalized.data = normalized.data.sort((a) =>
|
||||
a.attributes.is_primary ? -1 : 1
|
||||
);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets `is_primary` to `false` if it is missing in the response. The API
|
||||
* omits "zero" values per:
|
||||
* https://developers.google.com/protocol-buffers/docs/proto3#default
|
||||
*
|
||||
* TODO: generalize this so that all zero values are reified.
|
||||
* @override
|
||||
* @return {object}
|
||||
*/
|
||||
normalize() {
|
||||
const normalized = super.normalize(...arguments);
|
||||
if (!normalized.data.attributes.is_primary) {
|
||||
normalized.data.attributes.is_primary = false;
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'api/serializers/auth-method';
|
||||
@@ -60,7 +60,17 @@ export default function () {
|
||||
this.post('/auth-methods');
|
||||
this.get('/auth-methods/:id');
|
||||
this.patch('/auth-methods/:id');
|
||||
this.del('/auth-methods/:id');
|
||||
this.del('/auth-methods/:id', ({ authMethods }, { params: { id } }) => {
|
||||
const authMethod = authMethods.find(id);
|
||||
const scope = authMethod.scope;
|
||||
authMethod.destroy();
|
||||
// If the primary auth method is deleted, the associated scope's
|
||||
// primary_auth_method_id field should be set to null.
|
||||
if (scope.primaryAuthMethodId === id) {
|
||||
scope.update({ primaryAuthMethodId: null });
|
||||
}
|
||||
return new Response(202);
|
||||
});
|
||||
|
||||
// Auth Method Accounts
|
||||
this.get(
|
||||
@@ -413,9 +423,12 @@ export default function () {
|
||||
);
|
||||
this.get('/sessions/:id', function ({ sessions }, { params: { id } }) {
|
||||
const session = sessions.find(id);
|
||||
return session.update({
|
||||
status: pickRandomStatusString(),
|
||||
});
|
||||
if (!Ember.testing) {
|
||||
session.update({
|
||||
status: pickRandomStatusString(),
|
||||
});
|
||||
}
|
||||
return session;
|
||||
});
|
||||
this.post(
|
||||
'/sessions/:idMethod',
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import factory from '../generated/factories/auth-method';
|
||||
import { trait } from 'ember-cli-mirage';
|
||||
|
||||
const types = ['password', 'oidc'];
|
||||
|
||||
export default factory.extend({
|
||||
id: (i) => `auth-method-id-${i}`,
|
||||
|
||||
// Cycle through available types
|
||||
type: (i) => types[i % types.length],
|
||||
|
||||
/**
|
||||
* Adds accounts (with associated users) to auth method.
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import factory from '../generated/factories/target';
|
||||
import { trait } from 'ember-cli-mirage';
|
||||
import { random } from 'faker';
|
||||
import { random, datatype } from 'faker';
|
||||
|
||||
const randomBoolean = (chance = 0.5) => Math.random() < chance;
|
||||
const hostSetChance = 0.3;
|
||||
@@ -9,7 +9,7 @@ export default factory.extend({
|
||||
/**
|
||||
* -1 means "unlimited" and we want to generate these on occassion.
|
||||
*/
|
||||
session_connection_limit: () => random.arrayElement([-1, random.number()]),
|
||||
session_connection_limit: () => random.arrayElement([-1, datatype.number()]),
|
||||
|
||||
/**
|
||||
* Generates attributes fields by type.
|
||||
@@ -18,7 +18,7 @@ export default factory.extend({
|
||||
if (target.type === 'tcp') {
|
||||
target.update({
|
||||
attributes: {
|
||||
default_port: random.number(),
|
||||
default_port: datatype.number(),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedAccountFactory
|
||||
@@ -10,6 +10,6 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
version: () => random.number(),
|
||||
version: () => datatype.number(),
|
||||
attributes: () => ({ login_name: random.words() }),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedAuthMethodModelFactory
|
||||
@@ -10,5 +10,5 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
disabled: () => random.boolean(),
|
||||
disabled: () => datatype.boolean(),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedGroupModelFactory
|
||||
@@ -10,5 +10,5 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
disabled: () => random.boolean(),
|
||||
disabled: () => datatype.boolean(),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedHostCatalogModelFactory
|
||||
@@ -10,5 +10,5 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
disabled: () => random.boolean(),
|
||||
disabled: () => datatype.boolean(),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedHostSetModelFactory
|
||||
@@ -10,6 +10,6 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
version: () => random.number(),
|
||||
size: () => random.number(),
|
||||
version: () => datatype.number(),
|
||||
size: () => datatype.number(),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date, internet } from 'faker';
|
||||
import { random, date, internet, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedHostModelFactory
|
||||
@@ -10,7 +10,7 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
version: () => random.number(),
|
||||
version: () => datatype.number(),
|
||||
attributes: () => {
|
||||
return { address: internet.ipv6() };
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedProjectModelFactory
|
||||
@@ -10,5 +10,5 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
disabled: () => random.boolean(),
|
||||
disabled: () => datatype.boolean(),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedRoleModelFactory
|
||||
@@ -10,6 +10,6 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
disabled: () => random.boolean(),
|
||||
version: () => random.number(),
|
||||
disabled: () => datatype.boolean(),
|
||||
version: () => datatype.number(),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedScopeModelFactory
|
||||
@@ -9,5 +9,5 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
disabled: () => random.boolean(),
|
||||
disabled: () => datatype.boolean(),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedSessionModelFactory
|
||||
@@ -8,5 +8,5 @@ export default Factory.extend({
|
||||
type: 'tcp',
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
version: () => random.number(),
|
||||
version: () => datatype.number(),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedTargetModelFactory
|
||||
@@ -7,11 +7,11 @@ import { random, date } from 'faker';
|
||||
export default Factory.extend({
|
||||
type: 'tcp',
|
||||
name: () => random.words(),
|
||||
session_max_seconds: () => random.number(),
|
||||
session_connection_limit: () => random.number(),
|
||||
session_max_seconds: () => datatype.number(),
|
||||
session_connection_limit: () => datatype.number(),
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
disabled: () => random.boolean(),
|
||||
version: () => random.number(),
|
||||
disabled: () => datatype.boolean(),
|
||||
version: () => datatype.number(),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Factory } from 'ember-cli-mirage';
|
||||
import { random, date } from 'faker';
|
||||
import { random, date, datatype } from 'faker';
|
||||
|
||||
/**
|
||||
* GeneratedUserModelFactory
|
||||
@@ -10,5 +10,5 @@ export default Factory.extend({
|
||||
description: () => random.words(),
|
||||
created_time: () => date.recent(),
|
||||
updated_time: () => date.recent(),
|
||||
disabled: () => random.boolean(),
|
||||
disabled: () => datatype.boolean(),
|
||||
});
|
||||
@@ -12,7 +12,7 @@ import { Response } from 'miragejs';
|
||||
// */
|
||||
// export default function authenticateHandler(schema, request) {
|
||||
// const payload = JSON.parse(request.requestBody);
|
||||
// if (payload.credentials.login_name === 'error') {
|
||||
// if (payload.attributes.login_name === 'error') {
|
||||
// return new Response(400);
|
||||
// } else {
|
||||
// const cookieName = config.auth.passwordCookieName;
|
||||
@@ -23,32 +23,98 @@ import { Response } from 'miragejs';
|
||||
// }
|
||||
// }
|
||||
|
||||
// Quick and dirty counter system to simulate the polling behavior
|
||||
// of the API. If the OIDC flow is pending, the route returns 100.
|
||||
// If complete it returns 200 with the token JSON.
|
||||
let oidcAttemptCounter = 0;
|
||||
const oidcRequiredAttempts = 3;
|
||||
|
||||
const commandHandlers = {
|
||||
password: {
|
||||
login: (payload, scopeAttrs) => {
|
||||
if (payload.attributes.login_name === 'error') {
|
||||
return new Response(400);
|
||||
} else {
|
||||
return new Response(
|
||||
200,
|
||||
{},
|
||||
{
|
||||
attributes: {
|
||||
scope: scopeAttrs,
|
||||
id: 'token123',
|
||||
token: 'thetokenstring',
|
||||
account_id: '1',
|
||||
user_id: 'user123',
|
||||
auth_method_id: 'authmethod123',
|
||||
created_time: '',
|
||||
updated_time: '',
|
||||
last_used_time: '',
|
||||
expiration_time: '',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* OIDC authentication is a two-step process:
|
||||
*
|
||||
* 1. `authenticate:start` kicks of authentication by requesting some details
|
||||
* from the Boundary server about the request, including the third-party
|
||||
* URL to which to redirect the user.
|
||||
* 2. `authenticate` accepts the token_id and state parameters in
|
||||
* order to retrieve a Boundary token. This endpoint may be polled until
|
||||
* the authentication flow is completed.
|
||||
*/
|
||||
oidc: {
|
||||
start: () =>
|
||||
new Response(
|
||||
200,
|
||||
{},
|
||||
{
|
||||
attributes: {
|
||||
auth_url: 'https://www.duckduckgo.com',
|
||||
token_id: 'token_1234',
|
||||
},
|
||||
}
|
||||
),
|
||||
token: (_, scopeAttrs) => {
|
||||
oidcAttemptCounter++;
|
||||
if (oidcAttemptCounter < oidcRequiredAttempts) {
|
||||
return new Response(202);
|
||||
} else {
|
||||
return new Response(
|
||||
200,
|
||||
{},
|
||||
{
|
||||
attributes: {
|
||||
scope: scopeAttrs,
|
||||
id: 'token123',
|
||||
token: 'thetokenstring',
|
||||
account_id: '1',
|
||||
user_id: 'user123',
|
||||
auth_method_id: 'authmethod123',
|
||||
created_time: '',
|
||||
updated_time: '',
|
||||
last_used_time: '',
|
||||
expiration_time: '',
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export function authHandler({ scopes, authMethods }, request) {
|
||||
const payload = JSON.parse(request.requestBody);
|
||||
if (payload.credentials.login_name === 'error') {
|
||||
return new Response(400);
|
||||
} else {
|
||||
const id = request.params.id_method.split(':')[0];
|
||||
const authMethod = authMethods.find(id);
|
||||
const scope = scopes.find(authMethod.scopeId);
|
||||
const scopeAttrs = this.serialize(scopes.find(scope.id));
|
||||
return new Response(
|
||||
200,
|
||||
{},
|
||||
{
|
||||
scope: scopeAttrs,
|
||||
id: 'token123',
|
||||
token: 'thetokenstring',
|
||||
account_id: '1',
|
||||
user_id: 'authenticateduser',
|
||||
auth_method_id: 'authmethod123',
|
||||
created_time: '',
|
||||
updated_time: '',
|
||||
last_used_time: '',
|
||||
expiration_time: '',
|
||||
}
|
||||
);
|
||||
}
|
||||
const [, id] = request.params.id_method.match(/(?<id>.[^:]*)/);
|
||||
const { command } = payload;
|
||||
const authMethod = authMethods.find(id);
|
||||
const scope = scopes.find(authMethod.scopeId);
|
||||
const scopeAttrs = this.serialize(scopes.find(scope.id));
|
||||
return commandHandlers[authMethod.type][command](payload, scopeAttrs);
|
||||
}
|
||||
|
||||
export function deauthHandler() {
|
||||
|
||||
@@ -20,18 +20,22 @@ export default function (server) {
|
||||
server.create('user', { id: 'authenticateduser' });
|
||||
|
||||
// Auth
|
||||
server.createList(
|
||||
const globalAuthMethods = server.createList(
|
||||
'auth-method',
|
||||
1,
|
||||
{ scope: globalScope },
|
||||
'withAccountsAndUsers'
|
||||
);
|
||||
server.createList(
|
||||
const orgAuthMethods = server.createList(
|
||||
'auth-method',
|
||||
3,
|
||||
{ scope: orgScope },
|
||||
'withAccountsAndUsers'
|
||||
);
|
||||
// Assign primary auth methods per scope
|
||||
// TODO make this generic
|
||||
globalScope.update({ primaryAuthMethodId: globalAuthMethods[0].id });
|
||||
orgScope.update({ primaryAuthMethodId: orgAuthMethods[1].id });
|
||||
|
||||
// Groups and Users
|
||||
server.createList('group', 1, { scope: globalScope }, 'withMembers');
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Ember from 'ember';
|
||||
import config from 'ember-get-config';
|
||||
import { random } from 'faker';
|
||||
import { datatype } from 'faker';
|
||||
|
||||
export default function initializeMockIPC(server) {
|
||||
/**
|
||||
@@ -55,7 +55,7 @@ export default function initializeMockIPC(server) {
|
||||
});
|
||||
return {
|
||||
address: window.location.hostname,
|
||||
port: random.number(),
|
||||
port: datatype.number(),
|
||||
protocol: type,
|
||||
session_id: newSession.id,
|
||||
};
|
||||
|
||||
@@ -2,4 +2,17 @@ import ApplicationSerializer from './application';
|
||||
|
||||
export default ApplicationSerializer.extend({
|
||||
modelName: 'auth-method',
|
||||
|
||||
_hashForModel(model) {
|
||||
const json = ApplicationSerializer.prototype._hashForModel.apply(
|
||||
this,
|
||||
arguments
|
||||
);
|
||||
// If the scope marks this auth method as primary, designate it
|
||||
// as such with a read-only boolean field (this is how the API behaves).
|
||||
const isPrimary =
|
||||
model.scope && model.scope.primaryAuthMethodId === model.id;
|
||||
json.is_primary = isPrimary;
|
||||
return json;
|
||||
},
|
||||
});
|
||||
@@ -71,7 +71,7 @@
|
||||
"qunit-dom": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "10.* || >= 12"
|
||||
"node": ">=10 <=15"
|
||||
},
|
||||
"ember": {
|
||||
"edition": "octane"
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
|
||||
module('Unit | Serializer | auth method', function (hooks) {
|
||||
setupTest(hooks);
|
||||
|
||||
// Replace this with your real tests.
|
||||
test('it exists', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let serializer = store.serializerFor('auth-method');
|
||||
|
||||
assert.ok(serializer);
|
||||
});
|
||||
|
||||
test('it serializes records', function (assert) {
|
||||
let store = this.owner.lookup('service:store');
|
||||
let record = store.createRecord('auth-method', {});
|
||||
|
||||
let serializedRecord = record.serialize();
|
||||
|
||||
assert.ok(serializedRecord);
|
||||
});
|
||||
|
||||
test('it sorts primary first in normalizeArrayResponse', function (assert) {
|
||||
assert.expect(2);
|
||||
const store = this.owner.lookup('service:store');
|
||||
const serializer = store.serializerFor('auth-method');
|
||||
const modelClass = store.createRecord('auth-method').constructor;
|
||||
const payload = {
|
||||
items: [{ id: 1 }, { id: 2, is_primary: true }, { id: 3 }],
|
||||
};
|
||||
const normalizedArray = serializer.normalizeArrayResponse(
|
||||
store,
|
||||
modelClass,
|
||||
payload
|
||||
);
|
||||
assert.ok(payload.items[1].is_primary, 'Second payload item is primary');
|
||||
assert.deepEqual(
|
||||
normalizedArray,
|
||||
{
|
||||
included: [],
|
||||
data: [
|
||||
{
|
||||
id: '2',
|
||||
type: 'auth-method',
|
||||
attributes: { is_primary: true },
|
||||
relationships: {},
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
type: 'auth-method',
|
||||
attributes: { is_primary: false },
|
||||
relationships: {},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
type: 'auth-method',
|
||||
attributes: { is_primary: false },
|
||||
relationships: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
'First normalized item is primary'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -4,10 +4,31 @@ import fetch from 'fetch';
|
||||
|
||||
/**
|
||||
* Encapsulates common authenticator functionality.
|
||||
*
|
||||
* Upon session invalidation, deauthentication should be attempted at the URL
|
||||
* generated by `buildDeauthEndpointURL` on a best-effort basis.
|
||||
*
|
||||
* When a session is restored, a call should be made to the endpoint specified
|
||||
* by `buildTokenValidationEndpointURL`. If this endpoint responds with 401 or
|
||||
* 404, then the session is invalid and the restoration should be rejected.
|
||||
* All other responses should resolve the session restoration successfully.
|
||||
*/
|
||||
export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
|
||||
// =unimplemented methods
|
||||
|
||||
/**
|
||||
* Generates an auth method URL with which to authenticate.
|
||||
* @override
|
||||
* @param {object} options
|
||||
* @param {string} options.scope.scope_id
|
||||
* @param {string} options.authMethod.id
|
||||
* @return {string}
|
||||
*/
|
||||
buildAuthEndpointURL(/* {
|
||||
scope: { id: scopeID },
|
||||
authMethod: { id: authMethodID },
|
||||
} */) {}
|
||||
|
||||
/**
|
||||
* Generates a scope URL with which to deauthenticate.
|
||||
* @override
|
||||
@@ -51,9 +72,10 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
|
||||
*/
|
||||
async restore(data) {
|
||||
if (!data) return reject();
|
||||
return this.validateToken(data.token, data.id).then(() =>
|
||||
this.normalizeData(data)
|
||||
);
|
||||
return this.validateToken(
|
||||
data.attributes.token,
|
||||
data.attributes.id
|
||||
).then(() => this.normalizeData(data));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,6 +87,10 @@ export default class BaseAuthenticator extends SimpleAuthBaseAuthenticator {
|
||||
* @return {object}
|
||||
*/
|
||||
normalizeData(data, username) {
|
||||
// Pull fields up from `data.attributes` for easier access in JavaScript.
|
||||
// The `attributes` field exists on the Go side for its convenience but is
|
||||
// unnecessary here.
|
||||
Object.assign(data, data.attributes);
|
||||
// Add booleans indicated the scope type
|
||||
data.isGlobal = data?.scope?.type === 'global';
|
||||
data.isOrg = data?.scope?.type === 'org';
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import BaseAuthenticator from './base';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { reject } from 'rsvp';
|
||||
import fetch from 'fetch';
|
||||
|
||||
/**
|
||||
* The OIDC base authenticator encapsulates the multistep OIDC flow.
|
||||
*
|
||||
* 1. Start authentication flow: this step is actually a combination of two
|
||||
* sub steps:
|
||||
* 1. Initiate a call via `startAuthentication` to the API at
|
||||
* `buildAuthEndpointURL` using the `start` command to fetch the URL of
|
||||
* the external authentication provider (`auth_url`).
|
||||
* 2. Open `auth_url`. This may be either inline (same tab)
|
||||
* or external in a new tab or browser window.
|
||||
* 2. Requests the token from `buildAuthEndpointURL` using the `token` command.
|
||||
* A token isn't guaranteed to be ready, so the OIDC flow can be in a
|
||||
* "pending" state.
|
||||
*
|
||||
* This authenticator should not be used directly because it does not specify
|
||||
* URL-building endpoints of its own.
|
||||
* To use, generate an application authenticator in your app
|
||||
* `authenticators/application.js` and extend this class.
|
||||
*/
|
||||
export default class OIDCAuthenticator extends BaseAuthenticator {
|
||||
// =services
|
||||
|
||||
@service session;
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Kicks-off the OIDC flow: calls the `authenticate:start` action on the
|
||||
* specified auth method, which returns some meta data along with an
|
||||
* authorization request URL. The response data is saved for later and
|
||||
* authentication enters a pending state.
|
||||
*
|
||||
* @param {boolean} requestCookies request cookie tokens (default `true`)
|
||||
* @param {object} options
|
||||
* @return {object}
|
||||
*/
|
||||
async startAuthentication(options) {
|
||||
const url = this.buildAuthEndpointURL(options);
|
||||
const body = JSON.stringify({ command: 'start' });
|
||||
const response = await fetch(url, { method: 'post', body });
|
||||
const json = await response.json();
|
||||
if (response.status < 400) {
|
||||
// Store meta about the pending OIDC flow
|
||||
this.session.set('data.pending', {
|
||||
oidc: json,
|
||||
});
|
||||
return json;
|
||||
} else {
|
||||
return reject();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* OIDC is a _process_ and does not have a singular "authenticate" step. Thus
|
||||
* this authenticate method simply normalizes the data passed into it once
|
||||
* `attemptFetchToken` completes. Notably, no requests are made by calling
|
||||
* authenticate. A Boundary token must already have been obtained.
|
||||
* @param {object} json
|
||||
* @return {object}
|
||||
*/
|
||||
async authenticate(json) {
|
||||
return this.normalizeData(json);
|
||||
}
|
||||
|
||||
/**
|
||||
* This request attempts to fetch a Boundary token. This method represents a
|
||||
* single request the polling sequence used to wait for authentication.
|
||||
* There are three possible outcomes:
|
||||
*
|
||||
* 1. If the token is ready, it is persisted into the session, the session
|
||||
* enters an authenticated state, and this method returns `true`.
|
||||
* 2. If the token is not yet ready, this method returns false.
|
||||
* 3. If the request errors, this method returns to a rejection,
|
||||
* requiring appropriate error handling.
|
||||
*/
|
||||
async attemptFetchToken(options) {
|
||||
// Get the URL and setup the request body
|
||||
const url = this.buildAuthEndpointURL(options);
|
||||
const body = JSON.stringify({
|
||||
command: 'token',
|
||||
attributes: {
|
||||
//state: this.session.get('data.pending.oidc.attributes.state'),
|
||||
token_id: this.session.get('data.pending.oidc.attributes.token_id'),
|
||||
},
|
||||
});
|
||||
// Fetch the endpoint and get the response JSON
|
||||
const response = await fetch(url, { method: 'post', body });
|
||||
if (response.status === 202) {
|
||||
// The token isn't ready yet, keep trying.
|
||||
return false;
|
||||
} else if (response.status < 400) {
|
||||
// Response was successful, meaning a token was obtained.
|
||||
// Authenticate with the session service using the response JSON.
|
||||
const json = await response.json();
|
||||
await this.session.authenticate('authenticator:oidc', json);
|
||||
return true;
|
||||
} else {
|
||||
// Response errors, return a rejection for further handling.
|
||||
return reject();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,21 +34,6 @@ import fetch from 'fetch';
|
||||
*
|
||||
*/
|
||||
export default class PasswordAuthenticator extends BaseAuthenticator {
|
||||
// =unimplemented methods
|
||||
|
||||
/**
|
||||
* Generates an auth method URL with which to authenticate.
|
||||
* @override
|
||||
* @param {object} options
|
||||
* @param {string} options.scope.scope_id
|
||||
* @param {string} options.authMethod.id
|
||||
* @return {string}
|
||||
*/
|
||||
buildAuthEndpointURL(/* {
|
||||
scope: { id: scopeID },
|
||||
authMethod: { id: authMethodID },
|
||||
} */) {}
|
||||
|
||||
// =methods
|
||||
|
||||
/**
|
||||
@@ -71,8 +56,9 @@ export default class PasswordAuthenticator extends BaseAuthenticator {
|
||||
options
|
||||
) {
|
||||
const body = JSON.stringify({
|
||||
command: 'login',
|
||||
token_type: requestCookies ? 'cookie' : null,
|
||||
credentials: { login_name, password },
|
||||
attributes: { login_name, password },
|
||||
});
|
||||
const authEndpointURL = this.buildAuthEndpointURL(options);
|
||||
const response = await fetch(authEndpointURL, { method: 'post', body });
|
||||
|
||||
@@ -65,7 +65,7 @@
|
||||
"qunit-dom": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "10.* || >= 12"
|
||||
"node": ">=10 <=15"
|
||||
},
|
||||
"ember": {
|
||||
"edition": "octane"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import OIDCAuthenticator from 'auth/authenticators/oidc';
|
||||
|
||||
export default class ApplicationOIDCAuthenticator extends OIDCAuthenticator {
|
||||
buildTokenValidationEndpointURL(id) {
|
||||
return `/validate/${id}`;
|
||||
}
|
||||
|
||||
buildAuthEndpointURL() {
|
||||
return '/authenticate';
|
||||
}
|
||||
|
||||
buildDeauthEndpointURL() {
|
||||
return '/deauthenticate';
|
||||
}
|
||||
}
|
||||
@@ -32,29 +32,25 @@ module('Unit | Authenticator | base', function (hooks) {
|
||||
|
||||
test('it passes token validation on restoration if receiving 200', async function (assert) {
|
||||
assert.expect(1);
|
||||
const mockData = { id: 'token_123' };
|
||||
const id = 'token_123';
|
||||
const mockData = { attributes: { id } };
|
||||
const authenticator = this.owner.lookup('authenticator:base');
|
||||
this.server.get(
|
||||
authenticator.buildTokenValidationEndpointURL(mockData.id),
|
||||
() => {
|
||||
assert.ok(true, 'token validation was requested');
|
||||
return new Response(200);
|
||||
}
|
||||
);
|
||||
this.server.get(authenticator.buildTokenValidationEndpointURL(id), () => {
|
||||
assert.ok(true, 'token validation was requested');
|
||||
return new Response(200);
|
||||
});
|
||||
await authenticator.restore(mockData);
|
||||
});
|
||||
|
||||
test('it fails token validation on restoration if receiving 401', async function (assert) {
|
||||
assert.expect(2);
|
||||
const mockData = { id: 'token_123' };
|
||||
const id = 'token_123';
|
||||
const mockData = { attributes: { id } };
|
||||
const authenticator = this.owner.lookup('authenticator:base');
|
||||
this.server.get(
|
||||
authenticator.buildTokenValidationEndpointURL(mockData.id),
|
||||
() => {
|
||||
assert.ok(true, 'token validation was requested');
|
||||
return new Response(401);
|
||||
}
|
||||
);
|
||||
this.server.get(authenticator.buildTokenValidationEndpointURL(id), () => {
|
||||
assert.ok(true, 'token validation was requested');
|
||||
return new Response(401);
|
||||
});
|
||||
try {
|
||||
await authenticator.restore(mockData);
|
||||
} catch (e) {
|
||||
@@ -64,15 +60,13 @@ module('Unit | Authenticator | base', function (hooks) {
|
||||
|
||||
test('it fails token validation on restoration if receiving 404', async function (assert) {
|
||||
assert.expect(2);
|
||||
const mockData = { id: 'token_123' };
|
||||
const id = 'token_123';
|
||||
const mockData = { attributes: { id } };
|
||||
const authenticator = this.owner.lookup('authenticator:base');
|
||||
this.server.get(
|
||||
authenticator.buildTokenValidationEndpointURL(mockData.id),
|
||||
() => {
|
||||
assert.ok(true, 'token validation was requested');
|
||||
return new Response(404);
|
||||
}
|
||||
);
|
||||
this.server.get(authenticator.buildTokenValidationEndpointURL(id), () => {
|
||||
assert.ok(true, 'token validation was requested');
|
||||
return new Response(404);
|
||||
});
|
||||
try {
|
||||
await authenticator.restore(mockData);
|
||||
} catch (e) {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { module, test } from 'qunit';
|
||||
import { setupTest } from 'ember-qunit';
|
||||
import { Response } from 'miragejs';
|
||||
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
|
||||
|
||||
module('Unit | Authenticator | OIDC', function (hooks) {
|
||||
setupTest(hooks);
|
||||
setupMirage(hooks);
|
||||
|
||||
test('`startAuthentication` initiates the authentication flow and returns the response', async function (assert) {
|
||||
assert.expect(2);
|
||||
const authenticator = this.owner.lookup('authenticator:oidc');
|
||||
this.server.post(
|
||||
authenticator.buildAuthEndpointURL(),
|
||||
() => new Response(200, {}, { foo: 'bar' })
|
||||
);
|
||||
const response = await authenticator.startAuthentication();
|
||||
assert.ok(true, 'authentication succeeded');
|
||||
assert.deepEqual(response, { foo: 'bar' });
|
||||
});
|
||||
|
||||
test('`startAuthentication` rejects/errors if API returns an error response', async function (assert) {
|
||||
assert.expect(1);
|
||||
const authenticator = this.owner.lookup('authenticator:oidc');
|
||||
this.server.post(
|
||||
authenticator.buildAuthEndpointURL(),
|
||||
() => new Response(400)
|
||||
);
|
||||
// Because assert.throws doesn't work with async errors/promise rejections
|
||||
try {
|
||||
await authenticator.startAuthentication();
|
||||
} catch (e) {
|
||||
assert.ok(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -47,8 +47,9 @@ module('Unit | Authenticator | password', function (hooks) {
|
||||
(schema, request) => {
|
||||
const json = JSON.parse(request.requestBody);
|
||||
assert.deepEqual(json, {
|
||||
command: 'login',
|
||||
token_type: 'cookie',
|
||||
credentials: {
|
||||
attributes: {
|
||||
login_name: 'foo',
|
||||
password: 'bar',
|
||||
},
|
||||
|
||||
+9
-21
@@ -1,17 +1,15 @@
|
||||
import BasePasswordAuthenticator from 'auth/authenticators/password';
|
||||
import BaseOIDCAuthenticator from 'auth/authenticators/oidc';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
/**
|
||||
* A username/password authenticator that authenticates with
|
||||
* a scope/auth-method endpoint. Authentication occurs on an auth method
|
||||
* endpoint, while deauthentication occurs on a scope endpoint.
|
||||
* An OIDC authenticator that handles flow kick-off and polling.
|
||||
*/
|
||||
export default class PasswordAuthenticator extends BasePasswordAuthenticator {
|
||||
export default class OIDCAuthenticator extends BaseOIDCAuthenticator {
|
||||
// =services
|
||||
|
||||
@service store;
|
||||
|
||||
// =attributes
|
||||
// =methods
|
||||
|
||||
/**
|
||||
* Generates an auth method URL with which to authenticate.
|
||||
@@ -30,19 +28,6 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator {
|
||||
return adapter.buildURL('auth-method', authMethodID, options, 'findRecord');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a scope URL with which to deauthenticate.
|
||||
* @override
|
||||
* @param {object} options
|
||||
* @param {string} scopeID
|
||||
* @return {string}
|
||||
*/
|
||||
buildDeauthEndpointURL({ scope: { id: scopeID } }) {
|
||||
const adapter = this.store.adapterFor('application');
|
||||
const options = { adapterOptions: { method: 'deauthenticate' } };
|
||||
return adapter.buildURL('scope', scopeID, options, 'findRecord');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an auth token validation URL used to check tokens on restoration.
|
||||
* @override
|
||||
@@ -62,7 +47,7 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator {
|
||||
*/
|
||||
authenticate() {
|
||||
return super.authenticate(...arguments).then((data) => {
|
||||
const token = data?.token;
|
||||
const token = data?.attributes?.token;
|
||||
this.addTokenToAuthorization(token);
|
||||
return data;
|
||||
});
|
||||
@@ -76,7 +61,7 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator {
|
||||
* @return {object}
|
||||
*/
|
||||
restore(data) {
|
||||
const token = data?.token;
|
||||
const token = data?.attributes?.token;
|
||||
this.addTokenToAuthorization(token);
|
||||
return super.restore(data);
|
||||
}
|
||||
@@ -85,6 +70,9 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator {
|
||||
* 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).
|
||||
*
|
||||
* TODO: if cookies were requested, this step should be skipped.
|
||||
*
|
||||
* @param {string} token
|
||||
*/
|
||||
addTokenToAuthorization(token) {
|
||||
@@ -62,7 +62,7 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator {
|
||||
*/
|
||||
authenticate() {
|
||||
return super.authenticate(...arguments).then((data) => {
|
||||
const token = data?.token;
|
||||
const token = data?.attributes?.token;
|
||||
this.addTokenToAuthorization(token);
|
||||
return data;
|
||||
});
|
||||
@@ -76,7 +76,7 @@ export default class PasswordAuthenticator extends BasePasswordAuthenticator {
|
||||
* @return {object}
|
||||
*/
|
||||
restore(data) {
|
||||
const token = data?.token;
|
||||
const token = data?.attributes?.token;
|
||||
this.addTokenToAuthorization(token);
|
||||
return super.restore(data);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
<Rose::Form
|
||||
class="full-width"
|
||||
@onSubmit={{@submit}}
|
||||
@cancel={{false}}
|
||||
@disabled={{@disabled}}
|
||||
as |form|
|
||||
>
|
||||
|
||||
<form.actions
|
||||
@submitText={{t "actions.authenticate"}}
|
||||
@showCancel={{false}} />
|
||||
|
||||
</Rose::Form>
|
||||
@@ -5,7 +5,7 @@ import { getOwner } from '@ember/application';
|
||||
* confirmation via the confirmation service. If the user accepts, the
|
||||
* original method is executed. If the user denies, it is not.
|
||||
*/
|
||||
export function confirm(confirmationTextKey) {
|
||||
export function confirm(confirmationTextKey, options) {
|
||||
return function (_target, _propertyKey, desc) {
|
||||
const method = desc.value;
|
||||
desc.value = function () {
|
||||
@@ -15,7 +15,7 @@ export function confirm(confirmationTextKey) {
|
||||
const text = intl.t(confirmationTextKey);
|
||||
return (
|
||||
confirmService
|
||||
.confirm(text)
|
||||
.confirm(text, options)
|
||||
// If user confirmed, execute the decorated method
|
||||
.then(() => method.apply(this, arguments))
|
||||
// If the user denied, gracefully handle promise rejection with no-op
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'core/authenticators/oidc';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'core/components/form/authenticate/oidc';
|
||||
@@ -30,8 +30,10 @@
|
||||
"dependencies": {
|
||||
"api": "*",
|
||||
"auth": "*",
|
||||
"ember-browser-services": "^1.1.6",
|
||||
"ember-cli-babel": "^7.23.0",
|
||||
"ember-cli-htmlbars": "^5.3.1",
|
||||
"ember-feature-flags": "^6.0.0",
|
||||
"ember-get-config": "^0.3.0",
|
||||
"ember-intl": "^5.5.0",
|
||||
"ember-loading": "^0.4.0",
|
||||
@@ -71,7 +73,7 @@
|
||||
"eslint-plugin-ember": "^10.1.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
"faker": "^5.1.0",
|
||||
"faker": "^5.5.3",
|
||||
"loader.js": "^4.7.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.2.1",
|
||||
@@ -79,7 +81,7 @@
|
||||
"qunit-dom": "^1.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "10.* || >= 12"
|
||||
"node": ">=10 <=15"
|
||||
},
|
||||
"ember": {
|
||||
"edition": "octane"
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 377 132">
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M125.45,41.34V35.68h-5.18v5.66h-2.64V27.75h2.64v5.7h5.18v-5.7h2.64V41.34Zm12.32,0h-2.09l-.19-.67a5.69,5.69,0,0,1-3,.87c-1.87,0-2.66-1.23-2.66-2.92,0-2,.9-2.75,3-2.75h2.46v-1c0-1.09-.32-1.47-1.95-1.47a14.85,14.85,0,0,0-2.83.3l-.32-1.87a14,14,0,0,1,3.5-.46c3.21,0,4.15,1.08,4.15,3.54Zm-2.55-3.7h-1.89c-.84,0-1.07.22-1.07,1s.23,1,1,1a4.1,4.1,0,0,0,1.93-.51Zm7.91,3.9a12.53,12.53,0,0,1-3.52-.56L140,39.1a11.5,11.5,0,0,0,3,.43c1.13,0,1.3-.24,1.3-1s-.12-.9-1.78-1.29c-2.49-.58-2.79-1.18-2.79-3.08s.9-2.83,3.82-2.83a14.07,14.07,0,0,1,3.06.34l-.25,2a19,19,0,0,0-2.81-.28c-1.11,0-1.3.24-1.3.84,0,.79.06.85,1.44,1.19,2.85.73,3.13,1.09,3.13,3.1S146.21,41.54,143.13,41.54Zm11.7-.2V34.5c0-.53-.23-.79-.81-.79a7.22,7.22,0,0,0-2.67.83v6.8H148.8V27.56l2.55.38v4.34a9.38,9.38,0,0,1,3.73-.94c1.7,0,2.31,1.14,2.31,2.89v7.11Zm4.71-11.19v-2.4h2.55v2.4Zm0,11.19v-9.8h2.55v9.8Zm4.6-9.72c0-2.46,1.48-3.88,5-3.88a16.63,16.63,0,0,1,3.8.44l-.3,2.19a21.49,21.49,0,0,0-3.41-.34c-1.83,0-2.41.6-2.41,2v5.15c0,1.43.58,2,2.41,2a21.57,21.57,0,0,0,3.41-.35l.3,2.2a16.63,16.63,0,0,1-3.8.44c-3.48,0-5-1.43-5-3.88Zm14.41,9.92c-3.5,0-4.45-1.85-4.45-3.86V35.2c0-2,1-3.86,4.45-3.86S183,33.19,183,35.2v2.48C183,39.69,182.05,41.54,178.55,41.54Zm0-8.11c-1.37,0-1.89.58-1.89,1.69v2.64c0,1.1.52,1.69,1.89,1.69s1.88-.59,1.88-1.69V35.12C180.43,34,179.91,33.43,178.55,33.43Zm11.64.16A20.64,20.64,0,0,0,187.48,35v6.32h-2.55v-9.8h2.16l.16,1.09a11.23,11.23,0,0,1,2.69-1.29Zm10.22,4.49c0,2.17-1,3.46-3.38,3.46a14.74,14.74,0,0,1-2.72-.28v4l-2.56.38V31.54h2l.25.82a5.54,5.54,0,0,1,3.22-1c2.06,0,3.15,1.16,3.15,3.4Zm-6.1,1.11a11.85,11.85,0,0,0,2.26.26c.93,0,1.28-.43,1.28-1.31V34.68c0-.81-.31-1.25-1.26-1.25a3.66,3.66,0,0,0-2.28.89Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M139.64,70.53c3.08-1.33,4.48-3.21,4.48-6.53V59.82c0-5.45-3.09-8.59-10.72-8.59H117.55V91.52h16.94c7.33,0,11-3.56,11-9.25V78C145.51,74,143.33,71.31,139.64,70.53Zm-14.46-13h7.32c2.78,0,4,1,4,3.27v3.57c0,2.36-1.08,3.75-4.06,3.75h-7.25ZM137.89,81.3c0,3-1.46,3.93-4.91,3.93h-7.8v-11h8.47c3,0,4.24,1.33,4.24,4.06Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M161.54,61.45c-10.11,0-12.82,5.57-12.82,11.62v7.44c0,6,2.71,11.62,12.82,11.62s12.83-5.57,12.83-11.62V73.07C174.37,67,171.64,61.45,161.54,61.45ZM167,80.76c0,3.32-1.51,5.08-5.45,5.08s-5.44-1.76-5.44-5.08V72.83c0-3.33,1.51-5.08,5.44-5.08S167,69.5,167,72.83Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M196.51,62.06V82.51c-2.66,1.39-5.87,2.48-7.68,2.48s-2.36-.78-2.36-2.36V62.06h-7.38V83.42c0,5.26,1.75,8.71,6.65,8.71A29.44,29.44,0,0,0,197.54,89l.72,2.48h5.64V62.06Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M228.4,61.45a29.61,29.61,0,0,0-11.8,3.09l-.72-2.48h-5.64V91.52h7.39V71.08c2.66-1.39,5.87-2.48,7.68-2.48s2.36.78,2.36,2.35V91.52h7.38V70.17C235.05,64.9,233.3,61.45,228.4,61.45Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M257.5,50V62.31a40.19,40.19,0,0,0-7.87-.86c-6.83,0-9.74,3.88-9.74,10.41v10c0,6.71,3.15,10.22,9.08,10.22A15.49,15.49,0,0,0,258.29,89l.72,2.48h5.87V49Zm0,33.16a10.43,10.43,0,0,1-6.59,2.66c-2.73,0-3.63-1.33-3.63-3.75V71.68c0-2.66,1-3.93,3.68-3.93a32.33,32.33,0,0,1,6.54.79Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M280.8,61.45a38.34,38.34,0,0,0-10.11,1.4l.91,5.62a41.47,41.47,0,0,1,8.17-.9c4.71,0,5.62,1.15,5.62,4.42v3.08h-7.08c-6,0-8.59,2.3-8.59,8.29,0,5.08,2.3,8.77,7.68,8.77a16.18,16.18,0,0,0,8.78-2.6l.54,2h6.06V72.1C292.78,64.72,290.05,61.45,280.8,61.45Zm4.59,23.3a11.57,11.57,0,0,1-5.56,1.51c-2.3,0-3-.91-3-3,0-2.23.67-2.9,3.09-2.9h5.44Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M313,61.45a33,33,0,0,0-7.74,3.88l-.48-3.27h-6.24V91.52h7.38v-19a57.85,57.85,0,0,1,7.81-4.3Z">
|
||||
</path>
|
||||
|
||||
<path
|
||||
fill="#fff"
|
||||
d="M335.67,62.06,328.9,84.44l-6.72-22.38h-7.56l9.32,29.46h2.59l-3.68,11.92h7l3.93-11.92,9.43-29.46Z">
|
||||
</path>
|
||||
|
||||
<polygon
|
||||
fill="#f04e54"
|
||||
points="44.94 102.47 44.94 97.64 54.96 97.64 54.96 94.67 50.1 94.67 50.1 89.82 73.03 89.82 62.38 71.38 73.03 52.93 49.53 52.93 49.53 78.51 36.88 78.51 36.88 40.29 80.4 40.29 87.67 52.87 76.98 71.38 87.88 90.26 80.84 102.47 44.94 102.47">
|
||||
</polygon>
|
||||
|
||||
<rect
|
||||
fill="#f04e54"
|
||||
x="42.04"
|
||||
y="89.82"
|
||||
width="4.85"
|
||||
height="4.85">
|
||||
</rect>
|
||||
|
||||
<rect
|
||||
fill="#f04e54"
|
||||
x="36.88"
|
||||
y="97.61"
|
||||
width="4.85"
|
||||
height="4.85">
|
||||
</rect>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.2 KiB |
@@ -5,7 +5,7 @@
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
|
||||
<g id="logomark">
|
||||
<g class="logomark">
|
||||
<path
|
||||
d="M0 7.19579L11.4553 0.58728V5.55192L4.30398 9.68052V25.5537L0 23.069V7.19579Z"
|
||||
fill="currentColor">
|
||||
|
||||
|
Before Width: | Height: | Size: 633 B After Width: | Height: | Size: 636 B |
@@ -26,13 +26,11 @@ module('Unit | Authenticator | password', function (hooks) {
|
||||
|
||||
test('it adds an authorization header to application adapter on restore', async function (assert) {
|
||||
assert.expect(1);
|
||||
const mockData = { id: 'token1234', token: 'token1234' };
|
||||
this.server.get(
|
||||
authenticator.buildTokenValidationEndpointURL(mockData.id),
|
||||
() => {
|
||||
return new Response(200);
|
||||
}
|
||||
);
|
||||
const id = 'token1234';
|
||||
const mockData = { attributes: { id, token: 'token1234' } };
|
||||
this.server.get(authenticator.buildTokenValidationEndpointURL(id), () => {
|
||||
return new Response(200);
|
||||
});
|
||||
authenticator.restore(mockData);
|
||||
assert.equal(applicationAdapter.headers.Authorization, 'Bearer token1234');
|
||||
});
|
||||
|
||||
@@ -16,10 +16,11 @@ back: Back
|
||||
retry: Retry
|
||||
get-topic-help: Get help for this topic
|
||||
copy-to-clipboard: Copy to Clipboard
|
||||
copy-error-detail-to-clipboard: Copy error detail to clipboard
|
||||
authenticate: Authenticate
|
||||
deauthenticate: Deauthenticate
|
||||
choose-org: Choose Scope
|
||||
choose-different-org: Choose a Different Scope
|
||||
choose-different-org: Choose a different scope
|
||||
add-host-set: Create Host Set
|
||||
add-host: Create Host
|
||||
add-host-sets: Add Host Sets
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
auth:
|
||||
titles:
|
||||
no-scopes: No Scopes Available
|
||||
flow-success: Successful authentication
|
||||
flow-error: Could not authenticate
|
||||
descriptions:
|
||||
no-scopes: No scopes were found. To authenticate, ask your administrator to create an scope and grant you access.
|
||||
choose-org: Choose a scope to authenticate.
|
||||
authenticating-with-org: "You are authenticating with {name}."
|
||||
authenticating-with-org: "Scope: You are authenticating with organization \"{name}\""
|
||||
flow-success: You've successfully authenticated. You may now close this window and return to the client.
|
||||
flow-error: Something went wrong while authenticating. Please close this window and try again. If the problem persists, notify your administrator.
|
||||
origin:
|
||||
connected-to: Connected to
|
||||
titles:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
create-success: Created succesfully.
|
||||
save-success: Saved succesfully.
|
||||
create-success: Created successfully.
|
||||
save-success: Saved successfully.
|
||||
delete-success: Deleted successfully.
|
||||
add-success: Added successfully.
|
||||
remove-success: Removed successfully.
|
||||
|
||||
@@ -16,7 +16,7 @@ account:
|
||||
auth-method:
|
||||
title: Auth Method
|
||||
title_plural: Auth Methods
|
||||
description: Auth methods govern how an org's users can login to Boundary. Boundary’s default auth method is username & passwords.
|
||||
description: Auth methods allow users to authenticate within a scope.
|
||||
messages:
|
||||
welcome:
|
||||
title: Welcome to Auth Methods
|
||||
@@ -24,11 +24,29 @@ auth-method:
|
||||
none:
|
||||
title: No Auth Methods
|
||||
description: No auth methods were found for this scope.
|
||||
no-primary-auth-method:
|
||||
title: "Recommendation: Select a Primary Auth Method"
|
||||
description: A primary auth method auto-creates users when they authenticate for the first time. Scopes without primary auth methods may experience authentication failures.
|
||||
pending:
|
||||
title: Authentication Pending
|
||||
description: Complete authentication in the newly-opened window.
|
||||
types:
|
||||
password: Password
|
||||
oidc: OIDC
|
||||
actions:
|
||||
create: Create Auth Method
|
||||
delete: Delete Auth Method
|
||||
make-primary: Make Primary
|
||||
remove-as-primary: Remove as Primary
|
||||
notifications:
|
||||
make-primary-success: Auth method successfully elected to primary.
|
||||
remove-as-primary-success: Auth method successfully removed as primary.
|
||||
questions:
|
||||
make-primary: Make primary auth method?
|
||||
remove-as-primary: Remove primary auth method?
|
||||
make-primary-confirm: "A primary auth method auto-creates users on their first authentication attempt if needed. Note: if you change the primary auth method, the previous primary will no longer auto-create users."
|
||||
remove-as-primary-confirm: Removing the primary auth method may lead to authentication failures for this scope.
|
||||
no-see-window: Don't see the new window?
|
||||
group:
|
||||
title: Group
|
||||
title_plural: Groups
|
||||
|
||||
@@ -7,3 +7,4 @@ copied: Copied
|
||||
connection:
|
||||
success:
|
||||
description: "Local proxy address ({protocol}): {address}:{port}"
|
||||
primary: Primary
|
||||
@@ -1,3 +1,5 @@
|
||||
const namedBlockPolyfill = require('ember-named-blocks-polyfill/lib/named-blocks-polyfill-plugin');
|
||||
|
||||
module.exports = {
|
||||
stories: [
|
||||
'../addon/components/**/*.stories.(js|mdx)',
|
||||
@@ -7,5 +9,8 @@ module.exports = {
|
||||
'@storybook/addon-knobs',
|
||||
'@storybook/addon-actions',
|
||||
'@storybook/addon-docs',
|
||||
]
|
||||
],
|
||||
emberOptions: {
|
||||
polyfills: [namedBlockPolyfill],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
<div class="rose-dialog-cover-modal-overlay"></div>
|
||||
|
||||
<section
|
||||
...attributes
|
||||
role="dialog"
|
||||
class="rose-dialog-cover"
|
||||
id={{this.id}}
|
||||
aria-describedby={{concat "dialog-cover-header-" this.id}}
|
||||
tabindex="0"
|
||||
{{focus-trap}}
|
||||
>
|
||||
<header
|
||||
class="rose-dialog-cover-header"
|
||||
id={{concat "dialog-cover-header-" this.id}}
|
||||
>
|
||||
{{yield to="header"}}
|
||||
</header>
|
||||
<div class="rose-dialog-cover-body">
|
||||
{{yield to="body"}}
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
<Meta title="Rose|Dialog|Cover" component="Cover" />
|
||||
|
||||
# Dialog Cover
|
||||
|
||||
A template-only dialog component that covers the viewport and accepts arbitrary
|
||||
content. Note that this component is unique to Boundary and is not part of the
|
||||
Structure design system.
|
||||
|
||||
Prerequisite: `ember-named-blocks-polyfill` must be installed in your app's
|
||||
`dependencies` rather than `devDependencies`.
|
||||
|
||||
<Preview>
|
||||
<Story name="Cover" height="15rem">{{
|
||||
template: hbs`
|
||||
<Rose::Dialog::Cover>
|
||||
<:header>
|
||||
<h1>Cover Header</h1>
|
||||
</:header>
|
||||
<:body>
|
||||
<p>Cover body</p>
|
||||
</:body>
|
||||
</Rose::Dialog::Cover>
|
||||
`
|
||||
}}</Story>
|
||||
</Preview>
|
||||
|
||||
|
||||
```htmlbars
|
||||
<Rose::Dialog::Cover>
|
||||
<:header>
|
||||
<h2>Cover Header</h2>
|
||||
</:header>
|
||||
<:body>
|
||||
<p>Cover body</p>
|
||||
</:body>
|
||||
</Rose::Dialog::Cover>
|
||||
```
|
||||
@@ -0,0 +1,21 @@
|
||||
<fieldset ...attributes class='rose-form-fieldset' aria-describedby={{if (has-block "description") (concat 'description-' this.id)}}>
|
||||
<legend
|
||||
id={{concat 'title-' this.id}}
|
||||
class='rose-form-fieldset-title'
|
||||
>
|
||||
{{yield to='title'}}
|
||||
</legend>
|
||||
|
||||
{{#if (has-block "description")}}
|
||||
<div
|
||||
id={{concat 'description-' this.id}}
|
||||
class='rose-form-fieldset-description'
|
||||
>
|
||||
{{yield to='description'}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class='rose-form-fieldset-body'>
|
||||
{{yield to='body'}}
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -0,0 +1,11 @@
|
||||
import Component from '@glimmer/component';
|
||||
import { generateComponentID } from '../../../../utilities/component-auto-id';
|
||||
|
||||
export default class RoseFormFieldsetComponent extends Component {
|
||||
// =attributes
|
||||
|
||||
/**
|
||||
* @type {string}
|
||||
*/
|
||||
id = generateComponentID();
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
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|Form/Fieldset" component="Form/Fieldset" />
|
||||
|
||||
# Form Fieldset
|
||||
|
||||
Renders a simple fieldset with title, description and body.
|
||||
|
||||
```html
|
||||
<Rose::Form::Fieldset>
|
||||
<:title>Title</:title>
|
||||
<:description>Description</:description>
|
||||
<:body>Rest of the form</:body>
|
||||
</Rose::Form::Fieldset>
|
||||
```
|
||||
|
||||
<Preview>
|
||||
<Story name="Basic Fieldset">{{
|
||||
template: hbs`
|
||||
<Rose::Form::Fieldset>
|
||||
<:title>Title</:title>
|
||||
<:description>Description</:description>
|
||||
<:body>Rest of the form</:body>
|
||||
</Rose::Form::Fieldset>`
|
||||
}}</Story>
|
||||
</Preview>
|
||||
@@ -0,0 +1,11 @@
|
||||
<section ...attributes class="rose-frame">
|
||||
|
||||
<header class="rose-frame-header">
|
||||
{{yield to="header"}}
|
||||
</header>
|
||||
|
||||
<div class="rose-frame-body">
|
||||
{{yield to="body"}}
|
||||
</div>
|
||||
|
||||
</section>
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
import { Meta, Story, Preview } from '@storybook/addon-docs/blocks';
|
||||
import { hbs } from 'ember-cli-htmlbars';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
<Meta title="Rose|Frame" component="Frame" />
|
||||
|
||||
# Frame
|
||||
|
||||
A simple frame component with named blocks `:header` and `:body`. Note that
|
||||
this component is unique to Boundary and is not part of the Structure design
|
||||
system.
|
||||
|
||||
Prerequisite: `ember-named-blocks-polyfill` must be installed in your app's
|
||||
`dependencies` rather than `devDependencies`.
|
||||
|
||||
<Preview>
|
||||
<Story name="Frame" height="15rem">{{
|
||||
template: hbs`
|
||||
<Rose::Frame>
|
||||
<:header>
|
||||
<h2>Frame Header</h2>
|
||||
</:header>
|
||||
<:body>
|
||||
<p>Frame body</p>
|
||||
</:body>
|
||||
</Rose::Frame>
|
||||
`
|
||||
}}</Story>
|
||||
</Preview>
|
||||
|
||||
|
||||
```htmlbars
|
||||
<Rose::Frame>
|
||||
<:header>
|
||||
<h2>Frame Header</h2>
|
||||
</:header>
|
||||
<:body>
|
||||
<p>Frame body</p>
|
||||
</:body>
|
||||
</Rose::Frame>
|
||||
```
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
&.rose-badge-outline {
|
||||
--border-color: var(--ui-gray-starker-1);
|
||||
--background-color: none;
|
||||
}
|
||||
|
||||
&.rose-badge-dark {
|
||||
@@ -40,6 +41,7 @@
|
||||
&.rose-badge-informational-outline {
|
||||
--color: var(--info-starker-1);
|
||||
--border-color: var(--info-starker-1);
|
||||
--background-color: none;
|
||||
}
|
||||
|
||||
&.rose-badge-alert {
|
||||
@@ -50,6 +52,7 @@
|
||||
&.rose-badge-alert-outline {
|
||||
--color: var(--alert-starker-1);
|
||||
--border-color: var(--alert-starker-1);
|
||||
--background-color: none;
|
||||
}
|
||||
|
||||
&.rose-badge-error {
|
||||
@@ -60,6 +63,7 @@
|
||||
&.rose-badge-error-outline {
|
||||
--color: var(--failure-starker-1);
|
||||
--border-color: var(--failure-starker-1);
|
||||
--background-color: none;
|
||||
}
|
||||
|
||||
&.rose-badge-success {
|
||||
@@ -70,6 +74,7 @@
|
||||
&.rose-badge-success-outline {
|
||||
--color: var(--success-starker-1);
|
||||
--border-color: var(--success-starker-1);
|
||||
--background-color: none;
|
||||
}
|
||||
|
||||
&.rose-badge-warning {
|
||||
@@ -80,5 +85,6 @@
|
||||
&.rose-badge-warning-outline {
|
||||
--color: var(--warning-starker-1);
|
||||
--border-color: var(--warning-starker-1);
|
||||
--background-color: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
@use '../variables/sizing';
|
||||
@use '../utilities/type';
|
||||
@use '../variables/media';
|
||||
|
||||
$l: sizing.rems(l); // 16
|
||||
$m: sizing.rems(m); // 16
|
||||
$s: sizing.rems(s); // 12
|
||||
$xs: sizing.rems(xs); // 8
|
||||
$xxs: sizing.rems(xxs); // 4
|
||||
$xxxs: sizing.rems(xxxs); // 2
|
||||
$xxxxs: sizing.rems(xxxxs); // 1
|
||||
|
||||
.rose-frame {
|
||||
--headerColor: var(--ui-gray-starker-3);
|
||||
--headerIconColor: var(--headerColor);
|
||||
--headerBackgroundColor: var(--ui-gray-subtler-5);
|
||||
--headerBorderColor: var(--ui-gray-subtler-3);
|
||||
|
||||
@include type.type(s);
|
||||
border: $xxxxs solid var(--ui-gray-subtler-3);
|
||||
border-radius: $xxxs;
|
||||
//margin: $m;
|
||||
box-shadow: 0 $xxs $xxs rgba(var(--stark-components), var(--opacity-4));
|
||||
background-color: var(--background-color);
|
||||
|
||||
.rose-frame-header {
|
||||
padding: ($xs + $xxxs) $l;
|
||||
min-height: $l;
|
||||
color: var(--headerColor);
|
||||
background-color: var(--headerBackgroundColor);
|
||||
border-bottom: $xxxxs solid var(--headerBorderColor);
|
||||
border-top-left-radius: $xxxs;
|
||||
border-top-right-radius: $xxxs;
|
||||
}
|
||||
|
||||
.rose-frame-body {
|
||||
$bodyPadding: $l;
|
||||
|
||||
padding: $bodyPadding;
|
||||
|
||||
> .rose-separator {
|
||||
margin-left: -$bodyPadding;
|
||||
margin-right: -$bodyPadding;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
@import 'dialog';
|
||||
@import 'dropdown';
|
||||
@import 'form';
|
||||
@import 'frame';
|
||||
@import 'header';
|
||||
@import 'footer';
|
||||
@import 'layout';
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
.rose-dialog-cover {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 200;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--white);
|
||||
text-align: center;
|
||||
|
||||
a {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.rose-dialog-cover-header,
|
||||
.rose-dialog-cover-body {
|
||||
position: relative;
|
||||
top: -10vh;
|
||||
max-width: 650px;
|
||||
}
|
||||
}
|
||||
// overlay
|
||||
.rose-dialog-cover-modal-overlay {
|
||||
content: ' ';
|
||||
background-color: var(--black);
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
opacity: 0.85;
|
||||
}
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
@use '../variables/sizing';
|
||||
@use '../utilities/type';
|
||||
@use '../variables/media';
|
||||
@use '../../variables/sizing';
|
||||
@use '../../utilities/type';
|
||||
@use '../../variables/media';
|
||||
|
||||
$m: sizing.rems(m); // 16
|
||||
$s: sizing.rems(s); // 12
|
||||
@@ -0,0 +1,2 @@
|
||||
@import 'cover';
|
||||
@import 'dialog';
|
||||
@@ -0,0 +1,14 @@
|
||||
@use '../../variables/sizing';
|
||||
@use '../../utilities/type';
|
||||
|
||||
.rose-form-fieldset {
|
||||
.rose-form-fieldset-title {
|
||||
@include type.type(l, bold);
|
||||
}
|
||||
|
||||
.rose-form-fieldset-description {
|
||||
margin: sizing.rems(xxs) 0;
|
||||
@include type.type(xs);
|
||||
color: var(--ui-gray);
|
||||
}
|
||||
}
|
||||
@@ -5,3 +5,4 @@
|
||||
@import 'checkbox';
|
||||
@import 'select';
|
||||
@import 'radio';
|
||||
@import 'fieldset';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'rose/components/rose/dialog/cover';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'rose/components/rose/form/fieldset';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from 'rose/components/rose/frame';
|
||||
@@ -33,6 +33,7 @@
|
||||
"ember-cli-babel": "^7.23.0",
|
||||
"ember-cli-htmlbars": "^5.3.1",
|
||||
"ember-focus-trap": "^0.4.0",
|
||||
"ember-named-blocks-polyfill": "^0.2.4",
|
||||
"ember-radio-button": "^2.0.1",
|
||||
"ember-uuid": "^2.1.0",
|
||||
"emberx-select": "github:adopted-ember-addons/emberx-select#bc5f774"
|
||||
@@ -86,7 +87,7 @@
|
||||
"sass-lint": "^1.13.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "10.* || >= 12"
|
||||
"node": ">=10 <=15"
|
||||
},
|
||||
"ember": {
|
||||
"edition": "octane"
|
||||
|
||||
@@ -79,3 +79,15 @@
|
||||
</Rose::Dropdown>
|
||||
</form.radioGroup>
|
||||
</Rose::Form>
|
||||
|
||||
|
||||
<Rose::Dialog::Cover>
|
||||
<:header>
|
||||
<h1>Authentication Pending</h1>
|
||||
</:header>
|
||||
<:body>
|
||||
<p>Complete authentication in the newly-opened window.</p>
|
||||
<p>Don't see the new window? <a href="#">Try again.</a></p>
|
||||
<p><a href="#">Cancel</a></p>
|
||||
</:body>
|
||||
</Rose::Dialog::Cover>
|
||||
@@ -0,0 +1,29 @@
|
||||
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 | rose/dialog/cover', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
assert.expect(3);
|
||||
|
||||
await render(hbs`
|
||||
<Rose::Dialog::Cover>
|
||||
<:header>
|
||||
<h1>Authentication Pending</h1>
|
||||
</:header>
|
||||
<:body>
|
||||
<p>Complete authentication in the newly-opened window.</p>
|
||||
<p>Don't see the new window? <a href="#">Try again.</a></p>
|
||||
<p><a href="#">Cancel</a></p>
|
||||
</:body>
|
||||
</Rose::Dialog::Cover>
|
||||
`);
|
||||
|
||||
assert.ok(find('.rose-dialog-cover'));
|
||||
assert.ok(find('.rose-dialog-cover-header'));
|
||||
assert.ok(find('.rose-dialog-cover-body'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
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 | rose/form/fieldset', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
// Global values for mocking
|
||||
this.mockTitle = 'Title';
|
||||
this.mockDescription = 'Description';
|
||||
this.mockBody = 'Rest of the form';
|
||||
|
||||
// Wrap the rendering in a function to avoid repeat code
|
||||
async function renderComponent(noDescription) {
|
||||
if (noDescription === true) {
|
||||
return render(hbs`
|
||||
<Rose::Form::Fieldset>
|
||||
<:title>{{this.mockTitle}}</:title>
|
||||
<:body>{{this.mockBody}}</:body>
|
||||
</Rose::Form::Fieldset>
|
||||
`);
|
||||
}
|
||||
|
||||
return render(hbs`
|
||||
<Rose::Form::Fieldset>
|
||||
<:title>{{this.mockTitle}}</:title>
|
||||
<:description>{{this.mockDescription}}</:description>
|
||||
<:body>{{this.mockBody}}</:body>
|
||||
</Rose::Form::Fieldset>
|
||||
`);
|
||||
}
|
||||
|
||||
test('it renders the full component', async function (assert) {
|
||||
assert.expect(4);
|
||||
await renderComponent(false);
|
||||
assert.ok(find('.rose-form-fieldset'));
|
||||
assert.equal(
|
||||
find('.rose-form-fieldset-title').textContent.trim(),
|
||||
this.mockTitle
|
||||
);
|
||||
assert.equal(
|
||||
find('.rose-form-fieldset-description').textContent.trim(),
|
||||
this.mockDescription
|
||||
);
|
||||
assert.equal(
|
||||
find('.rose-form-fieldset-body').textContent.trim(),
|
||||
this.mockBody
|
||||
);
|
||||
});
|
||||
|
||||
test('it renders with no description', async function (assert) {
|
||||
assert.expect(4);
|
||||
await renderComponent(true);
|
||||
assert.ok(find('.rose-form-fieldset'));
|
||||
assert.equal(
|
||||
find('.rose-form-fieldset-title').textContent.trim(),
|
||||
this.mockTitle
|
||||
);
|
||||
assert.notOk(find('.rose-form-fieldset-description'));
|
||||
assert.equal(
|
||||
find('.rose-form-fieldset-body').textContent.trim(),
|
||||
this.mockBody
|
||||
);
|
||||
});
|
||||
|
||||
test('it supports aria description for fieldset with description provided', async function (assert) {
|
||||
assert.expect(1);
|
||||
await renderComponent(false);
|
||||
const descriptionElement = find('.rose-form-fieldset-description');
|
||||
const fieldsetElement = find('.rose-form-fieldset');
|
||||
|
||||
assert.equal(
|
||||
fieldsetElement.getAttribute('aria-describedby'),
|
||||
descriptionElement.id,
|
||||
'Fieldset is described by description'
|
||||
);
|
||||
});
|
||||
|
||||
test('it does not support aria description for fieldset with no description provided', async function (assert) {
|
||||
assert.expect(3);
|
||||
await renderComponent(true);
|
||||
const fieldsetElement = find('.rose-form-fieldset');
|
||||
|
||||
assert.ok(find('.rose-form-fieldset'));
|
||||
assert.notOk(find('.rose-form-fieldset-description'));
|
||||
assert.notOk(fieldsetElement.getAttribute('aria-describedby'));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
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 | rose/frame', function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test('it renders', async function (assert) {
|
||||
await render(hbs`
|
||||
<Rose::Frame>
|
||||
<:header>Header</:header>
|
||||
<:body>Body</:body>
|
||||
</Rose::Frame>
|
||||
`);
|
||||
|
||||
assert.ok(find('section > header'));
|
||||
assert.ok(find('section > div'));
|
||||
});
|
||||
});
|
||||
+13
-2
@@ -50,17 +50,28 @@
|
||||
"request": "~2.68.0",
|
||||
"tunnel-agent": "~0.6.0",
|
||||
"cryptiles": "~4.1.2",
|
||||
"prismjs": "~1.21.0",
|
||||
"prismjs": "~1.23.0",
|
||||
"npm-user-validate": "^1.0.1",
|
||||
"caniuse-lite": "^1.0.30001157",
|
||||
"immer": "^8.0.1",
|
||||
"node-notifier": "^8.0.1",
|
||||
"highlight.js": "^10.4.1",
|
||||
"hoek": "~4.2.0"
|
||||
"hoek": "~4.2.0",
|
||||
"elliptic": "~6.5.4",
|
||||
"react-dev-utils": "~11.0.4",
|
||||
"yargs-parser": "~20.2.7",
|
||||
"merge": "^2.1.1",
|
||||
"xmlhttprequest-ssl": "^1.6.2",
|
||||
"underscore": "^1.12.1",
|
||||
"hosted-git-info": "3.0.8",
|
||||
"trim": "~1.0.1"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
"path": "./node_modules/cz-conventional-changelog"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10 <=15"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
extends: 'octane',
|
||||
plugins: ['ember-template-lint-plugin-prettier'],
|
||||
extends: ['octane', 'ember-template-lint-plugin-prettier:recommended'],
|
||||
rules: {
|
||||
'no-bare-strings': true,
|
||||
'no-curly-component-invocation': {
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
{{#if this.scope.org}}
|
||||
{{#unless this.scope.org.isGlobal}}
|
||||
{{#if this.session.data.authenticated.isGlobal}}
|
||||
<crumbs.link @route="scopes.scope.scopes" @model="global">
|
||||
{{t "resources.org.title_plural"}}
|
||||
<crumbs.link @route='scopes.scope.scopes' @model='global'>
|
||||
{{t 'resources.org.title_plural'}}
|
||||
</crumbs.link>
|
||||
<crumbs.link @route="scopes.scope.edit" @model={{this.scope.org.id}}>
|
||||
<crumbs.link @route='scopes.scope.edit' @model={{this.scope.org.id}}>
|
||||
{{this.scope.org.displayName}}
|
||||
</crumbs.link>
|
||||
{{/if}}
|
||||
@@ -14,10 +14,10 @@
|
||||
{{/if}}
|
||||
|
||||
{{#if this.scope.project}}
|
||||
<crumbs.link @route="scopes.scope.scopes" @model={{this.scope.org.id}}>
|
||||
{{t "resources.project.title_plural"}}
|
||||
<crumbs.link @route='scopes.scope.scopes' @model={{this.scope.org.id}}>
|
||||
{{t 'resources.project.title_plural'}}
|
||||
</crumbs.link>
|
||||
<crumbs.link @route="scopes.scope.edit" @model={{this.scope.project.id}}>
|
||||
<crumbs.link @route='scopes.scope.edit' @model={{this.scope.project.id}}>
|
||||
{{this.scope.project.displayName}}
|
||||
</crumbs.link>
|
||||
{{/if}}
|
||||
@@ -34,4 +34,4 @@
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
|
||||
</Rose::Nav::Breadcrumbs>
|
||||
</Rose::Nav::Breadcrumbs>
|
||||
@@ -1,16 +1,16 @@
|
||||
<span
|
||||
id={{this.id}}
|
||||
class="copyable"
|
||||
class='copyable'
|
||||
{{did-insert this.register}}
|
||||
{{will-destroy this.tearDown}}
|
||||
>
|
||||
|
||||
{{#if this.copied}}
|
||||
<mark class="copyable-content">
|
||||
<mark class='copyable-content'>
|
||||
{{yield}}
|
||||
</mark>
|
||||
{{else}}
|
||||
<span class="copyable-content">
|
||||
<span class='copyable-content'>
|
||||
{{yield}}
|
||||
</span>
|
||||
{{/if}}
|
||||
@@ -19,12 +19,12 @@
|
||||
<Rose::Button
|
||||
id={{this.copyableButtonId}}
|
||||
aria-describedby={{this.id}}
|
||||
class="copyable-button"
|
||||
@style="ghost"
|
||||
class='copyable-button'
|
||||
@style='ghost'
|
||||
@iconOnly={{if this.copied this.successIcon this.actionIcon}}
|
||||
title={{if this.copied this.successText this.actionText}}
|
||||
>
|
||||
{{this.actionText}}
|
||||
</Rose::Button>
|
||||
{{/if}}
|
||||
</span>
|
||||
</span>
|
||||
@@ -1,5 +1,5 @@
|
||||
<Rose::Form
|
||||
class="full-width"
|
||||
class='full-width'
|
||||
@onSubmit={{fn this.submit @submit this.currentPassword this.newPassword}}
|
||||
@cancel={{fn this.cancel @cancel}}
|
||||
@disabled={{@model.isSaving}}
|
||||
@@ -8,14 +8,15 @@
|
||||
>
|
||||
|
||||
<form.input
|
||||
@name="currentPassword"
|
||||
@type="password"
|
||||
@name='currentPassword'
|
||||
@type='password'
|
||||
@value={{this.currentPassword}}
|
||||
@label={{t "form.current_password.label"}}
|
||||
@label={{t 'form.current_password.label'}}
|
||||
@error={{@model.errors.name}}
|
||||
readonly={{false}}
|
||||
autocomplete="current-password"
|
||||
as |field|>
|
||||
autocomplete='current-password'
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.name}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
@@ -26,14 +27,15 @@
|
||||
</form.input>
|
||||
|
||||
<form.input
|
||||
@name="newPassword"
|
||||
@type="password"
|
||||
@name='newPassword'
|
||||
@type='password'
|
||||
@value={{this.newPassword}}
|
||||
@label={{t "form.new_password.label"}}
|
||||
@label={{t 'form.new_password.label'}}
|
||||
@error={{@model.errors.name}}
|
||||
readonly={{false}}
|
||||
autocomplete="new-password"
|
||||
as |field|>
|
||||
autocomplete='new-password'
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.name}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
@@ -45,7 +47,8 @@
|
||||
|
||||
<form.actions
|
||||
@submitDisabled={{this.cannotSave}}
|
||||
@submitText={{t "actions.save"}}
|
||||
@cancelText={{t "actions.cancel"}} />
|
||||
@submitText={{t 'actions.save'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
|
||||
</Rose::Form>
|
||||
</Rose::Form>
|
||||
@@ -7,20 +7,22 @@
|
||||
as |form|
|
||||
>
|
||||
<form.input
|
||||
@value={{@model.type}}
|
||||
@label={{t "form.type.label"}}
|
||||
readonly={{true}}
|
||||
@disabled={{true}} />
|
||||
@value={{@model.type}}
|
||||
@label={{t 'form.type.label'}}
|
||||
readonly={{true}}
|
||||
@disabled={{true}}
|
||||
/>
|
||||
|
||||
<form.input
|
||||
@name="name"
|
||||
@type="name"
|
||||
@name='name'
|
||||
@type='name'
|
||||
@value={{@model.name}}
|
||||
@label={{t "form.name.label"}}
|
||||
@helperText={{t "form.name.help"}}
|
||||
@label={{t 'form.name.label'}}
|
||||
@helperText={{t 'form.name.help'}}
|
||||
@error={{@model.errors.name}}
|
||||
readonly={{false}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.name}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
@@ -31,15 +33,16 @@
|
||||
</form.input>
|
||||
|
||||
<form.textarea
|
||||
@name="description"
|
||||
@type="description"
|
||||
@name='description'
|
||||
@type='description'
|
||||
@value={{@model.description}}
|
||||
@label={{t "form.description.label"}}
|
||||
@helperText={{t "form.description.help"}}
|
||||
@label={{t 'form.description.label'}}
|
||||
@helperText={{t 'form.description.help'}}
|
||||
@error={{@model.errors.description}}
|
||||
disabled={{@model.isSaving}}
|
||||
readonly={{false}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.description}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.description as |error|}}
|
||||
@@ -50,13 +53,14 @@
|
||||
</form.textarea>
|
||||
|
||||
<form.input
|
||||
@name="login_name"
|
||||
@type="text"
|
||||
@name='login_name'
|
||||
@type='text'
|
||||
@value={{@model.attributes.login_name}}
|
||||
@label={{t "form.login_name.label"}}
|
||||
@label={{t 'form.login_name.label'}}
|
||||
@error={{@model.errors.login_name}}
|
||||
readonly={{false}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.login_name}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.login_name as |error|}}
|
||||
@@ -68,14 +72,15 @@
|
||||
|
||||
{{#if @model.isNew}}
|
||||
<form.input
|
||||
@name="password"
|
||||
@type="password"
|
||||
@name='password'
|
||||
@type='password'
|
||||
@value={{this.password}}
|
||||
@label={{t "form.password.label"}}
|
||||
@label={{t 'form.password.label'}}
|
||||
@error={{@model.errors.password}}
|
||||
readonly={{false}}
|
||||
autocomplete="new-password"
|
||||
as |field|>
|
||||
autocomplete='new-password'
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.password}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.password as |error|}}
|
||||
@@ -88,8 +93,9 @@
|
||||
|
||||
<form.actions
|
||||
@disabled={{if @model.cannotSave @model.cannotSave}}
|
||||
@enableEditText={{t "actions.edit-form"}}
|
||||
@submitText={{t "actions.save"}}
|
||||
@cancelText={{t "actions.cancel"}} />
|
||||
@enableEditText={{t 'actions.edit-form'}}
|
||||
@submitText={{t 'actions.save'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
|
||||
</Rose::Form>
|
||||
</Rose::Form>
|
||||
@@ -6,15 +6,16 @@
|
||||
>
|
||||
|
||||
<form.input
|
||||
@name="password"
|
||||
@type="password"
|
||||
@name='password'
|
||||
@type='password'
|
||||
@value={{this.password}}
|
||||
@label={{t "form.set_password.label"}}
|
||||
@label={{t 'form.set_password.label'}}
|
||||
@error={{@model.errors.password}}
|
||||
@helperText={{t "form.set_password.help"}}
|
||||
@helperText={{t 'form.set_password.help'}}
|
||||
readonly={{false}}
|
||||
autocomplete="new-password"
|
||||
as |field|>
|
||||
autocomplete='new-password'
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.password}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.password as |error|}}
|
||||
@@ -27,7 +28,8 @@
|
||||
<form.actions
|
||||
@submitDisabled={{this.cannotSave}}
|
||||
@showCancel={{false}}
|
||||
@submitText={{t "actions.save"}}
|
||||
@cancelText={{t "actions.cancel"}} />
|
||||
@submitText={{t 'actions.save'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
|
||||
</Rose::Form>
|
||||
</Rose::Form>
|
||||
@@ -1,7 +1,8 @@
|
||||
{{#if @model.type}}
|
||||
{{component (concat "form/auth-method/" @model.type)
|
||||
{{component
|
||||
(concat 'form/auth-method/' @model.type)
|
||||
model=@model
|
||||
submit=@submit
|
||||
cancel=@cancel
|
||||
}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
@@ -7,20 +7,22 @@
|
||||
as |form|
|
||||
>
|
||||
<form.input
|
||||
@value={{@model.type}}
|
||||
@label={{t "form.type.label"}}
|
||||
readonly={{true}}
|
||||
@disabled={{true}} />
|
||||
@value={{@model.type}}
|
||||
@label={{t 'form.type.label'}}
|
||||
readonly={{true}}
|
||||
@disabled={{true}}
|
||||
/>
|
||||
|
||||
<form.input
|
||||
@name="name"
|
||||
@type="name"
|
||||
@name='name'
|
||||
@type='name'
|
||||
@value={{@model.name}}
|
||||
@label={{t "form.name.label"}}
|
||||
@helperText={{t "form.name.help"}}
|
||||
@label={{t 'form.name.label'}}
|
||||
@helperText={{t 'form.name.help'}}
|
||||
@error={{@model.errors.name}}
|
||||
readonly={{false}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.name}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
@@ -31,15 +33,16 @@
|
||||
</form.input>
|
||||
|
||||
<form.textarea
|
||||
@name="description"
|
||||
@type="description"
|
||||
@name='description'
|
||||
@type='description'
|
||||
@value={{@model.description}}
|
||||
@label={{t "form.description.label"}}
|
||||
@helperText={{t "form.description.help"}}
|
||||
@label={{t 'form.description.label'}}
|
||||
@helperText={{t 'form.description.help'}}
|
||||
@error={{@model.errors.description}}
|
||||
disabled={{@model.isSaving}}
|
||||
readonly={{false}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.description}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.description as |error|}}
|
||||
@@ -51,7 +54,8 @@
|
||||
|
||||
<form.actions
|
||||
@disabled={{if @model.cannotSave @model.cannotSave}}
|
||||
@enableEditText={{t "actions.edit-form"}}
|
||||
@submitText={{t "actions.save"}}
|
||||
@cancelText={{t "actions.cancel"}} />
|
||||
</Rose::Form>
|
||||
@enableEditText={{t 'actions.edit-form'}}
|
||||
@submitText={{t 'actions.save'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
</Rose::Form>
|
||||
@@ -1,6 +1,6 @@
|
||||
{{#if this.availableUsers}}
|
||||
<Rose::Form
|
||||
class="full-width"
|
||||
class='full-width'
|
||||
@onSubmit={{fn this.submit @submit}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.isSaving}}
|
||||
@@ -9,8 +9,9 @@
|
||||
>
|
||||
|
||||
<form.actions
|
||||
@submitText={{t "resources.group.actions.add-members"}}
|
||||
@cancelText={{t "actions.cancel"}} />
|
||||
@submitText={{t 'resources.group.actions.add-members'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
|
||||
<Rose::Table as |table|>
|
||||
<table.header as |header|>
|
||||
@@ -26,7 +27,8 @@
|
||||
<form.checkbox
|
||||
@label={{user.id}}
|
||||
@description={{user.description}}
|
||||
onChange={{fn this.toggleMember user}} />
|
||||
onChange={{fn this.toggleMember user}}
|
||||
/>
|
||||
</row.cell>
|
||||
<row.cell>{{user.name}}</row.cell>
|
||||
</body.row>
|
||||
@@ -39,14 +41,17 @@
|
||||
|
||||
{{#unless this.availableUsers}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message @title={{t "resources.group.messages.no-members.title"}} as |message|>
|
||||
<Rose::Message
|
||||
@title={{t 'resources.group.messages.no-members.title'}}
|
||||
as |message|
|
||||
>
|
||||
<message.description>
|
||||
{{t "resources.group.messages.no-members.description"}}
|
||||
{{t 'resources.group.messages.no-members.description'}}
|
||||
</message.description>
|
||||
<message.link @route="scopes.scope.groups.group.members">
|
||||
<Rose::Icon @name="chevron-left" />
|
||||
{{t "actions.back"}}
|
||||
<message.link @route='scopes.scope.groups.group.members'>
|
||||
<Rose::Icon @name='chevron-left' />
|
||||
{{t 'actions.back'}}
|
||||
</message.link>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
{{/unless}}
|
||||
</Rose::Layout::Centered>
|
||||
{{/unless}}
|
||||
@@ -8,14 +8,15 @@
|
||||
>
|
||||
|
||||
<form.input
|
||||
@name="name"
|
||||
@type="name"
|
||||
@name='name'
|
||||
@type='name'
|
||||
@value={{@model.name}}
|
||||
@label={{t "form.name.label"}}
|
||||
@helperText={{t "form.name.help"}}
|
||||
@label={{t 'form.name.label'}}
|
||||
@helperText={{t 'form.name.help'}}
|
||||
@error={{@model.errors.name}}
|
||||
readonly={{false}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.name}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
@@ -26,15 +27,16 @@
|
||||
</form.input>
|
||||
|
||||
<form.textarea
|
||||
@name="description"
|
||||
@type="description"
|
||||
@name='description'
|
||||
@type='description'
|
||||
@value={{@model.description}}
|
||||
@label={{t "form.description.label"}}
|
||||
@helperText={{t "form.description.help"}}
|
||||
@label={{t 'form.description.label'}}
|
||||
@helperText={{t 'form.description.help'}}
|
||||
@error={{@model.errors.description}}
|
||||
disabled={{@model.isSaving}}
|
||||
readonly={{false}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.description}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.description as |error|}}
|
||||
@@ -46,8 +48,9 @@
|
||||
|
||||
<form.actions
|
||||
@disabled={{if @model.cannotSave @model.cannotSave}}
|
||||
@enableEditText={{t "actions.edit-form"}}
|
||||
@submitText={{t "actions.save"}}
|
||||
@cancelText={{t "actions.cancel"}} />
|
||||
@enableEditText={{t 'actions.edit-form'}}
|
||||
@submitText={{t 'actions.save'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
|
||||
</Rose::Form>
|
||||
</Rose::Form>
|
||||
@@ -8,14 +8,15 @@
|
||||
>
|
||||
|
||||
<form.input
|
||||
@name="name"
|
||||
@type="name"
|
||||
@name='name'
|
||||
@type='name'
|
||||
@value={{@model.name}}
|
||||
@label={{t "form.name.label"}}
|
||||
@helperText={{t "form.name.help"}}
|
||||
@label={{t 'form.name.label'}}
|
||||
@helperText={{t 'form.name.help'}}
|
||||
@error={{@model.errors.name}}
|
||||
readonly={{false}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.name}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
@@ -26,15 +27,16 @@
|
||||
</form.input>
|
||||
|
||||
<form.textarea
|
||||
@name="description"
|
||||
@type="description"
|
||||
@name='description'
|
||||
@type='description'
|
||||
@value={{@model.description}}
|
||||
@label={{t "form.description.label"}}
|
||||
@helperText={{t "form.description.help"}}
|
||||
@label={{t 'form.description.label'}}
|
||||
@helperText={{t 'form.description.help'}}
|
||||
@error={{@model.errors.description}}
|
||||
disabled={{@model.isSaving}}
|
||||
readonly={{false}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.description}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.description as |error|}}
|
||||
@@ -46,8 +48,9 @@
|
||||
|
||||
<form.actions
|
||||
@disabled={{if @model.cannotSave @model.cannotSave}}
|
||||
@enableEditText={{t "actions.edit-form"}}
|
||||
@submitText={{t "actions.save"}}
|
||||
@cancelText={{t "actions.cancel"}} />
|
||||
@enableEditText={{t 'actions.edit-form'}}
|
||||
@submitText={{t 'actions.save'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
|
||||
</Rose::Form>
|
||||
</Rose::Form>
|
||||
@@ -1,6 +1,6 @@
|
||||
{{#if this.hasAvailableHosts}}
|
||||
<Rose::Form
|
||||
class="full-width"
|
||||
class='full-width'
|
||||
@onSubmit={{fn this.submit @submit}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.isSaving}}
|
||||
@@ -9,15 +9,16 @@
|
||||
>
|
||||
|
||||
<form.actions
|
||||
@submitText={{t "actions.add-hosts"}}
|
||||
@cancelText={{t "actions.cancel"}} />
|
||||
@submitText={{t 'actions.add-hosts'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
|
||||
<Rose::Table as |table|>
|
||||
<table.header as |header|>
|
||||
<header.row as |row|>
|
||||
<row.headerCell>{{t "form.name.label"}}</row.headerCell>
|
||||
<row.headerCell>{{t "form.type.label"}}</row.headerCell>
|
||||
<row.headerCell>{{t "form.id.label"}}</row.headerCell>
|
||||
<row.headerCell>{{t 'form.name.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'form.type.label'}}</row.headerCell>
|
||||
<row.headerCell>{{t 'form.id.label'}}</row.headerCell>
|
||||
</header.row>
|
||||
</table.header>
|
||||
<table.body as |body|>
|
||||
@@ -27,7 +28,8 @@
|
||||
<form.checkbox
|
||||
@label={{host.id}}
|
||||
@description={{host.description}}
|
||||
onChange={{fn this.toggleHost host}} />
|
||||
onChange={{fn this.toggleHost host}}
|
||||
/>
|
||||
</row.cell>
|
||||
<row.cell>{{host.name}}</row.cell>
|
||||
<row.cell>
|
||||
@@ -43,14 +45,19 @@
|
||||
|
||||
{{#unless this.hasAvailableHosts}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message @title={{t "resources.host-set.host.messages.add-none.title"}} as |message|>
|
||||
<Rose::Message
|
||||
@title={{t 'resources.host-set.host.messages.add-none.title'}}
|
||||
as |message|
|
||||
>
|
||||
<message.description>
|
||||
{{t "resources.host-set.host.messages.add-none.description"}}
|
||||
{{t 'resources.host-set.host.messages.add-none.description'}}
|
||||
</message.description>
|
||||
<message.link @route="scopes.scope.host-catalogs.host-catalog.host-sets.host-set.hosts">
|
||||
<Rose::Icon @name="chevron-left" />
|
||||
{{t "actions.back"}}
|
||||
<message.link
|
||||
@route='scopes.scope.host-catalogs.host-catalog.host-sets.host-set.hosts'
|
||||
>
|
||||
<Rose::Icon @name='chevron-left' />
|
||||
{{t 'actions.back'}}
|
||||
</message.link>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
{{/unless}}
|
||||
{{/unless}}
|
||||
@@ -8,20 +8,22 @@
|
||||
>
|
||||
|
||||
<form.input
|
||||
@value={{@model.type}}
|
||||
@label={{t "form.type.label"}}
|
||||
readonly={{true}}
|
||||
@disabled={{true}} />
|
||||
@value={{@model.type}}
|
||||
@label={{t 'form.type.label'}}
|
||||
readonly={{true}}
|
||||
@disabled={{true}}
|
||||
/>
|
||||
|
||||
<form.input
|
||||
@name="name"
|
||||
@type="name"
|
||||
@name='name'
|
||||
@type='name'
|
||||
@value={{@model.name}}
|
||||
@label={{t "form.name.label"}}
|
||||
@helperText={{t "form.name.help"}}
|
||||
@label={{t 'form.name.label'}}
|
||||
@helperText={{t 'form.name.help'}}
|
||||
@error={{@model.errors.name}}
|
||||
readonly={{false}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.name}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
@@ -32,15 +34,16 @@
|
||||
</form.input>
|
||||
|
||||
<form.textarea
|
||||
@name="description"
|
||||
@type="description"
|
||||
@name='description'
|
||||
@type='description'
|
||||
@value={{@model.description}}
|
||||
@label={{t "form.description.label"}}
|
||||
@helperText={{t "form.description.help"}}
|
||||
@label={{t 'form.description.label'}}
|
||||
@helperText={{t 'form.description.help'}}
|
||||
@error={{@model.errors.description}}
|
||||
disabled={{@model.isSaving}}
|
||||
readonly={{false}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.description}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.description as |error|}}
|
||||
@@ -52,8 +55,9 @@
|
||||
|
||||
<form.actions
|
||||
@disabled={{if @model.cannotSave @model.cannotSave}}
|
||||
@enableEditText={{t "actions.edit-form"}}
|
||||
@submitText={{t "actions.save"}}
|
||||
@cancelText={{t "actions.cancel"}} />
|
||||
@enableEditText={{t 'actions.edit-form'}}
|
||||
@submitText={{t 'actions.save'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
|
||||
</Rose::Form>
|
||||
</Rose::Form>
|
||||
@@ -8,20 +8,22 @@
|
||||
>
|
||||
|
||||
<form.input
|
||||
@value={{@model.type}}
|
||||
@label={{t "form.type.label"}}
|
||||
readonly={{true}}
|
||||
@disabled={{true}} />
|
||||
@value={{@model.type}}
|
||||
@label={{t 'form.type.label'}}
|
||||
readonly={{true}}
|
||||
@disabled={{true}}
|
||||
/>
|
||||
|
||||
<form.input
|
||||
@name="name"
|
||||
@type="name"
|
||||
@name='name'
|
||||
@type='name'
|
||||
@value={{@model.name}}
|
||||
@label={{t "form.name.label"}}
|
||||
@label={{t 'form.name.label'}}
|
||||
@error={{@model.errors.name}}
|
||||
@helperText={{t "form.name.help"}}
|
||||
@helperText={{t 'form.name.help'}}
|
||||
readonly={{false}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.name}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
@@ -32,15 +34,16 @@
|
||||
</form.input>
|
||||
|
||||
<form.textarea
|
||||
@name="description"
|
||||
@type="description"
|
||||
@name='description'
|
||||
@type='description'
|
||||
@value={{@model.description}}
|
||||
@label={{t "form.description.label"}}
|
||||
@label={{t 'form.description.label'}}
|
||||
@error={{@model.errors.description}}
|
||||
@helperText={{t "form.description.help"}}
|
||||
@helperText={{t 'form.description.help'}}
|
||||
disabled={{@model.isSaving}}
|
||||
readonly={{false}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.description}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.description as |error|}}
|
||||
@@ -51,14 +54,15 @@
|
||||
</form.textarea>
|
||||
|
||||
<form.input
|
||||
@name="address"
|
||||
@type="text"
|
||||
@name='address'
|
||||
@type='text'
|
||||
@value={{@model.attributes.address}}
|
||||
@label={{t "form.address.label"}}
|
||||
@label={{t 'form.address.label'}}
|
||||
@error={{@model.errors.name}}
|
||||
@helperText={{t "form.address.help"}}
|
||||
@helperText={{t 'form.address.help'}}
|
||||
readonly={{false}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.name}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
@@ -70,8 +74,9 @@
|
||||
|
||||
<form.actions
|
||||
@disabled={{if @model.cannotSave @model.cannotSave}}
|
||||
@enableEditText={{t "actions.edit-form"}}
|
||||
@submitText={{t "actions.save"}}
|
||||
@cancelText={{t "actions.cancel"}} />
|
||||
@enableEditText={{t 'actions.edit-form'}}
|
||||
@submitText={{t 'actions.save'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
|
||||
</Rose::Form>
|
||||
</Rose::Form>
|
||||
@@ -1,6 +1,6 @@
|
||||
{{#if this.hasAvailablePrincipals}}
|
||||
<Rose::Form
|
||||
class="full-width"
|
||||
class='full-width'
|
||||
@onSubmit={{fn this.submit @submit}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.isSaving}}
|
||||
@@ -9,8 +9,9 @@
|
||||
>
|
||||
|
||||
<form.actions
|
||||
@submitText={{t "actions.add-principals"}}
|
||||
@cancelText={{t "actions.cancel"}} />
|
||||
@submitText={{t 'actions.add-principals'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
|
||||
<Rose::Table as |table|>
|
||||
<table.header as |header|>
|
||||
@@ -27,11 +28,17 @@
|
||||
<form.checkbox
|
||||
@label={{principal.id}}
|
||||
@description={{principal.description}}
|
||||
onChange={{fn this.togglePrincipal principal}} />
|
||||
onChange={{fn this.togglePrincipal principal}}
|
||||
/>
|
||||
</row.cell>
|
||||
<row.cell>{{principal.name}}</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Badge>{{t (concat 'resources.role.principal.types.' principal.constructor.modelName)}}</Rose::Badge>
|
||||
<Rose::Badge>{{t
|
||||
(concat
|
||||
'resources.role.principal.types.'
|
||||
principal.constructor.modelName
|
||||
)
|
||||
}}</Rose::Badge>
|
||||
</row.cell>
|
||||
</body.row>
|
||||
{{/each}}
|
||||
@@ -43,14 +50,17 @@
|
||||
|
||||
{{#unless this.hasAvailablePrincipals}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message @title={{t "resources.role.principal.messages.none.title"}} as |message|>
|
||||
<Rose::Message
|
||||
@title={{t 'resources.role.principal.messages.none.title'}}
|
||||
as |message|
|
||||
>
|
||||
<message.description>
|
||||
{{t "resources.role.principal.messages.none.description"}}
|
||||
{{t 'resources.role.principal.messages.none.description'}}
|
||||
</message.description>
|
||||
<message.link @route="scopes.scope.roles.role.principals">
|
||||
<Rose::Icon @name="chevron-left" />
|
||||
{{t "actions.back"}}
|
||||
<message.link @route='scopes.scope.roles.role.principals'>
|
||||
<Rose::Icon @name='chevron-left' />
|
||||
{{t 'actions.back'}}
|
||||
</message.link>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
{{/unless}}
|
||||
{{/unless}}
|
||||
@@ -5,28 +5,29 @@
|
||||
as |form|
|
||||
>
|
||||
<Rose::List::KeyValue as |list|>
|
||||
{{!-- Add grant field (only one per form) --}}
|
||||
{{! Add grant field (only one per form) }}
|
||||
<list.item as |item|>
|
||||
<form.input
|
||||
@contextual={{true}}
|
||||
@name="grant"
|
||||
@type="text"
|
||||
@name='grant'
|
||||
@type='text'
|
||||
@value={{this.newGrantString}}
|
||||
as |input|>
|
||||
as |input|
|
||||
>
|
||||
<item.key>
|
||||
<input.label>{{t "resources.role.grant.actions.create"}}</input.label>
|
||||
<input.label>{{t 'resources.role.grant.actions.create'}}</input.label>
|
||||
</item.key>
|
||||
<item.cell>
|
||||
<input.field title={{t "form.grant.help"}} />
|
||||
<input.field title={{t 'form.grant.help'}} />
|
||||
</item.cell>
|
||||
<item.cell>
|
||||
<Rose::Button
|
||||
@submit={{true}}
|
||||
@disabled={{this.cannotSave}}
|
||||
@style="secondary"
|
||||
title={{t "actions.add"}}
|
||||
@style='secondary'
|
||||
title={{t 'actions.add'}}
|
||||
>
|
||||
{{t "actions.add"}}
|
||||
{{t 'actions.add'}}
|
||||
</Rose::Button>
|
||||
</item.cell>
|
||||
</form.input>
|
||||
@@ -57,26 +58,27 @@
|
||||
<list.item as |item|>
|
||||
<form.input
|
||||
@contextual={{true}}
|
||||
@name="grant"
|
||||
@type="text"
|
||||
@name='grant'
|
||||
@type='text'
|
||||
@value={{grant.value}}
|
||||
@error={{@model.errors.grant_strings}}
|
||||
as |input|>
|
||||
as |input|
|
||||
>
|
||||
<item.key>
|
||||
<input.label>{{t "form.grant.label"}}</input.label>
|
||||
<input.label>{{t 'form.grant.label'}}</input.label>
|
||||
</item.key>
|
||||
<item.cell>
|
||||
<input.field title={{t "form.grant.help"}} />
|
||||
<input.field title={{t 'form.grant.help'}} />
|
||||
</item.cell>
|
||||
</form.input>
|
||||
<item.cell>
|
||||
<Rose::Button
|
||||
@style="secondary"
|
||||
@iconOnly="trash"
|
||||
title={{t "actions.remove"}}
|
||||
{{on "click" (fn @removeGrant grant.value)}}
|
||||
@style='secondary'
|
||||
@iconOnly='trash'
|
||||
title={{t 'actions.remove'}}
|
||||
{{on 'click' (fn @removeGrant grant.value)}}
|
||||
>
|
||||
{{t "actions.remove"}}
|
||||
{{t 'actions.remove'}}
|
||||
</Rose::Button>
|
||||
</item.cell>
|
||||
</list.item>
|
||||
@@ -85,7 +87,8 @@
|
||||
|
||||
<form.actions
|
||||
@disabled={{if @model.cannotSave @model.cannotSave}}
|
||||
@submitText={{t "actions.save"}}
|
||||
@cancelText={{t "actions.cancel"}} />
|
||||
@submitText={{t 'actions.save'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
|
||||
</Rose::Form>
|
||||
</Rose::Form>
|
||||
@@ -8,14 +8,15 @@
|
||||
>
|
||||
|
||||
<form.input
|
||||
@name="name"
|
||||
@type="name"
|
||||
@name='name'
|
||||
@type='name'
|
||||
@value={{@model.name}}
|
||||
@label={{t "form.name.label"}}
|
||||
@helperText={{t "form.name.help"}}
|
||||
@label={{t 'form.name.label'}}
|
||||
@helperText={{t 'form.name.help'}}
|
||||
@error={{@model.errors.name}}
|
||||
readonly={{false}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.name}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
@@ -26,15 +27,16 @@
|
||||
</form.input>
|
||||
|
||||
<form.textarea
|
||||
@name="description"
|
||||
@type="description"
|
||||
@name='description'
|
||||
@type='description'
|
||||
@value={{@model.description}}
|
||||
@label={{t "form.description.label"}}
|
||||
@helperText={{t "form.description.help"}}
|
||||
@label={{t 'form.description.label'}}
|
||||
@helperText={{t 'form.description.help'}}
|
||||
@error={{@model.errors.description}}
|
||||
disabled={{@model.isSaving}}
|
||||
readonly={{false}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.description}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.description as |error|}}
|
||||
@@ -46,46 +48,62 @@
|
||||
|
||||
{{#unless @model.isNew}}
|
||||
<fieldset
|
||||
aria-labelledby="legend-text-grant-scope"
|
||||
aria-describedby="helper-text-grant-scope"
|
||||
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"}}
|
||||
<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|>
|
||||
<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" (if this.selectedGrantScope.isOrg "app-icons/org" "app-icons/global")}}
|
||||
as |dropdown|>
|
||||
@icon={{if
|
||||
this.selectedGrantScope.isProject
|
||||
'app-icons/project'
|
||||
(if
|
||||
this.selectedGrantScope.isOrg 'app-icons/org' 'app-icons/global'
|
||||
)
|
||||
}}
|
||||
as |dropdown|
|
||||
>
|
||||
<ul>
|
||||
{{#each @grantScopes as |grantScope|}}
|
||||
<li class={{if grantScope.model.isProject "is-project"}}>
|
||||
<li class={{if grantScope.model.isProject 'is-project'}}>
|
||||
<dropdown.item>
|
||||
<radioGroup.radio
|
||||
@label={{grantScope.model.displayName}}
|
||||
@value={{grantScope.model.id}}
|
||||
@icon={{concat "app-icons/" grantScope.model.type}}
|
||||
@icon={{concat 'app-icons/' grantScope.model.type}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
<ul class="indent-label-1">
|
||||
<ul class='indent-label-1'>
|
||||
{{#each grantScope.subScopes as |subScope|}}
|
||||
<li class={{if subScope.model.isProject "is-project"}}>
|
||||
<li class={{if subScope.model.isProject 'is-project'}}>
|
||||
<dropdown.item>
|
||||
<radioGroup.radio
|
||||
@label={{subScope.model.displayName}}
|
||||
@value={{subScope.model.id}}
|
||||
@icon={{concat "app-icons/" subScope.model.type}}
|
||||
@icon={{concat 'app-icons/' subScope.model.type}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
<ul class="indent-label-2">
|
||||
<ul class='indent-label-2'>
|
||||
{{#each subScope.subScopes as |subSubScope|}}
|
||||
<li class={{if subSubScope.isProject "is-project"}}>
|
||||
<li class={{if subSubScope.isProject 'is-project'}}>
|
||||
<dropdown.item>
|
||||
<radioGroup.radio
|
||||
@label={{subSubScope.displayName}}
|
||||
@value={{subSubScope.id}}
|
||||
@icon={{concat "app-icons/" subSubScope.type}}
|
||||
@icon={{concat 'app-icons/' subSubScope.type}}
|
||||
/>
|
||||
</dropdown.item>
|
||||
</li>
|
||||
@@ -104,8 +122,9 @@
|
||||
|
||||
<form.actions
|
||||
@disabled={{if @model.cannotSave @model.cannotSave}}
|
||||
@enableEditText={{t "actions.edit-form"}}
|
||||
@submitText={{t "actions.save"}}
|
||||
@cancelText={{t "actions.cancel"}} />
|
||||
@enableEditText={{t 'actions.edit-form'}}
|
||||
@submitText={{t 'actions.save'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
|
||||
</Rose::Form>
|
||||
</Rose::Form>
|
||||
@@ -8,14 +8,15 @@
|
||||
>
|
||||
|
||||
<form.input
|
||||
@name="name"
|
||||
@type="name"
|
||||
@name='name'
|
||||
@type='name'
|
||||
@value={{@model.name}}
|
||||
@label={{t "form.name.label"}}
|
||||
@helperText={{t "form.name.help"}}
|
||||
@label={{t 'form.name.label'}}
|
||||
@helperText={{t 'form.name.help'}}
|
||||
@error={{@model.errors.name}}
|
||||
readonly={{false}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.name}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
@@ -26,15 +27,16 @@
|
||||
</form.input>
|
||||
|
||||
<form.textarea
|
||||
@name="description"
|
||||
@type="description"
|
||||
@name='description'
|
||||
@type='description'
|
||||
@value={{@model.description}}
|
||||
@label={{t "form.description.label"}}
|
||||
@helperText={{t "form.description.help"}}
|
||||
@label={{t 'form.description.label'}}
|
||||
@helperText={{t 'form.description.help'}}
|
||||
@error={{@model.errors.description}}
|
||||
disabled={{@model.isSaving}}
|
||||
readonly={{false}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.description}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.description as |error|}}
|
||||
@@ -46,8 +48,9 @@
|
||||
|
||||
<form.actions
|
||||
@disabled={{if @model.cannotSave @model.cannotSave}}
|
||||
@enableEditText={{t "actions.edit-form"}}
|
||||
@submitText={{t "actions.save"}}
|
||||
@cancelText={{t "actions.cancel"}} />
|
||||
@enableEditText={{t 'actions.edit-form'}}
|
||||
@submitText={{t 'actions.save'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
|
||||
</Rose::Form>
|
||||
</Rose::Form>
|
||||
@@ -1,6 +1,6 @@
|
||||
{{#if this.hasAvailableHostSets}}
|
||||
<Rose::Form
|
||||
class="full-width"
|
||||
class='full-width'
|
||||
@onSubmit={{fn this.submit @submit}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.isSaving}}
|
||||
@@ -9,8 +9,9 @@
|
||||
>
|
||||
|
||||
<form.actions
|
||||
@submitText={{t "actions.add-host-sets"}}
|
||||
@cancelText={{t "actions.cancel"}} />
|
||||
@submitText={{t 'actions.add-host-sets'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
|
||||
<Rose::Table as |table|>
|
||||
<table.header as |header|>
|
||||
@@ -28,11 +29,14 @@
|
||||
<form.checkbox
|
||||
@label={{hostSet.id}}
|
||||
@description={{hostSet.description}}
|
||||
onChange={{fn this.toggleHostSet hostSet}} />
|
||||
onChange={{fn this.toggleHostSet hostSet}}
|
||||
/>
|
||||
</row.cell>
|
||||
<row.cell>{{hostSet.name}}</row.cell>
|
||||
<row.cell>
|
||||
<Rose::Badge>{{t (concat 'resources.host-set.types.' hostSet.type)}}</Rose::Badge>
|
||||
<Rose::Badge>{{t
|
||||
(concat 'resources.host-set.types.' hostSet.type)
|
||||
}}</Rose::Badge>
|
||||
</row.cell>
|
||||
<row.cell>
|
||||
<code>{{hostSet.host_catalog_id}}</code>
|
||||
@@ -47,14 +51,17 @@
|
||||
|
||||
{{#unless this.hasAvailableHostSets}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message @title={{t "resources.target.host-set.messages.none.title"}} as |message|>
|
||||
<Rose::Message
|
||||
@title={{t 'resources.target.host-set.messages.none.title'}}
|
||||
as |message|
|
||||
>
|
||||
<message.description>
|
||||
{{t "resources.target.host-set.messages.none.description"}}
|
||||
{{t 'resources.target.host-set.messages.none.description'}}
|
||||
</message.description>
|
||||
<message.link @route="scopes.scope.targets.target.host-sets">
|
||||
<Rose::Icon @name="chevron-left" />
|
||||
{{t "actions.back"}}
|
||||
<message.link @route='scopes.scope.targets.target.host-sets'>
|
||||
<Rose::Icon @name='chevron-left' />
|
||||
{{t 'actions.back'}}
|
||||
</message.link>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
{{/unless}}
|
||||
{{/unless}}
|
||||
@@ -8,19 +8,21 @@
|
||||
>
|
||||
|
||||
<form.input
|
||||
@value={{@model.type}}
|
||||
@label={{t "form.type.label"}}
|
||||
readonly={{true}}
|
||||
@disabled={{true}} />
|
||||
@value={{@model.type}}
|
||||
@label={{t 'form.type.label'}}
|
||||
readonly={{true}}
|
||||
@disabled={{true}}
|
||||
/>
|
||||
|
||||
<form.input
|
||||
@name="name"
|
||||
@type="text"
|
||||
@name='name'
|
||||
@type='text'
|
||||
@value={{@model.name}}
|
||||
@label={{t "form.name.label"}}
|
||||
@helperText={{t "form.name.help"}}
|
||||
@label={{t 'form.name.label'}}
|
||||
@helperText={{t 'form.name.help'}}
|
||||
@error={{@model.errors.name}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.name}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.name as |error|}}
|
||||
@@ -31,13 +33,14 @@
|
||||
</form.input>
|
||||
|
||||
<form.textarea
|
||||
@name="description"
|
||||
@type="text"
|
||||
@name='description'
|
||||
@type='text'
|
||||
@value={{@model.description}}
|
||||
@label={{t "form.description.label"}}
|
||||
@helperText={{t "form.description.help"}}
|
||||
@label={{t 'form.description.label'}}
|
||||
@helperText={{t 'form.description.help'}}
|
||||
@error={{@model.errors.description}}
|
||||
as |field|>
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.description}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.description as |error|}}
|
||||
@@ -48,15 +51,16 @@
|
||||
</form.textarea>
|
||||
|
||||
<form.input
|
||||
@name="session_max_seconds"
|
||||
@type="number"
|
||||
@name='session_max_seconds'
|
||||
@type='number'
|
||||
@value={{@model.session_max_seconds}}
|
||||
@label={{t "form.session_max_seconds.label"}}
|
||||
@label={{t 'form.session_max_seconds.label'}}
|
||||
@error={{@model.errors.session_max_seconds}}
|
||||
@helperText={{t "form.session_max_seconds.help"}}
|
||||
min="0"
|
||||
step="1"
|
||||
as |field|>
|
||||
@helperText={{t 'form.session_max_seconds.help'}}
|
||||
min='0'
|
||||
step='1'
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.session_max_seconds}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.session_max_seconds as |error|}}
|
||||
@@ -67,15 +71,16 @@
|
||||
</form.input>
|
||||
|
||||
<form.input
|
||||
@name="session_connection_limit"
|
||||
@type="number"
|
||||
@name='session_connection_limit'
|
||||
@type='number'
|
||||
@value={{@model.session_connection_limit}}
|
||||
@label={{t "form.session_connection_limit.label"}}
|
||||
@label={{t 'form.session_connection_limit.label'}}
|
||||
@error={{@model.errors.session_connection_limit}}
|
||||
@helperText={{t "form.session_connection_limit.help"}}
|
||||
min="-1"
|
||||
step="1"
|
||||
as |field|>
|
||||
@helperText={{t 'form.session_connection_limit.help'}}
|
||||
min='-1'
|
||||
step='1'
|
||||
as |field|
|
||||
>
|
||||
{{#if @model.errors.session_connection_limit}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.session_connection_limit as |error|}}
|
||||
@@ -85,18 +90,19 @@
|
||||
{{/if}}
|
||||
</form.input>
|
||||
|
||||
{{!-- TODO this is a type-specific field --}}
|
||||
{{! TODO this is a type-specific field }}
|
||||
<form.input
|
||||
@name="default_port"
|
||||
@type="number"
|
||||
@name='default_port'
|
||||
@type='number'
|
||||
@value={{@model.attributes.default_port}}
|
||||
@label={{t "form.default_port.label"}}
|
||||
@label={{t 'form.default_port.label'}}
|
||||
@error={{@model.errors.default_port}}
|
||||
@helperText={{t "form.default_port.help"}}
|
||||
min="0"
|
||||
step="1"
|
||||
as |field|>
|
||||
{{!-- TODO need to figure out how to reference `attributes.errors` here --}}
|
||||
@helperText={{t 'form.default_port.help'}}
|
||||
min='0'
|
||||
step='1'
|
||||
as |field|
|
||||
>
|
||||
{{! TODO need to figure out how to reference `attributes.errors` here }}
|
||||
{{#if @model.errors.default_port}}
|
||||
<field.errors as |errors|>
|
||||
{{#each @model.errors.default_port as |error|}}
|
||||
@@ -108,8 +114,9 @@
|
||||
|
||||
<form.actions
|
||||
@disabled={{if @model.cannotSave @model.cannotSave}}
|
||||
@enableEditText={{t "actions.edit-form"}}
|
||||
@submitText={{t "actions.save"}}
|
||||
@cancelText={{t "actions.cancel"}} />
|
||||
@enableEditText={{t 'actions.edit-form'}}
|
||||
@submitText={{t 'actions.save'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
|
||||
</Rose::Form>
|
||||
</Rose::Form>
|
||||
@@ -1,6 +1,6 @@
|
||||
{{#if this.hasAvailableAccounts}}
|
||||
<Rose::Form
|
||||
class="full-width"
|
||||
class='full-width'
|
||||
@onSubmit={{fn this.submit @submit}}
|
||||
@cancel={{@cancel}}
|
||||
@disabled={{@model.isSaving}}
|
||||
@@ -9,8 +9,9 @@
|
||||
>
|
||||
|
||||
<form.actions
|
||||
@submitText={{t "resources.user.actions.add-accounts"}}
|
||||
@cancelText={{t "actions.cancel"}} />
|
||||
@submitText={{t 'resources.user.actions.add-accounts'}}
|
||||
@cancelText={{t 'actions.cancel'}}
|
||||
/>
|
||||
|
||||
<Rose::Table as |table|>
|
||||
<table.header as |header|>
|
||||
@@ -27,7 +28,8 @@
|
||||
<form.checkbox
|
||||
@label={{account.id}}
|
||||
@description={{account.description}}
|
||||
onChange={{fn this.toggleAccount account}} />
|
||||
onChange={{fn this.toggleAccount account}}
|
||||
/>
|
||||
</row.cell>
|
||||
<row.cell>{{account.username}}</row.cell>
|
||||
<row.cell>
|
||||
@@ -43,14 +45,17 @@
|
||||
|
||||
{{#unless this.hasAvailableAccounts}}
|
||||
<Rose::Layout::Centered>
|
||||
<Rose::Message @title={{t "resources.user.messages.no-accounts.title"}} as |message|>
|
||||
<Rose::Message
|
||||
@title={{t 'resources.user.messages.no-accounts.title'}}
|
||||
as |message|
|
||||
>
|
||||
<message.description>
|
||||
{{t "resources.user.messages.no-accounts.description"}}
|
||||
{{t 'resources.user.messages.no-accounts.description'}}
|
||||
</message.description>
|
||||
<message.link @route="scopes.scope.users.user.accounts">
|
||||
<Rose::Icon @name="chevron-left" />
|
||||
{{t "actions.back"}}
|
||||
<message.link @route='scopes.scope.users.user.accounts'>
|
||||
<Rose::Icon @name='chevron-left' />
|
||||
{{t 'actions.back'}}
|
||||
</message.link>
|
||||
</Rose::Message>
|
||||
</Rose::Layout::Centered>
|
||||
{{/unless}}
|
||||
{{/unless}}
|
||||
Loaded 100 of 328 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user