mirror of https://github.com/hashicorp/consul
Merge pull request #7857 from hashicorp/ui-staging-1-8
UI Release Merge (1.8: ui-staging merge)pull/7865/head
commit
6f9e511d99
|
@ -5,13 +5,13 @@ references:
|
||||||
images:
|
images:
|
||||||
go: &GOLANG_IMAGE circleci/golang:1.14.1
|
go: &GOLANG_IMAGE circleci/golang:1.14.1
|
||||||
middleman: &MIDDLEMAN_IMAGE hashicorp/middleman-hashicorp:0.3.40
|
middleman: &MIDDLEMAN_IMAGE hashicorp/middleman-hashicorp:0.3.40
|
||||||
ember: &EMBER_IMAGE circleci/node:8-browsers
|
ember: &EMBER_IMAGE circleci/node:12-browsers
|
||||||
|
|
||||||
paths:
|
paths:
|
||||||
test-results: &TEST_RESULTS_DIR /tmp/test-results
|
test-results: &TEST_RESULTS_DIR /tmp/test-results
|
||||||
|
|
||||||
cache:
|
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" }}
|
rubygem: &RUBYGEM_CACHE_KEY static-site-gems-v1-{{ checksum "Gemfile.lock" }}
|
||||||
|
|
||||||
environment: &ENVIRONMENT
|
environment: &ENVIRONMENT
|
||||||
|
@ -461,6 +461,9 @@ jobs:
|
||||||
- image: *EMBER_IMAGE
|
- image: *EMBER_IMAGE
|
||||||
environment:
|
environment:
|
||||||
EMBER_TEST_REPORT: test-results/report-oss.xml #outputs test report for CircleCI test summary
|
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:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
|
@ -469,7 +472,7 @@ jobs:
|
||||||
at: ui-v2
|
at: ui-v2
|
||||||
- run:
|
- run:
|
||||||
working_directory: ui-v2
|
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:
|
- store_test_results:
|
||||||
path: ui-v2/test-results
|
path: ui-v2/test-results
|
||||||
# run ember frontend tests
|
# run ember frontend tests
|
||||||
|
@ -478,6 +481,8 @@ jobs:
|
||||||
- image: *EMBER_IMAGE
|
- image: *EMBER_IMAGE
|
||||||
environment:
|
environment:
|
||||||
EMBER_TEST_REPORT: test-results/report-ent.xml #outputs test report for CircleCI test summary
|
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:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- restore_cache:
|
- restore_cache:
|
||||||
|
@ -486,9 +491,26 @@ jobs:
|
||||||
at: ui-v2
|
at: ui-v2
|
||||||
- run:
|
- run:
|
||||||
working_directory: ui-v2
|
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:
|
- store_test_results:
|
||||||
path: ui-v2/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:
|
envoy-integration-test-1.11.2:
|
||||||
docker:
|
docker:
|
||||||
|
@ -682,6 +704,9 @@ workflows:
|
||||||
- ember-test-ent:
|
- ember-test-ent:
|
||||||
requires:
|
requires:
|
||||||
- ember-build
|
- ember-build
|
||||||
|
- ember-coverage:
|
||||||
|
requires:
|
||||||
|
- ember-build
|
||||||
cherry-pick:
|
cherry-pick:
|
||||||
jobs:
|
jobs:
|
||||||
- cherry-picker:
|
- cherry-picker:
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
ARG ALPINE_VERSION=3.9
|
ARG ALPINE_VERSION=3.11
|
||||||
FROM alpine:${ALPINE_VERSION}
|
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 MAKE_VERSION=4.2.1-r2
|
||||||
ARG YARN_VERSION=1.19.1
|
ARG YARN_VERSION=1.19.1
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ coverage:
|
||||||
# https://docs.codecov.io/docs/commit-status#section-excluding-tests-example-
|
# https://docs.codecov.io/docs/commit-status#section-excluding-tests-example-
|
||||||
# TODO: should any paths be excluded from coverage metrics?
|
# TODO: should any paths be excluded from coverage metrics?
|
||||||
# paths:
|
# paths:
|
||||||
|
ui:
|
||||||
|
informational: true
|
||||||
# https://docs.codecov.io/docs/commit-status#section-changes-status
|
# https://docs.codecov.io/docs/commit-status#section-changes-status
|
||||||
# TODO: enable after eliminating current unexpected coverage changes?
|
# TODO: enable after eliminating current unexpected coverage changes?
|
||||||
changes: off
|
changes: off
|
||||||
|
@ -29,7 +31,9 @@ comment: false
|
||||||
|
|
||||||
# https://docs.codecov.io/docs/flags
|
# https://docs.codecov.io/docs/flags
|
||||||
# TODO: split out test coverage for API, SDK, UI, website?
|
# TODO: split out test coverage for API, SDK, UI, website?
|
||||||
# flags:
|
flags:
|
||||||
|
ui:
|
||||||
|
paths: /ui-v2/
|
||||||
|
|
||||||
ignore:
|
ignore:
|
||||||
- "agent/bindata_assetfs.go"
|
- "agent/bindata_assetfs.go"
|
||||||
|
|
|
@ -4,7 +4,6 @@
|
||||||
|
|
||||||
root = true
|
root = true
|
||||||
|
|
||||||
|
|
||||||
[*]
|
[*]
|
||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
|
|
|
@ -5,5 +5,14 @@
|
||||||
|
|
||||||
Setting `disableAnalytics` to true will prevent any data from being sent.
|
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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
root: true,
|
root: true,
|
||||||
|
parser: 'babel-eslint',
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: 2018,
|
ecmaVersion: 2018,
|
||||||
sourceType: 'module'
|
sourceType: 'module',
|
||||||
|
ecmaFeatures: {
|
||||||
|
legacyDecorators: true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
plugins: ['ember'],
|
plugins: ['ember'],
|
||||||
extends: ['eslint:recommended', 'plugin:ember/recommended'],
|
extends: ['eslint:recommended', 'plugin:ember/recommended'],
|
||||||
|
@ -11,7 +15,9 @@ module.exports = {
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
'no-unused-vars': ['error', { args: 'none' }],
|
'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: [
|
overrides: [
|
||||||
// node files
|
// node files
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
instrumentation:
|
||||||
|
excludes: [
|
||||||
|
"!app/+(utils|search)/**/*"
|
||||||
|
]
|
|
@ -1 +1 @@
|
||||||
10
|
12
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: 'recommended',
|
extends: 'octane',
|
||||||
rules: {
|
rules: {
|
||||||
'no-partial': false,
|
'no-partial': false,
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ module.exports = {
|
||||||
|
|
||||||
'self-closing-void-elements': false,
|
'self-closing-void-elements': false,
|
||||||
'no-unnecessary-concat': false,
|
'no-unnecessary-concat': false,
|
||||||
|
'no-quoteless-attributes': false,
|
||||||
'no-nested-interactive': false,
|
'no-nested-interactive': false,
|
||||||
|
|
||||||
'block-indentation': false,
|
'block-indentation': false,
|
||||||
|
@ -19,6 +20,15 @@ module.exports = {
|
||||||
'no-triple-curlies': false,
|
'no-triple-curlies': false,
|
||||||
'no-unused-block-params': false,
|
'no-unused-block-params': false,
|
||||||
'style-concatenation': 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
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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
|
|
@ -65,6 +65,15 @@ test-oss-ci: deps test-node
|
||||||
test-node:
|
test-node:
|
||||||
yarn run 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
|
test-parallel: deps
|
||||||
yarn run test:parallel
|
yarn run test:parallel
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import Adapter from './http';
|
import Adapter from './http';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import { env } from 'consul-ui/env';
|
|
||||||
|
|
||||||
export const DATACENTER_QUERY_PARAM = 'dc';
|
export const DATACENTER_QUERY_PARAM = 'dc';
|
||||||
export const NSPACE_QUERY_PARAM = 'ns';
|
export const NSPACE_QUERY_PARAM = 'ns';
|
||||||
export default Adapter.extend({
|
export default Adapter.extend({
|
||||||
repo: service('settings'),
|
|
||||||
client: service('client/http'),
|
client: service('client/http'),
|
||||||
|
env: service('env'),
|
||||||
formatNspace: function(nspace) {
|
formatNspace: function(nspace) {
|
||||||
if (env('CONSUL_NSPACES_ENABLED')) {
|
if (this.env.env('CONSUL_NSPACES_ENABLED')) {
|
||||||
return nspace !== '' ? { [NSPACE_QUERY_PARAM]: nspace } : undefined;
|
return nspace !== '' ? { [NSPACE_QUERY_PARAM]: nspace } : undefined;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -17,45 +16,9 @@ export default Adapter.extend({
|
||||||
[DATACENTER_QUERY_PARAM]: dc,
|
[DATACENTER_QUERY_PARAM]: dc,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
// TODO: kinda protected for the moment
|
// TODO: Deprecated, remove `request` usage from everywhere and replace with
|
||||||
// decide where this should go either read/write from http
|
// `HTTPAdapter.rpc`
|
||||||
// should somehow use this or vice versa
|
|
||||||
request: function(req, resp, obj, modelName) {
|
request: function(req, resp, obj, modelName) {
|
||||||
const client = this.client;
|
return this.rpc(...arguments);
|
||||||
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);
|
|
||||||
// });
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import Adapter from './application';
|
import Adapter from './application';
|
||||||
|
|
||||||
export default Adapter.extend({
|
export default Adapter.extend({
|
||||||
requestForFindAll: function(request) {
|
requestForQuery: function(request) {
|
||||||
return request`
|
return request`
|
||||||
GET /v1/catalog/datacenters
|
GET /v1/catalog/datacenters
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
import Adapter from 'ember-data/adapter';
|
import Adapter from 'ember-data/adapter';
|
||||||
import AdapterError from '@ember-data/adapter/error';
|
import AdapterError from '@ember-data/adapter/error';
|
||||||
import {
|
import {
|
||||||
|
@ -10,46 +11,75 @@ import {
|
||||||
ConflictError,
|
ConflictError,
|
||||||
InvalidError,
|
InvalidError,
|
||||||
} from 'ember-data/adapters/errors';
|
} from 'ember-data/adapters/errors';
|
||||||
// TODO: This is a little skeleton cb function
|
|
||||||
// is to be replaced soon with something slightly more involved
|
// TODO These are now exactly the same, apart from the fact that one uses
|
||||||
const responder = function(response) {
|
// `serialized, unserialized` and the other just `query`
|
||||||
return response;
|
// 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, serializer, client, type, query) {
|
const read = function(adapter, modelName, type, query = {}) {
|
||||||
return client
|
return adapter.rpc(
|
||||||
.request(function(request) {
|
function(adapter, request, query) {
|
||||||
return adapter[`requestFor${type}`](request, query);
|
return adapter[`requestFor${type}`](request, query);
|
||||||
})
|
},
|
||||||
.catch(function(e) {
|
function(serializer, respond, query) {
|
||||||
return adapter.error(e);
|
return serializer[`respondFor${type}`](respond, query);
|
||||||
})
|
},
|
||||||
.then(function(response) {
|
query,
|
||||||
return serializer[`respondFor${type}`](responder(response), query);
|
modelName
|
||||||
});
|
);
|
||||||
// TODO: Potentially add specific serializer errors here
|
|
||||||
// .catch(function(e) {
|
|
||||||
// return Promise.reject(e);
|
|
||||||
// });
|
|
||||||
};
|
};
|
||||||
const write = function(adapter, serializer, client, type, snapshot) {
|
const write = function(adapter, modelName, type, snapshot) {
|
||||||
const unserialized = snapshot.attributes();
|
return adapter.rpc(
|
||||||
const serialized = serializer.serialize(snapshot, {});
|
function(adapter, request, serialized, unserialized) {
|
||||||
return client
|
|
||||||
.request(function(request) {
|
|
||||||
return adapter[`requestFor${type}`](request, serialized, unserialized);
|
return adapter[`requestFor${type}`](request, serialized, unserialized);
|
||||||
})
|
},
|
||||||
.catch(function(e) {
|
function(serializer, respond, serialized, unserialized) {
|
||||||
return adapter.error(e);
|
return serializer[`respondFor${type}`](respond, serialized, unserialized);
|
||||||
})
|
},
|
||||||
.then(function(response) {
|
snapshot,
|
||||||
return serializer[`respondFor${type}`](responder(response), serialized, unserialized);
|
modelName
|
||||||
});
|
);
|
||||||
// TODO: Potentially add specific serializer errors here
|
|
||||||
// .catch(function(e) {
|
|
||||||
// return Promise.reject(e);
|
|
||||||
// });
|
|
||||||
};
|
};
|
||||||
export default Adapter.extend({
|
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) {
|
error: function(err) {
|
||||||
const errors = [
|
const errors = [
|
||||||
{
|
{
|
||||||
|
@ -94,24 +124,28 @@ export default Adapter.extend({
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = 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;
|
throw error;
|
||||||
},
|
},
|
||||||
query: function(store, type, query) {
|
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) {
|
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) {
|
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) {
|
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) {
|
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) {
|
deleteRecord: function(store, type, snapshot) {
|
||||||
return write(this, store.serializerFor(type.modelName), this.client, 'DeleteRecord', snapshot);
|
return write(this, type.modelName, 'DeleteRecord', snapshot);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -6,11 +6,14 @@ import { SLUG_KEY } from 'consul-ui/models/intention';
|
||||||
|
|
||||||
// TODO: Update to use this.formatDatacenter()
|
// TODO: Update to use this.formatDatacenter()
|
||||||
export default Adapter.extend({
|
export default Adapter.extend({
|
||||||
requestForQuery: function(request, { dc, index, id }) {
|
requestForQuery: function(request, { dc, filter, index }) {
|
||||||
return request`
|
return request`
|
||||||
GET /v1/connect/intentions?${{ dc }}
|
GET /v1/connect/intentions?${{ dc }}
|
||||||
|
|
||||||
${{ index }}
|
${{
|
||||||
|
index,
|
||||||
|
filter,
|
||||||
|
}}
|
||||||
`;
|
`;
|
||||||
},
|
},
|
||||||
requestForQueryRecord: function(request, { dc, index, id }) {
|
requestForQueryRecord: function(request, { dc, index, id }) {
|
||||||
|
|
|
@ -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
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
|
@ -104,6 +104,7 @@ export default Adapter.extend({
|
||||||
return request`
|
return request`
|
||||||
GET /v1/acl/token/self?${{ dc }}
|
GET /v1/acl/token/self?${{ dc }}
|
||||||
X-Consul-Token: ${secret}
|
X-Consul-Token: ${secret}
|
||||||
|
Cache-Control: no-store
|
||||||
|
|
||||||
${{ index }}
|
${{ index }}
|
||||||
`;
|
`;
|
||||||
|
@ -132,7 +133,7 @@ export default Adapter.extend({
|
||||||
return adapter.requestForSelf(request, serialized, data);
|
return adapter.requestForSelf(request, serialized, data);
|
||||||
},
|
},
|
||||||
function(serializer, respond, serialized, data) {
|
function(serializer, respond, serialized, data) {
|
||||||
return serializer.respondForQueryRecord(respond, serialized, data);
|
return serializer.respondForSelf(respond, serialized, data);
|
||||||
},
|
},
|
||||||
unserialized,
|
unserialized,
|
||||||
type.modelName
|
type.modelName
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
import Application from '@ember/application';
|
import Application from '@ember/application';
|
||||||
import Resolver from './resolver';
|
import Resolver from 'ember-resolver';
|
||||||
import loadInitializers from 'ember-load-initializers';
|
import loadInitializers from 'ember-load-initializers';
|
||||||
import config from './config/environment';
|
import config from './config/environment';
|
||||||
|
|
||||||
const App = Application.extend({
|
export default class App extends Application {
|
||||||
modulePrefix: config.modulePrefix,
|
modulePrefix = config.modulePrefix;
|
||||||
podModulePrefix: config.podModulePrefix,
|
podModulePrefix = config.podModulePrefix;
|
||||||
Resolver,
|
Resolver = Resolver;
|
||||||
});
|
}
|
||||||
|
|
||||||
loadInitializers(App, config.modulePrefix);
|
loadInitializers(App, config.modulePrefix);
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
{{!<form>}}
|
||||||
|
<FreetextFilter @searchable={{searchable}} @value={{search}} @placeholder="Search by name/token" />
|
||||||
|
<RadioGroup @keyboardAccess={{true}} @name="type" @value={{type}} @items={{filters}} @onchange={{action onchange}} />
|
||||||
|
{{!</form>}}
|
|
@ -0,0 +1,89 @@
|
||||||
|
{{yield}}
|
||||||
|
{{#if (not loading)}}
|
||||||
|
<header>
|
||||||
|
{{#each flashMessages.queue as |flash|}}
|
||||||
|
<FlashMessage @flash={{flash}} as |component flash|>
|
||||||
|
{{#let (lowercase component.flashType) (lowercase flash.action) as |status type|}}
|
||||||
|
{{! flashes automatically ucfirst the type }}
|
||||||
|
|
||||||
|
<p data-notification class={{concat status ' notification-' type}}>
|
||||||
|
<strong>
|
||||||
|
{{capitalize status}}!
|
||||||
|
</strong>
|
||||||
|
{{#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}}
|
||||||
|
</p>
|
||||||
|
{{/let}}
|
||||||
|
</FlashMessage>
|
||||||
|
{{/each}}
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
{{#if authorized}}
|
||||||
|
<nav aria-label="Breadcrumb">
|
||||||
|
<YieldSlot @name="breadcrumbs">{{yield}}</YieldSlot>
|
||||||
|
</nav>
|
||||||
|
{{/if}}
|
||||||
|
<div class="title">
|
||||||
|
<YieldSlot @name="header">
|
||||||
|
{{yield}}
|
||||||
|
</YieldSlot>
|
||||||
|
<div class="actions">
|
||||||
|
{{#if authorized}}
|
||||||
|
<YieldSlot @name="actions">{{yield}}</YieldSlot>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<YieldSlot @name="nav">
|
||||||
|
{{yield}}
|
||||||
|
</YieldSlot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{#if authorized}}
|
||||||
|
<YieldSlot @name="toolbar">
|
||||||
|
<input type="checkbox" id="toolbar-toggle" />
|
||||||
|
{{yield}}
|
||||||
|
</YieldSlot>
|
||||||
|
{{/if}}
|
||||||
|
</header>
|
||||||
|
{{/if}}
|
||||||
|
<div>
|
||||||
|
{{#if loading}}
|
||||||
|
<ConsulLoader />
|
||||||
|
{{else}}
|
||||||
|
{{#if (not enabled) }}
|
||||||
|
<YieldSlot @name="disabled">{{yield}}</YieldSlot>
|
||||||
|
{{else if (not authorized)}}
|
||||||
|
<YieldSlot @name="authorization">{{yield}}</YieldSlot>
|
||||||
|
{{else}}
|
||||||
|
<YieldSlot @name="content">{{yield}}</YieldSlot>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
|
@ -0,0 +1,56 @@
|
||||||
|
## AuthDialog
|
||||||
|
|
||||||
|
```handlebars
|
||||||
|
<AuthDialog @dc={{dc}} @nspace={{}} @onchange={{action 'change'}} as |api components|>
|
||||||
|
{{#let components.AuthForm components.AuthProfile as |AuthForm AuthProfile|}}
|
||||||
|
<BlockSlot @name="unauthorized">
|
||||||
|
Here's the login form:
|
||||||
|
<AuthForm />
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="authorized">
|
||||||
|
Here's your profile:
|
||||||
|
<AuthProfile />
|
||||||
|
<button onclick={{action api.logout}} />
|
||||||
|
</BlockSlot>
|
||||||
|
{{/let}}
|
||||||
|
</AuthDialog>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
|
||||||
|
---
|
|
@ -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: {},
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,40 @@
|
||||||
|
<StateChart @src={{chart}} as |State Guard Action dispatch state|>
|
||||||
|
|
||||||
|
<Guard @name="hasToken" @cond={{action 'hasToken'}} />
|
||||||
|
<Action @name="login" @exec={{action 'login'}} />
|
||||||
|
<Action @name="logout" @exec={{action 'logout'}} />
|
||||||
|
{{! This DataSource just permanently listens to any changes to the users }}
|
||||||
|
{{! token, whether thats a new token, a changed token or a deleted token }}
|
||||||
|
<DataSource
|
||||||
|
@src="settings://consul:token"
|
||||||
|
@onchange={{queue (action (mut token) value="data") (action dispatch "CHANGE") (action (mut previousToken) value="data")}}
|
||||||
|
/>
|
||||||
|
{{! This DataSink is just used for logging in from the form, }}
|
||||||
|
{{! or logging out via the exposed logout function }}
|
||||||
|
<DataSink
|
||||||
|
@sink="settings://consul:token"
|
||||||
|
as |sink|
|
||||||
|
>
|
||||||
|
{{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|}}
|
||||||
|
<State @matches="authorized">
|
||||||
|
{{#yield-slot name="authorized"}}
|
||||||
|
{{yield api components}}
|
||||||
|
{{/yield-slot}}
|
||||||
|
</State>
|
||||||
|
|
||||||
|
<State @matches="unauthorized">
|
||||||
|
{{#yield-slot name="unauthorized"}}
|
||||||
|
{{yield api components}}
|
||||||
|
{{/yield-slot}}
|
||||||
|
</State>
|
||||||
|
{{/let}}
|
||||||
|
</DataSink>
|
||||||
|
</StateChart>
|
|
@ -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' });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,18 @@
|
||||||
|
## AuthForm
|
||||||
|
|
||||||
|
```handlebars
|
||||||
|
<AuthForm as |api|></AuthForm>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
|
||||||
|
---
|
|
@ -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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
|
@ -0,0 +1,100 @@
|
||||||
|
<StateChart @src={{chart}} as |State Guard Action dispatch state|>
|
||||||
|
{{yield (hash
|
||||||
|
reset=(action dispatch "RESET")
|
||||||
|
focus=(action 'focus')
|
||||||
|
)}}
|
||||||
|
<Guard @name="hasValue" @cond={{action 'hasValue'}} />
|
||||||
|
{{!FIXME: Call this reset or similar }}
|
||||||
|
<Action @name="clearError" @exec={{queue (action (mut error) undefined) (action (mut secret) undefined)}} />
|
||||||
|
<div class="auth-form" ...attributes>
|
||||||
|
<State @matches="error">
|
||||||
|
{{#if error.status}}
|
||||||
|
<p role="alert" class="notice error">
|
||||||
|
{{#if value.Name}}
|
||||||
|
{{#if (eq error.status '403')}}
|
||||||
|
<strong>Consul login failed</strong><br />
|
||||||
|
We received a token from your OIDC provider but could not log in to Consul with it.
|
||||||
|
{{else if (eq error.status '401')}}
|
||||||
|
<strong>Could not log in to provider</strong><br />
|
||||||
|
The OIDC provider has rejected this access token. Please have an administrator check your auth method configuration.
|
||||||
|
{{else if (eq error.status '499')}}
|
||||||
|
<strong>SSO log in window closed</strong><br />
|
||||||
|
The OIDC provider window was closed. Please try again.
|
||||||
|
{{else}}
|
||||||
|
<strong>Error</strong><br />
|
||||||
|
{{error.detail}}
|
||||||
|
{{/if}}
|
||||||
|
{{else}}
|
||||||
|
{{#if (eq error.status '403')}}
|
||||||
|
<strong>Invalid token</strong><br />
|
||||||
|
The token entered does not exist. Please enter a valid token to log in.
|
||||||
|
{{else}}
|
||||||
|
<strong>Error</strong><br />
|
||||||
|
{{error.detail}}
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
|
</State>
|
||||||
|
<form onsubmit={{action dispatch "SUBMIT"}}>
|
||||||
|
<fieldset>
|
||||||
|
<label class={{concat "type-password" (if (and (state-matches state 'error') (not error.status)) ' has-error' '')}}>
|
||||||
|
<span>Log in with a token</span>
|
||||||
|
<input
|
||||||
|
{{ref this 'input'}}
|
||||||
|
disabled={{state-matches state "loading"}}
|
||||||
|
type="password"
|
||||||
|
name="auth[SecretID]"
|
||||||
|
placeholder="SecretID"
|
||||||
|
value={{secret}}
|
||||||
|
oninput={{queue
|
||||||
|
(action (mut secret) value="target.value")
|
||||||
|
(action (mut value) value="target.value")
|
||||||
|
(action dispatch "TYPING")
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<State @matches="error">
|
||||||
|
{{#if (not error.status)}}
|
||||||
|
<strong role="alert">
|
||||||
|
Please enter your secret
|
||||||
|
</strong>
|
||||||
|
{{/if}}
|
||||||
|
</State>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<button type="submit" disabled={{state-matches state "loading"}}>
|
||||||
|
Log in
|
||||||
|
</button>
|
||||||
|
<em>Contact your administrator for login credentials.</em>
|
||||||
|
</form>
|
||||||
|
{{#if (env 'CONSUL_SSO_ENABLED')}}
|
||||||
|
<DataSource
|
||||||
|
@src={{concat '/' (or nspace 'default') '/' dc '/oidc/providers'}}
|
||||||
|
@onchange={{queue (action (mut providers) value="data")}}
|
||||||
|
@onerror={{queue (action (mut error) value="error.errors.firstObject")}}
|
||||||
|
@loading="lazy"
|
||||||
|
/>
|
||||||
|
{{#if (gt providers.length 0)}}
|
||||||
|
<p>
|
||||||
|
<span>or</span>
|
||||||
|
</p>
|
||||||
|
{{/if}}
|
||||||
|
<OidcSelect
|
||||||
|
@items={{providers}}
|
||||||
|
@disabled={{state-matches state "loading"}}
|
||||||
|
@onchange={{queue (action (mut value)) (action dispatch "SUBMIT") }}
|
||||||
|
@onerror={{queue (action (mut error) value="error.errors.firstObject") (action dispatch "ERROR")}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
<State @matches="loading">
|
||||||
|
<TokenSource
|
||||||
|
@dc={{dc}}
|
||||||
|
@nspace={{or value.Namespace nspace}}
|
||||||
|
@type={{if value.Name 'oidc' 'secret'}}
|
||||||
|
@value={{if value.Name value.Name value}}
|
||||||
|
@onchange={{action onsubmit}}
|
||||||
|
@onerror={{queue (action (mut error) value="error.errors.firstObject") (action dispatch "ERROR")}}
|
||||||
|
/>
|
||||||
|
</State>
|
||||||
|
</StateChart>
|
|
@ -0,0 +1,21 @@
|
||||||
|
import Component from '@ember/component';
|
||||||
|
|
||||||
|
import chart from './chart.xstate';
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
tagName: '',
|
||||||
|
onsubmit: function(e) {},
|
||||||
|
onchange: function(e) {},
|
||||||
|
init: function() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.chart = chart;
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
hasValue: function(context, event, meta) {
|
||||||
|
return this.value !== '' && typeof this.value !== 'undefined';
|
||||||
|
},
|
||||||
|
focus: function() {
|
||||||
|
this.input.focus();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,20 @@
|
||||||
|
## AuthProfile
|
||||||
|
|
||||||
|
```handlebars
|
||||||
|
<AuthProfile @item={{token}} />
|
||||||
|
```
|
||||||
|
|
||||||
|
A straightforward partial-like component for rendering a user profile.
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
| Argument | Type | Default | Description |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `item` | `Object` | | A Consul shaped token object (currently only requires an AccessorID property to be set |
|
||||||
|
|
||||||
|
### See
|
||||||
|
|
||||||
|
- [Component Source Code](./index.js)
|
||||||
|
- [Template Source Code](./index.hbs)
|
||||||
|
|
||||||
|
---
|
|
@ -0,0 +1,9 @@
|
||||||
|
<dl>
|
||||||
|
<dt>
|
||||||
|
<span>My ACL Token</span><br />
|
||||||
|
AccessorID
|
||||||
|
</dt>
|
||||||
|
<dd>
|
||||||
|
{{substr item.AccessorID -8}}
|
||||||
|
</dd>
|
||||||
|
</dl>
|
|
@ -0,0 +1,4 @@
|
||||||
|
{{!<form>}}
|
||||||
|
<FreetextFilter @searchable={{searchable}} @value={{search}} @placeholder="Search" />
|
||||||
|
<RadioGroup @keyboardAccess={{true}} @name="status" @value={{status}} @items={{array (hash label="All (Any Status)" value="") (hash label="Critical Checks" value="critical") (hash label="Warning Checks" value="warning") (hash label="Passing Checks" value="passing")}} @onchange={{action onchange}} />
|
||||||
|
{{!</form>}}
|
|
@ -0,0 +1,10 @@
|
||||||
|
<form class="catalog-toolbar" data-test-catalog-toolbar>
|
||||||
|
<FreetextFilter @searchable={{searchable}} @value={{value}} @placeholder="Search" />
|
||||||
|
<PopoverSelect
|
||||||
|
data-popover-select
|
||||||
|
@selected={{selected}}
|
||||||
|
@options={{options}}
|
||||||
|
@onchange={{onchange}}
|
||||||
|
@title='Sort By'
|
||||||
|
/>
|
||||||
|
</form>
|
|
@ -0,0 +1,6 @@
|
||||||
|
{{yield}}
|
||||||
|
{{#if (gt items.length 0)}}
|
||||||
|
<YieldSlot @name="set" @params={{block-params items}}>{{yield}}</YieldSlot>
|
||||||
|
{{else}}
|
||||||
|
<YieldSlot @name="empty">{{yield}}</YieldSlot>
|
||||||
|
{{/if}}
|
|
@ -0,0 +1,29 @@
|
||||||
|
<div ...attributes>
|
||||||
|
{{yield}}
|
||||||
|
<YieldSlot @name="create">{{yield}}</YieldSlot>
|
||||||
|
<label class="type-text">
|
||||||
|
<span><YieldSlot @name="label">{{yield}}</YieldSlot></span>
|
||||||
|
{{#if isOpen}}
|
||||||
|
<DataSource
|
||||||
|
@src={{concat '/' (or nspace 'default') '/' dc '/' (pluralize type)}}
|
||||||
|
@onchange={{action (mut allOptions) value="data"}}
|
||||||
|
/>
|
||||||
|
{{/if}}
|
||||||
|
<PowerSelect
|
||||||
|
@search={{action "search"}}
|
||||||
|
@options={{options}}
|
||||||
|
@loadingMessage="Loading..."
|
||||||
|
@searchMessage="No possible options"
|
||||||
|
@searchPlaceholder={{placeholder}}
|
||||||
|
@onOpen={{action (mut isOpen) true}}
|
||||||
|
@onClose={{action (mut isOpen) false}}
|
||||||
|
@onChange={{action "change" "items[]" items}} as |item|>
|
||||||
|
<YieldSlot @name="option" @params={{block-params item}}>{{yield}}</YieldSlot>
|
||||||
|
</PowerSelect>
|
||||||
|
</label>
|
||||||
|
{{#if (gt items.length 0)}}
|
||||||
|
<YieldSlot @name="set">{{yield}}</YieldSlot>
|
||||||
|
{{else}}
|
||||||
|
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
|
@ -2,13 +2,13 @@ import Component from '@ember/component';
|
||||||
import { get, set, computed } from '@ember/object';
|
import { get, set, computed } from '@ember/object';
|
||||||
import { alias } from '@ember/object/computed';
|
import { alias } from '@ember/object/computed';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import { Promise } from 'rsvp';
|
|
||||||
|
|
||||||
import SlotsMixin from 'block-slots';
|
import SlotsMixin from 'block-slots';
|
||||||
import WithListeners from 'consul-ui/mixins/with-listeners';
|
import WithListeners from 'consul-ui/mixins/with-listeners';
|
||||||
|
|
||||||
export default Component.extend(SlotsMixin, WithListeners, {
|
export default Component.extend(SlotsMixin, WithListeners, {
|
||||||
onchange: function() {},
|
onchange: function() {},
|
||||||
|
tagName: '',
|
||||||
|
|
||||||
error: function() {},
|
error: function() {},
|
||||||
type: '',
|
type: '',
|
||||||
|
@ -54,11 +54,6 @@ export default Component.extend(SlotsMixin, WithListeners, {
|
||||||
reset: function() {
|
reset: function() {
|
||||||
this.form.clear({ Datacenter: this.dc, Namespace: this.nspace });
|
this.form.clear({ Datacenter: this.dc, Namespace: this.nspace });
|
||||||
},
|
},
|
||||||
open: function() {
|
|
||||||
if (!get(this, 'allOptions.closed')) {
|
|
||||||
set(this, 'allOptions', this.repo.findAllByDatacenter(this.dc, this.nspace));
|
|
||||||
}
|
|
||||||
},
|
|
||||||
save: function(item, items, success = function() {}) {
|
save: function(item, items, success = function() {}) {
|
||||||
// Specifically this saves an 'new' option/child
|
// Specifically this saves an 'new' option/child
|
||||||
// and then adds it to the selectedOptions, not options
|
// and then adds it to the selectedOptions, not options
|
||||||
|
@ -69,8 +64,9 @@ export default Component.extend(SlotsMixin, WithListeners, {
|
||||||
// need to be sure that its saved before adding/closing the modal for now
|
// need to be sure that its saved before adding/closing the modal for now
|
||||||
// and we don't open the modal on prop change yet
|
// and we don't open the modal on prop change yet
|
||||||
item = repo.persist(item);
|
item = repo.persist(item);
|
||||||
this.listen(item, 'message', e => {
|
this.listen(item, {
|
||||||
this.actions.change.bind(this)(
|
message: e => {
|
||||||
|
this.actions.change.apply(this, [
|
||||||
{
|
{
|
||||||
target: {
|
target: {
|
||||||
name: 'items[]',
|
name: 'items[]',
|
||||||
|
@ -78,11 +74,12 @@ export default Component.extend(SlotsMixin, WithListeners, {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
items,
|
items,
|
||||||
e.data
|
e.data,
|
||||||
);
|
]);
|
||||||
success();
|
success();
|
||||||
|
},
|
||||||
|
error: e => this.error(e),
|
||||||
});
|
});
|
||||||
this.listen(item, 'error', this.error.bind(this));
|
|
||||||
},
|
},
|
||||||
remove: function(item, items) {
|
remove: function(item, items) {
|
||||||
const prop = this.repo.getSlugKey();
|
const prop = this.repo.getSlugKey();
|
|
@ -0,0 +1,11 @@
|
||||||
|
<IvyCodemirror @value={{value}} @name={{name}} @class={{class}} @options={{options}} @valueUpdated={{action onkeyup}} />
|
||||||
|
<pre><code>{{yield}}</code></pre>
|
||||||
|
{{#if (and (not readonly) (not syntax))}}
|
||||||
|
<PowerSelect
|
||||||
|
@onChange={{action "change"}}
|
||||||
|
@selected={{mode}}
|
||||||
|
@searchEnabled={{false}}
|
||||||
|
@options={{modes}} as |mode|>
|
||||||
|
{{mode.name}}
|
||||||
|
</PowerSelect>
|
||||||
|
{{/if}}
|
|
@ -0,0 +1,11 @@
|
||||||
|
{{yield}}
|
||||||
|
<YieldSlot @name="action" @params={{block-params confirm cancel}}>
|
||||||
|
{{#if (or permanent (not confirming))}}
|
||||||
|
{{yield}}
|
||||||
|
{{/if}}
|
||||||
|
</YieldSlot>
|
||||||
|
<YieldSlot @name="dialog" @params={{block-params execute cancel message actionName}}>
|
||||||
|
{{#if confirming }}
|
||||||
|
{{yield}}
|
||||||
|
{{/if}}
|
||||||
|
</YieldSlot>
|
|
@ -0,0 +1,19 @@
|
||||||
|
{{#if item}}
|
||||||
|
{{#let (if _externalSource _externalSource (service/external-source item)) as |externalSource|}}
|
||||||
|
{{#if externalSource}}
|
||||||
|
{{#if (has-block)}}
|
||||||
|
{{yield
|
||||||
|
(component 'consul-external-source' item=item _externalSource=externalSource)
|
||||||
|
}}
|
||||||
|
{{else}}
|
||||||
|
<span data-test-external-source={{externalSource}} class="consul-external-source {{externalSource}}">
|
||||||
|
{{#if (eq externalSource 'aws')}}
|
||||||
|
<span>Registered via {{uppercase externalSource}}</span>
|
||||||
|
{{else}}
|
||||||
|
<span>Registered via {{capitalize externalSource}}</span>
|
||||||
|
{{/if}}
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
||||||
|
{{/let}}
|
||||||
|
{{/if}}
|
|
@ -1,6 +1,5 @@
|
||||||
import Component from '@ember/component';
|
import Component from '@ember/component';
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
tagName: 'dl',
|
tagName: '',
|
||||||
classNames: ['tag-list'],
|
|
||||||
});
|
});
|
|
@ -0,0 +1,123 @@
|
||||||
|
<form onsubmit={{action 'submit' _item}}>
|
||||||
|
<fieldset>
|
||||||
|
<div role="group">
|
||||||
|
<fieldset>
|
||||||
|
<h2>Source</h2>
|
||||||
|
<label data-test-source-element class="type-text{{if _item.error.SourceName ' has-error'}}">
|
||||||
|
<span>Source Service</span>
|
||||||
|
<PowerSelectWithCreate
|
||||||
|
@options={{_services}}
|
||||||
|
@searchField="Name"
|
||||||
|
@selected={{SourceName}}
|
||||||
|
@searchPlaceholder="Type service name"
|
||||||
|
@buildSuggestion={{action "createNewLabel" "Use a Consul Service called '{{term}}'"}}
|
||||||
|
@showCreateWhen={{action "isUnique"}}
|
||||||
|
@onCreate={{action "change" "SourceName"}}
|
||||||
|
@onChange={{action "change" "SourceName"}} as |service|>
|
||||||
|
{{#if (eq service.Name '*') }}
|
||||||
|
* (All Services)
|
||||||
|
{{else}}
|
||||||
|
{{service.Name}}
|
||||||
|
{{/if}}
|
||||||
|
</PowerSelectWithCreate>
|
||||||
|
<em>Search for an existing service, or enter any Service name.</em>
|
||||||
|
</label>
|
||||||
|
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
|
||||||
|
<label data-test-source-nspace class="type-text{{if _item.error.SourceNS ' has-error'}}">
|
||||||
|
<span>Source Namespace</span>
|
||||||
|
<PowerSelectWithCreate
|
||||||
|
@options={{_nspaces}}
|
||||||
|
@searchField="Name"
|
||||||
|
@selected={{SourceNS}}
|
||||||
|
@searchPlaceholder="Type namespace name"
|
||||||
|
@buildSuggestion={{action "createNewLabel" "Use a Consul Namespace called '{{term}}'"}}
|
||||||
|
@showCreateWhen={{action "isUnique"}}
|
||||||
|
@onCreate={{action "change" "SourceNS"}}
|
||||||
|
@onChange={{action "change" "SourceNS"}} as |nspace|>
|
||||||
|
{{#if (eq nspace.Name '*') }}
|
||||||
|
* (All Namespaces)
|
||||||
|
{{else}}
|
||||||
|
{{nspace.Name}}
|
||||||
|
{{/if}}
|
||||||
|
</PowerSelectWithCreate>
|
||||||
|
<em>Search for an existing namespace, or enter any Namespace name.</em>
|
||||||
|
</label>
|
||||||
|
{{/if}}
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<h2>Destination</h2>
|
||||||
|
<label data-test-destination-element class="type-text{{if _item.error.DestinationName ' has-error'}}">
|
||||||
|
<span>Destination Service</span>
|
||||||
|
<PowerSelectWithCreate
|
||||||
|
@options={{_services}}
|
||||||
|
@searchField="Name"
|
||||||
|
@selected={{DestinationName}}
|
||||||
|
@searchPlaceholder="Type service name"
|
||||||
|
@buildSuggestion={{action "createNewLabel" "Use a Consul Service called '{{term}}'"}}
|
||||||
|
@showCreateWhen={{action "isUnique"}}
|
||||||
|
@onCreate={{action "change" "DestinationName"}}
|
||||||
|
@onChange={{action "change" "DestinationName"}} as |service|>
|
||||||
|
{{#if (eq service.Name '*') }}
|
||||||
|
* (All Services)
|
||||||
|
{{else}}
|
||||||
|
{{service.Name}}
|
||||||
|
{{/if}}
|
||||||
|
</PowerSelectWithCreate>
|
||||||
|
<em>Search for an existing service, or enter any Service name.</em>
|
||||||
|
</label>
|
||||||
|
{{#if (env 'CONSUL_NSPACES_ENABLED')}}
|
||||||
|
<label data-test-destination-nspace class="type-text{{if _item.error.DestinationNS ' has-error'}}">
|
||||||
|
<span>Destination Namespace</span>
|
||||||
|
<PowerSelectWithCreate
|
||||||
|
@options={{_nspaces}}
|
||||||
|
@searchField="Name"
|
||||||
|
@selected={{DestinationNS}}
|
||||||
|
@searchPlaceholder="Type namespace name"
|
||||||
|
@buildSuggestion={{action "createNewLabel" "Use a future Consul Namespace called '{{term}}'"}}
|
||||||
|
@showCreateWhen={{action "isUnique"}}
|
||||||
|
@onCreate={{action "change" "DestinationNS"}}
|
||||||
|
@onChange={{action "change" "DestinationNS"}} as |nspace|>
|
||||||
|
{{#if (eq nspace.Name '*') }}
|
||||||
|
* (All Namespaces)
|
||||||
|
{{else}}
|
||||||
|
{{nspace.Name}}
|
||||||
|
{{/if}}
|
||||||
|
</PowerSelectWithCreate>
|
||||||
|
<em>For the destination, you may choose any namespace for which you have access.</em>
|
||||||
|
</label>
|
||||||
|
{{/if}}
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
<div role="radiogroup" class={{if _item.error.Action ' has-error'}}>
|
||||||
|
{{#each (array 'allow' 'deny') as |intent|}}
|
||||||
|
<label>
|
||||||
|
<span>{{ capitalize intent }}</span>
|
||||||
|
<input type="radio" name="Action" value="{{intent}}" checked={{if (eq _item.Action intent) 'checked'}} onchange={{ action 'change' }}/>
|
||||||
|
</label>
|
||||||
|
{{/each}}
|
||||||
|
</div>
|
||||||
|
<label class="type-text{{if _item.error.Description ' has-error'}}">
|
||||||
|
<span>Description (Optional)</span>
|
||||||
|
<input type="text" name="Description" value="{{_item.Description}}" placeholder="Description (Optional)" onchange={{action 'change'}} />
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
<div>
|
||||||
|
{{#if _item.isNew }}
|
||||||
|
<button type="submit" disabled={{if (or _item.isPristine _item.isInvalid) 'disabled'}}>Save</button>
|
||||||
|
{{ else }}
|
||||||
|
<button type="submit" disabled={{if _item.isInvalid 'disabled'}}>Save</button>
|
||||||
|
{{/if}}
|
||||||
|
<button type="reset" onclick={{action oncancel _item}}>Cancel</button>
|
||||||
|
{{# if (and _item.ID (not-eq _item.ID 'anonymous')) }}
|
||||||
|
<ConfirmationDialog @message="Are you sure you want to delete this Intention?">
|
||||||
|
<BlockSlot @name="action" as |confirm|>
|
||||||
|
<button data-test-delete type="button" class="type-delete" {{action confirm ondelete _item}}>Delete</button>
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="dialog" as |execute cancel message|>
|
||||||
|
<DeleteConfirmation @message={{message}} @execute={{execute}} @cancel={{cancel}} />
|
||||||
|
</BlockSlot>
|
||||||
|
</ConfirmationDialog>
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
|
@ -0,0 +1,113 @@
|
||||||
|
import Component from '@ember/component';
|
||||||
|
import { inject as service } from '@ember/service';
|
||||||
|
import { setProperties, set, get } from '@ember/object';
|
||||||
|
import { assert } from '@ember/debug';
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
tagName: '',
|
||||||
|
dom: service('dom'),
|
||||||
|
builder: service('form'),
|
||||||
|
init: function() {
|
||||||
|
this._super(...arguments);
|
||||||
|
this.form = this.builder.form('intention');
|
||||||
|
},
|
||||||
|
didReceiveAttrs: function() {
|
||||||
|
this._super(...arguments);
|
||||||
|
if (this.item && this.services && this.nspaces) {
|
||||||
|
let services = this.services || [];
|
||||||
|
let nspaces = this.nspaces || [];
|
||||||
|
let source = services.findBy('Name', this.item.SourceName);
|
||||||
|
if (!source) {
|
||||||
|
source = { Name: this.item.SourceName };
|
||||||
|
services = [source].concat(services);
|
||||||
|
}
|
||||||
|
let destination = services.findBy('Name', this.item.DestinationName);
|
||||||
|
if (!destination) {
|
||||||
|
destination = { Name: this.item.DestinationName };
|
||||||
|
services = [destination].concat(services);
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourceNS = nspaces.findBy('Name', this.item.SourceNS);
|
||||||
|
if (!sourceNS) {
|
||||||
|
sourceNS = { Name: this.item.SourceNS };
|
||||||
|
nspaces = [sourceNS].concat(nspaces);
|
||||||
|
}
|
||||||
|
let destinationNS = this.nspaces.findBy('Name', this.item.DestinationNS);
|
||||||
|
if (!destinationNS) {
|
||||||
|
destinationNS = { Name: this.item.DestinationNS };
|
||||||
|
nspaces = [destinationNS].concat(nspaces);
|
||||||
|
}
|
||||||
|
// TODO: Use this.{item,services} when we have this.args
|
||||||
|
setProperties(this, {
|
||||||
|
_item: this.form.setData(this.item).getData(),
|
||||||
|
_services: services,
|
||||||
|
_nspaces: nspaces,
|
||||||
|
SourceName: source,
|
||||||
|
DestinationName: destination,
|
||||||
|
SourceNS: sourceNS,
|
||||||
|
DestinationNS: destinationNS,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
assert('@item, @services and @nspaces are required arguments', false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
createNewLabel: function(template, term) {
|
||||||
|
return template.replace(/{{term}}/g, term);
|
||||||
|
},
|
||||||
|
isUnique: function(term) {
|
||||||
|
return !this._services.findBy('Name', term);
|
||||||
|
},
|
||||||
|
submit: function(item, e) {
|
||||||
|
e.preventDefault();
|
||||||
|
this.onsubmit(...arguments);
|
||||||
|
},
|
||||||
|
change: function(e, value, item) {
|
||||||
|
const event = this.dom.normalizeEvent(e, value);
|
||||||
|
const form = this.form;
|
||||||
|
const target = event.target;
|
||||||
|
|
||||||
|
let name, selected, match;
|
||||||
|
switch (target.name) {
|
||||||
|
case 'SourceName':
|
||||||
|
case 'DestinationName':
|
||||||
|
case 'SourceNS':
|
||||||
|
case 'DestinationNS':
|
||||||
|
name = selected = target.value;
|
||||||
|
// Names can be selected Service EmberObjects or typed in strings
|
||||||
|
// if its not a string, use the `Name` from the Service EmberObject
|
||||||
|
if (typeof name !== 'string') {
|
||||||
|
name = get(target.value, 'Name');
|
||||||
|
}
|
||||||
|
// mutate the value with the string name
|
||||||
|
// which will be handled by the form
|
||||||
|
target.value = name;
|
||||||
|
// these are 'non-form' variables so not on `item`
|
||||||
|
// these variables also exist in the template so we know
|
||||||
|
// the current selection
|
||||||
|
// basically the difference between
|
||||||
|
// `item.DestinationName` and just `DestinationName`
|
||||||
|
// see if the name is already in the list
|
||||||
|
match = this._services.filterBy('Name', name);
|
||||||
|
if (match.length === 0) {
|
||||||
|
// if its not make a new 'fake' Service that doesn't exist yet
|
||||||
|
// and add it to the possible services to make an intention between
|
||||||
|
selected = { Name: name };
|
||||||
|
switch (target.name) {
|
||||||
|
case 'SourceName':
|
||||||
|
case 'DestinationName':
|
||||||
|
set(this, '_services', [selected].concat(this._services.toArray()));
|
||||||
|
break;
|
||||||
|
case 'SourceNS':
|
||||||
|
case 'DestinationNS':
|
||||||
|
set(this, '_nspaces', [selected].concat(this._nspaces.toArray()));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set(this, target.name, selected);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
form.handleEvent(event);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,73 @@
|
||||||
|
<TabularCollection class="consul-intention-list" @items={{items}} as |item index|>
|
||||||
|
<BlockSlot @name="header">
|
||||||
|
<th>Source</th>
|
||||||
|
<th> </th>
|
||||||
|
<th>Destination</th>
|
||||||
|
<th>Precedence</th>
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="row">
|
||||||
|
<td class="source" data-test-intention={{item.ID}}>
|
||||||
|
<a href={{href-to 'dc.intentions.edit' item.ID}} data-test-intention-source={{item.SourceName}}>
|
||||||
|
{{#if (eq item.SourceName '*') }}
|
||||||
|
All Services (*)
|
||||||
|
{{else}}
|
||||||
|
{{item.SourceName}}
|
||||||
|
{{/if}}
|
||||||
|
{{! TODO: slugify }}
|
||||||
|
<em class={{concat 'nspace-' (or item.SourceNS 'default')}}>{{or item.SourceNS 'default'}}</em>
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="intent-{{item.Action}}" data-test-intention-action="{{item.Action}}">
|
||||||
|
<strong>{{item.Action}}</strong>
|
||||||
|
</td>
|
||||||
|
<td class="destination" data-test-intention-destination="{{item.DestinationName}}">
|
||||||
|
<span>
|
||||||
|
{{#if (eq item.DestinationName '*') }}
|
||||||
|
All Services (*)
|
||||||
|
{{else}}
|
||||||
|
{{item.DestinationName}}
|
||||||
|
{{/if}}
|
||||||
|
{{! TODO: slugify }}
|
||||||
|
<em class={{concat 'nspace-' (or item.DestinationNS 'default')}}>{{or item.DestinationNS 'default'}}</em>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="precedence">
|
||||||
|
{{item.Precedence}}
|
||||||
|
</td>
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="actions" as |index change checked|>
|
||||||
|
<PopoverMenu @expanded={{if (eq checked index) true false}} @onchange={{action change index}} @keyboardAccess={{false}}>
|
||||||
|
<BlockSlot @name="trigger">
|
||||||
|
More
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="menu" as |confirm send keypressClick change|>
|
||||||
|
<li role="none">
|
||||||
|
<a role="menuitem" tabindex="-1" href={{href-to 'dc.intentions.edit' item.ID}}>Edit</a>
|
||||||
|
</li>
|
||||||
|
<li role="none" class="dangerous">
|
||||||
|
<label for={{confirm}} role="menuitem" tabindex="-1" onkeypress={{keypressClick}} data-test-delete>Delete</label>
|
||||||
|
<div role="menu">
|
||||||
|
<div class="confirmation-alert warning">
|
||||||
|
<div>
|
||||||
|
<header>
|
||||||
|
Confirm Delete
|
||||||
|
</header>
|
||||||
|
<p>
|
||||||
|
Are you sure you want to delete this intention?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<li class="dangerous">
|
||||||
|
<button tabindex="-1" type="button" class="type-delete" onclick={{queue (action change) (action ondelete item)}}>Delete</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<label for={{confirm}}>Cancel</label>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</BlockSlot>
|
||||||
|
</PopoverMenu>
|
||||||
|
</BlockSlot>
|
||||||
|
</TabularCollection>
|
|
@ -0,0 +1,5 @@
|
||||||
|
import Component from '@ember/component';
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
tagName: '',
|
||||||
|
});
|
|
@ -0,0 +1,11 @@
|
||||||
|
{{#if item.Kind}}
|
||||||
|
{{#if (has-block)}}
|
||||||
|
{{yield
|
||||||
|
(component 'consul-kind' item=item)
|
||||||
|
}}
|
||||||
|
{{else}}
|
||||||
|
<span data-test-kind={{item.Kind}} class="consul-kind gateway">
|
||||||
|
<span>{{titleize (humanize item.Kind)}}</span>
|
||||||
|
</span>
|
||||||
|
{{/if}}
|
||||||
|
{{/if}}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import Component from '@ember/component';
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
tagName: '',
|
||||||
|
});
|
|
@ -0,0 +1,16 @@
|
||||||
|
## ConsulLoader
|
||||||
|
|
||||||
|
`<ConsulLoader />`
|
||||||
|
|
||||||
|
Simple template-only component to show the circulr animated Consul loader animation.
|
||||||
|
|
||||||
|
| Argument | Type | Default | Description |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
|
||||||
|
|
||||||
|
### See
|
||||||
|
|
||||||
|
- [Component Source Code](./index.js)
|
||||||
|
- [Template Source Code](./index.hbs)
|
||||||
|
|
||||||
|
---
|
|
@ -51,4 +51,3 @@
|
||||||
<circle r="9" cx="22" cy="22" style="transform-origin: 22px 22px" />
|
<circle r="9" cx="22" cy="22" style="transform-origin: 22px 22px" />
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
@ -0,0 +1,5 @@
|
||||||
|
import Component from '@ember/component';
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
tagName: '',
|
||||||
|
});
|
|
@ -0,0 +1,27 @@
|
||||||
|
## ConsulMetadataList
|
||||||
|
|
||||||
|
`<ConsulMetadataList @items={{meta}} />`
|
||||||
|
|
||||||
|
A presentational component for presenting Consul Metadata
|
||||||
|
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
| Argument/Attribute | Type | Default | Description |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `items` | `array` | | A an array of entries or `[key, value]` pairs as returned by `Object.entries()` |
|
||||||
|
|
||||||
|
### Example
|
||||||
|
|
||||||
|
The following example shows how to construct the required structure from the
|
||||||
|
Consul API using a `object-entries` helper.
|
||||||
|
|
||||||
|
```handlebars
|
||||||
|
<ConsulMetadataList @items={{object-entries item.Meta}} />
|
||||||
|
```
|
||||||
|
|
||||||
|
### See
|
||||||
|
|
||||||
|
- [Component Source Code](./index.js)
|
||||||
|
- [TemplateSource Code](./index.hbs)
|
||||||
|
|
||||||
|
---
|
|
@ -0,0 +1,19 @@
|
||||||
|
<TabularCollection
|
||||||
|
data-test-metadata
|
||||||
|
@items={{items}} as |item index|
|
||||||
|
>
|
||||||
|
<BlockSlot @name="header">
|
||||||
|
<th>Key</th>
|
||||||
|
<th>Value</th>
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="row">
|
||||||
|
<td>
|
||||||
|
<span>
|
||||||
|
{{object-at 0 item}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span>{{object-at 1 item}}</span>
|
||||||
|
</td>
|
||||||
|
</BlockSlot>
|
||||||
|
</TabularCollection>
|
|
@ -0,0 +1,5 @@
|
||||||
|
import Component from '@ember/component';
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
tagName: '',
|
||||||
|
});
|
|
@ -0,0 +1,47 @@
|
||||||
|
{{yield}}
|
||||||
|
{{#if (gt items.length 0)}}
|
||||||
|
<ListCollection @cellHeight={{73}} @items={{items}} class="consul-service-instance-list" as |item index|>
|
||||||
|
<a href={{href-to routeName item.Service.Service item.Node.Node (or item.Service.ID item.Service.Service)}}>
|
||||||
|
{{item.Service.ID}}
|
||||||
|
</a>
|
||||||
|
<ul>
|
||||||
|
<ConsulExternalSource @item={{item.Service}} as |ExternalSource|>
|
||||||
|
<li>
|
||||||
|
<ExternalSource />
|
||||||
|
</li>
|
||||||
|
</ConsulExternalSource>
|
||||||
|
{{#with (reject-by 'ServiceID' '' item.Checks) as |checks|}}
|
||||||
|
<li class={{service/instance-checks checks}}>
|
||||||
|
{{checks.length}} service checks
|
||||||
|
</li>
|
||||||
|
{{/with}}
|
||||||
|
{{#with (filter-by 'ServiceID' '' item.Checks) as |checks|}}
|
||||||
|
<li class={{service/instance-checks checks}}>
|
||||||
|
{{checks.length}} node checks
|
||||||
|
</li>
|
||||||
|
{{/with}}
|
||||||
|
{{#if (get proxies item.Service.ID)}}
|
||||||
|
<li class="proxy">
|
||||||
|
connected with proxy
|
||||||
|
</li>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (gt item.Node.Node.length 0)}}
|
||||||
|
<li class="node">
|
||||||
|
<a href={{href-to 'dc.nodes.show' item.Node.Node}}>{{item.Node.Node}}</a>
|
||||||
|
</li>
|
||||||
|
{{/if}}
|
||||||
|
<li class="address" data-test-address>
|
||||||
|
{{#if (not-eq item.Service.Address '')}}
|
||||||
|
{{item.Service.Address}}:{{item.Service.Port}}
|
||||||
|
{{else}}
|
||||||
|
{{item.Node.Address}}:{{item.Service.Port}}
|
||||||
|
{{/if}}
|
||||||
|
</li>
|
||||||
|
<TagList @item={{item.Service}} as |Tags|>
|
||||||
|
<li>
|
||||||
|
<Tags />
|
||||||
|
</li>
|
||||||
|
</TagList>
|
||||||
|
</ul>
|
||||||
|
</ListCollection>
|
||||||
|
{{/if}}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import Component from '@ember/component';
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
tagName: '',
|
||||||
|
});
|
|
@ -0,0 +1,35 @@
|
||||||
|
{{yield}}
|
||||||
|
{{#if (gt items.length 0)}}
|
||||||
|
<ListCollection @cellHeight={{73}} @items={{items}} class="consul-service-list" as |item index|>
|
||||||
|
<a data-test-service-name href={{href-to routeName item.Name}} class={{service/health-checks item}}>
|
||||||
|
{{item.Name}}
|
||||||
|
</a>
|
||||||
|
<ul>
|
||||||
|
<ConsulKind @item={{item}} as |Kind|>
|
||||||
|
<li>
|
||||||
|
<Kind />
|
||||||
|
</li>
|
||||||
|
</ConsulKind>
|
||||||
|
<ConsulExternalSource @item={{item}} as |ExternalSource|>
|
||||||
|
<li>
|
||||||
|
<ExternalSource />
|
||||||
|
</li>
|
||||||
|
</ConsulExternalSource>
|
||||||
|
{{#if (not-eq item.InstanceCount 0)}}
|
||||||
|
<li>
|
||||||
|
{{format-number item.InstanceCount}} {{pluralize item.InstanceCount 'Instance' without-count=true}}
|
||||||
|
</li>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (get proxies item.Name)}}
|
||||||
|
<li data-test-proxy class="proxy">
|
||||||
|
connected with proxy
|
||||||
|
</li>
|
||||||
|
{{/if}}
|
||||||
|
<TagList @item={{item}} as |Tags|>
|
||||||
|
<li>
|
||||||
|
<Tags />
|
||||||
|
</li>
|
||||||
|
</TagList>
|
||||||
|
</ul>
|
||||||
|
</ListCollection>
|
||||||
|
{{/if}}
|
|
@ -0,0 +1,5 @@
|
||||||
|
import Component from '@ember/component';
|
||||||
|
|
||||||
|
export default Component.extend({
|
||||||
|
tagName: '',
|
||||||
|
});
|
|
@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
## CopyButton
|
||||||
|
|
||||||
|
```handlebars
|
||||||
|
{{! inline }}
|
||||||
|
<CopyButton
|
||||||
|
@value={{stringToCopy}}
|
||||||
|
@name="Thing"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<CopyButton
|
||||||
|
@value={{stringToCopy}}
|
||||||
|
@name="Thing"
|
||||||
|
>
|
||||||
|
Copy me!
|
||||||
|
</CopyButton>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
|
||||||
|
---
|
|
@ -0,0 +1,17 @@
|
||||||
|
<FeedbackDialog @type="inline">
|
||||||
|
<BlockSlot @name="action" as |success error|>
|
||||||
|
<Ref @target={{this}} @name="success" @value={{success}} />
|
||||||
|
<Ref @target={{this}} @name="error" @value={{error}} />
|
||||||
|
<button id={{guid}} title={{concat "Copy " name " to the clipboard"}} ...attributes type="button" class="copy-btn" data-clipboard-text={{value}}>{{~yield~}}</button>
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="success" as |transition|>
|
||||||
|
<p class={{transition}}>
|
||||||
|
Copied {{name}}!
|
||||||
|
</p>
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="error" as |transition|>
|
||||||
|
<p class={{transition}}>
|
||||||
|
Sorry, something went wrong!
|
||||||
|
</p>
|
||||||
|
</BlockSlot>
|
||||||
|
</FeedbackDialog>
|
|
@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,62 @@
|
||||||
|
## DataSink
|
||||||
|
|
||||||
|
```handlebars
|
||||||
|
<DataSink
|
||||||
|
@sink="/dc/nspace/intentions/{{intentions.uid}}"
|
||||||
|
@onchange={{action (mut items) value="data"}}
|
||||||
|
@onerror={{action (mut error) value="error"}}
|
||||||
|
as |api|
|
||||||
|
></DataSink>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
|
<DataSink @src="/dc/nspace/intentions/{{intention.uid}}"
|
||||||
|
@onchange={{action (mut item) value="data"}}
|
||||||
|
@onerror={{action (mut error) value="error"}}
|
||||||
|
as |api|
|
||||||
|
>
|
||||||
|
<button type="button" onclick={{action api.open (hash Name="New Name")}}>Create/Update</button>
|
||||||
|
<button type="button" onclick={{action api.open null}}>Delete</button>
|
||||||
|
</DataSink>
|
||||||
|
{{item.Name}}
|
||||||
|
```
|
||||||
|
|
||||||
|
```handlebars
|
||||||
|
<DataSink @src="/dc/nspace/intentions/{{intention.uid}}"
|
||||||
|
@data=(hash Name="New Name")
|
||||||
|
@onchange={{action (mut item) value="data"}}
|
||||||
|
@onerror={{action (mut error) value="error"}}
|
||||||
|
></DataSink>
|
||||||
|
{{item.Name}}
|
||||||
|
```
|
||||||
|
|
||||||
|
### See
|
||||||
|
|
||||||
|
- [Component Source Code](./index.js)
|
||||||
|
- [Template Source Code](./index.hbs)
|
||||||
|
|
||||||
|
---
|
|
@ -0,0 +1,4 @@
|
||||||
|
{{yield (hash
|
||||||
|
open=(action 'open')
|
||||||
|
state=state
|
||||||
|
)}}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,61 @@
|
||||||
|
## DataSource
|
||||||
|
|
||||||
|
```handlebars
|
||||||
|
<DataSource
|
||||||
|
@src="/dc/nspace/services"
|
||||||
|
@loading="eager"
|
||||||
|
@onchange={{action (mut items) value="data"}}
|
||||||
|
@onerror={{action (mut error) value="error"}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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 `<DataSource />` 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}}
|
||||||
|
<DataSource @src="/dc/nspace/services"
|
||||||
|
@onchange={{action (mut items) value="data"}}
|
||||||
|
@onerror={{action (mut error) value="error"}}
|
||||||
|
/>
|
||||||
|
{{! 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}}
|
||||||
|
<DataSource @src="settings://consul:token"
|
||||||
|
@onchange={{action (mut token) value="data"}}
|
||||||
|
@onerror={{action (mut error) value="error"}}
|
||||||
|
/>
|
||||||
|
{{! 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)
|
||||||
|
|
||||||
|
---
|
|
@ -0,0 +1,4 @@
|
||||||
|
{{#if (eq loading "lazy")}}
|
||||||
|
{{! in order to use intersection observer we need a DOM element on the page}}
|
||||||
|
<data id={{guid}} aria-hidden="true" style="width: 0;height: 0;font-size: 0;padding: 0;margin: 0;" />
|
||||||
|
{{/if}}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -4,11 +4,11 @@
|
||||||
{{selected.nodes}} {
|
{{selected.nodes}} {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
|
|
||||||
background-color: {{css-var '--white'}};
|
background-color: var(--white);
|
||||||
border: {{css-var '--decor-border-100'}};
|
border: var(--decor-border-100);
|
||||||
border-radius: {{css-var '--decor-radius-300'}};
|
border-radius: var(--decor-radius-200);
|
||||||
border-color: {{css-var '--gray-500'}};
|
border-color: var(--gray-500);
|
||||||
box-shadow: 0 8px 10px 0 rgba(0, 0, 0, 0.1);
|
box-shadow: var(--decor-elevation-600);
|
||||||
}
|
}
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#if selected.edges }}
|
{{#if selected.edges }}
|
||||||
|
@ -28,7 +28,7 @@
|
||||||
</header>
|
</header>
|
||||||
<div role="group">
|
<div role="group">
|
||||||
{{#each routes as |item|}}
|
{{#each routes as |item|}}
|
||||||
{{route-card item=item onclick=(action 'click')}}
|
<RouteCard @item={{item}} @onclick={{action "click"}} />
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
</header>
|
</header>
|
||||||
<div role="group">
|
<div role="group">
|
||||||
{{#each (sort-by 'Name' splitters) as |item|}}
|
{{#each (sort-by 'Name' splitters) as |item|}}
|
||||||
{{splitter-card item=item onclick=(action 'click')}}
|
<SplitterCard @item={{item}} @onclick={{action "click"}} />
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -58,7 +58,7 @@
|
||||||
</header>
|
</header>
|
||||||
<div role="group">
|
<div role="group">
|
||||||
{{#each (sort-by 'Name' resolvers) as |item|}}
|
{{#each (sort-by 'Name' resolvers) as |item|}}
|
||||||
{{resolver-card item=item onclick=(action 'click')}}
|
<ResolverCard @item={{item}} @onclick={{action "click"}} />
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -1,7 +1,6 @@
|
||||||
import Component from '@ember/component';
|
import Component from '@ember/component';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import { set, get, computed } from '@ember/object';
|
import { set, get, computed } from '@ember/object';
|
||||||
import { next } from '@ember/runloop';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createRoute,
|
createRoute,
|
||||||
|
@ -147,8 +146,6 @@ export default Component.extend({
|
||||||
// the developer access to the mouse event therefore we just use JS to add our events
|
// the developer access to the mouse event therefore we just use JS to add our events
|
||||||
// revisit this post Octane
|
// revisit this post Octane
|
||||||
addPathListeners: function() {
|
addPathListeners: function() {
|
||||||
// TODO: Figure out if we can remove this next
|
|
||||||
next(() => {
|
|
||||||
this._listeners.remove();
|
this._listeners.remove();
|
||||||
this._listeners.add(this.dom.document(), {
|
this._listeners.add(this.dom.document(), {
|
||||||
click: e => {
|
click: e => {
|
||||||
|
@ -166,7 +163,6 @@ export default Component.extend({
|
||||||
mouseout: e => this.actions.hideSplit.apply(this, [e]),
|
mouseout: e => this.actions.hideSplit.apply(this, [e]),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
|
||||||
// TODO: currently don't think there is a way to listen
|
// TODO: currently don't think there is a way to listen
|
||||||
// for an element being removed inside a component, possibly
|
// for an element being removed inside a component, possibly
|
||||||
// using IntersectionObserver. It's a tiny detail, but we just always
|
// using IntersectionObserver. It's a tiny detail, but we just always
|
|
@ -9,10 +9,11 @@ export default Component.extend({
|
||||||
},
|
},
|
||||||
didInsertElement: function() {
|
didInsertElement: function() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.buffer.add(this.getBufferName(), this.element);
|
this._element = this.buffer.add(this.getBufferName(), this.element);
|
||||||
},
|
},
|
||||||
didDestroyElement: function() {
|
didDestroyElement: function() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.buffer.remove(this.getBufferName());
|
this.buffer.remove(this.getBufferName(), this._element);
|
||||||
|
this._element = null;
|
||||||
},
|
},
|
||||||
});
|
});
|
|
@ -0,0 +1,21 @@
|
||||||
|
{{yield}}
|
||||||
|
<div class="empty-state" ...attributes>
|
||||||
|
<header>
|
||||||
|
{{#yield-slot name="header"}}
|
||||||
|
{{yield}}
|
||||||
|
{{/yield-slot}}
|
||||||
|
{{#yield-slot name="subheader"}}
|
||||||
|
{{yield}}
|
||||||
|
{{/yield-slot}}
|
||||||
|
</header>
|
||||||
|
<p>
|
||||||
|
{{#yield-slot name="body"}}
|
||||||
|
{{yield}}
|
||||||
|
{{/yield-slot}}
|
||||||
|
</p>
|
||||||
|
{{#yield-slot name="actions"}}
|
||||||
|
<ul>
|
||||||
|
{{yield}}
|
||||||
|
</ul>
|
||||||
|
{{/yield-slot}}
|
||||||
|
</div>
|
|
@ -0,0 +1,6 @@
|
||||||
|
import Component from '@ember/component';
|
||||||
|
import Slotted from 'block-slots';
|
||||||
|
|
||||||
|
export default Component.extend(Slotted, {
|
||||||
|
tagName: '',
|
||||||
|
});
|
|
@ -0,0 +1,9 @@
|
||||||
|
{{yield}}
|
||||||
|
{{#if (eq state 'success') }}
|
||||||
|
<YieldSlot @name="success" @params={{block-params transition}}>{{yield}}</YieldSlot>
|
||||||
|
{{else if (eq state 'error') }}
|
||||||
|
<YieldSlot @name="error" @params={{block-params transition}}>{{yield}}</YieldSlot>
|
||||||
|
{{/if}}
|
||||||
|
{{#if (or permanent (eq state 'ready')) }}
|
||||||
|
<YieldSlot @name="action" @params={{block-params success error}}>{{yield}}{{message}}</YieldSlot>
|
||||||
|
{{/if}}
|
|
@ -1,7 +1,6 @@
|
||||||
import Component from '@ember/component';
|
import Component from '@ember/component';
|
||||||
import { set } from '@ember/object';
|
import { set } from '@ember/object';
|
||||||
import { inject as service } from '@ember/service';
|
import { inject as service } from '@ember/service';
|
||||||
import { Promise } from 'rsvp';
|
|
||||||
|
|
||||||
import SlotsMixin from 'block-slots';
|
import SlotsMixin from 'block-slots';
|
||||||
const STATE_READY = 'ready';
|
const STATE_READY = 'ready';
|
|
@ -6,6 +6,7 @@ import WithListeners from 'consul-ui/mixins/with-listeners';
|
||||||
// match anything that isn't a [ or ] into multiple groups
|
// match anything that isn't a [ or ] into multiple groups
|
||||||
const propRe = /([^[\]])+/g;
|
const propRe = /([^[\]])+/g;
|
||||||
export default Component.extend(WithListeners, SlotsMixin, {
|
export default Component.extend(WithListeners, SlotsMixin, {
|
||||||
|
tagName: '',
|
||||||
onreset: function() {},
|
onreset: function() {},
|
||||||
onchange: function() {},
|
onchange: function() {},
|
||||||
onerror: function() {},
|
onerror: function() {},
|
|
@ -0,0 +1,6 @@
|
||||||
|
<EmberNativeScrollable @tagName="ul" @content-size={{_contentSize}} @scroll-left={{_scrollLeft}} @scroll-top={{_scrollTop}} @scrollChange={{action "scrollChange"}} @clientSizeChange={{action "clientSizeChange"}}>
|
||||||
|
<li></li>
|
||||||
|
{{~#each _cells as |cell|~}}
|
||||||
|
<li style={{{cell.style}}}>{{yield cell.item cell.index }}</li>
|
||||||
|
{{~/each~}}
|
||||||
|
</EmberNativeScrollable>
|
|
@ -12,7 +12,7 @@ export default Component.extend(WithResizing, {
|
||||||
height: 500,
|
height: 500,
|
||||||
cellHeight: 113,
|
cellHeight: 113,
|
||||||
style: style('getStyle'),
|
style: style('getStyle'),
|
||||||
classNames: ['list-collection'],
|
classNames: ['grid-collection'],
|
||||||
init: function() {
|
init: function() {
|
||||||
this._super(...arguments);
|
this._super(...arguments);
|
||||||
this.columns = [25, 25, 25, 25];
|
this.columns = [25, 25, 25, 25];
|
|
@ -1,3 +1,4 @@
|
||||||
|
<ModalLayer />
|
||||||
<header role="banner" data-test-navigation>
|
<header role="banner" data-test-navigation>
|
||||||
<a data-test-main-nav-logo href={{href-to 'index'}}><svg width="28" height="27" xmlns="http://www.w3.org/2000/svg"><title>Consul</title><path d="M13.284 16.178a2.876 2.876 0 1 1-.008-5.751 2.876 2.876 0 0 1 .008 5.75zm5.596-1.547a1.333 1.333 0 1 1 0-2.667 1.333 1.333 0 0 1 0 2.667zm4.853 1.249a1.271 1.271 0 1 1 .027-.107c0 .031 0 .067-.027.107zm-.937-3.436a1.333 1.333 0 1 1 .986-1.595c.033.172.033.348 0 .52-.07.53-.465.96-.986 1.075zm4.72 3.29a1.333 1.333 0 1 1-1.076-1.538 1.333 1.333 0 0 1 1.116 1.417.342.342 0 0 0-.027.12h-.013zm-1.08-3.33a1.333 1.333 0 1 1 1.088-1.524c.014.114.014.229 0 .342a1.333 1.333 0 0 1-1.102 1.182h.014zm-.925 7.925a1.333 1.333 0 1 1 .165-.547c-.01.193-.067.38-.165.547zm-.48-12.191a1.333 1.333 0 1 1 .507-1.814c.14.237.198.514.164.787-.038.438-.289.828-.67 1.045v-.018zM13.333 26.667C5.97 26.667 0 20.697 0 13.333 0 5.97 5.97 0 13.333 0c2.929-.01 5.778.955 8.098 2.742L19.8 4.89a10.667 10.667 0 0 0-17.133 8.444 10.667 10.667 0 0 0 17.137 8.471l1.627 2.13a13.218 13.218 0 0 1-8.098 2.733z" fill="#FFF"/></svg></a>
|
<a data-test-main-nav-logo href={{href-to 'index'}}><svg width="28" height="27" xmlns="http://www.w3.org/2000/svg"><title>Consul</title><path d="M13.284 16.178a2.876 2.876 0 1 1-.008-5.751 2.876 2.876 0 0 1 .008 5.75zm5.596-1.547a1.333 1.333 0 1 1 0-2.667 1.333 1.333 0 0 1 0 2.667zm4.853 1.249a1.271 1.271 0 1 1 .027-.107c0 .031 0 .067-.027.107zm-.937-3.436a1.333 1.333 0 1 1 .986-1.595c.033.172.033.348 0 .52-.07.53-.465.96-.986 1.075zm4.72 3.29a1.333 1.333 0 1 1-1.076-1.538 1.333 1.333 0 0 1 1.116 1.417.342.342 0 0 0-.027.12h-.013zm-1.08-3.33a1.333 1.333 0 1 1 1.088-1.524c.014.114.014.229 0 .342a1.333 1.333 0 0 1-1.102 1.182h.014zm-.925 7.925a1.333 1.333 0 1 1 .165-.547c-.01.193-.067.38-.165.547zm-.48-12.191a1.333 1.333 0 1 1 .507-1.814c.14.237.198.514.164.787-.038.438-.289.828-.67 1.045v-.018zM13.333 26.667C5.97 26.667 0 20.697 0 13.333 0 5.97 5.97 0 13.333 0c2.929-.01 5.778.955 8.098 2.742L19.8 4.89a10.667 10.667 0 0 0-17.133 8.444 10.667 10.667 0 0 0 17.137 8.471l1.627 2.13a13.218 13.218 0 0 1-8.098 2.733z" fill="#FFF"/></svg></a>
|
||||||
<input type="checkbox" name="menu" id="main-nav-toggle" onchange={{action 'change'}} />
|
<input type="checkbox" name="menu" id="main-nav-toggle" onchange={{action 'change'}} />
|
||||||
|
@ -16,20 +17,25 @@
|
||||||
{{#if (and (eq nspaces.length 1) (not canManageNspaces)) }}
|
{{#if (and (eq nspaces.length 1) (not canManageNspaces)) }}
|
||||||
<span data-test-nspace-selected={{nspace.Name}}>{{nspace.Name}}</span>
|
<span data-test-nspace-selected={{nspace.Name}}>{{nspace.Name}}</span>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
{{#popover-menu}}
|
<PopoverMenu @position="left">
|
||||||
{{#block-slot name='trigger'}}
|
<BlockSlot @name="trigger">
|
||||||
{{nspace.Name}}
|
{{nspace.Name}}
|
||||||
{{/block-slot}}
|
</BlockSlot>
|
||||||
{{#if (is-href 'dc.nspaces')}}
|
{{#if (is-href 'dc.nspaces')}}
|
||||||
{{#block-slot name='header'}}
|
<BlockSlot @name="header">
|
||||||
<p>
|
<p>
|
||||||
Namespaces themselves are not namespaced, so switching will not change the current view.
|
Namespaces themselves are not namespaced, so switching will not change the current view.
|
||||||
</p>
|
</p>
|
||||||
{{/block-slot}}
|
</BlockSlot>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{#block-slot name='menu'}}
|
<BlockSlot @name="menu">
|
||||||
<li role="separator">
|
<li role="separator">
|
||||||
Namespaces
|
Namespaces
|
||||||
|
<DataSource
|
||||||
|
@src="/*/*/namespaces"
|
||||||
|
@onchange={{action (mut nspaces) value="data"}}
|
||||||
|
@loading="lazy"
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
{{#each (reject-by 'DeletedAt' nspaces) as |item|}}
|
{{#each (reject-by 'DeletedAt' nspaces) as |item|}}
|
||||||
<li role="none" class={{if (eq nspace.Name item.Name) 'is-active'}}>
|
<li role="none" class={{if (eq nspace.Name item.Name) 'is-active'}}>
|
||||||
|
@ -42,8 +48,8 @@
|
||||||
<a tabindex="-1" role="menuitem" href={{href-to 'dc.nspaces' dc.Name}}>Manage namespaces</a>
|
<a tabindex="-1" role="menuitem" href={{href-to 'dc.nspaces' dc.Name}}>Manage namespaces</a>
|
||||||
</li>
|
</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/block-slot}}
|
</BlockSlot>
|
||||||
{{/popover-menu}}
|
</PopoverMenu>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</li>
|
</li>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
@ -51,21 +57,26 @@
|
||||||
{{#if (or (not dcs) (eq dcs.length 1)) }}
|
{{#if (or (not dcs) (eq dcs.length 1)) }}
|
||||||
<span data-test-datacenter-selected={{dc.Name}}>{{dc.Name}}</span>
|
<span data-test-datacenter-selected={{dc.Name}}>{{dc.Name}}</span>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
{{#popover-menu}}
|
<PopoverMenu @position="left">
|
||||||
{{#block-slot name='trigger'}}
|
<BlockSlot @name="trigger">
|
||||||
{{dc.Name}}
|
{{dc.Name}}
|
||||||
{{/block-slot}}
|
</BlockSlot>
|
||||||
{{#block-slot name='menu'}}
|
<BlockSlot @name="menu">
|
||||||
<li role="separator">
|
<li role="separator">
|
||||||
Datacenters
|
Datacenters
|
||||||
|
<DataSource
|
||||||
|
@src="/*/*/datacenters"
|
||||||
|
@onchange={{action (mut dcs) value="data"}}
|
||||||
|
@loading="lazy"
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
{{#each dcs as |item|}}
|
{{#each dcs as |item|}}
|
||||||
<li role="none" data-test-datacenter-picker class={{if (eq dc.Name item.Name) 'is-active'}}>
|
<li role="none" data-test-datacenter-picker class={{if (eq dc.Name item.Name) 'is-active'}}>
|
||||||
<a tabindex="-1" role="menuitem" href={{href-mut (hash dc=item.Name)}}>{{item.Name}}</a>
|
<a tabindex="-1" role="menuitem" href={{href-mut (hash dc=item.Name)}}>{{item.Name}}</a>
|
||||||
</li>
|
</li>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
{{/block-slot}}
|
</BlockSlot>
|
||||||
{{/popover-menu}}
|
</PopoverMenu>
|
||||||
|
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</li>
|
</li>
|
||||||
|
@ -89,12 +100,79 @@
|
||||||
</nav>
|
</nav>
|
||||||
<nav>
|
<nav>
|
||||||
<ul>
|
<ul>
|
||||||
<li data-test-main-nav-docs>
|
<li data-test-main-nav-help>
|
||||||
<a href="{{ env 'CONSUL_DOCS_URL'}}" rel="help noopener noreferrer" target="_blank">Documentation</a>
|
<PopoverMenu @position="right">
|
||||||
|
<BlockSlot @name="trigger">
|
||||||
|
Help
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="menu" as |id send keypressClick change|>
|
||||||
|
<li role="none" class="docs-link">
|
||||||
|
<a tabindex="-1" role="menuitem" href={{env 'CONSUL_DOCS_URL'}} rel="noopener noreferrer" target="_blank" onclick={{change}}>Documentation</a>
|
||||||
|
</li>
|
||||||
|
<li role="none" class="learn-link">
|
||||||
|
<a tabindex="-1" role="menuitem" href={{concat (env 'CONSUL_DOCS_LEARN_URL') '/consul'}} rel="noopener noreferrer" target="_blank" onclick={{change}}>HashiCorp Learn</a>
|
||||||
|
</li>
|
||||||
|
<li role="separator"></li>
|
||||||
|
<li role="none" class="feedback-link">
|
||||||
|
<a tabindex="-1" role="menuitem" href={{env 'CONSUL_REPO_ISSUES_URL'}} target="_blank" rel="noopener noreferrer" onclick={{change}}>Provide Feedback</a>
|
||||||
|
</li>
|
||||||
|
</BlockSlot>
|
||||||
|
</PopoverMenu>
|
||||||
</li>
|
</li>
|
||||||
<li data-test-main-nav-settings class={{if (is-href 'settings') 'is-active'}}>
|
<li data-test-main-nav-settings class={{if (is-href 'settings') 'is-active'}}>
|
||||||
<a href={{href-to 'settings'}}>Settings</a>
|
<a href={{href-to 'settings'}}>Settings</a>
|
||||||
</li>
|
</li>
|
||||||
|
{{#if (env 'CONSUL_ACLS_ENABLED')}}
|
||||||
|
<li data-test-main-nav-auth>
|
||||||
|
<AuthDialog
|
||||||
|
@dc={{dc.Name}}
|
||||||
|
@nspace={{nspace.Name}}
|
||||||
|
@onchange={{action onchange}} as |authDialog components|
|
||||||
|
>
|
||||||
|
{{#let components.AuthForm components.AuthProfile as |AuthForm AuthProfile|}}
|
||||||
|
<BlockSlot @name="unauthorized">
|
||||||
|
<label tabindex="0" for="login-toggle" onkeypress={{action 'keypressClick'}}>
|
||||||
|
<span>Log in</span>
|
||||||
|
</label>
|
||||||
|
<ModalDialog @name="login-toggle" @onclose={{action 'close'}} @onopen={{action 'open'}}>
|
||||||
|
<BlockSlot @name="header">
|
||||||
|
<h2>Log in to Consul</h2>
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="body">
|
||||||
|
<AuthForm as |api|>
|
||||||
|
<Ref @target={{this}} @name="authForm" @value={{api}} />
|
||||||
|
</AuthForm>
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="actions" as |close|>
|
||||||
|
<button type="button" onclick={{action close}}>
|
||||||
|
Continue without logging in
|
||||||
|
</button>
|
||||||
|
</BlockSlot>
|
||||||
|
</ModalDialog>
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="authorized">
|
||||||
|
<PopoverMenu @position="right">
|
||||||
|
<BlockSlot @name="trigger">
|
||||||
|
Logout
|
||||||
|
</BlockSlot>
|
||||||
|
<BlockSlot @name="menu">
|
||||||
|
{{!TODO: It might be nice to use one of our recursive components here}}
|
||||||
|
{{#if authDialog.token.AccessorID}}
|
||||||
|
<li role="none">
|
||||||
|
<AuthProfile />
|
||||||
|
</li>
|
||||||
|
<li role="separator"></li>
|
||||||
|
{{/if}}
|
||||||
|
<li class="dangerous" role="none">
|
||||||
|
<button type="button" tabindex="-1" role="menuitem" onclick={{action authDialog.logout}}>Logout</button>
|
||||||
|
</li>
|
||||||
|
</BlockSlot>
|
||||||
|
</PopoverMenu>
|
||||||
|
</BlockSlot>
|
||||||
|
{{/let}}
|
||||||
|
</AuthDialog>
|
||||||
|
</li>
|
||||||
|
{{/if}}
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
@ -108,4 +186,3 @@
|
||||||
<a data-test-footer-docs href="{{env 'CONSUL_DOCS_URL'}}" rel="help noopener noreferrer" target="_blank">Documentation</a>
|
<a data-test-footer-docs href="{{env 'CONSUL_DOCS_URL'}}" rel="help noopener noreferrer" target="_blank">Documentation</a>
|
||||||
{{{concat '<!-- ' (env 'CONSUL_GIT_SHA') '-->'}}}
|
{{{concat '<!-- ' (env 'CONSUL_GIT_SHA') '-->'}}}
|
||||||
</footer>
|
</footer>
|
||||||
{{modal-layer}}
|
|
|
@ -4,7 +4,9 @@ import { computed } from '@ember/object';
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
dom: service('dom'),
|
dom: service('dom'),
|
||||||
|
|
||||||
didInsertElement: function() {
|
didInsertElement: function() {
|
||||||
|
this._super(...arguments);
|
||||||
this.dom.root().classList.remove('template-with-vertical-menu');
|
this.dom.root().classList.remove('template-with-vertical-menu');
|
||||||
},
|
},
|
||||||
// TODO: Right now this is the only place where we need permissions
|
// TODO: Right now this is the only place where we need permissions
|
||||||
|
@ -17,6 +19,15 @@ export default Component.extend({
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
|
keypressClick: function(e) {
|
||||||
|
e.target.dispatchEvent(new MouseEvent('click'));
|
||||||
|
},
|
||||||
|
open: function() {
|
||||||
|
this.authForm.focus();
|
||||||
|
},
|
||||||
|
close: function() {
|
||||||
|
this.authForm.reset();
|
||||||
|
},
|
||||||
change: function(e) {
|
change: function(e) {
|
||||||
const win = this.dom.viewport();
|
const win = this.dom.viewport();
|
||||||
const $root = this.dom.root();
|
const $root = this.dom.root();
|
|
@ -0,0 +1,9 @@
|
||||||
|
{{#if (and (lt passing 1) (lt warning 1) (lt critical 1) )}}
|
||||||
|
<span title="No Healthchecks" class="zero">0</span>
|
||||||
|
{{else}}
|
||||||
|
<dl class="healthcheck-info">
|
||||||
|
<HealthcheckStatus @width={{passingWidth}} @name="passing" @value={{passing}} />
|
||||||
|
<HealthcheckStatus @width={{warningWidth}} @name="warning" @value={{warning}} />
|
||||||
|
<HealthcheckStatus @width={{criticalWidth}} @name="critical" @value={{critical}} />
|
||||||
|
</dl>
|
||||||
|
{{/if}}
|
|
@ -1,18 +1,19 @@
|
||||||
<ul data-test-healthchecks>
|
<ul>
|
||||||
{{#each (sort-by (action 'sortChecksByImportance') items) as |item| }}
|
{{#each (sort-by (action 'sortChecksByImportance') items) as |item| }}
|
||||||
{{! TODO: this component and its child should be moved to a single component }}
|
{{! TODO: this component and its child should be moved to a single component }}
|
||||||
{{#healthcheck-output
|
<HealthcheckOutput class={{item.Status}} @tagName="li">
|
||||||
data-test-node-healthcheck=item.Name
|
<BlockSlot @name="header">
|
||||||
class=item.Status
|
|
||||||
tagName='li'
|
|
||||||
}}
|
|
||||||
{{#block-slot name='header'}}
|
|
||||||
<h3>{{item.Name}}</h3>
|
<h3>{{item.Name}}</h3>
|
||||||
{{/block-slot}}
|
</BlockSlot>
|
||||||
{{#block-slot name='content'}}
|
<BlockSlot @name="content">
|
||||||
<dl>
|
<dl>
|
||||||
|
{{#if (eq item.ServiceName "")}}
|
||||||
|
<dt>NodeName</dt>
|
||||||
|
<dd>{{item.Node}}</dd>
|
||||||
|
{{else}}
|
||||||
<dt>ServiceName</dt>
|
<dt>ServiceName</dt>
|
||||||
<dd>{{or item.ServiceName '-'}}</dd>
|
<dd>{{item.ServiceName}}</dd>
|
||||||
|
{{/if}}
|
||||||
</dl>
|
</dl>
|
||||||
<dl>
|
<dl>
|
||||||
<dt>CheckID</dt>
|
<dt>CheckID</dt>
|
||||||
|
@ -36,25 +37,11 @@
|
||||||
<dt>Output</dt>
|
<dt>Output</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<pre><code>{{item.Output}}</code></pre>
|
<pre><code>{{item.Output}}</code></pre>
|
||||||
{{#feedback-dialog type='inline'}}
|
<CopyButton @value={{item.Output}} @name="output" />
|
||||||
{{#block-slot name='action' as |success error|}}
|
|
||||||
{{copy-button success=(action success) error=(action error) clipboardText=item.Output title='copy output to clipboard'}}
|
|
||||||
{{/block-slot}}
|
|
||||||
{{#block-slot name='success' as |transition|}}
|
|
||||||
<p class={{transition}}>
|
|
||||||
Copied output!
|
|
||||||
</p>
|
|
||||||
{{/block-slot}}
|
|
||||||
{{#block-slot name='error' as |transition|}}
|
|
||||||
<p class={{transition}}>
|
|
||||||
Sorry, something went wrong!
|
|
||||||
</p>
|
|
||||||
{{/block-slot}}
|
|
||||||
{{/feedback-dialog}}
|
|
||||||
</dd>
|
</dd>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</dl>
|
</dl>
|
||||||
{{/block-slot}}
|
</BlockSlot>
|
||||||
{{/healthcheck-output}}
|
</HealthcheckOutput>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</ul>
|
</ul>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue