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:
|
||||
go: &GOLANG_IMAGE circleci/golang:1.14.1
|
||||
middleman: &MIDDLEMAN_IMAGE hashicorp/middleman-hashicorp:0.3.40
|
||||
ember: &EMBER_IMAGE circleci/node:8-browsers
|
||||
ember: &EMBER_IMAGE circleci/node:12-browsers
|
||||
|
||||
paths:
|
||||
test-results: &TEST_RESULTS_DIR /tmp/test-results
|
||||
|
||||
cache:
|
||||
yarn: &YARN_CACHE_KEY consul-ui-v1-{{ checksum "ui-v2/yarn.lock" }}
|
||||
yarn: &YARN_CACHE_KEY consul-ui-v2-{{ checksum "ui-v2/yarn.lock" }}
|
||||
rubygem: &RUBYGEM_CACHE_KEY static-site-gems-v1-{{ checksum "Gemfile.lock" }}
|
||||
|
||||
environment: &ENVIRONMENT
|
||||
|
@ -461,6 +461,9 @@ jobs:
|
|||
- image: *EMBER_IMAGE
|
||||
environment:
|
||||
EMBER_TEST_REPORT: test-results/report-oss.xml #outputs test report for CircleCI test summary
|
||||
EMBER_TEST_PARALLEL: true #enables test parallelization with ember-exam
|
||||
CONSUL_NSPACES_ENABLED: 0
|
||||
parallelism: 2
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -469,7 +472,7 @@ jobs:
|
|||
at: ui-v2
|
||||
- run:
|
||||
working_directory: ui-v2
|
||||
command: make test-oss-ci
|
||||
command: node_modules/.bin/ember exam --split=$CIRCLE_NODE_TOTAL --partition=`expr $CIRCLE_NODE_INDEX + 1` --path dist --silent -r xunit
|
||||
- store_test_results:
|
||||
path: ui-v2/test-results
|
||||
# run ember frontend tests
|
||||
|
@ -478,6 +481,8 @@ jobs:
|
|||
- image: *EMBER_IMAGE
|
||||
environment:
|
||||
EMBER_TEST_REPORT: test-results/report-ent.xml #outputs test report for CircleCI test summary
|
||||
EMBER_TEST_PARALLEL: true #enables test parallelization with ember-exam
|
||||
parallelism: 2
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
|
@ -486,9 +491,26 @@ jobs:
|
|||
at: ui-v2
|
||||
- run:
|
||||
working_directory: ui-v2
|
||||
command: make test-ci
|
||||
command: node_modules/.bin/ember exam --split=$CIRCLE_NODE_TOTAL --partition=`expr $CIRCLE_NODE_INDEX + 1` --path dist --silent -r xunit
|
||||
- store_test_results:
|
||||
path: ui-v2/test-results
|
||||
# run ember frontend unit tests to produce coverage report
|
||||
ember-coverage:
|
||||
docker:
|
||||
- image: *EMBER_IMAGE
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: *YARN_CACHE_KEY
|
||||
- attach_workspace:
|
||||
at: ui-v2
|
||||
- run:
|
||||
working_directory: ui-v2
|
||||
command: make test-coverage-ci
|
||||
- run:
|
||||
name: codecov ui upload
|
||||
working_directory: ui-v2
|
||||
command: bash <(curl -s https://codecov.io/bash) -v -c -C $CIRCLE_SHA1 -F ui
|
||||
|
||||
envoy-integration-test-1.11.2:
|
||||
docker:
|
||||
|
@ -682,6 +704,9 @@ workflows:
|
|||
- ember-test-ent:
|
||||
requires:
|
||||
- ember-build
|
||||
- ember-coverage:
|
||||
requires:
|
||||
- ember-build
|
||||
cherry-pick:
|
||||
jobs:
|
||||
- cherry-picker:
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
ARG ALPINE_VERSION=3.9
|
||||
ARG ALPINE_VERSION=3.11
|
||||
FROM alpine:${ALPINE_VERSION}
|
||||
|
||||
ARG NODEJS_VERSION=10.14.2-r0
|
||||
ARG NODEJS_VERSION=12.15.0-r1
|
||||
ARG MAKE_VERSION=4.2.1-r2
|
||||
ARG YARN_VERSION=1.19.1
|
||||
|
||||
|
|
|
@ -10,6 +10,8 @@ coverage:
|
|||
# https://docs.codecov.io/docs/commit-status#section-excluding-tests-example-
|
||||
# TODO: should any paths be excluded from coverage metrics?
|
||||
# paths:
|
||||
ui:
|
||||
informational: true
|
||||
# https://docs.codecov.io/docs/commit-status#section-changes-status
|
||||
# TODO: enable after eliminating current unexpected coverage changes?
|
||||
changes: off
|
||||
|
@ -29,7 +31,9 @@ comment: false
|
|||
|
||||
# https://docs.codecov.io/docs/flags
|
||||
# TODO: split out test coverage for API, SDK, UI, website?
|
||||
# flags:
|
||||
flags:
|
||||
ui:
|
||||
paths: /ui-v2/
|
||||
|
||||
ignore:
|
||||
- "agent/bindata_assetfs.go"
|
||||
|
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
|
|
|
@ -5,5 +5,14 @@
|
|||
|
||||
Setting `disableAnalytics` to true will prevent any data from being sent.
|
||||
*/
|
||||
"disableAnalytics": false
|
||||
"disableAnalytics": false,
|
||||
/**
|
||||
We use a nested in /components folder structure:
|
||||
/components/component-name/index.{hbs,js}
|
||||
*/
|
||||
"componentStructure": "nested",
|
||||
/**
|
||||
We currently use classic components
|
||||
*/
|
||||
"componentClass": "@ember/component"
|
||||
}
|
||||
|
|
|
@ -1,8 +1,12 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parser: 'babel-eslint',
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018,
|
||||
sourceType: 'module'
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
legacyDecorators: true
|
||||
}
|
||||
},
|
||||
plugins: ['ember'],
|
||||
extends: ['eslint:recommended', 'plugin:ember/recommended'],
|
||||
|
@ -11,7 +15,9 @@ module.exports = {
|
|||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { args: 'none' }],
|
||||
'ember/no-new-mixins': ['warn']
|
||||
'ember/no-new-mixins': ['warn'],
|
||||
'ember/no-jquery': 'warn',
|
||||
'ember/no-global-jquery': 'warn'
|
||||
},
|
||||
overrides: [
|
||||
// node files
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
instrumentation:
|
||||
excludes: [
|
||||
"!app/+(utils|search)/**/*"
|
||||
]
|
|
@ -1 +1 @@
|
|||
10
|
||||
12
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
module.exports = {
|
||||
extends: 'recommended',
|
||||
extends: 'octane',
|
||||
rules: {
|
||||
'no-partial': false,
|
||||
|
||||
|
@ -10,6 +10,7 @@ module.exports = {
|
|||
|
||||
'self-closing-void-elements': false,
|
||||
'no-unnecessary-concat': false,
|
||||
'no-quoteless-attributes': false,
|
||||
'no-nested-interactive': false,
|
||||
|
||||
'block-indentation': false,
|
||||
|
@ -19,6 +20,15 @@ module.exports = {
|
|||
'no-triple-curlies': false,
|
||||
'no-unused-block-params': false,
|
||||
'style-concatenation': false,
|
||||
'link-rel-noopener': false
|
||||
'link-rel-noopener': false,
|
||||
|
||||
'no-implicit-this': false,
|
||||
'no-curly-component-invocation': false,
|
||||
'no-action': false,
|
||||
'no-negated-condition': false,
|
||||
'no-invalid-role': false,
|
||||
|
||||
'no-unnecessary-component-helper': false,
|
||||
'link-href-attributes': false
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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:
|
||||
yarn run test:node
|
||||
|
||||
test-coverage: deps
|
||||
yarn run test:coverage
|
||||
|
||||
test-coverage-view: deps
|
||||
yarn run test:coverage:view
|
||||
|
||||
test-coverage-ci: deps
|
||||
yarn run test:coverage:ci
|
||||
|
||||
test-parallel: deps
|
||||
yarn run test:parallel
|
||||
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import Adapter from './http';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { env } from 'consul-ui/env';
|
||||
|
||||
export const DATACENTER_QUERY_PARAM = 'dc';
|
||||
export const NSPACE_QUERY_PARAM = 'ns';
|
||||
export default Adapter.extend({
|
||||
repo: service('settings'),
|
||||
client: service('client/http'),
|
||||
env: service('env'),
|
||||
formatNspace: function(nspace) {
|
||||
if (env('CONSUL_NSPACES_ENABLED')) {
|
||||
if (this.env.env('CONSUL_NSPACES_ENABLED')) {
|
||||
return nspace !== '' ? { [NSPACE_QUERY_PARAM]: nspace } : undefined;
|
||||
}
|
||||
},
|
||||
|
@ -17,45 +16,9 @@ export default Adapter.extend({
|
|||
[DATACENTER_QUERY_PARAM]: dc,
|
||||
};
|
||||
},
|
||||
// TODO: kinda protected for the moment
|
||||
// decide where this should go either read/write from http
|
||||
// should somehow use this or vice versa
|
||||
// TODO: Deprecated, remove `request` usage from everywhere and replace with
|
||||
// `HTTPAdapter.rpc`
|
||||
request: function(req, resp, obj, modelName) {
|
||||
const client = this.client;
|
||||
const store = this.store;
|
||||
const adapter = this;
|
||||
|
||||
let unserialized, serialized;
|
||||
const serializer = store.serializerFor(modelName);
|
||||
// workable way to decide whether this is a snapshot
|
||||
// essentially 'is attributable'.
|
||||
// Snapshot is private so we can't do instanceof here
|
||||
// and using obj.constructor.name gets changed/minified
|
||||
// during compilation so you can't rely on it
|
||||
// checking for `attributes` being a function is more
|
||||
// reliable as that is the thing we need to call
|
||||
if (typeof obj.attributes === 'function') {
|
||||
unserialized = obj.attributes();
|
||||
serialized = serializer.serialize(obj, {});
|
||||
} else {
|
||||
unserialized = obj;
|
||||
serialized = unserialized;
|
||||
}
|
||||
|
||||
return client
|
||||
.request(function(request) {
|
||||
return req(adapter, request, serialized, unserialized);
|
||||
})
|
||||
.catch(function(e) {
|
||||
return adapter.error(e);
|
||||
})
|
||||
.then(function(respond) {
|
||||
// TODO: When HTTPAdapter:responder changes, this will also need to change
|
||||
return resp(serializer, respond, serialized, unserialized);
|
||||
});
|
||||
// TODO: Potentially add specific serializer errors here
|
||||
// .catch(function(e) {
|
||||
// return Promise.reject(e);
|
||||
// });
|
||||
return this.rpc(...arguments);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Adapter from './application';
|
||||
|
||||
export default Adapter.extend({
|
||||
requestForFindAll: function(request) {
|
||||
requestForQuery: function(request) {
|
||||
return request`
|
||||
GET /v1/catalog/datacenters
|
||||
`;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { inject as service } from '@ember/service';
|
||||
import Adapter from 'ember-data/adapter';
|
||||
import AdapterError from '@ember-data/adapter/error';
|
||||
import {
|
||||
|
@ -10,46 +11,75 @@ import {
|
|||
ConflictError,
|
||||
InvalidError,
|
||||
} from 'ember-data/adapters/errors';
|
||||
// TODO: This is a little skeleton cb function
|
||||
// is to be replaced soon with something slightly more involved
|
||||
const responder = function(response) {
|
||||
return response;
|
||||
};
|
||||
const read = function(adapter, serializer, client, type, query) {
|
||||
return client
|
||||
.request(function(request) {
|
||||
|
||||
// TODO These are now exactly the same, apart from the fact that one uses
|
||||
// `serialized, unserialized` and the other just `query`
|
||||
// they could actually be one function now, but would be nice to think about
|
||||
// the naming of things (serialized vs query etc)
|
||||
const read = function(adapter, modelName, type, query = {}) {
|
||||
return adapter.rpc(
|
||||
function(adapter, request, query) {
|
||||
return adapter[`requestFor${type}`](request, query);
|
||||
})
|
||||
.catch(function(e) {
|
||||
return adapter.error(e);
|
||||
})
|
||||
.then(function(response) {
|
||||
return serializer[`respondFor${type}`](responder(response), query);
|
||||
});
|
||||
// TODO: Potentially add specific serializer errors here
|
||||
// .catch(function(e) {
|
||||
// return Promise.reject(e);
|
||||
// });
|
||||
},
|
||||
function(serializer, respond, query) {
|
||||
return serializer[`respondFor${type}`](respond, query);
|
||||
},
|
||||
query,
|
||||
modelName
|
||||
);
|
||||
};
|
||||
const write = function(adapter, serializer, client, type, snapshot) {
|
||||
const unserialized = snapshot.attributes();
|
||||
const serialized = serializer.serialize(snapshot, {});
|
||||
return client
|
||||
.request(function(request) {
|
||||
const write = function(adapter, modelName, type, snapshot) {
|
||||
return adapter.rpc(
|
||||
function(adapter, request, serialized, unserialized) {
|
||||
return adapter[`requestFor${type}`](request, serialized, unserialized);
|
||||
})
|
||||
.catch(function(e) {
|
||||
return adapter.error(e);
|
||||
})
|
||||
.then(function(response) {
|
||||
return serializer[`respondFor${type}`](responder(response), serialized, unserialized);
|
||||
});
|
||||
// TODO: Potentially add specific serializer errors here
|
||||
// .catch(function(e) {
|
||||
// return Promise.reject(e);
|
||||
// });
|
||||
},
|
||||
function(serializer, respond, serialized, unserialized) {
|
||||
return serializer[`respondFor${type}`](respond, serialized, unserialized);
|
||||
},
|
||||
snapshot,
|
||||
modelName
|
||||
);
|
||||
};
|
||||
export default Adapter.extend({
|
||||
client: service('client/http'),
|
||||
rpc: function(req, resp, obj, modelName) {
|
||||
const client = this.client;
|
||||
const store = this.store;
|
||||
const adapter = this;
|
||||
|
||||
let unserialized, serialized;
|
||||
const serializer = store.serializerFor(modelName);
|
||||
// workable way to decide whether this is a snapshot
|
||||
// essentially 'is attributable'.
|
||||
// Snapshot is private so we can't do instanceof here
|
||||
// and using obj.constructor.name gets changed/minified
|
||||
// during compilation so you can't rely on it
|
||||
// checking for `attributes` being a function is more
|
||||
// reliable as that is the thing we need to call
|
||||
if (typeof obj.attributes === 'function') {
|
||||
unserialized = obj.attributes();
|
||||
serialized = serializer.serialize(obj, {});
|
||||
} else {
|
||||
unserialized = obj;
|
||||
serialized = unserialized;
|
||||
}
|
||||
|
||||
return client
|
||||
.request(function(request) {
|
||||
return req(adapter, request, serialized, unserialized);
|
||||
})
|
||||
.catch(function(e) {
|
||||
return adapter.error(e);
|
||||
})
|
||||
.then(function(respond) {
|
||||
// TODO: When HTTPAdapter:responder changes, this will also need to change
|
||||
return resp(serializer, respond, serialized, unserialized);
|
||||
});
|
||||
// TODO: Potentially add specific serializer errors here
|
||||
// .catch(function(e) {
|
||||
// return Promise.reject(e);
|
||||
// });
|
||||
},
|
||||
error: function(err) {
|
||||
const errors = [
|
||||
{
|
||||
|
@ -94,24 +124,28 @@ export default Adapter.extend({
|
|||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
// TODO: This comes originates from ember-data
|
||||
// This can be confusing if you need to use this with Promise.reject
|
||||
// Consider changing this to return the error and then
|
||||
// throw from the call site instead
|
||||
throw error;
|
||||
},
|
||||
query: function(store, type, query) {
|
||||
return read(this, store.serializerFor(type.modelName), this.client, 'Query', query);
|
||||
return read(this, type.modelName, 'Query', query);
|
||||
},
|
||||
queryRecord: function(store, type, query) {
|
||||
return read(this, store.serializerFor(type.modelName), this.client, 'QueryRecord', query);
|
||||
return read(this, type.modelName, 'QueryRecord', query);
|
||||
},
|
||||
findAll: function(store, type) {
|
||||
return read(this, store.serializerFor(type.modelName), this.client, 'FindAll');
|
||||
return read(this, type.modelName, 'FindAll');
|
||||
},
|
||||
createRecord: function(store, type, snapshot) {
|
||||
return write(this, store.serializerFor(type.modelName), this.client, 'CreateRecord', snapshot);
|
||||
return write(this, type.modelName, 'CreateRecord', snapshot);
|
||||
},
|
||||
updateRecord: function(store, type, snapshot) {
|
||||
return write(this, store.serializerFor(type.modelName), this.client, 'UpdateRecord', snapshot);
|
||||
return write(this, type.modelName, 'UpdateRecord', snapshot);
|
||||
},
|
||||
deleteRecord: function(store, type, snapshot) {
|
||||
return write(this, store.serializerFor(type.modelName), this.client, 'DeleteRecord', snapshot);
|
||||
return write(this, type.modelName, 'DeleteRecord', snapshot);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -6,11 +6,14 @@ import { SLUG_KEY } from 'consul-ui/models/intention';
|
|||
|
||||
// TODO: Update to use this.formatDatacenter()
|
||||
export default Adapter.extend({
|
||||
requestForQuery: function(request, { dc, index, id }) {
|
||||
requestForQuery: function(request, { dc, filter, index }) {
|
||||
return request`
|
||||
GET /v1/connect/intentions?${{ dc }}
|
||||
|
||||
${{ index }}
|
||||
${{
|
||||
index,
|
||||
filter,
|
||||
}}
|
||||
`;
|
||||
},
|
||||
requestForQueryRecord: function(request, { dc, index, id }) {
|
||||
|
|
|
@ -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`
|
||||
GET /v1/acl/token/self?${{ dc }}
|
||||
X-Consul-Token: ${secret}
|
||||
Cache-Control: no-store
|
||||
|
||||
${{ index }}
|
||||
`;
|
||||
|
@ -132,7 +133,7 @@ export default Adapter.extend({
|
|||
return adapter.requestForSelf(request, serialized, data);
|
||||
},
|
||||
function(serializer, respond, serialized, data) {
|
||||
return serializer.respondForQueryRecord(respond, serialized, data);
|
||||
return serializer.respondForSelf(respond, serialized, data);
|
||||
},
|
||||
unserialized,
|
||||
type.modelName
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import Application from '@ember/application';
|
||||
import Resolver from './resolver';
|
||||
import Resolver from 'ember-resolver';
|
||||
import loadInitializers from 'ember-load-initializers';
|
||||
import config from './config/environment';
|
||||
|
||||
const App = Application.extend({
|
||||
modulePrefix: config.modulePrefix,
|
||||
podModulePrefix: config.podModulePrefix,
|
||||
Resolver,
|
||||
});
|
||||
export default class App extends Application {
|
||||
modulePrefix = config.modulePrefix;
|
||||
podModulePrefix = config.podModulePrefix;
|
||||
Resolver = Resolver;
|
||||
}
|
||||
|
||||
loadInitializers(App, config.modulePrefix);
|
||||
|
||||
export default App;
|
||||
|
|
|
@ -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 { alias } from '@ember/object/computed';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { Promise } from 'rsvp';
|
||||
|
||||
import SlotsMixin from 'block-slots';
|
||||
import WithListeners from 'consul-ui/mixins/with-listeners';
|
||||
|
||||
export default Component.extend(SlotsMixin, WithListeners, {
|
||||
onchange: function() {},
|
||||
tagName: '',
|
||||
|
||||
error: function() {},
|
||||
type: '',
|
||||
|
@ -54,11 +54,6 @@ export default Component.extend(SlotsMixin, WithListeners, {
|
|||
reset: function() {
|
||||
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() {}) {
|
||||
// Specifically this saves an 'new' option/child
|
||||
// and then adds it to the selectedOptions, not options
|
||||
|
@ -69,20 +64,22 @@ export default Component.extend(SlotsMixin, WithListeners, {
|
|||
// 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
|
||||
item = repo.persist(item);
|
||||
this.listen(item, 'message', e => {
|
||||
this.actions.change.bind(this)(
|
||||
{
|
||||
target: {
|
||||
name: 'items[]',
|
||||
value: items,
|
||||
this.listen(item, {
|
||||
message: e => {
|
||||
this.actions.change.apply(this, [
|
||||
{
|
||||
target: {
|
||||
name: 'items[]',
|
||||
value: items,
|
||||
},
|
||||
},
|
||||
},
|
||||
items,
|
||||
e.data
|
||||
);
|
||||
success();
|
||||
items,
|
||||
e.data,
|
||||
]);
|
||||
success();
|
||||
},
|
||||
error: e => this.error(e),
|
||||
});
|
||||
this.listen(item, 'error', this.error.bind(this));
|
||||
},
|
||||
remove: function(item, items) {
|
||||
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';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'dl',
|
||||
classNames: ['tag-list'],
|
||||
tagName: '',
|
||||
});
|
|
@ -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" />
|
||||
</g>
|
||||
</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}} {
|
||||
opacity: 1 !important;
|
||||
|
||||
background-color: {{css-var '--white'}};
|
||||
border: {{css-var '--decor-border-100'}};
|
||||
border-radius: {{css-var '--decor-radius-300'}};
|
||||
border-color: {{css-var '--gray-500'}};
|
||||
box-shadow: 0 8px 10px 0 rgba(0, 0, 0, 0.1);
|
||||
background-color: var(--white);
|
||||
border: var(--decor-border-100);
|
||||
border-radius: var(--decor-radius-200);
|
||||
border-color: var(--gray-500);
|
||||
box-shadow: var(--decor-elevation-600);
|
||||
}
|
||||
{{/if}}
|
||||
{{#if selected.edges }}
|
||||
|
@ -28,7 +28,7 @@
|
|||
</header>
|
||||
<div role="group">
|
||||
{{#each routes as |item|}}
|
||||
{{route-card item=item onclick=(action 'click')}}
|
||||
<RouteCard @item={{item}} @onclick={{action "click"}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -43,7 +43,7 @@
|
|||
</header>
|
||||
<div role="group">
|
||||
{{#each (sort-by 'Name' splitters) as |item|}}
|
||||
{{splitter-card item=item onclick=(action 'click')}}
|
||||
<SplitterCard @item={{item}} @onclick={{action "click"}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -58,7 +58,7 @@
|
|||
</header>
|
||||
<div role="group">
|
||||
{{#each (sort-by 'Name' resolvers) as |item|}}
|
||||
{{resolver-card item=item onclick=(action 'click')}}
|
||||
<ResolverCard @item={{item}} @onclick={{action "click"}} />
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
|
@ -1,7 +1,6 @@
|
|||
import Component from '@ember/component';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { set, get, computed } from '@ember/object';
|
||||
import { next } from '@ember/runloop';
|
||||
|
||||
import {
|
||||
createRoute,
|
||||
|
@ -147,24 +146,21 @@ export default Component.extend({
|
|||
// the developer access to the mouse event therefore we just use JS to add our events
|
||||
// revisit this post Octane
|
||||
addPathListeners: function() {
|
||||
// TODO: Figure out if we can remove this next
|
||||
next(() => {
|
||||
this._listeners.remove();
|
||||
this._listeners.add(this.dom.document(), {
|
||||
click: e => {
|
||||
// all route/splitter/resolver components currently
|
||||
// have classes that end in '-card'
|
||||
if (!this.dom.closest('[class$="-card"]', e.target)) {
|
||||
set(this, 'active', false);
|
||||
set(this, 'selectedId', '');
|
||||
}
|
||||
},
|
||||
});
|
||||
[...this.dom.elements('path.split', this.element)].forEach(item => {
|
||||
this._listeners.add(item, {
|
||||
mouseover: e => this.actions.showSplit.apply(this, [e]),
|
||||
mouseout: e => this.actions.hideSplit.apply(this, [e]),
|
||||
});
|
||||
this._listeners.remove();
|
||||
this._listeners.add(this.dom.document(), {
|
||||
click: e => {
|
||||
// all route/splitter/resolver components currently
|
||||
// have classes that end in '-card'
|
||||
if (!this.dom.closest('[class$="-card"]', e.target)) {
|
||||
set(this, 'active', false);
|
||||
set(this, 'selectedId', '');
|
||||
}
|
||||
},
|
||||
});
|
||||
[...this.dom.elements('path.split', this.element)].forEach(item => {
|
||||
this._listeners.add(item, {
|
||||
mouseover: e => this.actions.showSplit.apply(this, [e]),
|
||||
mouseout: e => this.actions.hideSplit.apply(this, [e]),
|
||||
});
|
||||
});
|
||||
// TODO: currently don't think there is a way to listen
|
|
@ -9,10 +9,11 @@ export default Component.extend({
|
|||
},
|
||||
didInsertElement: function() {
|
||||
this._super(...arguments);
|
||||
this.buffer.add(this.getBufferName(), this.element);
|
||||
this._element = this.buffer.add(this.getBufferName(), this.element);
|
||||
},
|
||||
didDestroyElement: function() {
|
||||
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 { set } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { Promise } from 'rsvp';
|
||||
|
||||
import SlotsMixin from 'block-slots';
|
||||
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
|
||||
const propRe = /([^[\]])+/g;
|
||||
export default Component.extend(WithListeners, SlotsMixin, {
|
||||
tagName: '',
|
||||
onreset: function() {},
|
||||
onchange: 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,
|
||||
cellHeight: 113,
|
||||
style: style('getStyle'),
|
||||
classNames: ['list-collection'],
|
||||
classNames: ['grid-collection'],
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this.columns = [25, 25, 25, 25];
|
|
@ -1,3 +1,4 @@
|
|||
<ModalLayer />
|
||||
<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>
|
||||
<input type="checkbox" name="menu" id="main-nav-toggle" onchange={{action 'change'}} />
|
||||
|
@ -16,20 +17,25 @@
|
|||
{{#if (and (eq nspaces.length 1) (not canManageNspaces)) }}
|
||||
<span data-test-nspace-selected={{nspace.Name}}>{{nspace.Name}}</span>
|
||||
{{ else }}
|
||||
{{#popover-menu}}
|
||||
{{#block-slot name='trigger'}}
|
||||
<PopoverMenu @position="left">
|
||||
<BlockSlot @name="trigger">
|
||||
{{nspace.Name}}
|
||||
{{/block-slot}}
|
||||
</BlockSlot>
|
||||
{{#if (is-href 'dc.nspaces')}}
|
||||
{{#block-slot name='header'}}
|
||||
<BlockSlot @name="header">
|
||||
<p>
|
||||
Namespaces themselves are not namespaced, so switching will not change the current view.
|
||||
</p>
|
||||
{{/block-slot}}
|
||||
</BlockSlot>
|
||||
{{/if}}
|
||||
{{#block-slot name='menu'}}
|
||||
<BlockSlot @name="menu">
|
||||
<li role="separator">
|
||||
Namespaces
|
||||
<DataSource
|
||||
@src="/*/*/namespaces"
|
||||
@onchange={{action (mut nspaces) value="data"}}
|
||||
@loading="lazy"
|
||||
/>
|
||||
</li>
|
||||
{{#each (reject-by 'DeletedAt' nspaces) as |item|}}
|
||||
<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>
|
||||
</li>
|
||||
{{/if}}
|
||||
{{/block-slot}}
|
||||
{{/popover-menu}}
|
||||
</BlockSlot>
|
||||
</PopoverMenu>
|
||||
{{/if}}
|
||||
</li>
|
||||
{{/if}}
|
||||
|
@ -51,21 +57,26 @@
|
|||
{{#if (or (not dcs) (eq dcs.length 1)) }}
|
||||
<span data-test-datacenter-selected={{dc.Name}}>{{dc.Name}}</span>
|
||||
{{ else }}
|
||||
{{#popover-menu}}
|
||||
{{#block-slot name='trigger'}}
|
||||
<PopoverMenu @position="left">
|
||||
<BlockSlot @name="trigger">
|
||||
{{dc.Name}}
|
||||
{{/block-slot}}
|
||||
{{#block-slot name='menu'}}
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="menu">
|
||||
<li role="separator">
|
||||
Datacenters
|
||||
<DataSource
|
||||
@src="/*/*/datacenters"
|
||||
@onchange={{action (mut dcs) value="data"}}
|
||||
@loading="lazy"
|
||||
/>
|
||||
</li>
|
||||
{{#each dcs as |item|}}
|
||||
<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>
|
||||
</li>
|
||||
{{/each}}
|
||||
{{/block-slot}}
|
||||
{{/popover-menu}}
|
||||
</BlockSlot>
|
||||
</PopoverMenu>
|
||||
|
||||
{{/if}}
|
||||
</li>
|
||||
|
@ -89,12 +100,79 @@
|
|||
</nav>
|
||||
<nav>
|
||||
<ul>
|
||||
<li data-test-main-nav-docs>
|
||||
<a href="{{ env 'CONSUL_DOCS_URL'}}" rel="help noopener noreferrer" target="_blank">Documentation</a>
|
||||
<li data-test-main-nav-help>
|
||||
<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 data-test-main-nav-settings class={{if (is-href 'settings') 'is-active'}}>
|
||||
<a href={{href-to 'settings'}}>Settings</a>
|
||||
</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>
|
||||
</nav>
|
||||
</div>
|
||||
|
@ -107,5 +185,4 @@
|
|||
<p data-test-footer-version>Consul {{env 'CONSUL_VERSION'}}</p>
|
||||
<a data-test-footer-docs href="{{env 'CONSUL_DOCS_URL'}}" rel="help noopener noreferrer" target="_blank">Documentation</a>
|
||||
{{{concat '<!-- ' (env 'CONSUL_GIT_SHA') '-->'}}}
|
||||
</footer>
|
||||
{{modal-layer}}
|
||||
</footer>
|
|
@ -4,7 +4,9 @@ import { computed } from '@ember/object';
|
|||
|
||||
export default Component.extend({
|
||||
dom: service('dom'),
|
||||
|
||||
didInsertElement: function() {
|
||||
this._super(...arguments);
|
||||
this.dom.root().classList.remove('template-with-vertical-menu');
|
||||
},
|
||||
// TODO: Right now this is the only place where we need permissions
|
||||
|
@ -17,6 +19,15 @@ export default Component.extend({
|
|||
);
|
||||
}),
|
||||
actions: {
|
||||
keypressClick: function(e) {
|
||||
e.target.dispatchEvent(new MouseEvent('click'));
|
||||
},
|
||||
open: function() {
|
||||
this.authForm.focus();
|
||||
},
|
||||
close: function() {
|
||||
this.authForm.reset();
|
||||
},
|
||||
change: function(e) {
|
||||
const win = this.dom.viewport();
|
||||
const $root = this.dom.root();
|
|
@ -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| }}
|
||||
{{! TODO: this component and its child should be moved to a single component }}
|
||||
{{#healthcheck-output
|
||||
data-test-node-healthcheck=item.Name
|
||||
class=item.Status
|
||||
tagName='li'
|
||||
}}
|
||||
{{#block-slot name='header'}}
|
||||
<HealthcheckOutput class={{item.Status}} @tagName="li">
|
||||
<BlockSlot @name="header">
|
||||
<h3>{{item.Name}}</h3>
|
||||
{{/block-slot}}
|
||||
{{#block-slot name='content'}}
|
||||
</BlockSlot>
|
||||
<BlockSlot @name="content">
|
||||
<dl>
|
||||
{{#if (eq item.ServiceName "")}}
|
||||
<dt>NodeName</dt>
|
||||
<dd>{{item.Node}}</dd>
|
||||
{{else}}
|
||||
<dt>ServiceName</dt>
|
||||
<dd>{{or item.ServiceName '-'}}</dd>
|
||||
<dd>{{item.ServiceName}}</dd>
|
||||
{{/if}}
|
||||
</dl>
|
||||
<dl>
|
||||
<dt>CheckID</dt>
|
||||
|
@ -36,25 +37,11 @@
|
|||
<dt>Output</dt>
|
||||
<dd>
|
||||
<pre><code>{{item.Output}}</code></pre>
|
||||
{{#feedback-dialog type='inline'}}
|
||||
{{#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}}
|
||||
<CopyButton @value={{item.Output}} @name="output" />
|
||||
</dd>
|
||||
{{/if}}
|
||||
</dl>
|
||||
{{/block-slot}}
|
||||
{{/healthcheck-output}}
|
||||
</BlockSlot>
|
||||
</HealthcheckOutput>
|
||||
{{/each}}
|
||||
</ul>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue