diff --git a/.circleci/config.yml b/.circleci/config.yml
index 0d1ed3f2d6..ac68c205e0 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -5,13 +5,13 @@ references:
images:
go: &GOLANG_IMAGE circleci/golang:1.14.1
middleman: &MIDDLEMAN_IMAGE hashicorp/middleman-hashicorp:0.3.40
- ember: &EMBER_IMAGE circleci/node:8-browsers
+ ember: &EMBER_IMAGE circleci/node:12-browsers
paths:
test-results: &TEST_RESULTS_DIR /tmp/test-results
cache:
- yarn: &YARN_CACHE_KEY consul-ui-v1-{{ checksum "ui-v2/yarn.lock" }}
+ yarn: &YARN_CACHE_KEY consul-ui-v2-{{ checksum "ui-v2/yarn.lock" }}
rubygem: &RUBYGEM_CACHE_KEY static-site-gems-v1-{{ checksum "Gemfile.lock" }}
environment: &ENVIRONMENT
@@ -461,6 +461,9 @@ jobs:
- image: *EMBER_IMAGE
environment:
EMBER_TEST_REPORT: test-results/report-oss.xml #outputs test report for CircleCI test summary
+ EMBER_TEST_PARALLEL: true #enables test parallelization with ember-exam
+ CONSUL_NSPACES_ENABLED: 0
+ parallelism: 2
steps:
- checkout
- restore_cache:
@@ -469,7 +472,7 @@ jobs:
at: ui-v2
- run:
working_directory: ui-v2
- command: make test-oss-ci
+ command: node_modules/.bin/ember exam --split=$CIRCLE_NODE_TOTAL --partition=`expr $CIRCLE_NODE_INDEX + 1` --path dist --silent -r xunit
- store_test_results:
path: ui-v2/test-results
# run ember frontend tests
@@ -478,6 +481,8 @@ jobs:
- image: *EMBER_IMAGE
environment:
EMBER_TEST_REPORT: test-results/report-ent.xml #outputs test report for CircleCI test summary
+ EMBER_TEST_PARALLEL: true #enables test parallelization with ember-exam
+ parallelism: 2
steps:
- checkout
- restore_cache:
@@ -486,9 +491,26 @@ jobs:
at: ui-v2
- run:
working_directory: ui-v2
- command: make test-ci
+ command: node_modules/.bin/ember exam --split=$CIRCLE_NODE_TOTAL --partition=`expr $CIRCLE_NODE_INDEX + 1` --path dist --silent -r xunit
- store_test_results:
path: ui-v2/test-results
+ # run ember frontend unit tests to produce coverage report
+ ember-coverage:
+ docker:
+ - image: *EMBER_IMAGE
+ steps:
+ - checkout
+ - restore_cache:
+ key: *YARN_CACHE_KEY
+ - attach_workspace:
+ at: ui-v2
+ - run:
+ working_directory: ui-v2
+ command: make test-coverage-ci
+ - run:
+ name: codecov ui upload
+ working_directory: ui-v2
+ command: bash <(curl -s https://codecov.io/bash) -v -c -C $CIRCLE_SHA1 -F ui
envoy-integration-test-1.11.2:
docker:
@@ -682,6 +704,9 @@ workflows:
- ember-test-ent:
requires:
- ember-build
+ - ember-coverage:
+ requires:
+ - ember-build
cherry-pick:
jobs:
- cherry-picker:
diff --git a/build-support/docker/Build-UI.dockerfile b/build-support/docker/Build-UI.dockerfile
index 642ad13ca8..dc0532e4e2 100644
--- a/build-support/docker/Build-UI.dockerfile
+++ b/build-support/docker/Build-UI.dockerfile
@@ -1,7 +1,7 @@
-ARG ALPINE_VERSION=3.9
+ARG ALPINE_VERSION=3.11
FROM alpine:${ALPINE_VERSION}
-ARG NODEJS_VERSION=10.14.2-r0
+ARG NODEJS_VERSION=12.15.0-r1
ARG MAKE_VERSION=4.2.1-r2
ARG YARN_VERSION=1.19.1
diff --git a/codecov.yml b/codecov.yml
index 9bb643f3ae..1839b76dca 100644
--- a/codecov.yml
+++ b/codecov.yml
@@ -10,6 +10,8 @@ coverage:
# https://docs.codecov.io/docs/commit-status#section-excluding-tests-example-
# TODO: should any paths be excluded from coverage metrics?
# paths:
+ ui:
+ informational: true
# https://docs.codecov.io/docs/commit-status#section-changes-status
# TODO: enable after eliminating current unexpected coverage changes?
changes: off
@@ -29,7 +31,9 @@ comment: false
# https://docs.codecov.io/docs/flags
# TODO: split out test coverage for API, SDK, UI, website?
-# flags:
+flags:
+ ui:
+ paths: /ui-v2/
ignore:
- "agent/bindata_assetfs.go"
diff --git a/ui-v2/.editorconfig b/ui-v2/.editorconfig
index 13bc6da1f4..189bfcf026 100644
--- a/ui-v2/.editorconfig
+++ b/ui-v2/.editorconfig
@@ -4,7 +4,6 @@
root = true
-
[*]
end_of_line = lf
charset = utf-8
diff --git a/ui-v2/.ember-cli b/ui-v2/.ember-cli
index ee64cfed2a..7f520c46ae 100644
--- a/ui-v2/.ember-cli
+++ b/ui-v2/.ember-cli
@@ -5,5 +5,14 @@
Setting `disableAnalytics` to true will prevent any data from being sent.
*/
- "disableAnalytics": false
+ "disableAnalytics": false,
+ /**
+ We use a nested in /components folder structure:
+ /components/component-name/index.{hbs,js}
+ */
+ "componentStructure": "nested",
+ /**
+ We currently use classic components
+ */
+ "componentClass": "@ember/component"
}
diff --git a/ui-v2/.eslintrc.js b/ui-v2/.eslintrc.js
index e35e1db2a9..ce7e9cee4a 100644
--- a/ui-v2/.eslintrc.js
+++ b/ui-v2/.eslintrc.js
@@ -1,8 +1,12 @@
module.exports = {
root: true,
+ parser: 'babel-eslint',
parserOptions: {
ecmaVersion: 2018,
- sourceType: 'module'
+ sourceType: 'module',
+ ecmaFeatures: {
+ legacyDecorators: true
+ }
},
plugins: ['ember'],
extends: ['eslint:recommended', 'plugin:ember/recommended'],
@@ -11,7 +15,9 @@ module.exports = {
},
rules: {
'no-unused-vars': ['error', { args: 'none' }],
- 'ember/no-new-mixins': ['warn']
+ 'ember/no-new-mixins': ['warn'],
+ 'ember/no-jquery': 'warn',
+ 'ember/no-global-jquery': 'warn'
},
overrides: [
// node files
diff --git a/ui-v2/.istanbul.yml b/ui-v2/.istanbul.yml
new file mode 100644
index 0000000000..5485bf6d76
--- /dev/null
+++ b/ui-v2/.istanbul.yml
@@ -0,0 +1,4 @@
+instrumentation:
+ excludes: [
+ "!app/+(utils|search)/**/*"
+ ]
diff --git a/ui-v2/.nvmrc b/ui-v2/.nvmrc
index f599e28b8a..48082f72f0 100644
--- a/ui-v2/.nvmrc
+++ b/ui-v2/.nvmrc
@@ -1 +1 @@
-10
+12
diff --git a/ui-v2/.template-lintrc.js b/ui-v2/.template-lintrc.js
index c80b1341e6..a87bfda6cc 100644
--- a/ui-v2/.template-lintrc.js
+++ b/ui-v2/.template-lintrc.js
@@ -1,7 +1,7 @@
'use strict';
module.exports = {
- extends: 'recommended',
+ extends: 'octane',
rules: {
'no-partial': false,
@@ -10,6 +10,7 @@ module.exports = {
'self-closing-void-elements': false,
'no-unnecessary-concat': false,
+ 'no-quoteless-attributes': false,
'no-nested-interactive': false,
'block-indentation': false,
@@ -19,6 +20,15 @@ module.exports = {
'no-triple-curlies': false,
'no-unused-block-params': false,
'style-concatenation': false,
- 'link-rel-noopener': false
+ 'link-rel-noopener': false,
+
+ 'no-implicit-this': false,
+ 'no-curly-component-invocation': false,
+ 'no-action': false,
+ 'no-negated-condition': false,
+ 'no-invalid-role': false,
+
+ 'no-unnecessary-component-helper': false,
+ 'link-href-attributes': false
},
};
diff --git a/ui-v2/.travis.yml b/ui-v2/.travis.yml
new file mode 100644
index 0000000000..a0208ca37f
--- /dev/null
+++ b/ui-v2/.travis.yml
@@ -0,0 +1,27 @@
+---
+language: node_js
+node_js:
+ - "10"
+
+dist: trusty
+
+addons:
+ chrome: stable
+
+cache:
+ directories:
+ - $HOME/.npm
+
+env:
+ global:
+ # See https://git.io/vdao3 for details.
+ - JOBS=1
+
+branches:
+ only:
+ - master
+
+script:
+ - npm run lint:hbs
+ - npm run lint:js
+ - npm test
diff --git a/ui-v2/GNUmakefile b/ui-v2/GNUmakefile
index cfed057e4e..dfff38541f 100644
--- a/ui-v2/GNUmakefile
+++ b/ui-v2/GNUmakefile
@@ -65,6 +65,15 @@ test-oss-ci: deps test-node
test-node:
yarn run test:node
+test-coverage: deps
+ yarn run test:coverage
+
+test-coverage-view: deps
+ yarn run test:coverage:view
+
+test-coverage-ci: deps
+ yarn run test:coverage:ci
+
test-parallel: deps
yarn run test:parallel
diff --git a/ui-v2/app/adapters/application.js b/ui-v2/app/adapters/application.js
index 5dede706f4..45cb2e7291 100644
--- a/ui-v2/app/adapters/application.js
+++ b/ui-v2/app/adapters/application.js
@@ -1,14 +1,13 @@
import Adapter from './http';
import { inject as service } from '@ember/service';
-import { env } from 'consul-ui/env';
export const DATACENTER_QUERY_PARAM = 'dc';
export const NSPACE_QUERY_PARAM = 'ns';
export default Adapter.extend({
- repo: service('settings'),
client: service('client/http'),
+ env: service('env'),
formatNspace: function(nspace) {
- if (env('CONSUL_NSPACES_ENABLED')) {
+ if (this.env.env('CONSUL_NSPACES_ENABLED')) {
return nspace !== '' ? { [NSPACE_QUERY_PARAM]: nspace } : undefined;
}
},
@@ -17,45 +16,9 @@ export default Adapter.extend({
[DATACENTER_QUERY_PARAM]: dc,
};
},
- // TODO: kinda protected for the moment
- // decide where this should go either read/write from http
- // should somehow use this or vice versa
+ // TODO: Deprecated, remove `request` usage from everywhere and replace with
+ // `HTTPAdapter.rpc`
request: function(req, resp, obj, modelName) {
- const client = this.client;
- const store = this.store;
- const adapter = this;
-
- let unserialized, serialized;
- const serializer = store.serializerFor(modelName);
- // workable way to decide whether this is a snapshot
- // essentially 'is attributable'.
- // Snapshot is private so we can't do instanceof here
- // and using obj.constructor.name gets changed/minified
- // during compilation so you can't rely on it
- // checking for `attributes` being a function is more
- // reliable as that is the thing we need to call
- if (typeof obj.attributes === 'function') {
- unserialized = obj.attributes();
- serialized = serializer.serialize(obj, {});
- } else {
- unserialized = obj;
- serialized = unserialized;
- }
-
- return client
- .request(function(request) {
- return req(adapter, request, serialized, unserialized);
- })
- .catch(function(e) {
- return adapter.error(e);
- })
- .then(function(respond) {
- // TODO: When HTTPAdapter:responder changes, this will also need to change
- return resp(serializer, respond, serialized, unserialized);
- });
- // TODO: Potentially add specific serializer errors here
- // .catch(function(e) {
- // return Promise.reject(e);
- // });
+ return this.rpc(...arguments);
},
});
diff --git a/ui-v2/app/adapters/dc.js b/ui-v2/app/adapters/dc.js
index 3d17d70b83..e98b054657 100644
--- a/ui-v2/app/adapters/dc.js
+++ b/ui-v2/app/adapters/dc.js
@@ -1,7 +1,7 @@
import Adapter from './application';
export default Adapter.extend({
- requestForFindAll: function(request) {
+ requestForQuery: function(request) {
return request`
GET /v1/catalog/datacenters
`;
diff --git a/ui-v2/app/adapters/http.js b/ui-v2/app/adapters/http.js
index 294b7c83a5..e8bd64e244 100644
--- a/ui-v2/app/adapters/http.js
+++ b/ui-v2/app/adapters/http.js
@@ -1,3 +1,4 @@
+import { inject as service } from '@ember/service';
import Adapter from 'ember-data/adapter';
import AdapterError from '@ember-data/adapter/error';
import {
@@ -10,46 +11,75 @@ import {
ConflictError,
InvalidError,
} from 'ember-data/adapters/errors';
-// TODO: This is a little skeleton cb function
-// is to be replaced soon with something slightly more involved
-const responder = function(response) {
- return response;
-};
-const read = function(adapter, serializer, client, type, query) {
- return client
- .request(function(request) {
+
+// TODO These are now exactly the same, apart from the fact that one uses
+// `serialized, unserialized` and the other just `query`
+// they could actually be one function now, but would be nice to think about
+// the naming of things (serialized vs query etc)
+const read = function(adapter, modelName, type, query = {}) {
+ return adapter.rpc(
+ function(adapter, request, query) {
return adapter[`requestFor${type}`](request, query);
- })
- .catch(function(e) {
- return adapter.error(e);
- })
- .then(function(response) {
- return serializer[`respondFor${type}`](responder(response), query);
- });
- // TODO: Potentially add specific serializer errors here
- // .catch(function(e) {
- // return Promise.reject(e);
- // });
+ },
+ function(serializer, respond, query) {
+ return serializer[`respondFor${type}`](respond, query);
+ },
+ query,
+ modelName
+ );
};
-const write = function(adapter, serializer, client, type, snapshot) {
- const unserialized = snapshot.attributes();
- const serialized = serializer.serialize(snapshot, {});
- return client
- .request(function(request) {
+const write = function(adapter, modelName, type, snapshot) {
+ return adapter.rpc(
+ function(adapter, request, serialized, unserialized) {
return adapter[`requestFor${type}`](request, serialized, unserialized);
- })
- .catch(function(e) {
- return adapter.error(e);
- })
- .then(function(response) {
- return serializer[`respondFor${type}`](responder(response), serialized, unserialized);
- });
- // TODO: Potentially add specific serializer errors here
- // .catch(function(e) {
- // return Promise.reject(e);
- // });
+ },
+ function(serializer, respond, serialized, unserialized) {
+ return serializer[`respondFor${type}`](respond, serialized, unserialized);
+ },
+ snapshot,
+ modelName
+ );
};
export default Adapter.extend({
+ client: service('client/http'),
+ rpc: function(req, resp, obj, modelName) {
+ const client = this.client;
+ const store = this.store;
+ const adapter = this;
+
+ let unserialized, serialized;
+ const serializer = store.serializerFor(modelName);
+ // workable way to decide whether this is a snapshot
+ // essentially 'is attributable'.
+ // Snapshot is private so we can't do instanceof here
+ // and using obj.constructor.name gets changed/minified
+ // during compilation so you can't rely on it
+ // checking for `attributes` being a function is more
+ // reliable as that is the thing we need to call
+ if (typeof obj.attributes === 'function') {
+ unserialized = obj.attributes();
+ serialized = serializer.serialize(obj, {});
+ } else {
+ unserialized = obj;
+ serialized = unserialized;
+ }
+
+ return client
+ .request(function(request) {
+ return req(adapter, request, serialized, unserialized);
+ })
+ .catch(function(e) {
+ return adapter.error(e);
+ })
+ .then(function(respond) {
+ // TODO: When HTTPAdapter:responder changes, this will also need to change
+ return resp(serializer, respond, serialized, unserialized);
+ });
+ // TODO: Potentially add specific serializer errors here
+ // .catch(function(e) {
+ // return Promise.reject(e);
+ // });
+ },
error: function(err) {
const errors = [
{
@@ -94,24 +124,28 @@ export default Adapter.extend({
} catch (e) {
error = e;
}
+ // TODO: This comes originates from ember-data
+ // This can be confusing if you need to use this with Promise.reject
+ // Consider changing this to return the error and then
+ // throw from the call site instead
throw error;
},
query: function(store, type, query) {
- return read(this, store.serializerFor(type.modelName), this.client, 'Query', query);
+ return read(this, type.modelName, 'Query', query);
},
queryRecord: function(store, type, query) {
- return read(this, store.serializerFor(type.modelName), this.client, 'QueryRecord', query);
+ return read(this, type.modelName, 'QueryRecord', query);
},
findAll: function(store, type) {
- return read(this, store.serializerFor(type.modelName), this.client, 'FindAll');
+ return read(this, type.modelName, 'FindAll');
},
createRecord: function(store, type, snapshot) {
- return write(this, store.serializerFor(type.modelName), this.client, 'CreateRecord', snapshot);
+ return write(this, type.modelName, 'CreateRecord', snapshot);
},
updateRecord: function(store, type, snapshot) {
- return write(this, store.serializerFor(type.modelName), this.client, 'UpdateRecord', snapshot);
+ return write(this, type.modelName, 'UpdateRecord', snapshot);
},
deleteRecord: function(store, type, snapshot) {
- return write(this, store.serializerFor(type.modelName), this.client, 'DeleteRecord', snapshot);
+ return write(this, type.modelName, 'DeleteRecord', snapshot);
},
});
diff --git a/ui-v2/app/adapters/intention.js b/ui-v2/app/adapters/intention.js
index fb27a5a0aa..d98afe66b9 100644
--- a/ui-v2/app/adapters/intention.js
+++ b/ui-v2/app/adapters/intention.js
@@ -6,11 +6,14 @@ import { SLUG_KEY } from 'consul-ui/models/intention';
// TODO: Update to use this.formatDatacenter()
export default Adapter.extend({
- requestForQuery: function(request, { dc, index, id }) {
+ requestForQuery: function(request, { dc, filter, index }) {
return request`
GET /v1/connect/intentions?${{ dc }}
- ${{ index }}
+ ${{
+ index,
+ filter,
+ }}
`;
},
requestForQueryRecord: function(request, { dc, index, id }) {
diff --git a/ui-v2/app/adapters/oidc-provider.js b/ui-v2/app/adapters/oidc-provider.js
new file mode 100644
index 0000000000..f2b65da2e2
--- /dev/null
+++ b/ui-v2/app/adapters/oidc-provider.js
@@ -0,0 +1,97 @@
+import Adapter from './application';
+import { inject as service } from '@ember/service';
+
+import { env } from 'consul-ui/env';
+import nonEmptySet from 'consul-ui/utils/non-empty-set';
+
+let Namespace;
+if (env('CONSUL_NSPACES_ENABLED')) {
+ Namespace = nonEmptySet('Namespace');
+} else {
+ Namespace = () => ({});
+}
+export default Adapter.extend({
+ env: service('env'),
+ requestForQuery: function(request, { dc, ns, index }) {
+ return request`
+ GET /v1/internal/ui/oidc-auth-methods?${{ dc }}
+
+ ${{
+ index,
+ ...this.formatNspace(ns),
+ }}
+ `;
+ },
+ requestForQueryRecord: function(request, { dc, ns, id }) {
+ if (typeof id === 'undefined') {
+ throw new Error('You must specify an id');
+ }
+ return request`
+ POST /v1/acl/oidc/auth-url?${{ dc }}
+ Cache-Control: no-store
+
+ ${{
+ ...Namespace(ns),
+ AuthMethod: id,
+ RedirectURI: `${this.env.var('CONSUL_BASE_UI_URL')}/oidc/callback`,
+ }}
+ `;
+ },
+ requestForAuthorize: function(request, { dc, ns, id, code, state }) {
+ if (typeof id === 'undefined') {
+ throw new Error('You must specify an id');
+ }
+ if (typeof code === 'undefined') {
+ throw new Error('You must specify an code');
+ }
+ if (typeof state === 'undefined') {
+ throw new Error('You must specify an state');
+ }
+ return request`
+ POST /v1/acl/oidc/callback?${{ dc }}
+ Cache-Control: no-store
+
+ ${{
+ ...Namespace(ns),
+ AuthMethod: id,
+ Code: code,
+ State: state,
+ }}
+ `;
+ },
+ requestForLogout: function(request, { id }) {
+ if (typeof id === 'undefined') {
+ throw new Error('You must specify an id');
+ }
+ return request`
+ POST /v1/acl/logout
+ Cache-Control: no-store
+ X-Consul-Token: ${id}
+ `;
+ },
+ authorize: function(store, type, id, snapshot) {
+ return this.request(
+ function(adapter, request, serialized, unserialized) {
+ return adapter.requestForAuthorize(request, serialized, unserialized);
+ },
+ function(serializer, respond, serialized, unserialized) {
+ return serializer.respondForAuthorize(respond, serialized, unserialized);
+ },
+ snapshot,
+ type.modelName
+ );
+ },
+ logout: function(store, type, id, snapshot) {
+ return this.request(
+ function(adapter, request, serialized, unserialized) {
+ return adapter.requestForLogout(request, serialized, unserialized);
+ },
+ function(serializer, respond, serialized, unserialized) {
+ // its ok to return nothing here for the moment at least
+ return {};
+ },
+ snapshot,
+ type.modelName
+ );
+ },
+});
diff --git a/ui-v2/app/adapters/token.js b/ui-v2/app/adapters/token.js
index cc1f8d29ca..afed469dbf 100644
--- a/ui-v2/app/adapters/token.js
+++ b/ui-v2/app/adapters/token.js
@@ -104,6 +104,7 @@ export default Adapter.extend({
return request`
GET /v1/acl/token/self?${{ dc }}
X-Consul-Token: ${secret}
+ Cache-Control: no-store
${{ index }}
`;
@@ -132,7 +133,7 @@ export default Adapter.extend({
return adapter.requestForSelf(request, serialized, data);
},
function(serializer, respond, serialized, data) {
- return serializer.respondForQueryRecord(respond, serialized, data);
+ return serializer.respondForSelf(respond, serialized, data);
},
unserialized,
type.modelName
diff --git a/ui-v2/app/app.js b/ui-v2/app/app.js
index f08aaaf030..d8e2088b6b 100644
--- a/ui-v2/app/app.js
+++ b/ui-v2/app/app.js
@@ -1,14 +1,12 @@
import Application from '@ember/application';
-import Resolver from './resolver';
+import Resolver from 'ember-resolver';
import loadInitializers from 'ember-load-initializers';
import config from './config/environment';
-const App = Application.extend({
- modulePrefix: config.modulePrefix,
- podModulePrefix: config.podModulePrefix,
- Resolver,
-});
+export default class App extends Application {
+ modulePrefix = config.modulePrefix;
+ podModulePrefix = config.podModulePrefix;
+ Resolver = Resolver;
+}
loadInitializers(App, config.modulePrefix);
-
-export default App;
diff --git a/ui-v2/app/components/acl-filter/index.hbs b/ui-v2/app/components/acl-filter/index.hbs
new file mode 100644
index 0000000000..503e0c1f20
--- /dev/null
+++ b/ui-v2/app/components/acl-filter/index.hbs
@@ -0,0 +1,4 @@
+{{!
}}
diff --git a/ui-v2/app/components/acl-filter.js b/ui-v2/app/components/acl-filter/index.js
similarity index 100%
rename from ui-v2/app/components/acl-filter.js
rename to ui-v2/app/components/acl-filter/index.js
diff --git a/ui-v2/app/templates/components/action-group.hbs b/ui-v2/app/components/action-group/index.hbs
similarity index 100%
rename from ui-v2/app/templates/components/action-group.hbs
rename to ui-v2/app/components/action-group/index.hbs
diff --git a/ui-v2/app/components/action-group.js b/ui-v2/app/components/action-group/index.js
similarity index 100%
rename from ui-v2/app/components/action-group.js
rename to ui-v2/app/components/action-group/index.js
diff --git a/ui-v2/app/components/app-view/index.hbs b/ui-v2/app/components/app-view/index.hbs
new file mode 100644
index 0000000000..b66f00fbca
--- /dev/null
+++ b/ui-v2/app/components/app-view/index.hbs
@@ -0,0 +1,89 @@
+{{yield}}
+{{#if (not loading)}}
+
+{{#each flashMessages.queue as |flash|}}
+
+ {{#let (lowercase component.flashType) (lowercase flash.action) as |status type|}}
+ {{! flashes automatically ucfirst the type }}
+
+
+
+ {{capitalize status}}!
+
+ {{#yield-slot name="notification" params=(block-params status type flash.item)}}
+ {{yield}}
+ {{#if (eq type 'logout')}}
+ {{#if (eq status 'success') }}
+ You are now logged out.
+ {{else}}
+ There was an error logging out.
+ {{/if}}
+ {{else if (eq type 'authorize')}}
+ {{#if (eq status 'success') }}
+ You are now logged in.
+ {{else}}
+ There was an error, please check your SecretID/Token
+ {{/if}}
+ {{/if}}
+ {{else}}
+ {{#if (eq type 'logout')}}
+ {{#if (eq status 'success') }}
+ You are now logged out.
+ {{else}}
+ There was an error logging out.
+ {{/if}}
+ {{else if (eq type 'authorize')}}
+ {{#if (eq status 'success') }}
+ You are now logged in.
+ {{else}}
+ There was an error, please check your SecretID/Token
+ {{/if}}
+ {{/if}}
+ {{/yield-slot}}
+
diff --git a/ui-v2/app/components/app-view.js b/ui-v2/app/components/app-view/index.js
similarity index 100%
rename from ui-v2/app/components/app-view.js
rename to ui-v2/app/components/app-view/index.js
diff --git a/ui-v2/app/templates/components/aria-menu.hbs b/ui-v2/app/components/aria-menu/index.hbs
similarity index 100%
rename from ui-v2/app/templates/components/aria-menu.hbs
rename to ui-v2/app/components/aria-menu/index.hbs
diff --git a/ui-v2/app/components/aria-menu.js b/ui-v2/app/components/aria-menu/index.js
similarity index 100%
rename from ui-v2/app/components/aria-menu.js
rename to ui-v2/app/components/aria-menu/index.js
diff --git a/ui-v2/app/components/auth-dialog/README.mdx b/ui-v2/app/components/auth-dialog/README.mdx
new file mode 100644
index 0000000000..676d553796
--- /dev/null
+++ b/ui-v2/app/components/auth-dialog/README.mdx
@@ -0,0 +1,56 @@
+## AuthDialog
+
+```handlebars
+
+ {{#let components.AuthForm components.AuthProfile as |AuthForm AuthProfile|}}
+
+ Here's the login form:
+
+
+
+ Here's your profile:
+
+
+
+ {{/let}}
+
+```
+
+### Arguments
+
+A component to help orchestrate a login/logout flow.
+
+| Argument | Type | Default | Description |
+| --- | --- | --- | --- |
+| `dc` | `String` | | The name of the current datacenter |
+| `nspace` | `String` | | The name of the current namespace |
+| `onchange` | `Function` | | An action to fire when the users token has changed (logged in/logged out/token changed) |
+
+### Methods/Actions/api
+
+| Method/Action | Description |
+| --- | --- |
+| `login` | Login with a specified token |
+| `logout` | Logout (delete token) |
+| `token` | The current token itself (as a property not a method) |
+
+### Components
+
+| Name | Description |
+| --- | --- |
+| [`AuthForm`](../auth-form/README.mdx) | Renders an Authorization form |
+| [`AuthProfile`](../auth-profile/README.mdx) | Renders a User Profile |
+
+### Slots
+
+| Name | Description |
+| --- | --- |
+| `unauthorized` | This slot is only rendered when the user doesn't have a token |
+| `authorized` | This slot is only rendered whtn the user has a token.|
+
+### See
+
+- [Component Source Code](./index.js)
+- [Template Source Code](./index.hbs)
+
+---
diff --git a/ui-v2/app/components/auth-dialog/chart.xstate.js b/ui-v2/app/components/auth-dialog/chart.xstate.js
new file mode 100644
index 0000000000..ce47307453
--- /dev/null
+++ b/ui-v2/app/components/auth-dialog/chart.xstate.js
@@ -0,0 +1,34 @@
+export default {
+ id: 'auth-dialog',
+ initial: 'idle',
+ on: {
+ CHANGE: [
+ {
+ target: 'authorized',
+ cond: 'hasToken',
+ actions: ['login'],
+ },
+ {
+ target: 'unauthorized',
+ actions: ['logout'],
+ },
+ ],
+ },
+ states: {
+ idle: {
+ on: {
+ CHANGE: [
+ {
+ target: 'authorized',
+ cond: 'hasToken',
+ },
+ {
+ target: 'unauthorized',
+ },
+ ],
+ },
+ },
+ unauthorized: {},
+ authorized: {},
+ },
+};
diff --git a/ui-v2/app/components/auth-dialog/index.hbs b/ui-v2/app/components/auth-dialog/index.hbs
new file mode 100644
index 0000000000..e12a4d05d6
--- /dev/null
+++ b/ui-v2/app/components/auth-dialog/index.hbs
@@ -0,0 +1,40 @@
+
+
+
+
+
+ {{! This DataSource just permanently listens to any changes to the users }}
+ {{! token, whether thats a new token, a changed token or a deleted token }}
+
+ {{! This DataSink is just used for logging in from the form, }}
+ {{! or logging out via the exposed logout function }}
+
+ {{yield}}
+ {{#let (hash
+ login=(action sink.open)
+ logout=(action sink.open null)
+ token=token
+ ) (hash
+ AuthProfile=(component 'auth-profile' item=token)
+ AuthForm=(component 'auth-form' dc=dc nspace=nspace onsubmit=(action sink.open value="data"))
+ ) as |api components|}}
+
+ {{#yield-slot name="authorized"}}
+ {{yield api components}}
+ {{/yield-slot}}
+
+
+
+ {{#yield-slot name="unauthorized"}}
+ {{yield api components}}
+ {{/yield-slot}}
+
+ {{/let}}
+
+
diff --git a/ui-v2/app/components/auth-dialog/index.js b/ui-v2/app/components/auth-dialog/index.js
new file mode 100644
index 0000000000..041fee483b
--- /dev/null
+++ b/ui-v2/app/components/auth-dialog/index.js
@@ -0,0 +1,42 @@
+import Component from '@ember/component';
+import Slotted from 'block-slots';
+import { inject as service } from '@ember/service';
+import { get } from '@ember/object';
+import chart from './chart.xstate';
+
+export default Component.extend(Slotted, {
+ tagName: '',
+ repo: service('repository/oidc-provider'),
+ init: function() {
+ this._super(...arguments);
+ this.chart = chart;
+ },
+ actions: {
+ hasToken: function() {
+ return typeof this.token.AccessorID !== 'undefined';
+ },
+ login: function() {
+ let prev = get(this, 'previousToken.AccessorID');
+ let current = get(this, 'token.AccessorID');
+ if (prev === null) {
+ prev = get(this, 'previousToken.SecretID');
+ }
+ if (current === null) {
+ current = get(this, 'token.SecretID');
+ }
+ let type = 'authorize';
+ if (typeof prev !== 'undefined' && prev !== current) {
+ type = 'use';
+ }
+ this.onchange({ data: get(this, 'token'), type: type });
+ },
+ logout: function() {
+ if (typeof get(this, 'previousToken.AuthMethod') !== 'undefined') {
+ // we are ok to fire and forget here
+ this.repo.logout(get(this, 'previousToken.SecretID'));
+ }
+ this.previousToken = null;
+ this.onchange({ data: null, type: 'logout' });
+ },
+ },
+});
diff --git a/ui-v2/app/components/auth-form/README.mdx b/ui-v2/app/components/auth-form/README.mdx
new file mode 100644
index 0000000000..942e9d8137
--- /dev/null
+++ b/ui-v2/app/components/auth-form/README.mdx
@@ -0,0 +1,18 @@
+## AuthForm
+
+```handlebars
+
+```
+
+### Methods/Actions/api
+
+| Method/Action | Description |
+| --- | --- |
+| `reset` | Reset the form back to its original empty/non-error state |
+
+### See
+
+- [Component Source Code](./index.js)
+- [Template Source Code](./index.hbs)
+
+---
diff --git a/ui-v2/app/components/auth-form/chart.xstate.js b/ui-v2/app/components/auth-form/chart.xstate.js
new file mode 100644
index 0000000000..dc4b281059
--- /dev/null
+++ b/ui-v2/app/components/auth-form/chart.xstate.js
@@ -0,0 +1,55 @@
+export default {
+ id: 'auth-form',
+ initial: 'idle',
+ on: {
+ RESET: [
+ {
+ target: 'idle',
+ },
+ ],
+ },
+ states: {
+ idle: {
+ entry: ['clearError'],
+ on: {
+ SUBMIT: [
+ {
+ target: 'loading',
+ cond: 'hasValue',
+ },
+ {
+ target: 'error',
+ },
+ ],
+ },
+ },
+ loading: {
+ on: {
+ ERROR: [
+ {
+ target: 'error',
+ },
+ ],
+ },
+ },
+ error: {
+ exit: ['clearError'],
+ on: {
+ TYPING: [
+ {
+ target: 'idle',
+ },
+ ],
+ SUBMIT: [
+ {
+ target: 'loading',
+ cond: 'hasValue',
+ },
+ {
+ target: 'error',
+ },
+ ],
+ },
+ },
+ },
+};
diff --git a/ui-v2/app/components/auth-form/index.hbs b/ui-v2/app/components/auth-form/index.hbs
new file mode 100644
index 0000000000..4da0b5b12e
--- /dev/null
+++ b/ui-v2/app/components/auth-form/index.hbs
@@ -0,0 +1,100 @@
+
+ {{yield (hash
+ reset=(action dispatch "RESET")
+ focus=(action 'focus')
+ )}}
+
+ {{!FIXME: Call this reset or similar }}
+
+
+
+ {{#if error.status}}
+
+ {{#if value.Name}}
+ {{#if (eq error.status '403')}}
+ Consul login failed
+ We received a token from your OIDC provider but could not log in to Consul with it.
+ {{else if (eq error.status '401')}}
+ Could not log in to provider
+ The OIDC provider has rejected this access token. Please have an administrator check your auth method configuration.
+ {{else if (eq error.status '499')}}
+ SSO log in window closed
+ The OIDC provider window was closed. Please try again.
+ {{else}}
+ Error
+ {{error.detail}}
+ {{/if}}
+ {{else}}
+ {{#if (eq error.status '403')}}
+ Invalid token
+ The token entered does not exist. Please enter a valid token to log in.
+ {{else}}
+ Error
+ {{error.detail}}
+ {{/if}}
+ {{/if}}
+
+
+{{/if}}
\ No newline at end of file
diff --git a/ui-v2/app/components/consul-service-list/index.js b/ui-v2/app/components/consul-service-list/index.js
new file mode 100644
index 0000000000..4798652642
--- /dev/null
+++ b/ui-v2/app/components/consul-service-list/index.js
@@ -0,0 +1,5 @@
+import Component from '@ember/component';
+
+export default Component.extend({
+ tagName: '',
+});
diff --git a/ui-v2/app/components/copy-button.js b/ui-v2/app/components/copy-button.js
deleted file mode 100644
index 55b61f7447..0000000000
--- a/ui-v2/app/components/copy-button.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import Component from '@ember/component';
-import { inject as service } from '@ember/service';
-
-import WithListeners from 'consul-ui/mixins/with-listeners';
-
-export default Component.extend(WithListeners, {
- clipboard: service('clipboard/os'),
- tagName: 'button',
- classNames: ['copy-btn'],
- buttonType: 'button',
- disabled: false,
- error: function() {},
- success: function() {},
- attributeBindings: [
- 'clipboardText:data-clipboard-text',
- 'clipboardTarget:data-clipboard-target',
- 'clipboardAction:data-clipboard-action',
- 'buttonType:type',
- 'disabled',
- 'aria-label',
- 'title',
- ],
- delegateClickEvent: true,
-
- didInsertElement: function() {
- this._super(...arguments);
- const clipboard = this.clipboard.execute(
- this.delegateClickEvent ? `#${this.elementId}` : this.element
- );
- ['success', 'error'].map(event => {
- return this.listen(clipboard, event, () => {
- if (!this.disabled) {
- this[event](...arguments);
- }
- });
- });
- },
-});
diff --git a/ui-v2/app/components/copy-button/README.mdx b/ui-v2/app/components/copy-button/README.mdx
new file mode 100644
index 0000000000..9e307ab510
--- /dev/null
+++ b/ui-v2/app/components/copy-button/README.mdx
@@ -0,0 +1,32 @@
+## CopyButton
+
+```handlebars
+{{! inline }}
+
+
+
+ Copy me!
+
+```
+
+### Arguments
+
+| Argument | Type | Default | Description |
+| --- | --- | --- | --- |
+| `value` | `String` | | The string to be copied to the clipboard on click |
+| `name` | `String` | | The 'Name' of the string to be copied. Mainly used for giving feedback to the user |
+
+This component renders a simple button, when clicked copies the value (the `@value` attribute) to the users clipboard. A simple piece of feedback is given to the user in the form of a tooltip. When used inline an empty button is rendered.
+
+### See
+
+- [Component Source Code](./index.js)
+- [Template Source Code](./index.hbs)
+
+---
diff --git a/ui-v2/app/components/copy-button/index.hbs b/ui-v2/app/components/copy-button/index.hbs
new file mode 100644
index 0000000000..f1ee923884
--- /dev/null
+++ b/ui-v2/app/components/copy-button/index.hbs
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+ Copied {{name}}!
+
+
+
+
+ Sorry, something went wrong!
+
+
+
diff --git a/ui-v2/app/components/copy-button/index.js b/ui-v2/app/components/copy-button/index.js
new file mode 100644
index 0000000000..e0b3d0ec13
--- /dev/null
+++ b/ui-v2/app/components/copy-button/index.js
@@ -0,0 +1,29 @@
+import Component from '@ember/component';
+import { inject as service } from '@ember/service';
+
+export default Component.extend({
+ clipboard: service('clipboard/os'),
+ dom: service('dom'),
+ tagName: '',
+ init: function() {
+ this._super(...arguments);
+ this.guid = this.dom.guid(this);
+ this._listeners = this.dom.listeners();
+ },
+ willDestroyElement: function() {
+ this._super(...arguments);
+ this._listeners.remove();
+ },
+ didInsertElement: function() {
+ this._super(...arguments);
+ const component = this;
+ this._listeners.add(this.clipboard.execute(`#${this.guid}`), {
+ success: function() {
+ component.success(...arguments);
+ },
+ error: function() {
+ component.error(...arguments);
+ },
+ });
+ },
+});
diff --git a/ui-v2/app/components/data-sink/README.mdx b/ui-v2/app/components/data-sink/README.mdx
new file mode 100644
index 0000000000..9eaa0ff4ab
--- /dev/null
+++ b/ui-v2/app/components/data-sink/README.mdx
@@ -0,0 +1,62 @@
+## DataSink
+
+```handlebars
+
+```
+
+### Arguments
+
+| Argument | Type | Default | Description |
+| --- | --- | --- | --- |
+| `sink` | `String` | | The location of the sink, this should map to a string based URI |
+| `data` | `Object` | | The data to be saved to the current instance, null or an empty string means remove |
+| `onchange` | `Function` | | The action to fire when the data has arrived to the sink. Emits an Event-like object with a `data` property containing the data, if the data was deleted this is `undefined`. |
+| `onerror` | `Function` | | The action to fire when an error occurs. Emits ErrorEvent object with an `error` property containing the Error. |
+
+### Methods/Actions/api
+
+| Method/Action | Description |
+| --- | --- |
+| `open` | Manually add or remove fom the data sink |
+
+The component takes a `sink` or an identifier (a uri) for the location of a sink and then emits `onchange` events whenever that data has been arrived to the sink (whether persisted or removed). If an error occurs whilst listening for data changes, an `onerror` event is emitted.
+
+Behind the scenes in the Consul UI we map URIs back to our `ember-data` backed `Repositories` meaning we can essentially redesign the URIs used for our data to more closely fit our needs. For example we currently require that **all** HTTP API URIs begin with `/dc/nspace/` values whether they require them or not.
+
+`DataSink` is not just restricted to HTTP API data, and can be configured to listen for data changes using a variety of methods and sources. For example we have also configured `DataSink` to send data to `LocalStorage` using the `settings://` pseudo-protocol in the URI (See examples below).
+
+
+### Examples
+
+```handlebars
+
+
+
+
+ {{item.Name}}
+```
+
+```handlebars
+
+ {{item.Name}}
+```
+
+### See
+
+- [Component Source Code](./index.js)
+- [Template Source Code](./index.hbs)
+
+---
diff --git a/ui-v2/app/components/data-sink/index.hbs b/ui-v2/app/components/data-sink/index.hbs
new file mode 100644
index 0000000000..a809747a7c
--- /dev/null
+++ b/ui-v2/app/components/data-sink/index.hbs
@@ -0,0 +1,4 @@
+{{yield (hash
+ open=(action 'open')
+ state=state
+)}}
diff --git a/ui-v2/app/components/data-sink/index.js b/ui-v2/app/components/data-sink/index.js
new file mode 100644
index 0000000000..c863e5dc16
--- /dev/null
+++ b/ui-v2/app/components/data-sink/index.js
@@ -0,0 +1,105 @@
+import Component from '@ember/component';
+import { inject as service } from '@ember/service';
+import { set, get, computed } from '@ember/object';
+
+import { once } from 'consul-ui/utils/dom/event-source';
+
+export default Component.extend({
+ tagName: '',
+
+ service: service('data-sink/service'),
+ dom: service('dom'),
+ logger: service('logger'),
+
+ onchange: function(e) {},
+ onerror: function(e) {},
+
+ state: computed('instance', 'instance.{dirtyType,isSaving}', function() {
+ let id;
+ const isSaving = get(this, 'instance.isSaving');
+ const dirtyType = get(this, 'instance.dirtyType');
+ if (typeof isSaving === 'undefined' && typeof dirtyType === 'undefined') {
+ id = 'idle';
+ } else {
+ switch (dirtyType) {
+ case 'created':
+ id = isSaving ? 'creating' : 'create';
+ break;
+ case 'updated':
+ id = isSaving ? 'updating' : 'update';
+ break;
+ case 'deleted':
+ case undefined:
+ id = isSaving ? 'removing' : 'remove';
+ break;
+ }
+ id = `active.${id}`;
+ }
+ return {
+ matches: name => id.indexOf(name) !== -1,
+ };
+ }),
+
+ init: function() {
+ this._super(...arguments);
+ this._listeners = this.dom.listeners();
+ },
+ willDestroy: function() {
+ this._super(...arguments);
+ this._listeners.remove();
+ },
+ source: function(cb) {
+ const source = once(cb);
+ const error = err => {
+ set(this, 'instance', undefined);
+ try {
+ this.onerror(err);
+ this.logger.execute(err);
+ } catch (err) {
+ this.logger.execute(err);
+ }
+ };
+ this._listeners.add(source, {
+ message: e => {
+ try {
+ set(this, 'instance', undefined);
+ this.onchange(e);
+ } catch (err) {
+ error(err);
+ }
+ },
+ error: e => error(e),
+ });
+ return source;
+ },
+ didInsertElement: function() {
+ this._super(...arguments);
+ if (typeof this.data !== 'undefined') {
+ this.actions.open.apply(this, [this.data]);
+ }
+ },
+ persist: function(data, instance) {
+ set(this, 'instance', this.service.prepare(this.sink, data, instance));
+ this.source(() => this.service.persist(this.sink, this.instance));
+ },
+ remove: function(instance) {
+ set(this, 'instance', this.service.prepare(this.sink, null, instance));
+ this.source(() => this.service.remove(this.sink, this.instance));
+ },
+ actions: {
+ open: function(data, instance) {
+ if (instance instanceof Event) {
+ instance = undefined;
+ }
+ if (typeof data === 'undefined') {
+ throw new Error('You must specify data to save, or null to remove');
+ }
+ // potentially allow {} and "" as 'remove' flags
+ if (data === null || data === '') {
+ this.remove(instance);
+ } else {
+ this.persist(data, instance);
+ }
+ },
+ },
+});
diff --git a/ui-v2/app/components/data-source/README.mdx b/ui-v2/app/components/data-source/README.mdx
new file mode 100644
index 0000000000..63c485a3db
--- /dev/null
+++ b/ui-v2/app/components/data-source/README.mdx
@@ -0,0 +1,61 @@
+## DataSource
+
+```handlebars
+
+```
+
+### Arguments
+
+| Argument | Type | Default | Description |
+| --- | --- | --- | --- |
+| `src` | `String` | | The source to subscribe to updates to, this should map to a string based URI |
+| `loading` | `String` | eager | Allows the browser to defer loading offscreen DataSources (`eager\|lazy`). Setting to `lazy` only loads the data when the DataSource is visible in the DOM (inc. `display: none\|block;`) |
+| `onchange` | `Function` | | The action to fire when the data changes. Emits an Event-like object with a `data` property containing the data. |
+| `onerror` | `Function` | | The action to fire when an error occurs. Emits ErrorEvent object with an `error` property containing the Error. |
+
+The component takes a `src` or an identifier (a uri) for some data and then emits `onchange` events whenever that data changes. If an error occurs whilst listening for data changes, an `onerror` event is emitted.
+
+Setting `@loading="lazy"` uses `IntersectionObserver` to activate/deactive event emitting until the `` element is displayed in the DOM. This means you can use CSS `display: none|block;` to control the loading and stopping loading of data for usage with CSS based tabs and such-like.
+
+Consuls HTTP API DataSources use Consul's blocking query support for live updating of data.
+
+Behind the scenes in the Consul UI we map URIs back to our `ember-data` backed `Repositories` meaning we can essentially redesign the URIs used for our data to more closely fit our needs. For example we currently require that **all** HTTP API URIs begin with `/dc/nspace/` values whether they require them or not.
+
+`DataSource` is not just restricted to HTTP API data, and can be configured to listen for data changes using a variety of methods and sources. For example we have also configured `DataSource` to listen to `LocalStorage` changes using the `settings://` pseudo-protocol in the URI (See examples below).
+
+
+### Example
+
+Straightforward usage can use `mut` to easily update data within a template
+
+```handlebars
+ {{! listen for HTTP API changes}}
+
+ {{! the value of items will change whenever the data changes}}
+ {{#each items as |item|}}
+ {{item.Name}} {{! < Prints the item name }}
+ {{/each}}
+
+ {{! listen for Settings (local storage) changes}}
+
+ {{! the value of token will change whenever the data changes}}
+ {{token.AccessorID}} {{! < Prints the token AccessorID }}
+```
+
+### See
+
+- [Component Source Code](./index.js)
+- [Template Source Code](./index.hbs)
+
+---
diff --git a/ui-v2/app/components/data-source/index.hbs b/ui-v2/app/components/data-source/index.hbs
new file mode 100644
index 0000000000..3e9f7250ec
--- /dev/null
+++ b/ui-v2/app/components/data-source/index.hbs
@@ -0,0 +1,4 @@
+{{#if (eq loading "lazy")}}
+{{! in order to use intersection observer we need a DOM element on the page}}
+
+{{/if}}
diff --git a/ui-v2/app/components/data-source/index.js b/ui-v2/app/components/data-source/index.js
new file mode 100644
index 0000000000..5409e9c57f
--- /dev/null
+++ b/ui-v2/app/components/data-source/index.js
@@ -0,0 +1,129 @@
+import Component from '@ember/component';
+import { inject as service } from '@ember/service';
+import { set } from '@ember/object';
+import { schedule } from '@ember/runloop';
+
+/**
+ * Utility function to set, but actually replace if we should replace
+ * then call a function on the thing to be replaced (usually a clean up function)
+ *
+ * @param obj - target object with the property to replace
+ * @param prop {string} - property to replace on the target object
+ * @param value - value to use for replacement
+ * @param destroy {(prev: any, value: any) => any} - teardown function
+ */
+const replace = function(
+ obj,
+ prop,
+ value,
+ destroy = (prev = null, value) => (typeof prev === 'function' ? prev() : null)
+) {
+ const prev = obj[prop];
+ if (prev !== value) {
+ destroy(prev, value);
+ }
+ return set(obj, prop, value);
+};
+
+export default Component.extend({
+ tagName: '',
+
+ data: service('data-source/service'),
+ dom: service('dom'),
+ logger: service('logger'),
+
+ onchange: function(e) {},
+ onerror: function(e) {},
+
+ loading: 'eager',
+
+ isIntersecting: false,
+
+ init: function() {
+ this._super(...arguments);
+ this._listeners = this.dom.listeners();
+ this._lazyListeners = this.dom.listeners();
+ this.guid = this.dom.guid(this);
+ },
+ willDestroy: function() {
+ this.actions.close.apply(this);
+ this._listeners.remove();
+ this._lazyListeners.remove();
+ },
+
+ didInsertElement: function() {
+ this._super(...arguments);
+ if (this.loading === 'lazy') {
+ this._lazyListeners.add(
+ this.dom.isInViewport(this.dom.element(`#${this.guid}`), inViewport => {
+ set(this, 'isIntersecting', inViewport);
+ if (!this.isIntersecting) {
+ this.actions.close.bind(this)();
+ } else {
+ this.actions.open.bind(this)();
+ }
+ })
+ );
+ }
+ },
+ didReceiveAttrs: function() {
+ this._super(...arguments);
+ if (this.loading === 'eager') {
+ this._lazyListeners.remove();
+ }
+ if (this.loading === 'eager' || this.isIntersecting) {
+ this.actions.open.bind(this)();
+ }
+ },
+ actions: {
+ // keep this argumentless
+ open: function() {
+ // get a new source and replace the old one, cleaning up as we go
+ const source = replace(this, 'source', this.data.open(this.src, this), (prev, source) => {
+ // Makes sure any previous source (if different) is ALWAYS closed
+ this.data.close(prev, this);
+ });
+ const error = err => {
+ try {
+ this.onerror(err);
+ this.logger.execute(err);
+ } catch (err) {
+ this.logger.execute(err);
+ }
+ };
+ // set up the listeners (which auto cleanup on component destruction)
+ const remove = this._listeners.add(this.source, {
+ message: e => {
+ try {
+ this.onchange(e);
+ } catch (err) {
+ error(err);
+ }
+ },
+ error: e => error(e),
+ });
+ replace(this, '_remove', remove);
+ // dispatch the current data of the source if we have any
+ if (typeof source.getCurrentEvent === 'function') {
+ const currentEvent = source.getCurrentEvent();
+ if (currentEvent) {
+ schedule('afterRender', () => {
+ try {
+ this.onchange(currentEvent);
+ } catch (err) {
+ error(err);
+ }
+ });
+ }
+ }
+ },
+ // keep this argumentless
+ close: function() {
+ if (typeof this.source !== 'undefined') {
+ this.data.close(this.source, this);
+ replace(this, '_remove', undefined);
+ set(this, 'source', undefined);
+ }
+ },
+ },
+});
diff --git a/ui-v2/app/templates/components/delete-confirmation.hbs b/ui-v2/app/components/delete-confirmation/index.hbs
similarity index 100%
rename from ui-v2/app/templates/components/delete-confirmation.hbs
rename to ui-v2/app/components/delete-confirmation/index.hbs
diff --git a/ui-v2/app/components/delete-confirmation.js b/ui-v2/app/components/delete-confirmation/index.js
similarity index 100%
rename from ui-v2/app/components/delete-confirmation.js
rename to ui-v2/app/components/delete-confirmation/index.js
diff --git a/ui-v2/app/templates/components/discovery-chain.hbs b/ui-v2/app/components/discovery-chain/index.hbs
similarity index 90%
rename from ui-v2/app/templates/components/discovery-chain.hbs
rename to ui-v2/app/components/discovery-chain/index.hbs
index 9a8f4f3597..d5638d5d01 100644
--- a/ui-v2/app/templates/components/discovery-chain.hbs
+++ b/ui-v2/app/components/discovery-chain/index.hbs
@@ -4,11 +4,11 @@
{{selected.nodes}} {
opacity: 1 !important;
- background-color: {{css-var '--white'}};
- border: {{css-var '--decor-border-100'}};
- border-radius: {{css-var '--decor-radius-300'}};
- border-color: {{css-var '--gray-500'}};
- box-shadow: 0 8px 10px 0 rgba(0, 0, 0, 0.1);
+ background-color: var(--white);
+ border: var(--decor-border-100);
+ border-radius: var(--decor-radius-200);
+ border-color: var(--gray-500);
+ box-shadow: var(--decor-elevation-600);
}
{{/if}}
{{#if selected.edges }}
@@ -28,7 +28,7 @@
Documentation
{{{concat ''}}}
-
- {{modal-layer}}
\ No newline at end of file
+
\ No newline at end of file
diff --git a/ui-v2/app/components/hashicorp-consul.js b/ui-v2/app/components/hashicorp-consul/index.js
similarity index 82%
rename from ui-v2/app/components/hashicorp-consul.js
rename to ui-v2/app/components/hashicorp-consul/index.js
index f705f1db4c..72f5f9f90e 100644
--- a/ui-v2/app/components/hashicorp-consul.js
+++ b/ui-v2/app/components/hashicorp-consul/index.js
@@ -4,7 +4,9 @@ import { computed } from '@ember/object';
export default Component.extend({
dom: service('dom'),
+
didInsertElement: function() {
+ this._super(...arguments);
this.dom.root().classList.remove('template-with-vertical-menu');
},
// TODO: Right now this is the only place where we need permissions
@@ -17,6 +19,15 @@ export default Component.extend({
);
}),
actions: {
+ keypressClick: function(e) {
+ e.target.dispatchEvent(new MouseEvent('click'));
+ },
+ open: function() {
+ this.authForm.focus();
+ },
+ close: function() {
+ this.authForm.reset();
+ },
change: function(e) {
const win = this.dom.viewport();
const $root = this.dom.root();
diff --git a/ui-v2/app/components/healthcheck-info/index.hbs b/ui-v2/app/components/healthcheck-info/index.hbs
new file mode 100644
index 0000000000..5661472fc3
--- /dev/null
+++ b/ui-v2/app/components/healthcheck-info/index.hbs
@@ -0,0 +1,9 @@
+{{#if (and (lt passing 1) (lt warning 1) (lt critical 1) )}}
+ 0
+{{else}}
+
+
+
+
+
+{{/if}}
diff --git a/ui-v2/app/components/healthcheck-info.js b/ui-v2/app/components/healthcheck-info/index.js
similarity index 100%
rename from ui-v2/app/components/healthcheck-info.js
rename to ui-v2/app/components/healthcheck-info/index.js
diff --git a/ui-v2/app/templates/components/healthcheck-list.hbs b/ui-v2/app/components/healthcheck-list/index.hbs
similarity index 51%
rename from ui-v2/app/templates/components/healthcheck-list.hbs
rename to ui-v2/app/components/healthcheck-list/index.hbs
index 561b98fcff..fb07ce39ef 100644
--- a/ui-v2/app/templates/components/healthcheck-list.hbs
+++ b/ui-v2/app/components/healthcheck-list/index.hbs
@@ -1,18 +1,19 @@
-
+
{{#each (sort-by (action 'sortChecksByImportance') items) as |item| }}
{{! TODO: this component and its child should be moved to a single component }}
- {{#healthcheck-output
- data-test-node-healthcheck=item.Name
- class=item.Status
- tagName='li'
- }}
- {{#block-slot name='header'}}
+
+
diff --git a/ui-v2/app/components/healthcheck-list.js b/ui-v2/app/components/healthcheck-list/index.js
similarity index 100%
rename from ui-v2/app/components/healthcheck-list.js
rename to ui-v2/app/components/healthcheck-list/index.js
diff --git a/ui-v2/app/components/healthcheck-output/index.hbs b/ui-v2/app/components/healthcheck-output/index.hbs
new file mode 100644
index 0000000000..789e14afa7
--- /dev/null
+++ b/ui-v2/app/components/healthcheck-output/index.hbs
@@ -0,0 +1,8 @@
+{{! TODO: this component and its parent should be moved to a single component }}
+{{yield}}
+
+
+ {{yield}}
+
+ {{yield}}
+
\ No newline at end of file
diff --git a/ui-v2/app/components/healthcheck-output.js b/ui-v2/app/components/healthcheck-output/index.js
similarity index 100%
rename from ui-v2/app/components/healthcheck-output.js
rename to ui-v2/app/components/healthcheck-output/index.js
diff --git a/ui-v2/app/templates/components/healthcheck-status.hbs b/ui-v2/app/components/healthcheck-status/index.hbs
similarity index 100%
rename from ui-v2/app/templates/components/healthcheck-status.hbs
rename to ui-v2/app/components/healthcheck-status/index.hbs
diff --git a/ui-v2/app/components/healthcheck-status.js b/ui-v2/app/components/healthcheck-status/index.js
similarity index 100%
rename from ui-v2/app/components/healthcheck-status.js
rename to ui-v2/app/components/healthcheck-status/index.js
diff --git a/ui-v2/app/templates/components/healthchecked-resource.hbs b/ui-v2/app/components/healthchecked-resource/index.hbs
similarity index 83%
rename from ui-v2/app/templates/components/healthchecked-resource.hbs
rename to ui-v2/app/components/healthchecked-resource/index.hbs
index 8f7f0461a0..19d7f49091 100644
--- a/ui-v2/app/templates/components/healthchecked-resource.hbs
+++ b/ui-v2/app/components/healthchecked-resource/index.hbs
@@ -1,19 +1,19 @@
-{{#stats-card}}
- {{#block-slot name='icon'}}{{yield}}{{/block-slot}}
- {{#block-slot name='mini-stat'}}
+
+ {{yield}}
+
{{#if (eq checks.length 0)}}
{{checks.length}}
{{else if (eq checks.length healthy.length)}}
{{healthy.length}}
{{/if}}
- {{/block-slot}}
- {{#block-slot name='header'}}
+
+ {{name}}{{address}}
- {{/block-slot}}
- {{#block-slot name='body'}}
+
+
{{#if (not-eq checks.length healthy.length)}}
{{#each unhealthy as |item|}}
@@ -34,5 +34,5 @@
{{/if}}
{{/if}}
- {{/block-slot}}
-{{/stats-card}}
+
+
diff --git a/ui-v2/app/components/healthchecked-resource.js b/ui-v2/app/components/healthchecked-resource/index.js
similarity index 100%
rename from ui-v2/app/components/healthchecked-resource.js
rename to ui-v2/app/components/healthchecked-resource/index.js
diff --git a/ui-v2/app/components/intention-filter/index.hbs b/ui-v2/app/components/intention-filter/index.hbs
new file mode 100644
index 0000000000..291cd3c4ba
--- /dev/null
+++ b/ui-v2/app/components/intention-filter/index.hbs
@@ -0,0 +1,4 @@
+{{!
}}
\ No newline at end of file
diff --git a/ui-v2/app/components/intention-filter.js b/ui-v2/app/components/intention-filter/index.js
similarity index 100%
rename from ui-v2/app/components/intention-filter.js
rename to ui-v2/app/components/intention-filter/index.js
diff --git a/ui-v2/app/components/jwt-source/README.mdx b/ui-v2/app/components/jwt-source/README.mdx
new file mode 100644
index 0000000000..51b1a12f22
--- /dev/null
+++ b/ui-v2/app/components/jwt-source/README.mdx
@@ -0,0 +1,24 @@
+## JwtSource
+
+```handlebars
+
+```
+
+### Arguments
+
+| Argument | Type | Default | Description |
+| --- | --- | --- | --- |
+| `src` | `String` | | The source to subscribe to updates to, this should map to a string based URI |
+| `onchange` | `Function` | | The action to fire when the data changes. Emits an Event-like object with a `data` property containing the jwt data, in this case the autorizationCode and the status |
+| `onerror` | `Function` | | The action to fire when an error occurs. Emits ErrorEvent object with an `error` property containing the Error. |
+
+This component will go through the steps of requesting a JWT token from a third party oauth provider. `src` should contain the full URL of the authorization URL for the 3rd party provider. Once the user has logged into the 3rd party provider the `onchange` event will be fired containing an event-like object whose data contains the JWT information.
+
+The component need only be place into the DOM in order to begin the OAuth dance.
+
+### See
+
+- [Component Source Code](./index.js)
+- [Template Source Code](./index.hbs)
+
+---
diff --git a/ui-v2/app/components/jwt-source/index.js b/ui-v2/app/components/jwt-source/index.js
new file mode 100644
index 0000000000..da84dc672c
--- /dev/null
+++ b/ui-v2/app/components/jwt-source/index.js
@@ -0,0 +1,31 @@
+import Component from '@ember/component';
+import { inject as service } from '@ember/service';
+import { fromPromise } from 'consul-ui/utils/dom/event-source';
+
+export default Component.extend({
+ repo: service('repository/oidc-provider'),
+ dom: service('dom'),
+ tagName: '',
+ onchange: function(e) {},
+ onerror: function(e) {},
+ init: function() {
+ this._super(...arguments);
+ this._listeners = this.dom.listeners();
+ },
+ willDestroy: function() {
+ this._super(...arguments);
+ this.repo.close();
+ this._listeners.remove();
+ },
+ didInsertElement: function() {
+ if (this.source) {
+ this.source.close();
+ }
+ // TODO: Could this use once? Double check but I don't think it can
+ this.source = fromPromise(this.repo.findCodeByURL(this.src));
+ this._listeners.add(this.source, {
+ message: e => this.onchange(e),
+ error: e => this.onerror(e),
+ });
+ },
+});
diff --git a/ui-v2/app/components/list-collection/index.hbs b/ui-v2/app/components/list-collection/index.hbs
new file mode 100644
index 0000000000..9e077dd6ee
--- /dev/null
+++ b/ui-v2/app/components/list-collection/index.hbs
@@ -0,0 +1,6 @@
+
+
+ {{~#each _cells as |cell|~}}
+
{{yield cell.item cell.index }}
+ {{~/each~}}
+
\ No newline at end of file
diff --git a/ui-v2/app/components/list-collection/index.js b/ui-v2/app/components/list-collection/index.js
new file mode 100644
index 0000000000..23c3cc8394
--- /dev/null
+++ b/ui-v2/app/components/list-collection/index.js
@@ -0,0 +1,53 @@
+import { inject as service } from '@ember/service';
+import { computed, get } from '@ember/object';
+import Component from 'ember-collection/components/ember-collection';
+import PercentageColumns from 'ember-collection/layouts/percentage-columns';
+import style from 'ember-computed-style';
+import WithResizing from 'consul-ui/mixins/with-resizing';
+
+export default Component.extend(WithResizing, {
+ dom: service('dom'),
+ tagName: 'div',
+ attributeBindings: ['style'],
+ height: 500,
+ style: style('getStyle'),
+ classNames: ['list-collection'],
+ init: function() {
+ this._super(...arguments);
+ this.columns = [100];
+ },
+ didReceiveAttrs: function() {
+ this._super(...arguments);
+ this._cellLayout = this['cell-layout'] = new PercentageColumns(
+ get(this, 'items.length'),
+ get(this, 'columns'),
+ get(this, 'cellHeight')
+ );
+ },
+ getStyle: computed('height', function() {
+ return {
+ height: get(this, 'height'),
+ };
+ }),
+ resize: function(e) {
+ // TODO: This top part is very similar to resize in tabular-collection
+ // see if it make sense to DRY out
+ const dom = get(this, 'dom');
+ const $appContent = dom.element('main > div');
+ if ($appContent) {
+ const border = 1;
+ const rect = this.element.getBoundingClientRect();
+ const $footer = dom.element('footer[role="contentinfo"]');
+ const space = rect.top + $footer.clientHeight + border;
+ const height = e.detail.height - space;
+ this.set('height', Math.max(0, height));
+ this.updateItems();
+ this.updateScrollPosition();
+ }
+ },
+ actions: {
+ click: function(e) {
+ return this.dom.clickFirstAnchor(e, '.list-collection > ul > li');
+ },
+ },
+});
diff --git a/ui-v2/app/templates/components/modal-dialog.hbs b/ui-v2/app/components/modal-dialog/index.hbs
similarity index 62%
rename from ui-v2/app/templates/components/modal-dialog.hbs
rename to ui-v2/app/components/modal-dialog/index.hbs
index 59e1e284a7..97e12e5bf3 100644
--- a/ui-v2/app/templates/components/modal-dialog.hbs
+++ b/ui-v2/app/components/modal-dialog/index.hbs
@@ -6,13 +6,13 @@
+
\ No newline at end of file
diff --git a/ui-v2/app/components/popover-menu.js b/ui-v2/app/components/popover-menu/index.js
similarity index 88%
rename from ui-v2/app/components/popover-menu.js
rename to ui-v2/app/components/popover-menu/index.js
index 55c1ba5262..3079f9eb30 100644
--- a/ui-v2/app/components/popover-menu.js
+++ b/ui-v2/app/components/popover-menu/index.js
@@ -9,6 +9,9 @@ export default Component.extend(Slotted, {
expanded: false,
keyboardAccess: true,
onchange: function() {},
+ // TODO: this needs to be made dynamic/auto detect
+ // for now use this to set left/right explicitly
+ position: '',
init: function() {
this._super(...arguments);
this.guid = this.dom.guid(this);
diff --git a/ui-v2/app/components/popover-select/index.hbs b/ui-v2/app/components/popover-select/index.hbs
new file mode 100644
index 0000000000..364505d6dc
--- /dev/null
+++ b/ui-v2/app/components/popover-select/index.hbs
@@ -0,0 +1,19 @@
+
+
+
+
+ {{selected.value}}
+
+
+
+
+ {{title}}
+
+ {{#each options as |option|}}
+
+
+
+ {{/each}}
+
+
+
diff --git a/ui-v2/app/components/popover-select/index.js b/ui-v2/app/components/popover-select/index.js
new file mode 100644
index 0000000000..2efe0e9296
--- /dev/null
+++ b/ui-v2/app/components/popover-select/index.js
@@ -0,0 +1,19 @@
+import Component from '@ember/component';
+
+export default Component.extend({
+ actions: {
+ change: function(option, e) {
+ // We fake an event here, which could be a bit of a footbun if we treat
+ // it completely like an event, we should be abe to avoid doing this
+ // when we move to glimmer components (this.args.selected vs this.selected)
+ this.onchange({
+ target: {
+ selected: option,
+ },
+ // make this vaguely event like to avoid
+ // having a separate property
+ preventDefault: function(e) {},
+ });
+ },
+ },
+});
diff --git a/ui-v2/app/templates/components/radio-group.hbs b/ui-v2/app/components/radio-group/index.hbs
similarity index 100%
rename from ui-v2/app/templates/components/radio-group.hbs
rename to ui-v2/app/components/radio-group/index.hbs
diff --git a/ui-v2/app/components/radio-group.js b/ui-v2/app/components/radio-group/index.js
similarity index 100%
rename from ui-v2/app/components/radio-group.js
rename to ui-v2/app/components/radio-group/index.js
diff --git a/ui-v2/app/components/ref/README.mdx b/ui-v2/app/components/ref/README.mdx
new file mode 100644
index 0000000000..5a810da949
--- /dev/null
+++ b/ui-v2/app/components/ref/README.mdx
@@ -0,0 +1,52 @@
+## Ref
+
+``
+
+| Argument | Type | Default | Description |
+| --- | --- | --- | --- |
+| `target` | `Object` | | The object to assign the property/value to |
+| `name` | `String` | | The property name |
+| `value` | `Object` | | The value |
+
+`` allows component users use an author defined public API of a component. The component is renderless in that it yields nothing to the DOM.
+
+The component takes a property name and value and sets it on the specified target, similar to the `{{ref this "name"}}` modifier.
+
+Occasionally it's necessary call actions belonging to a component from outside the component. For example, you may have a form that needs submitting by clicking a button in another area of the
+page. In order to do this, the button needs access to the `submit` action of the form component.
+
+This can be thought of as providing the public API for the component, the author of the component has control over what the user of the component can and can't call in this way.
+
+### Example
+
+Here we provide a public API for a form component whilst authoring.
+
+```handlebars
+{{! /components/form/index.hbs }}
+
+```
+
+The user of the component now has access to the public API of the ember/glimmer `
+...
+
+```
+
+### See
+
+- [Component Source Code](./index.js)
+
+---
diff --git a/ui-v2/app/components/ref/index.js b/ui-v2/app/components/ref/index.js
new file mode 100644
index 0000000000..e96f699b69
--- /dev/null
+++ b/ui-v2/app/components/ref/index.js
@@ -0,0 +1,9 @@
+import Component from '@ember/component';
+import { set } from '@ember/object';
+
+export default Component.extend({
+ tagName: '',
+ didReceiveAttrs: function() {
+ set(this.target, this.name, this.value);
+ },
+});
diff --git a/ui-v2/app/templates/components/resolver-card.hbs b/ui-v2/app/components/resolver-card/index.hbs
similarity index 100%
rename from ui-v2/app/templates/components/resolver-card.hbs
rename to ui-v2/app/components/resolver-card/index.hbs
diff --git a/ui-v2/app/components/resolver-card/index.js b/ui-v2/app/components/resolver-card/index.js
new file mode 100644
index 0000000000..4798652642
--- /dev/null
+++ b/ui-v2/app/components/resolver-card/index.js
@@ -0,0 +1,5 @@
+import Component from '@ember/component';
+
+export default Component.extend({
+ tagName: '',
+});
diff --git a/ui-v2/app/templates/components/role-form.hbs b/ui-v2/app/components/role-form/index.hbs
similarity index 88%
rename from ui-v2/app/templates/components/role-form.hbs
rename to ui-v2/app/components/role-form/index.hbs
index aade000271..460c6d6904 100644
--- a/ui-v2/app/templates/components/role-form.hbs
+++ b/ui-v2/app/components/role-form/index.hbs
@@ -1,5 +1,5 @@
{{yield}}
-
- {{/block-slot}}
- {{/popover-menu}}
- {{/block-slot}}
- {{/tabular-collection}}
- {{/block-slot}}
-{{/child-selector}}
+
+
+
+
+
+
diff --git a/ui-v2/app/components/role-selector.js b/ui-v2/app/components/role-selector/index.js
similarity index 95%
rename from ui-v2/app/components/role-selector.js
rename to ui-v2/app/components/role-selector/index.js
index 574e6ec8ad..05a0244533 100644
--- a/ui-v2/app/components/role-selector.js
+++ b/ui-v2/app/components/role-selector/index.js
@@ -1,4 +1,4 @@
-import ChildSelectorComponent from './child-selector';
+import ChildSelectorComponent from '../child-selector/index';
import { inject as service } from '@ember/service';
import { set } from '@ember/object';
import { alias } from '@ember/object/computed';
diff --git a/ui-v2/app/templates/components/route-card.hbs b/ui-v2/app/components/route-card/index.hbs
similarity index 100%
rename from ui-v2/app/templates/components/route-card.hbs
rename to ui-v2/app/components/route-card/index.hbs
diff --git a/ui-v2/app/components/route-card.js b/ui-v2/app/components/route-card/index.js
similarity index 100%
rename from ui-v2/app/components/route-card.js
rename to ui-v2/app/components/route-card/index.js
diff --git a/ui-v2/app/templates/components/secret-button.hbs b/ui-v2/app/components/secret-button/index.hbs
similarity index 100%
rename from ui-v2/app/templates/components/secret-button.hbs
rename to ui-v2/app/components/secret-button/index.hbs
diff --git a/ui-v2/app/components/copy-button-feedback.js b/ui-v2/app/components/secret-button/index.js
similarity index 100%
rename from ui-v2/app/components/copy-button-feedback.js
rename to ui-v2/app/components/secret-button/index.js
diff --git a/ui-v2/app/templates/components/service-identity.hbs b/ui-v2/app/components/service-identity/index.hbs
similarity index 100%
rename from ui-v2/app/templates/components/service-identity.hbs
rename to ui-v2/app/components/service-identity/index.hbs
diff --git a/ui-v2/app/components/service-identity/index.js b/ui-v2/app/components/service-identity/index.js
new file mode 100644
index 0000000000..4798652642
--- /dev/null
+++ b/ui-v2/app/components/service-identity/index.js
@@ -0,0 +1,5 @@
+import Component from '@ember/component';
+
+export default Component.extend({
+ tagName: '',
+});
diff --git a/ui-v2/app/templates/components/sort-control.hbs b/ui-v2/app/components/sort-control/index.hbs
similarity index 100%
rename from ui-v2/app/templates/components/sort-control.hbs
rename to ui-v2/app/components/sort-control/index.hbs
diff --git a/ui-v2/app/components/sort-control.js b/ui-v2/app/components/sort-control/index.js
similarity index 100%
rename from ui-v2/app/components/sort-control.js
rename to ui-v2/app/components/sort-control/index.js
diff --git a/ui-v2/app/components/splitter-card.js b/ui-v2/app/components/splitter-card.js
deleted file mode 100644
index 5570647734..0000000000
--- a/ui-v2/app/components/splitter-card.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import Component from '@ember/component';
-
-export default Component.extend({});
diff --git a/ui-v2/app/templates/components/splitter-card.hbs b/ui-v2/app/components/splitter-card/index.hbs
similarity index 100%
rename from ui-v2/app/templates/components/splitter-card.hbs
rename to ui-v2/app/components/splitter-card/index.hbs
diff --git a/ui-v2/app/components/secret-button.js b/ui-v2/app/components/splitter-card/index.js
similarity index 100%
rename from ui-v2/app/components/secret-button.js
rename to ui-v2/app/components/splitter-card/index.js
diff --git a/ui-v2/app/components/state-chart/README.mdx b/ui-v2/app/components/state-chart/README.mdx
new file mode 100644
index 0000000000..b11c56a4b8
--- /dev/null
+++ b/ui-v2/app/components/state-chart/README.mdx
@@ -0,0 +1,57 @@
+## StateChart
+
+```handlebars
+
+
+```
+
+`` is a renderless component that eases rendering of different states
+from within templates using XState State Machine and Statechart objects.
+
+### Arguments
+
+| Argument/Attribute | Type | Default | Description |
+| --- | --- | --- | --- |
+| `chart` | `object` | | An xstate statechart/state machine object |
+| `initial` | `String` | The initial value of the state chart itself | The initial state of the machine/chart (defaults to whatever is defined on the object itself) |
+
+The component currently yields 3 conextual components:
+
+- ``: Used for rendering matching certain states ([also see State Component](../state/README.mdx))
+- ``: Used to wire together ember actions to xstate actions.
+- ``: Used to wire together ember actions or props to xstate guards.
+
+and 2 further objects:
+
+- `dispatch`: An action to dispatch an xstate event
+- `state`: The state object itself for usage in the `state-matches` helper
+
+### Example
+
+```handlebars
+
+
+
+
+ Currently Idle
+
+
+ Currently Loading
+
+
+ Idle and loading
+ Load
+
+
+```
+
+### See
+
+- [Component Source Code](./index.js)
+- [Template Source Code](./index.hbs)
+
+---
diff --git a/ui-v2/app/templates/components/templated-anchor.hbs b/ui-v2/app/components/state-chart/action/index.hbs
similarity index 100%
rename from ui-v2/app/templates/components/templated-anchor.hbs
rename to ui-v2/app/components/state-chart/action/index.hbs
diff --git a/ui-v2/app/components/state-chart/action/index.js b/ui-v2/app/components/state-chart/action/index.js
new file mode 100644
index 0000000000..2e22f6f048
--- /dev/null
+++ b/ui-v2/app/components/state-chart/action/index.js
@@ -0,0 +1,13 @@
+import Component from '@ember/component';
+
+export default Component.extend({
+ tagName: '',
+ didInsertElement: function() {
+ this._super(...arguments);
+ this.chart.addAction(this.name, (context, event) => this.exec(context, event));
+ },
+ willDestroy: function() {
+ this._super(...arguments);
+ this.chart.removeAction(this.type);
+ },
+});
diff --git a/ui-v2/app/components/state-chart/guard/index.hbs b/ui-v2/app/components/state-chart/guard/index.hbs
new file mode 100644
index 0000000000..fb5c4b157d
--- /dev/null
+++ b/ui-v2/app/components/state-chart/guard/index.hbs
@@ -0,0 +1 @@
+{{yield}}
\ No newline at end of file
diff --git a/ui-v2/app/components/state-chart/guard/index.js b/ui-v2/app/components/state-chart/guard/index.js
new file mode 100644
index 0000000000..1eb060f5bf
--- /dev/null
+++ b/ui-v2/app/components/state-chart/guard/index.js
@@ -0,0 +1,20 @@
+import Component from '@ember/component';
+
+export default Component.extend({
+ tagName: '',
+ didInsertElement: function() {
+ this._super(...arguments);
+ const component = this;
+ this.chart.addGuard(this.name, function() {
+ if (typeof component.cond === 'function') {
+ return component.cond(...arguments);
+ } else {
+ return component.cond;
+ }
+ });
+ },
+ willDestroy: function() {
+ this._super(...arguments);
+ this.chart.removeGuard(this.name);
+ },
+});
diff --git a/ui-v2/app/components/state-chart/index.hbs b/ui-v2/app/components/state-chart/index.hbs
new file mode 100644
index 0000000000..2095cac618
--- /dev/null
+++ b/ui-v2/app/components/state-chart/index.hbs
@@ -0,0 +1,7 @@
+{{yield
+ (component 'state' state=state)
+ (component 'state-chart/guard' chart=this)
+ (component 'state-chart/action' chart=this)
+ (action 'dispatch')
+ state
+}}
\ No newline at end of file
diff --git a/ui-v2/app/components/state-chart/index.js b/ui-v2/app/components/state-chart/index.js
new file mode 100644
index 0000000000..710202e27f
--- /dev/null
+++ b/ui-v2/app/components/state-chart/index.js
@@ -0,0 +1,74 @@
+import Component from '@ember/component';
+import { inject as service } from '@ember/service';
+import { set } from '@ember/object';
+
+export default Component.extend({
+ chart: service('state'),
+ tagName: '',
+ ontransition: function(e) {},
+ init: function() {
+ this._super(...arguments);
+ this._actions = {};
+ this._guards = {};
+ },
+ didReceiveAttrs: function() {
+ if (typeof this.machine !== 'undefined') {
+ this.machine.stop();
+ }
+ if (typeof this.initial !== 'undefined') {
+ this.src.initial = this.initial;
+ }
+ this.machine = this.chart.interpret(this.src, {
+ onTransition: state => {
+ const e = new CustomEvent('transition', { detail: state });
+ this.ontransition(e);
+ if (!e.defaultPrevented) {
+ state.actions.forEach(item => {
+ const action = this._actions[item.type];
+ if (typeof action === 'function') {
+ this._actions[item.type](item.type, state.context, state.event);
+ }
+ });
+ }
+ set(this, 'state', state);
+ },
+ onGuard: (name, ...rest) => {
+ return this._guards[name](...rest);
+ },
+ });
+ },
+ didInsertElement: function() {
+ this._super(...arguments);
+ // xstate has initialState xstate/fsm has state
+ set(this, 'state', this.machine.initialState || this.machine.state);
+ // set(this, 'state', this.machine.initialState);
+ this.machine.start();
+ },
+ willDestroy: function() {
+ this._super(...arguments);
+ this.machine.stop();
+ },
+ addAction: function(name, value) {
+ this._actions[name] = value;
+ },
+ removeAction: function(name) {
+ delete this._actions[name];
+ },
+ addGuard: function(name, value) {
+ this._guards[name] = value;
+ },
+ removeGuard: function(name) {
+ delete this._guards[name];
+ },
+ dispatch: function(eventName, payload) {
+ this.machine.send(eventName, payload);
+ },
+ actions: {
+ dispatch: function(eventName, e) {
+ if (e && e.preventDefault) {
+ e.preventDefault();
+ }
+ this.dispatch(eventName);
+ },
+ },
+});
diff --git a/ui-v2/app/components/state/README.mdx b/ui-v2/app/components/state/README.mdx
new file mode 100644
index 0000000000..8a59e4df11
--- /dev/null
+++ b/ui-v2/app/components/state/README.mdx
@@ -0,0 +1,38 @@
+## State
+
+`Currently Idle`
+
+`` is a renderless component that eases rendering of different states
+from within templates. State objects could be manually made state objects and
+xstate state objects. It's very similar to a normal conditional in that if the
+state identifier matches the current state, the contents of the component will
+be shown.
+
+### Arguments
+
+| Argument/Attribute | Type | Default | Description |
+| --- | --- | --- | --- |
+| `state` | `object` | | An object that implements a `match` method |
+| `matches` | `String\|Array` | | A state identifier (or array of state identifiers) to match on |
+
+
+### Example
+
+```handlebars
+
+ Currently Idle
+
+
+ Currently Loading
+
+
+ Idle and loading
+
+```
+
+### See
+
+- [Component Source Code](./index.js)
+- [Template Source Code](./index.hbs)
+
+---
diff --git a/ui-v2/app/components/state/index.hbs b/ui-v2/app/components/state/index.hbs
new file mode 100644
index 0000000000..a848649944
--- /dev/null
+++ b/ui-v2/app/components/state/index.hbs
@@ -0,0 +1,3 @@
+{{#if rendering}}
+ {{yield}}
+{{/if}}
\ No newline at end of file
diff --git a/ui-v2/app/components/state/index.js b/ui-v2/app/components/state/index.js
new file mode 100644
index 0000000000..117863d93d
--- /dev/null
+++ b/ui-v2/app/components/state/index.js
@@ -0,0 +1,18 @@
+import Component from '@ember/component';
+import { set } from '@ember/object';
+import { inject as service } from '@ember/service';
+
+export default Component.extend({
+ service: service('state'),
+ tagName: '',
+ didReceiveAttrs: function() {
+ if (typeof this.state === 'undefined') {
+ return;
+ }
+ let match = true;
+ if (typeof this.matches !== 'undefined') {
+ match = this.service.matches(this.state, this.matches);
+ }
+ set(this, 'rendering', match);
+ },
+});
diff --git a/ui-v2/app/components/stats-card/index.hbs b/ui-v2/app/components/stats-card/index.hbs
new file mode 100644
index 0000000000..d04776dd36
--- /dev/null
+++ b/ui-v2/app/components/stats-card/index.hbs
@@ -0,0 +1,9 @@
+{{yield}}
+
+{{~/each~}}
+
\ No newline at end of file
diff --git a/ui-v2/app/components/tabular-collection.js b/ui-v2/app/components/tabular-collection/index.js
similarity index 100%
rename from ui-v2/app/components/tabular-collection.js
rename to ui-v2/app/components/tabular-collection/index.js
diff --git a/ui-v2/app/templates/components/tabular-details.hbs b/ui-v2/app/components/tabular-details/index.hbs
similarity index 80%
rename from ui-v2/app/templates/components/tabular-details.hbs
rename to ui-v2/app/components/tabular-details/index.hbs
index be8793f316..90aabf96b8 100644
--- a/ui-v2/app/templates/components/tabular-details.hbs
+++ b/ui-v2/app/components/tabular-details/index.hbs
@@ -2,14 +2,14 @@
diff --git a/ui-v2/app/components/tabular-details.js b/ui-v2/app/components/tabular-details/index.js
similarity index 100%
rename from ui-v2/app/components/tabular-details.js
rename to ui-v2/app/components/tabular-details/index.js
diff --git a/ui-v2/app/components/tag-list/index.hbs b/ui-v2/app/components/tag-list/index.hbs
new file mode 100644
index 0000000000..ead1355091
--- /dev/null
+++ b/ui-v2/app/components/tag-list/index.hbs
@@ -0,0 +1,16 @@
+{{#if (gt item.Tags.length 0)}}
+ {{#if (has-block)}}
+ {{yield
+ (component 'tag-list' item=item)
+ }}
+ {{else}}
+
+
Tags
+
+ {{#each item.Tags as |item|}}
+ {{item}}
+ {{/each}}
+
+
+ {{/if}}
+{{/if}}
diff --git a/ui-v2/app/components/tag-list/index.js b/ui-v2/app/components/tag-list/index.js
new file mode 100644
index 0000000000..4798652642
--- /dev/null
+++ b/ui-v2/app/components/tag-list/index.js
@@ -0,0 +1,5 @@
+import Component from '@ember/component';
+
+export default Component.extend({
+ tagName: '',
+});
diff --git a/ui-v2/app/components/templated-anchor/index.hbs b/ui-v2/app/components/templated-anchor/index.hbs
new file mode 100644
index 0000000000..fb5c4b157d
--- /dev/null
+++ b/ui-v2/app/components/templated-anchor/index.hbs
@@ -0,0 +1 @@
+{{yield}}
\ No newline at end of file
diff --git a/ui-v2/app/components/templated-anchor.js b/ui-v2/app/components/templated-anchor/index.js
similarity index 100%
rename from ui-v2/app/components/templated-anchor.js
rename to ui-v2/app/components/templated-anchor/index.js
diff --git a/ui-v2/app/components/toggle-button.js b/ui-v2/app/components/toggle-button.js
deleted file mode 100644
index 9ead016799..0000000000
--- a/ui-v2/app/components/toggle-button.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import Component from '@ember/component';
-import { inject as service } from '@ember/service';
-import { set } from '@ember/object';
-
-export default Component.extend({
- dom: service('dom'),
- // TODO(octane): Remove when we can move to glimmer components
- // so we aren't using ember-test-selectors
- // supportsDataTestProperties: true,
- // the above doesn't seem to do anything so still need to find a way
- // to pass through data-test-properties
- tagName: '',
- // TODO: reserved for the moment but we don't need it yet
- onblur: null,
- checked: false,
- onchange: function() {},
- init: function() {
- this._super(...arguments);
- this.guid = this.dom.guid(this);
- this._listeners = this.dom.listeners();
- },
- didInsertElement: function() {
- this._super(...arguments);
- // TODO(octane): move to ref
- set(this, 'input', this.dom.element(`#toggle-button-${this.guid}`));
- set(this, 'label', this.input.nextElementSibling);
- },
- willDestroyElement: function() {
- this._super(...arguments);
- this._listeners.remove();
- },
- actions: {
- click: function(e) {
- e.preventDefault();
- this.input.checked = !this.input.checked;
- // manually dispatched mouse events have a detail = 0
- // real mouse events have the number of click counts
- if (e.detail !== 0) {
- e.target.blur();
- }
- this.actions.change.apply(this, [e]);
- },
- change: function(e) {
- if (this.input.checked) {
- // default onblur event
- this._listeners.remove();
- this._listeners.add(this.dom.document(), 'click', e => {
- if (this.dom.isOutside(this.label, e.target)) {
- if (this.dom.isOutside(this.label.nextElementSibling, e.target)) {
- if (this.input.checked) {
- this.input.checked = false;
- // TODO: This should be an event
- this.onchange({ target: this.input });
- }
- this._listeners.remove();
- }
- }
- });
- }
- // TODO: This should be an event
- this.onchange({ target: this.input });
- },
- },
-});
diff --git a/ui-v2/app/components/toggle-button/README.mdx b/ui-v2/app/components/toggle-button/README.mdx
new file mode 100644
index 0000000000..99300e5152
--- /dev/null
+++ b/ui-v2/app/components/toggle-button/README.mdx
@@ -0,0 +1,54 @@
+## ToggleButton
+
+`Toggle`
+
+`` is a straightforward combination of a `` and `` to allow you to easily setup CSS based (`input:checked ~ *`) visual toggling. The body of the component ends up inside the ``.
+
+Additionally, a `clickoutside` is currently included, so if the toggle is in an 'on' state, clicking outside the `` itself will un-toggle the component. This could be changed in a future version for this to be configurable/preventable and/or use/rely on a modifer instead.
+
+### Arguments
+
+| Argument/Attribute | Type | Default | Description |
+| --- | --- | --- | --- |
+| `checked` | `Boolean` | false | The default value of the toggle on/off (true/false) |
+| `onchange` | `Function` | | The action to fire when the data changes. Emits an Event-like object with a `target` that is a reference to the underlying standard DOM input element. |
+
+### Methods/Actions/api
+
+| Method/Action | Description |
+| --- | --- | --- | --- |
+| `click` | Fire a click event on the ToggleButton/input which reverse the state of the toggle. |
+
+### Example
+
+Here is an example of a simple CSS based dropdown menu. Note: The general sibling selector (`~`) should be used as the label/button itself is the adjacent sibling (`+`).
+
+```handlebars
+
-{{/if}}
diff --git a/ui-v2/app/templates/components/healthcheck-output.hbs b/ui-v2/app/templates/components/healthcheck-output.hbs
deleted file mode 100644
index 3a434fbd23..0000000000
--- a/ui-v2/app/templates/components/healthcheck-output.hbs
+++ /dev/null
@@ -1,8 +0,0 @@
-{{! TODO: this component and its parent should be moved to a single component }}
-{{yield}}
-
\ No newline at end of file
diff --git a/ui-v2/app/templates/components/intention-filter.hbs b/ui-v2/app/templates/components/intention-filter.hbs
deleted file mode 100644
index 2a8c0ecd03..0000000000
--- a/ui-v2/app/templates/components/intention-filter.hbs
+++ /dev/null
@@ -1,4 +0,0 @@
-{{!}}
\ No newline at end of file
diff --git a/ui-v2/app/templates/components/list-collection.hbs b/ui-v2/app/templates/components/list-collection.hbs
deleted file mode 100644
index 9bc42e3b66..0000000000
--- a/ui-v2/app/templates/components/list-collection.hbs
+++ /dev/null
@@ -1,6 +0,0 @@
-{{#ember-native-scrollable tagName='ul' content-size=_contentSize scroll-left=_scrollLeft scroll-top=_scrollTop scrollChange=(action "scrollChange") clientSizeChange=(action "clientSizeChange")}}
-
- {{~#each _cells as |cell|~}}
-
{{yield cell.item cell.index }}
- {{~/each~}}
-{{/ember-native-scrollable}}
\ No newline at end of file
diff --git a/ui-v2/app/templates/components/phrase-editor.hbs b/ui-v2/app/templates/components/phrase-editor.hbs
deleted file mode 100644
index d0e8d5e74e..0000000000
--- a/ui-v2/app/templates/components/phrase-editor.hbs
+++ /dev/null
@@ -1,11 +0,0 @@
-
- {{#each value as |item index|}}
-
- Remove{{item}}
-
- {{/each}}
-
-
- Search
-
-
\ No newline at end of file
diff --git a/ui-v2/app/templates/components/policy-selector.hbs b/ui-v2/app/templates/components/policy-selector.hbs
deleted file mode 100644
index d5318f025e..0000000000
--- a/ui-v2/app/templates/components/policy-selector.hbs
+++ /dev/null
@@ -1,89 +0,0 @@
-{{#child-selector repo=repo dc=dc nspace=nspace type="policy" placeholder="Search for policy" items=items}}
- {{yield}}
- {{#block-slot name='label'}}
- Apply an existing policy
- {{/block-slot}}
- {{#block-slot name='create'}}
- {{#yield-slot name='trigger'}}
- {{yield}}
- {{else}}
-
- Create new policy
-
- {{!TODO: potentially call trigger something else}}
- {{!the modal has to go here so that if you provide a slot to trigger it doesn't get rendered}}
- {{#modal-dialog data-test-policy-form onopen=(action 'open') name="new-policy-toggle"}}
- {{#block-slot name='header'}}
-
- {{#confirmation-dialog message='Are you sure you want to remove this policy from this token?'}}
- {{#block-slot name='action' as |confirm|}}
- Remove
- {{/block-slot}}
- {{#block-slot name='dialog' as |execute cancel message|}}
-
- The Access Control List (ACL) system, which controls permissions for this UI, is enabled for this cluster. Your token ID will be saved locally and persist through visits.
-
-
+
+
+
You are not authorized
+
+
+
Error 403
+
+
+
+ You must be granted permissions to view this data. Login or ask your administrator if you think you should have access.
+
- {{/block-slot}}
- {{#block-slot name='actions'}}
+
+
{{#if (not create) }}
- {{#feedback-dialog type='inline'}}
- {{#block-slot name='action' as |success error|}}
- {{#copy-button success=(action success) error=(action error) clipboardText=item.ID title='copy token ID to clipboard'}}
- Copy token ID
- {{/copy-button}}
- {{/block-slot}}
- {{#block-slot name='success' as |transition|}}
-
- Copied token ID!
-
- {{/block-slot}}
- {{#block-slot name='error' as |transition|}}
-
- Sorry, something went wrong!
-
- {{/block-slot}}
- {{/feedback-dialog}}
+
+ Copy token ID
+ Clone token
- {{#confirmation-dialog message='Are you sure you want to use this ACL token?'}}
- {{#block-slot name='action' as |confirm|}}
+
+ Use token
- {{/block-slot}}
- {{#block-slot name='dialog' as |execute cancel message|}}
+
+
{{message}}
Confirm UseCancel
- {{/block-slot}}
- {{/confirmation-dialog}}
+
+
{{/if}}
- {{/block-slot}}
- {{#block-slot name='content'}}
+
+
{{ partial 'dc/acls/form'}}
- {{/block-slot}}
-{{/app-view}}
\ No newline at end of file
+
+
\ No newline at end of file
diff --git a/ui-v2/app/templates/dc/acls/index.hbs b/ui-v2/app/templates/dc/acls/index.hbs
index 88c50c2e55..46a7620546 100644
--- a/ui-v2/app/templates/dc/acls/index.hbs
+++ b/ui-v2/app/templates/dc/acls/index.hbs
@@ -1,32 +1,30 @@
-{{#app-view class="acl list" loading=isLoading}}
- {{#block-slot name='notification' as |status type|}}
+
+
{{partial 'dc/acls/notifications'}}
- {{/block-slot}}
- {{#block-slot name='header'}}
+
+
- {{/block-slot}}
- {{/changeable-set}}
- {{/block-slot}}
-{{/app-view}}
\ No newline at end of file
+
+
+
+
\ No newline at end of file
diff --git a/ui-v2/app/templates/dc/acls/tokens/-fieldsets-legacy.hbs b/ui-v2/app/templates/dc/acls/tokens/-fieldsets-legacy.hbs
index 213f8d1133..a96aac9cdb 100644
--- a/ui-v2/app/templates/dc/acls/tokens/-fieldsets-legacy.hbs
+++ b/ui-v2/app/templates/dc/acls/tokens/-fieldsets-legacy.hbs
@@ -1,7 +1,7 @@
Name
- {{input value=item.Description name='name' autofocus='autofocus'}}
+
{{#if false}}
@@ -15,12 +15,12 @@
{{/if}}
Rules (HCL Format)
- {{code-editor class=(if item.error.Rules 'error') name='Rules' syntax='hcl' value=item.Rules onkeyup=(action 'change' 'Rules')}}
+
{{#if create }}
ID
- {{ input value=item.ID }}
+
We'll generate a UUID if this field is left empty.
{{/if}}
diff --git a/ui-v2/app/templates/dc/acls/tokens/-fieldsets.hbs b/ui-v2/app/templates/dc/acls/tokens/-fieldsets.hbs
index f7e50db434..048a2d3fd9 100644
--- a/ui-v2/app/templates/dc/acls/tokens/-fieldsets.hbs
+++ b/ui-v2/app/templates/dc/acls/tokens/-fieldsets.hbs
@@ -1,12 +1,11 @@
{{#if create }}
-
- Restrict this token to a local datacenter?
- Local tokens get set in the Raft store of the local DC and do not ever get transmitted to the primary DC or replicated to any other DC.
+
-
- No
+
+ Restrict this token to a local datacenter?
+ Local tokens get set in the Raft store of the local DC and do not ever get transmitted to the primary DC or replicated to any other DC.
diff --git a/ui-v2/app/templates/dc/acls/tokens/-form.hbs b/ui-v2/app/templates/dc/acls/tokens/-form.hbs
index b8847f6d01..5904fb4f82 100644
--- a/ui-v2/app/templates/dc/acls/tokens/-form.hbs
+++ b/ui-v2/app/templates/dc/acls/tokens/-form.hbs
@@ -14,14 +14,14 @@
{{/if}}
Cancel
{{# if (and (not create) (not (token/is-anonymous item)) (not-eq item.AccessorID token.AccessorID) ) }}
- {{#confirmation-dialog message='Are you sure you want to delete this Token?'}}
- {{#block-slot name='action' as |confirm|}}
+
+ Delete
- {{/block-slot}}
- {{#block-slot name='dialog' as |execute cancel message|}}
- {{delete-confirmation message=message execute=execute cancel=cancel}}
- {{/block-slot}}
- {{/confirmation-dialog}}
+
+
+
+
+
{{/if}}
diff --git a/ui-v2/app/templates/dc/acls/tokens/-notifications.hbs b/ui-v2/app/templates/dc/acls/tokens/-notifications.hbs
index 5c98ecf4bf..c5770bf1e3 100644
--- a/ui-v2/app/templates/dc/acls/tokens/-notifications.hbs
+++ b/ui-v2/app/templates/dc/acls/tokens/-notifications.hbs
@@ -16,24 +16,12 @@
{{else}}
There was an error deleting the token.
{{/if}}
-{{ else if (eq type 'logout')}}
- {{#if (eq status 'success') }}
- You are now logged out.
- {{else}}
- There was an error logging out.
- {{/if}}
{{ else if (eq type 'clone')}}
{{#if (eq status 'success') }}
The token has been cloned as {{truncate subject.AccessorID 8 false}}
{{else}}
There was an error cloning the token.
{{/if}}
-{{ else if (eq type 'authorize')}}
- {{#if (eq status 'success') }}
- You are now logged in.
- {{else}}
- There was an error, please check your SecretID/Token
- {{/if}}
{{ else if (eq type 'use')}}
{{#if (eq status 'success') }}
You are now using the new ACL token
diff --git a/ui-v2/app/templates/dc/acls/tokens/edit.hbs b/ui-v2/app/templates/dc/acls/tokens/edit.hbs
index ea4d80b26b..86cd683681 100644
--- a/ui-v2/app/templates/dc/acls/tokens/edit.hbs
+++ b/ui-v2/app/templates/dc/acls/tokens/edit.hbs
@@ -7,22 +7,27 @@
{{else}}
{{title 'Access Controls'}}
{{/if}}
-{{#app-view class=(concat 'token ' (if (or isAuthorized isEnabled) 'edit' 'list')) loading=isLoading authorized=isAuthorized enabled=isEnabled}}
- {{#block-slot name='notification' as |status type|}}
+
+
{{partial 'dc/acls/tokens/notifications'}}
- {{/block-slot}}
- {{#block-slot name='disabled'}}
+
+
{{partial 'dc/acls/disabled'}}
- {{/block-slot}}
- {{#block-slot name='authorization'}}
+
+
{{partial 'dc/acls/authorization'}}
- {{/block-slot}}
- {{#block-slot name='breadcrumbs'}}
+
+
Update. We have upgraded our ACL system by allowing you to create reusable policies which you can then apply to tokens. Don't worry, even though this token was written in the old style, it is still valid. However, we do recommend upgrading your old tokens to the new style. Learn how in our documentation.
{{/if}}
@@ -65,11 +70,11 @@
AccessorID
- {{copy-button-feedback title="Copy AccessorID to the clipboard" copy=item.AccessorID name="AccessorID"}} {{item.AccessorID}}
+ {{item.AccessorID}}
Token
- {{copy-button-feedback title="Copy SecretID to the clipboard" copy=item.SecretID name="Token"}} {{#secret-button}}{{item.SecretID}}{{/secret-button}}
+ {{item.SecretID}}
Update. We have upgraded our ACL System to allow the creation of reusable policies that can be applied to tokens. Read more about the changes and how to upgrade legacy tokens in our documentation.
- {{/block-slot}}
- {{/changeable-set}}
- {{/block-slot}}
-{{/app-view}}
\ No newline at end of file
+
+
+
+
\ No newline at end of file
diff --git a/ui-v2/app/templates/dc/kv/-form.hbs b/ui-v2/app/templates/dc/kv/-form.hbs
index e23f2db46e..e92f88cb45 100644
--- a/ui-v2/app/templates/dc/kv/-form.hbs
+++ b/ui-v2/app/templates/dc/kv/-form.hbs
@@ -15,10 +15,10 @@
Code
Warning. This KV has a lock session. You can edit KV's with lock sessions, but we recommend doing so with care, or not doing so at all. It may negatively impact the active node it's associated with. See below for more details on the Lock Session and see our documentation for more information.
@@ -64,19 +64,19 @@
{{/if}}
- {{#confirmation-dialog message='Are you sure you want to invalidate this session?'}}
- {{#block-slot name='action' as |confirm|}}
+
+ Invalidate Session
- {{/block-slot}}
- {{#block-slot name='dialog' as |execute cancel message|}}
+
+
\ No newline at end of file
diff --git a/ui-v2/app/templates/dc/nodes/show/metadata.hbs b/ui-v2/app/templates/dc/nodes/show/metadata.hbs
new file mode 100644
index 0000000000..a7c0308f34
--- /dev/null
+++ b/ui-v2/app/templates/dc/nodes/show/metadata.hbs
@@ -0,0 +1,11 @@
+
+
+{{#if item.Meta}}
+
+{{else}}
+
+ This node has no metadata.
+
+{{/if}}
+
+
diff --git a/ui-v2/app/templates/dc/nodes/show/rtt.hbs b/ui-v2/app/templates/dc/nodes/show/rtt.hbs
new file mode 100644
index 0000000000..506eb2a34d
--- /dev/null
+++ b/ui-v2/app/templates/dc/nodes/show/rtt.hbs
@@ -0,0 +1,26 @@
+
\ No newline at end of file
diff --git a/ui-v2/app/templates/dc/nodes/-sessions.hbs b/ui-v2/app/templates/dc/nodes/show/sessions.hbs
similarity index 68%
rename from ui-v2/app/templates/dc/nodes/-sessions.hbs
rename to ui-v2/app/templates/dc/nodes/show/sessions.hbs
index 8ffe6c2992..30bf919f52 100644
--- a/ui-v2/app/templates/dc/nodes/-sessions.hbs
+++ b/ui-v2/app/templates/dc/nodes/show/sessions.hbs
@@ -1,10 +1,12 @@
+
- {{#confirmation-dialog message='Are you sure you want to invalidate this session?'}}
- {{#block-slot name='action' as |confirm|}}
+
+ Invalidate
- {{/block-slot}}
- {{#block-slot name='dialog' as |execute cancel message|}}
+
+
+ The following list shows individual HTTP paths exposed through Envoy for external services like Prometheus. Read more about this in our documentation.
+
+
+
+
Path
+
Protocol
+
Listener port
+
Local path port
+
Combined addressService address, listener port, and path all combined into one URL.
+
+
+ {{#each proxy.Proxy.Expose.Paths as |path|}}
+
+ You may have visited a URL that is loading an unknown resource, so you can try going back to the root or try re-submitting your ACL Token/SecretID by going back to ACLs.
+
- Consul returned an error.
- You may have visited a URL that is loading an unknown resource, so you can try going back to the root or try re-submitting your ACL Token/SecretID by going back to ACLs.
- Try looking in our documentation
-