diff --git a/.circleci/config.yml b/.circleci/config.yml index 0d1ed3f2d6..ac68c205e0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -5,13 +5,13 @@ references: images: go: &GOLANG_IMAGE circleci/golang:1.14.1 middleman: &MIDDLEMAN_IMAGE hashicorp/middleman-hashicorp:0.3.40 - ember: &EMBER_IMAGE circleci/node:8-browsers + ember: &EMBER_IMAGE circleci/node:12-browsers paths: test-results: &TEST_RESULTS_DIR /tmp/test-results cache: - yarn: &YARN_CACHE_KEY consul-ui-v1-{{ checksum "ui-v2/yarn.lock" }} + yarn: &YARN_CACHE_KEY consul-ui-v2-{{ checksum "ui-v2/yarn.lock" }} rubygem: &RUBYGEM_CACHE_KEY static-site-gems-v1-{{ checksum "Gemfile.lock" }} environment: &ENVIRONMENT @@ -461,6 +461,9 @@ jobs: - image: *EMBER_IMAGE environment: EMBER_TEST_REPORT: test-results/report-oss.xml #outputs test report for CircleCI test summary + EMBER_TEST_PARALLEL: true #enables test parallelization with ember-exam + CONSUL_NSPACES_ENABLED: 0 + parallelism: 2 steps: - checkout - restore_cache: @@ -469,7 +472,7 @@ jobs: at: ui-v2 - run: working_directory: ui-v2 - command: make test-oss-ci + command: node_modules/.bin/ember exam --split=$CIRCLE_NODE_TOTAL --partition=`expr $CIRCLE_NODE_INDEX + 1` --path dist --silent -r xunit - store_test_results: path: ui-v2/test-results # run ember frontend tests @@ -478,6 +481,8 @@ jobs: - image: *EMBER_IMAGE environment: EMBER_TEST_REPORT: test-results/report-ent.xml #outputs test report for CircleCI test summary + EMBER_TEST_PARALLEL: true #enables test parallelization with ember-exam + parallelism: 2 steps: - checkout - restore_cache: @@ -486,9 +491,26 @@ jobs: at: ui-v2 - run: working_directory: ui-v2 - command: make test-ci + command: node_modules/.bin/ember exam --split=$CIRCLE_NODE_TOTAL --partition=`expr $CIRCLE_NODE_INDEX + 1` --path dist --silent -r xunit - store_test_results: path: ui-v2/test-results + # run ember frontend unit tests to produce coverage report + ember-coverage: + docker: + - image: *EMBER_IMAGE + steps: + - checkout + - restore_cache: + key: *YARN_CACHE_KEY + - attach_workspace: + at: ui-v2 + - run: + working_directory: ui-v2 + command: make test-coverage-ci + - run: + name: codecov ui upload + working_directory: ui-v2 + command: bash <(curl -s https://codecov.io/bash) -v -c -C $CIRCLE_SHA1 -F ui envoy-integration-test-1.11.2: docker: @@ -682,6 +704,9 @@ workflows: - ember-test-ent: requires: - ember-build + - ember-coverage: + requires: + - ember-build cherry-pick: jobs: - cherry-picker: diff --git a/build-support/docker/Build-UI.dockerfile b/build-support/docker/Build-UI.dockerfile index 642ad13ca8..dc0532e4e2 100644 --- a/build-support/docker/Build-UI.dockerfile +++ b/build-support/docker/Build-UI.dockerfile @@ -1,7 +1,7 @@ -ARG ALPINE_VERSION=3.9 +ARG ALPINE_VERSION=3.11 FROM alpine:${ALPINE_VERSION} -ARG NODEJS_VERSION=10.14.2-r0 +ARG NODEJS_VERSION=12.15.0-r1 ARG MAKE_VERSION=4.2.1-r2 ARG YARN_VERSION=1.19.1 diff --git a/codecov.yml b/codecov.yml index 9bb643f3ae..1839b76dca 100644 --- a/codecov.yml +++ b/codecov.yml @@ -10,6 +10,8 @@ coverage: # https://docs.codecov.io/docs/commit-status#section-excluding-tests-example- # TODO: should any paths be excluded from coverage metrics? # paths: + ui: + informational: true # https://docs.codecov.io/docs/commit-status#section-changes-status # TODO: enable after eliminating current unexpected coverage changes? changes: off @@ -29,7 +31,9 @@ comment: false # https://docs.codecov.io/docs/flags # TODO: split out test coverage for API, SDK, UI, website? -# flags: +flags: + ui: + paths: /ui-v2/ ignore: - "agent/bindata_assetfs.go" diff --git a/ui-v2/.editorconfig b/ui-v2/.editorconfig index 13bc6da1f4..189bfcf026 100644 --- a/ui-v2/.editorconfig +++ b/ui-v2/.editorconfig @@ -4,7 +4,6 @@ root = true - [*] end_of_line = lf charset = utf-8 diff --git a/ui-v2/.ember-cli b/ui-v2/.ember-cli index ee64cfed2a..7f520c46ae 100644 --- a/ui-v2/.ember-cli +++ b/ui-v2/.ember-cli @@ -5,5 +5,14 @@ Setting `disableAnalytics` to true will prevent any data from being sent. */ - "disableAnalytics": false + "disableAnalytics": false, + /** + We use a nested in /components folder structure: + /components/component-name/index.{hbs,js} + */ + "componentStructure": "nested", + /** + We currently use classic components + */ + "componentClass": "@ember/component" } diff --git a/ui-v2/.eslintrc.js b/ui-v2/.eslintrc.js index e35e1db2a9..ce7e9cee4a 100644 --- a/ui-v2/.eslintrc.js +++ b/ui-v2/.eslintrc.js @@ -1,8 +1,12 @@ module.exports = { root: true, + parser: 'babel-eslint', parserOptions: { ecmaVersion: 2018, - sourceType: 'module' + sourceType: 'module', + ecmaFeatures: { + legacyDecorators: true + } }, plugins: ['ember'], extends: ['eslint:recommended', 'plugin:ember/recommended'], @@ -11,7 +15,9 @@ module.exports = { }, rules: { 'no-unused-vars': ['error', { args: 'none' }], - 'ember/no-new-mixins': ['warn'] + 'ember/no-new-mixins': ['warn'], + 'ember/no-jquery': 'warn', + 'ember/no-global-jquery': 'warn' }, overrides: [ // node files diff --git a/ui-v2/.istanbul.yml b/ui-v2/.istanbul.yml new file mode 100644 index 0000000000..5485bf6d76 --- /dev/null +++ b/ui-v2/.istanbul.yml @@ -0,0 +1,4 @@ +instrumentation: + excludes: [ + "!app/+(utils|search)/**/*" + ] diff --git a/ui-v2/.nvmrc b/ui-v2/.nvmrc index f599e28b8a..48082f72f0 100644 --- a/ui-v2/.nvmrc +++ b/ui-v2/.nvmrc @@ -1 +1 @@ -10 +12 diff --git a/ui-v2/.template-lintrc.js b/ui-v2/.template-lintrc.js index c80b1341e6..a87bfda6cc 100644 --- a/ui-v2/.template-lintrc.js +++ b/ui-v2/.template-lintrc.js @@ -1,7 +1,7 @@ 'use strict'; module.exports = { - extends: 'recommended', + extends: 'octane', rules: { 'no-partial': false, @@ -10,6 +10,7 @@ module.exports = { 'self-closing-void-elements': false, 'no-unnecessary-concat': false, + 'no-quoteless-attributes': false, 'no-nested-interactive': false, 'block-indentation': false, @@ -19,6 +20,15 @@ module.exports = { 'no-triple-curlies': false, 'no-unused-block-params': false, 'style-concatenation': false, - 'link-rel-noopener': false + 'link-rel-noopener': false, + + 'no-implicit-this': false, + 'no-curly-component-invocation': false, + 'no-action': false, + 'no-negated-condition': false, + 'no-invalid-role': false, + + 'no-unnecessary-component-helper': false, + 'link-href-attributes': false }, }; diff --git a/ui-v2/.travis.yml b/ui-v2/.travis.yml new file mode 100644 index 0000000000..a0208ca37f --- /dev/null +++ b/ui-v2/.travis.yml @@ -0,0 +1,27 @@ +--- +language: node_js +node_js: + - "10" + +dist: trusty + +addons: + chrome: stable + +cache: + directories: + - $HOME/.npm + +env: + global: + # See https://git.io/vdao3 for details. + - JOBS=1 + +branches: + only: + - master + +script: + - npm run lint:hbs + - npm run lint:js + - npm test diff --git a/ui-v2/GNUmakefile b/ui-v2/GNUmakefile index cfed057e4e..dfff38541f 100644 --- a/ui-v2/GNUmakefile +++ b/ui-v2/GNUmakefile @@ -65,6 +65,15 @@ test-oss-ci: deps test-node test-node: yarn run test:node +test-coverage: deps + yarn run test:coverage + +test-coverage-view: deps + yarn run test:coverage:view + +test-coverage-ci: deps + yarn run test:coverage:ci + test-parallel: deps yarn run test:parallel diff --git a/ui-v2/app/adapters/application.js b/ui-v2/app/adapters/application.js index 5dede706f4..45cb2e7291 100644 --- a/ui-v2/app/adapters/application.js +++ b/ui-v2/app/adapters/application.js @@ -1,14 +1,13 @@ import Adapter from './http'; import { inject as service } from '@ember/service'; -import { env } from 'consul-ui/env'; export const DATACENTER_QUERY_PARAM = 'dc'; export const NSPACE_QUERY_PARAM = 'ns'; export default Adapter.extend({ - repo: service('settings'), client: service('client/http'), + env: service('env'), formatNspace: function(nspace) { - if (env('CONSUL_NSPACES_ENABLED')) { + if (this.env.env('CONSUL_NSPACES_ENABLED')) { return nspace !== '' ? { [NSPACE_QUERY_PARAM]: nspace } : undefined; } }, @@ -17,45 +16,9 @@ export default Adapter.extend({ [DATACENTER_QUERY_PARAM]: dc, }; }, - // TODO: kinda protected for the moment - // decide where this should go either read/write from http - // should somehow use this or vice versa + // TODO: Deprecated, remove `request` usage from everywhere and replace with + // `HTTPAdapter.rpc` request: function(req, resp, obj, modelName) { - const client = this.client; - const store = this.store; - const adapter = this; - - let unserialized, serialized; - const serializer = store.serializerFor(modelName); - // workable way to decide whether this is a snapshot - // essentially 'is attributable'. - // Snapshot is private so we can't do instanceof here - // and using obj.constructor.name gets changed/minified - // during compilation so you can't rely on it - // checking for `attributes` being a function is more - // reliable as that is the thing we need to call - if (typeof obj.attributes === 'function') { - unserialized = obj.attributes(); - serialized = serializer.serialize(obj, {}); - } else { - unserialized = obj; - serialized = unserialized; - } - - return client - .request(function(request) { - return req(adapter, request, serialized, unserialized); - }) - .catch(function(e) { - return adapter.error(e); - }) - .then(function(respond) { - // TODO: When HTTPAdapter:responder changes, this will also need to change - return resp(serializer, respond, serialized, unserialized); - }); - // TODO: Potentially add specific serializer errors here - // .catch(function(e) { - // return Promise.reject(e); - // }); + return this.rpc(...arguments); }, }); diff --git a/ui-v2/app/adapters/dc.js b/ui-v2/app/adapters/dc.js index 3d17d70b83..e98b054657 100644 --- a/ui-v2/app/adapters/dc.js +++ b/ui-v2/app/adapters/dc.js @@ -1,7 +1,7 @@ import Adapter from './application'; export default Adapter.extend({ - requestForFindAll: function(request) { + requestForQuery: function(request) { return request` GET /v1/catalog/datacenters `; diff --git a/ui-v2/app/adapters/http.js b/ui-v2/app/adapters/http.js index 294b7c83a5..e8bd64e244 100644 --- a/ui-v2/app/adapters/http.js +++ b/ui-v2/app/adapters/http.js @@ -1,3 +1,4 @@ +import { inject as service } from '@ember/service'; import Adapter from 'ember-data/adapter'; import AdapterError from '@ember-data/adapter/error'; import { @@ -10,46 +11,75 @@ import { ConflictError, InvalidError, } from 'ember-data/adapters/errors'; -// TODO: This is a little skeleton cb function -// is to be replaced soon with something slightly more involved -const responder = function(response) { - return response; -}; -const read = function(adapter, serializer, client, type, query) { - return client - .request(function(request) { + +// TODO These are now exactly the same, apart from the fact that one uses +// `serialized, unserialized` and the other just `query` +// they could actually be one function now, but would be nice to think about +// the naming of things (serialized vs query etc) +const read = function(adapter, modelName, type, query = {}) { + return adapter.rpc( + function(adapter, request, query) { return adapter[`requestFor${type}`](request, query); - }) - .catch(function(e) { - return adapter.error(e); - }) - .then(function(response) { - return serializer[`respondFor${type}`](responder(response), query); - }); - // TODO: Potentially add specific serializer errors here - // .catch(function(e) { - // return Promise.reject(e); - // }); + }, + function(serializer, respond, query) { + return serializer[`respondFor${type}`](respond, query); + }, + query, + modelName + ); }; -const write = function(adapter, serializer, client, type, snapshot) { - const unserialized = snapshot.attributes(); - const serialized = serializer.serialize(snapshot, {}); - return client - .request(function(request) { +const write = function(adapter, modelName, type, snapshot) { + return adapter.rpc( + function(adapter, request, serialized, unserialized) { return adapter[`requestFor${type}`](request, serialized, unserialized); - }) - .catch(function(e) { - return adapter.error(e); - }) - .then(function(response) { - return serializer[`respondFor${type}`](responder(response), serialized, unserialized); - }); - // TODO: Potentially add specific serializer errors here - // .catch(function(e) { - // return Promise.reject(e); - // }); + }, + function(serializer, respond, serialized, unserialized) { + return serializer[`respondFor${type}`](respond, serialized, unserialized); + }, + snapshot, + modelName + ); }; export default Adapter.extend({ + client: service('client/http'), + rpc: function(req, resp, obj, modelName) { + const client = this.client; + const store = this.store; + const adapter = this; + + let unserialized, serialized; + const serializer = store.serializerFor(modelName); + // workable way to decide whether this is a snapshot + // essentially 'is attributable'. + // Snapshot is private so we can't do instanceof here + // and using obj.constructor.name gets changed/minified + // during compilation so you can't rely on it + // checking for `attributes` being a function is more + // reliable as that is the thing we need to call + if (typeof obj.attributes === 'function') { + unserialized = obj.attributes(); + serialized = serializer.serialize(obj, {}); + } else { + unserialized = obj; + serialized = unserialized; + } + + return client + .request(function(request) { + return req(adapter, request, serialized, unserialized); + }) + .catch(function(e) { + return adapter.error(e); + }) + .then(function(respond) { + // TODO: When HTTPAdapter:responder changes, this will also need to change + return resp(serializer, respond, serialized, unserialized); + }); + // TODO: Potentially add specific serializer errors here + // .catch(function(e) { + // return Promise.reject(e); + // }); + }, error: function(err) { const errors = [ { @@ -94,24 +124,28 @@ export default Adapter.extend({ } catch (e) { error = e; } + // TODO: This comes originates from ember-data + // This can be confusing if you need to use this with Promise.reject + // Consider changing this to return the error and then + // throw from the call site instead throw error; }, query: function(store, type, query) { - return read(this, store.serializerFor(type.modelName), this.client, 'Query', query); + return read(this, type.modelName, 'Query', query); }, queryRecord: function(store, type, query) { - return read(this, store.serializerFor(type.modelName), this.client, 'QueryRecord', query); + return read(this, type.modelName, 'QueryRecord', query); }, findAll: function(store, type) { - return read(this, store.serializerFor(type.modelName), this.client, 'FindAll'); + return read(this, type.modelName, 'FindAll'); }, createRecord: function(store, type, snapshot) { - return write(this, store.serializerFor(type.modelName), this.client, 'CreateRecord', snapshot); + return write(this, type.modelName, 'CreateRecord', snapshot); }, updateRecord: function(store, type, snapshot) { - return write(this, store.serializerFor(type.modelName), this.client, 'UpdateRecord', snapshot); + return write(this, type.modelName, 'UpdateRecord', snapshot); }, deleteRecord: function(store, type, snapshot) { - return write(this, store.serializerFor(type.modelName), this.client, 'DeleteRecord', snapshot); + return write(this, type.modelName, 'DeleteRecord', snapshot); }, }); diff --git a/ui-v2/app/adapters/intention.js b/ui-v2/app/adapters/intention.js index fb27a5a0aa..d98afe66b9 100644 --- a/ui-v2/app/adapters/intention.js +++ b/ui-v2/app/adapters/intention.js @@ -6,11 +6,14 @@ import { SLUG_KEY } from 'consul-ui/models/intention'; // TODO: Update to use this.formatDatacenter() export default Adapter.extend({ - requestForQuery: function(request, { dc, index, id }) { + requestForQuery: function(request, { dc, filter, index }) { return request` GET /v1/connect/intentions?${{ dc }} - ${{ index }} + ${{ + index, + filter, + }} `; }, requestForQueryRecord: function(request, { dc, index, id }) { diff --git a/ui-v2/app/adapters/oidc-provider.js b/ui-v2/app/adapters/oidc-provider.js new file mode 100644 index 0000000000..f2b65da2e2 --- /dev/null +++ b/ui-v2/app/adapters/oidc-provider.js @@ -0,0 +1,97 @@ +import Adapter from './application'; +import { inject as service } from '@ember/service'; + +import { env } from 'consul-ui/env'; +import nonEmptySet from 'consul-ui/utils/non-empty-set'; + +let Namespace; +if (env('CONSUL_NSPACES_ENABLED')) { + Namespace = nonEmptySet('Namespace'); +} else { + Namespace = () => ({}); +} +export default Adapter.extend({ + env: service('env'), + requestForQuery: function(request, { dc, ns, index }) { + return request` + GET /v1/internal/ui/oidc-auth-methods?${{ dc }} + + ${{ + index, + ...this.formatNspace(ns), + }} + `; + }, + requestForQueryRecord: function(request, { dc, ns, id }) { + if (typeof id === 'undefined') { + throw new Error('You must specify an id'); + } + return request` + POST /v1/acl/oidc/auth-url?${{ dc }} + Cache-Control: no-store + + ${{ + ...Namespace(ns), + AuthMethod: id, + RedirectURI: `${this.env.var('CONSUL_BASE_UI_URL')}/oidc/callback`, + }} + `; + }, + requestForAuthorize: function(request, { dc, ns, id, code, state }) { + if (typeof id === 'undefined') { + throw new Error('You must specify an id'); + } + if (typeof code === 'undefined') { + throw new Error('You must specify an code'); + } + if (typeof state === 'undefined') { + throw new Error('You must specify an state'); + } + return request` + POST /v1/acl/oidc/callback?${{ dc }} + Cache-Control: no-store + + ${{ + ...Namespace(ns), + AuthMethod: id, + Code: code, + State: state, + }} + `; + }, + requestForLogout: function(request, { id }) { + if (typeof id === 'undefined') { + throw new Error('You must specify an id'); + } + return request` + POST /v1/acl/logout + Cache-Control: no-store + X-Consul-Token: ${id} + `; + }, + authorize: function(store, type, id, snapshot) { + return this.request( + function(adapter, request, serialized, unserialized) { + return adapter.requestForAuthorize(request, serialized, unserialized); + }, + function(serializer, respond, serialized, unserialized) { + return serializer.respondForAuthorize(respond, serialized, unserialized); + }, + snapshot, + type.modelName + ); + }, + logout: function(store, type, id, snapshot) { + return this.request( + function(adapter, request, serialized, unserialized) { + return adapter.requestForLogout(request, serialized, unserialized); + }, + function(serializer, respond, serialized, unserialized) { + // its ok to return nothing here for the moment at least + return {}; + }, + snapshot, + type.modelName + ); + }, +}); diff --git a/ui-v2/app/adapters/token.js b/ui-v2/app/adapters/token.js index cc1f8d29ca..afed469dbf 100644 --- a/ui-v2/app/adapters/token.js +++ b/ui-v2/app/adapters/token.js @@ -104,6 +104,7 @@ export default Adapter.extend({ return request` GET /v1/acl/token/self?${{ dc }} X-Consul-Token: ${secret} + Cache-Control: no-store ${{ index }} `; @@ -132,7 +133,7 @@ export default Adapter.extend({ return adapter.requestForSelf(request, serialized, data); }, function(serializer, respond, serialized, data) { - return serializer.respondForQueryRecord(respond, serialized, data); + return serializer.respondForSelf(respond, serialized, data); }, unserialized, type.modelName diff --git a/ui-v2/app/app.js b/ui-v2/app/app.js index f08aaaf030..d8e2088b6b 100644 --- a/ui-v2/app/app.js +++ b/ui-v2/app/app.js @@ -1,14 +1,12 @@ import Application from '@ember/application'; -import Resolver from './resolver'; +import Resolver from 'ember-resolver'; import loadInitializers from 'ember-load-initializers'; import config from './config/environment'; -const App = Application.extend({ - modulePrefix: config.modulePrefix, - podModulePrefix: config.podModulePrefix, - Resolver, -}); +export default class App extends Application { + modulePrefix = config.modulePrefix; + podModulePrefix = config.podModulePrefix; + Resolver = Resolver; +} loadInitializers(App, config.modulePrefix); - -export default App; diff --git a/ui-v2/app/components/acl-filter/index.hbs b/ui-v2/app/components/acl-filter/index.hbs new file mode 100644 index 0000000000..503e0c1f20 --- /dev/null +++ b/ui-v2/app/components/acl-filter/index.hbs @@ -0,0 +1,4 @@ +{{!
}} + + +{{!}} diff --git a/ui-v2/app/components/acl-filter.js b/ui-v2/app/components/acl-filter/index.js similarity index 100% rename from ui-v2/app/components/acl-filter.js rename to ui-v2/app/components/acl-filter/index.js diff --git a/ui-v2/app/templates/components/action-group.hbs b/ui-v2/app/components/action-group/index.hbs similarity index 100% rename from ui-v2/app/templates/components/action-group.hbs rename to ui-v2/app/components/action-group/index.hbs diff --git a/ui-v2/app/components/action-group.js b/ui-v2/app/components/action-group/index.js similarity index 100% rename from ui-v2/app/components/action-group.js rename to ui-v2/app/components/action-group/index.js diff --git a/ui-v2/app/components/app-view/index.hbs b/ui-v2/app/components/app-view/index.hbs new file mode 100644 index 0000000000..b66f00fbca --- /dev/null +++ b/ui-v2/app/components/app-view/index.hbs @@ -0,0 +1,89 @@ +{{yield}} +{{#if (not loading)}} +
+{{#each flashMessages.queue as |flash|}} + + {{#let (lowercase component.flashType) (lowercase flash.action) as |status type|}} + {{! flashes automatically ucfirst the type }} + +

+ + {{capitalize status}}! + + {{#yield-slot name="notification" params=(block-params status type flash.item)}} + {{yield}} + {{#if (eq type 'logout')}} + {{#if (eq status 'success') }} + You are now logged out. + {{else}} + There was an error logging out. + {{/if}} + {{else if (eq type 'authorize')}} + {{#if (eq status 'success') }} + You are now logged in. + {{else}} + There was an error, please check your SecretID/Token + {{/if}} + {{/if}} + {{else}} + {{#if (eq type 'logout')}} + {{#if (eq status 'success') }} + You are now logged out. + {{else}} + There was an error logging out. + {{/if}} + {{else if (eq type 'authorize')}} + {{#if (eq status 'success') }} + You are now logged in. + {{else}} + There was an error, please check your SecretID/Token + {{/if}} + {{/if}} + {{/yield-slot}} +

+ {{/let}} +
+{{/each}} +
+
+ {{#if authorized}} + + {{/if}} +
+ + {{yield}} + +
+ {{#if authorized}} + {{yield}} + {{/if}} +
+
+ + {{yield}} + +
+
+ {{#if authorized}} + + + {{yield}} + + {{/if}} +
+{{/if}} +
+{{#if loading}} + +{{else}} + {{#if (not enabled) }} + {{yield}} + {{else if (not authorized)}} + {{yield}} + {{else}} + {{yield}} + {{/if}} +{{/if}} +
diff --git a/ui-v2/app/components/app-view.js b/ui-v2/app/components/app-view/index.js similarity index 100% rename from ui-v2/app/components/app-view.js rename to ui-v2/app/components/app-view/index.js diff --git a/ui-v2/app/templates/components/aria-menu.hbs b/ui-v2/app/components/aria-menu/index.hbs similarity index 100% rename from ui-v2/app/templates/components/aria-menu.hbs rename to ui-v2/app/components/aria-menu/index.hbs diff --git a/ui-v2/app/components/aria-menu.js b/ui-v2/app/components/aria-menu/index.js similarity index 100% rename from ui-v2/app/components/aria-menu.js rename to ui-v2/app/components/aria-menu/index.js diff --git a/ui-v2/app/components/auth-dialog/README.mdx b/ui-v2/app/components/auth-dialog/README.mdx new file mode 100644 index 0000000000..676d553796 --- /dev/null +++ b/ui-v2/app/components/auth-dialog/README.mdx @@ -0,0 +1,56 @@ +## AuthDialog + +```handlebars + + {{#let components.AuthForm components.AuthProfile as |AuthForm AuthProfile|}} + + Here's the login form: + + + + Here's your profile: + + + Contact your administrator for login credentials. + +{{#if (env 'CONSUL_SSO_ENABLED')}} + + {{#if (gt providers.length 0)}} +

+ or +

+ {{/if}} + +{{/if}} + + + + + \ No newline at end of file diff --git a/ui-v2/app/components/auth-form/index.js b/ui-v2/app/components/auth-form/index.js new file mode 100644 index 0000000000..c6a95e2775 --- /dev/null +++ b/ui-v2/app/components/auth-form/index.js @@ -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(); + }, + }, +}); diff --git a/ui-v2/app/components/auth-profile/README.mdx b/ui-v2/app/components/auth-profile/README.mdx new file mode 100644 index 0000000000..f4fa59771d --- /dev/null +++ b/ui-v2/app/components/auth-profile/README.mdx @@ -0,0 +1,20 @@ +## AuthProfile + +```handlebars + +``` + +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) + +--- diff --git a/ui-v2/app/components/auth-profile/index.hbs b/ui-v2/app/components/auth-profile/index.hbs new file mode 100644 index 0000000000..6f46fd37cb --- /dev/null +++ b/ui-v2/app/components/auth-profile/index.hbs @@ -0,0 +1,9 @@ +
+
+ My ACL Token
+ AccessorID +
+
+ {{substr item.AccessorID -8}} +
+
\ No newline at end of file diff --git a/ui-v2/app/components/resolver-card.js b/ui-v2/app/components/auth-profile/index.js similarity index 100% rename from ui-v2/app/components/resolver-card.js rename to ui-v2/app/components/auth-profile/index.js diff --git a/ui-v2/app/components/catalog-filter/index.hbs b/ui-v2/app/components/catalog-filter/index.hbs new file mode 100644 index 0000000000..314cbbf816 --- /dev/null +++ b/ui-v2/app/components/catalog-filter/index.hbs @@ -0,0 +1,4 @@ +{{!
}} + + +{{!}} diff --git a/ui-v2/app/components/catalog-filter.js b/ui-v2/app/components/catalog-filter/index.js similarity index 100% rename from ui-v2/app/components/catalog-filter.js rename to ui-v2/app/components/catalog-filter/index.js diff --git a/ui-v2/app/components/catalog-toolbar/index.hbs b/ui-v2/app/components/catalog-toolbar/index.hbs new file mode 100644 index 0000000000..334489bb45 --- /dev/null +++ b/ui-v2/app/components/catalog-toolbar/index.hbs @@ -0,0 +1,10 @@ +
+ + + diff --git a/ui-v2/app/components/service-identity.js b/ui-v2/app/components/catalog-toolbar/index.js similarity index 100% rename from ui-v2/app/components/service-identity.js rename to ui-v2/app/components/catalog-toolbar/index.js diff --git a/ui-v2/app/components/changeable-set/index.hbs b/ui-v2/app/components/changeable-set/index.hbs new file mode 100644 index 0000000000..68e9bca979 --- /dev/null +++ b/ui-v2/app/components/changeable-set/index.hbs @@ -0,0 +1,6 @@ +{{yield}} +{{#if (gt items.length 0)}} + {{yield}} +{{else}} + {{yield}} +{{/if}} \ No newline at end of file diff --git a/ui-v2/app/components/changeable-set.js b/ui-v2/app/components/changeable-set/index.js similarity index 100% rename from ui-v2/app/components/changeable-set.js rename to ui-v2/app/components/changeable-set/index.js diff --git a/ui-v2/app/components/child-selector/index.hbs b/ui-v2/app/components/child-selector/index.hbs new file mode 100644 index 0000000000..1b13235f0e --- /dev/null +++ b/ui-v2/app/components/child-selector/index.hbs @@ -0,0 +1,29 @@ +
+{{yield}} + {{yield}} + +{{#if (gt items.length 0)}} + {{yield}} +{{else}} + +{{/if}} +
\ No newline at end of file diff --git a/ui-v2/app/components/child-selector.js b/ui-v2/app/components/child-selector/index.js similarity index 86% rename from ui-v2/app/components/child-selector.js rename to ui-v2/app/components/child-selector/index.js index bda8edf211..c1f58ef5b8 100644 --- a/ui-v2/app/components/child-selector.js +++ b/ui-v2/app/components/child-selector/index.js @@ -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(); diff --git a/ui-v2/app/components/code-editor/index.hbs b/ui-v2/app/components/code-editor/index.hbs new file mode 100644 index 0000000000..a3d12d9e36 --- /dev/null +++ b/ui-v2/app/components/code-editor/index.hbs @@ -0,0 +1,11 @@ + +
{{yield}}
+{{#if (and (not readonly) (not syntax))}} + + {{mode.name}} + +{{/if}} diff --git a/ui-v2/app/components/code-editor.js b/ui-v2/app/components/code-editor/index.js similarity index 100% rename from ui-v2/app/components/code-editor.js rename to ui-v2/app/components/code-editor/index.js diff --git a/ui-v2/app/components/confirmation-dialog/index.hbs b/ui-v2/app/components/confirmation-dialog/index.hbs new file mode 100644 index 0000000000..1ffade5cf3 --- /dev/null +++ b/ui-v2/app/components/confirmation-dialog/index.hbs @@ -0,0 +1,11 @@ +{{yield}} + +{{#if (or permanent (not confirming))}} + {{yield}} +{{/if}} + + +{{#if confirming }} + {{yield}} +{{/if}} + \ No newline at end of file diff --git a/ui-v2/app/components/confirmation-dialog.js b/ui-v2/app/components/confirmation-dialog/index.js similarity index 100% rename from ui-v2/app/components/confirmation-dialog.js rename to ui-v2/app/components/confirmation-dialog/index.js diff --git a/ui-v2/app/components/consul-external-source/index.hbs b/ui-v2/app/components/consul-external-source/index.hbs new file mode 100644 index 0000000000..f2e7e8c2f3 --- /dev/null +++ b/ui-v2/app/components/consul-external-source/index.hbs @@ -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}} + + {{#if (eq externalSource 'aws')}} + Registered via {{uppercase externalSource}} + {{else}} + Registered via {{capitalize externalSource}} + {{/if}} + + {{/if}} + {{/if}} + {{/let}} +{{/if}} diff --git a/ui-v2/app/components/tag-list.js b/ui-v2/app/components/consul-external-source/index.js similarity index 64% rename from ui-v2/app/components/tag-list.js rename to ui-v2/app/components/consul-external-source/index.js index 1656e4a23c..4798652642 100644 --- a/ui-v2/app/components/tag-list.js +++ b/ui-v2/app/components/consul-external-source/index.js @@ -1,6 +1,5 @@ import Component from '@ember/component'; export default Component.extend({ - tagName: 'dl', - classNames: ['tag-list'], + tagName: '', }); diff --git a/ui-v2/app/components/consul-intention-form/index.hbs b/ui-v2/app/components/consul-intention-form/index.hbs new file mode 100644 index 0000000000..c6b38a9c02 --- /dev/null +++ b/ui-v2/app/components/consul-intention-form/index.hbs @@ -0,0 +1,123 @@ +
+
+
+
+

Source

+ + {{#if (env 'CONSUL_NSPACES_ENABLED')}} + +{{/if}} +
+
+

Destination

+ + {{#if (env 'CONSUL_NSPACES_ENABLED')}} + +{{/if}} +
+
+
+ {{#each (array 'allow' 'deny') as |intent|}} + + {{/each}} +
+ +
+
+{{#if _item.isNew }} + +{{ else }} + +{{/if}} + +{{# if (and _item.ID (not-eq _item.ID 'anonymous')) }} + + + + + + + + +{{/if}} +
+
+ diff --git a/ui-v2/app/components/consul-intention-form/index.js b/ui-v2/app/components/consul-intention-form/index.js new file mode 100644 index 0000000000..9c608d0e08 --- /dev/null +++ b/ui-v2/app/components/consul-intention-form/index.js @@ -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); + }, + }, +}); diff --git a/ui-v2/app/components/consul-intention-list/index.hbs b/ui-v2/app/components/consul-intention-list/index.hbs new file mode 100644 index 0000000000..aafe0b4863 --- /dev/null +++ b/ui-v2/app/components/consul-intention-list/index.hbs @@ -0,0 +1,73 @@ + + + Source +   + Destination + Precedence + + + + + {{#if (eq item.SourceName '*') }} + All Services (*) + {{else}} + {{item.SourceName}} + {{/if}} + {{! TODO: slugify }} + {{or item.SourceNS 'default'}} + + + + {{item.Action}} + + + + {{#if (eq item.DestinationName '*') }} + All Services (*) + {{else}} + {{item.DestinationName}} + {{/if}} + {{! TODO: slugify }} + {{or item.DestinationNS 'default'}} + + + + {{item.Precedence}} + + + + + + More + + +
  • + Edit +
  • +
  • + +
    +
    +
    +
    + Confirm Delete +
    +

    + Are you sure you want to delete this intention? +

    +
    +
      +
    • + +
    • +
    • + +
    • +
    +
    +
    +
  • +
    +
    +
    +
    diff --git a/ui-v2/app/components/consul-intention-list/index.js b/ui-v2/app/components/consul-intention-list/index.js new file mode 100644 index 0000000000..4798652642 --- /dev/null +++ b/ui-v2/app/components/consul-intention-list/index.js @@ -0,0 +1,5 @@ +import Component from '@ember/component'; + +export default Component.extend({ + tagName: '', +}); diff --git a/ui-v2/app/components/consul-kind/index.hbs b/ui-v2/app/components/consul-kind/index.hbs new file mode 100644 index 0000000000..2d7bbce76e --- /dev/null +++ b/ui-v2/app/components/consul-kind/index.hbs @@ -0,0 +1,11 @@ +{{#if item.Kind}} + {{#if (has-block)}} + {{yield + (component 'consul-kind' item=item) + }} + {{else}} + + {{titleize (humanize item.Kind)}} + + {{/if}} +{{/if}} diff --git a/ui-v2/app/components/consul-kind/index.js b/ui-v2/app/components/consul-kind/index.js new file mode 100644 index 0000000000..4798652642 --- /dev/null +++ b/ui-v2/app/components/consul-kind/index.js @@ -0,0 +1,5 @@ +import Component from '@ember/component'; + +export default Component.extend({ + tagName: '', +}); diff --git a/ui-v2/app/components/consul-loader/README.mdx b/ui-v2/app/components/consul-loader/README.mdx new file mode 100644 index 0000000000..b65305eeb7 --- /dev/null +++ b/ui-v2/app/components/consul-loader/README.mdx @@ -0,0 +1,16 @@ +## 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) + +--- diff --git a/ui-v2/app/templates/-consul-loading.hbs b/ui-v2/app/components/consul-loader/index.hbs similarity index 99% rename from ui-v2/app/templates/-consul-loading.hbs rename to ui-v2/app/components/consul-loader/index.hbs index 871f2b2ac0..1711fdc0e1 100644 --- a/ui-v2/app/templates/-consul-loading.hbs +++ b/ui-v2/app/components/consul-loader/index.hbs @@ -51,4 +51,3 @@ - diff --git a/ui-v2/app/components/consul-loader/index.js b/ui-v2/app/components/consul-loader/index.js new file mode 100644 index 0000000000..4798652642 --- /dev/null +++ b/ui-v2/app/components/consul-loader/index.js @@ -0,0 +1,5 @@ +import Component from '@ember/component'; + +export default Component.extend({ + tagName: '', +}); diff --git a/ui-v2/app/components/consul-metadata-list/README.mdx b/ui-v2/app/components/consul-metadata-list/README.mdx new file mode 100644 index 0000000000..cb5c04d79c --- /dev/null +++ b/ui-v2/app/components/consul-metadata-list/README.mdx @@ -0,0 +1,27 @@ +## ConsulMetadataList + +`` + +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 + +``` + +### See + +- [Component Source Code](./index.js) +- [TemplateSource Code](./index.hbs) + +--- diff --git a/ui-v2/app/components/consul-metadata-list/index.hbs b/ui-v2/app/components/consul-metadata-list/index.hbs new file mode 100644 index 0000000000..3d1febfaae --- /dev/null +++ b/ui-v2/app/components/consul-metadata-list/index.hbs @@ -0,0 +1,19 @@ + + + Key + Value + + + + + {{object-at 0 item}} + + + + {{object-at 1 item}} + + + diff --git a/ui-v2/app/components/consul-metadata-list/index.js b/ui-v2/app/components/consul-metadata-list/index.js new file mode 100644 index 0000000000..4798652642 --- /dev/null +++ b/ui-v2/app/components/consul-metadata-list/index.js @@ -0,0 +1,5 @@ +import Component from '@ember/component'; + +export default Component.extend({ + tagName: '', +}); diff --git a/ui-v2/app/components/consul-service-instance-list/index.hbs b/ui-v2/app/components/consul-service-instance-list/index.hbs new file mode 100644 index 0000000000..6ac671e593 --- /dev/null +++ b/ui-v2/app/components/consul-service-instance-list/index.hbs @@ -0,0 +1,47 @@ +{{yield}} +{{#if (gt items.length 0)}} + + + {{item.Service.ID}} + +
      + +
    • + +
    • +
      + {{#with (reject-by 'ServiceID' '' item.Checks) as |checks|}} +
    • + {{checks.length}} service checks +
    • + {{/with}} + {{#with (filter-by 'ServiceID' '' item.Checks) as |checks|}} +
    • + {{checks.length}} node checks +
    • + {{/with}} + {{#if (get proxies item.Service.ID)}} +
    • + connected with proxy +
    • + {{/if}} + {{#if (gt item.Node.Node.length 0)}} +
    • + {{item.Node.Node}} +
    • + {{/if}} +
    • + {{#if (not-eq item.Service.Address '')}} + {{item.Service.Address}}:{{item.Service.Port}} + {{else}} + {{item.Node.Address}}:{{item.Service.Port}} + {{/if}} +
    • + +
    • + +
    • +
      +
    +
    +{{/if}} \ No newline at end of file diff --git a/ui-v2/app/components/consul-service-instance-list/index.js b/ui-v2/app/components/consul-service-instance-list/index.js new file mode 100644 index 0000000000..4798652642 --- /dev/null +++ b/ui-v2/app/components/consul-service-instance-list/index.js @@ -0,0 +1,5 @@ +import Component from '@ember/component'; + +export default Component.extend({ + tagName: '', +}); diff --git a/ui-v2/app/components/consul-service-list/index.hbs b/ui-v2/app/components/consul-service-list/index.hbs new file mode 100644 index 0000000000..a4f10402a9 --- /dev/null +++ b/ui-v2/app/components/consul-service-list/index.hbs @@ -0,0 +1,35 @@ +{{yield}} +{{#if (gt items.length 0)}} + + + {{item.Name}} + +
      + +
    • + +
    • +
      + +
    • + +
    • +
      + {{#if (not-eq item.InstanceCount 0)}} +
    • + {{format-number item.InstanceCount}} {{pluralize item.InstanceCount 'Instance' without-count=true}} +
    • + {{/if}} + {{#if (get proxies item.Name)}} +
    • + connected with proxy +
    • + {{/if}} + +
    • + +
    • +
      +
    +
    +{{/if}} \ No newline at end of file diff --git a/ui-v2/app/components/consul-service-list/index.js b/ui-v2/app/components/consul-service-list/index.js new file mode 100644 index 0000000000..4798652642 --- /dev/null +++ b/ui-v2/app/components/consul-service-list/index.js @@ -0,0 +1,5 @@ +import Component from '@ember/component'; + +export default Component.extend({ + tagName: '', +}); diff --git a/ui-v2/app/components/copy-button.js b/ui-v2/app/components/copy-button.js deleted file mode 100644 index 55b61f7447..0000000000 --- a/ui-v2/app/components/copy-button.js +++ /dev/null @@ -1,38 +0,0 @@ -import Component from '@ember/component'; -import { inject as service } from '@ember/service'; - -import WithListeners from 'consul-ui/mixins/with-listeners'; - -export default Component.extend(WithListeners, { - clipboard: service('clipboard/os'), - tagName: 'button', - classNames: ['copy-btn'], - buttonType: 'button', - disabled: false, - error: function() {}, - success: function() {}, - attributeBindings: [ - 'clipboardText:data-clipboard-text', - 'clipboardTarget:data-clipboard-target', - 'clipboardAction:data-clipboard-action', - 'buttonType:type', - 'disabled', - 'aria-label', - 'title', - ], - delegateClickEvent: true, - - didInsertElement: function() { - this._super(...arguments); - const clipboard = this.clipboard.execute( - this.delegateClickEvent ? `#${this.elementId}` : this.element - ); - ['success', 'error'].map(event => { - return this.listen(clipboard, event, () => { - if (!this.disabled) { - this[event](...arguments); - } - }); - }); - }, -}); diff --git a/ui-v2/app/components/copy-button/README.mdx b/ui-v2/app/components/copy-button/README.mdx new file mode 100644 index 0000000000..9e307ab510 --- /dev/null +++ b/ui-v2/app/components/copy-button/README.mdx @@ -0,0 +1,32 @@ +## CopyButton + +```handlebars +{{! inline }} + + + + Copy me! + +``` + +### Arguments + +| Argument | Type | Default | Description | +| --- | --- | --- | --- | +| `value` | `String` | | The string to be copied to the clipboard on click | +| `name` | `String` | | The 'Name' of the string to be copied. Mainly used for giving feedback to the user | + +This component renders a simple button, when clicked copies the value (the `@value` attribute) to the users clipboard. A simple piece of feedback is given to the user in the form of a tooltip. When used inline an empty button is rendered. + +### See + +- [Component Source Code](./index.js) +- [Template Source Code](./index.hbs) + +--- diff --git a/ui-v2/app/components/copy-button/index.hbs b/ui-v2/app/components/copy-button/index.hbs new file mode 100644 index 0000000000..f1ee923884 --- /dev/null +++ b/ui-v2/app/components/copy-button/index.hbs @@ -0,0 +1,17 @@ + + + + + + + +

    + Copied {{name}}! +

    +
    + +

    + Sorry, something went wrong! +

    +
    +
    diff --git a/ui-v2/app/components/copy-button/index.js b/ui-v2/app/components/copy-button/index.js new file mode 100644 index 0000000000..e0b3d0ec13 --- /dev/null +++ b/ui-v2/app/components/copy-button/index.js @@ -0,0 +1,29 @@ +import Component from '@ember/component'; +import { inject as service } from '@ember/service'; + +export default Component.extend({ + clipboard: service('clipboard/os'), + dom: service('dom'), + tagName: '', + init: function() { + this._super(...arguments); + this.guid = this.dom.guid(this); + this._listeners = this.dom.listeners(); + }, + willDestroyElement: function() { + this._super(...arguments); + this._listeners.remove(); + }, + didInsertElement: function() { + this._super(...arguments); + const component = this; + this._listeners.add(this.clipboard.execute(`#${this.guid}`), { + success: function() { + component.success(...arguments); + }, + error: function() { + component.error(...arguments); + }, + }); + }, +}); diff --git a/ui-v2/app/components/data-sink/README.mdx b/ui-v2/app/components/data-sink/README.mdx new file mode 100644 index 0000000000..9eaa0ff4ab --- /dev/null +++ b/ui-v2/app/components/data-sink/README.mdx @@ -0,0 +1,62 @@ +## DataSink + +```handlebars + +``` + +### Arguments + +| Argument | Type | Default | Description | +| --- | --- | --- | --- | +| `sink` | `String` | | The location of the sink, this should map to a string based URI | +| `data` | `Object` | | The data to be saved to the current instance, null or an empty string means remove | +| `onchange` | `Function` | | The action to fire when the data has arrived to the sink. Emits an Event-like object with a `data` property containing the data, if the data was deleted this is `undefined`. | +| `onerror` | `Function` | | The action to fire when an error occurs. Emits ErrorEvent object with an `error` property containing the Error. | + +### Methods/Actions/api + +| Method/Action | Description | +| --- | --- | +| `open` | Manually add or remove fom the data sink | + +The component takes a `sink` or an identifier (a uri) for the location of a sink and then emits `onchange` events whenever that data has been arrived to the sink (whether persisted or removed). If an error occurs whilst listening for data changes, an `onerror` event is emitted. + +Behind the scenes in the Consul UI we map URIs back to our `ember-data` backed `Repositories` meaning we can essentially redesign the URIs used for our data to more closely fit our needs. For example we currently require that **all** HTTP API URIs begin with `/dc/nspace/` values whether they require them or not. + +`DataSink` is not just restricted to HTTP API data, and can be configured to listen for data changes using a variety of methods and sources. For example we have also configured `DataSink` to send data to `LocalStorage` using the `settings://` pseudo-protocol in the URI (See examples below). + + +### Examples + +```handlebars + + + + + {{item.Name}} +``` + +```handlebars + + {{item.Name}} +``` + +### See + +- [Component Source Code](./index.js) +- [Template Source Code](./index.hbs) + +--- diff --git a/ui-v2/app/components/data-sink/index.hbs b/ui-v2/app/components/data-sink/index.hbs new file mode 100644 index 0000000000..a809747a7c --- /dev/null +++ b/ui-v2/app/components/data-sink/index.hbs @@ -0,0 +1,4 @@ +{{yield (hash + open=(action 'open') + state=state +)}} diff --git a/ui-v2/app/components/data-sink/index.js b/ui-v2/app/components/data-sink/index.js new file mode 100644 index 0000000000..c863e5dc16 --- /dev/null +++ b/ui-v2/app/components/data-sink/index.js @@ -0,0 +1,105 @@ +import Component from '@ember/component'; +import { inject as service } from '@ember/service'; +import { set, get, computed } from '@ember/object'; + +import { once } from 'consul-ui/utils/dom/event-source'; + +export default Component.extend({ + tagName: '', + + service: service('data-sink/service'), + dom: service('dom'), + logger: service('logger'), + + onchange: function(e) {}, + onerror: function(e) {}, + + state: computed('instance', 'instance.{dirtyType,isSaving}', function() { + let id; + const isSaving = get(this, 'instance.isSaving'); + const dirtyType = get(this, 'instance.dirtyType'); + if (typeof isSaving === 'undefined' && typeof dirtyType === 'undefined') { + id = 'idle'; + } else { + switch (dirtyType) { + case 'created': + id = isSaving ? 'creating' : 'create'; + break; + case 'updated': + id = isSaving ? 'updating' : 'update'; + break; + case 'deleted': + case undefined: + id = isSaving ? 'removing' : 'remove'; + break; + } + id = `active.${id}`; + } + return { + matches: name => id.indexOf(name) !== -1, + }; + }), + + init: function() { + this._super(...arguments); + this._listeners = this.dom.listeners(); + }, + willDestroy: function() { + this._super(...arguments); + this._listeners.remove(); + }, + source: function(cb) { + const source = once(cb); + const error = err => { + set(this, 'instance', undefined); + try { + this.onerror(err); + this.logger.execute(err); + } catch (err) { + this.logger.execute(err); + } + }; + this._listeners.add(source, { + message: e => { + try { + set(this, 'instance', undefined); + this.onchange(e); + } catch (err) { + error(err); + } + }, + error: e => error(e), + }); + return source; + }, + didInsertElement: function() { + this._super(...arguments); + if (typeof this.data !== 'undefined') { + this.actions.open.apply(this, [this.data]); + } + }, + persist: function(data, instance) { + set(this, 'instance', this.service.prepare(this.sink, data, instance)); + this.source(() => this.service.persist(this.sink, this.instance)); + }, + remove: function(instance) { + set(this, 'instance', this.service.prepare(this.sink, null, instance)); + this.source(() => this.service.remove(this.sink, this.instance)); + }, + actions: { + open: function(data, instance) { + if (instance instanceof Event) { + instance = undefined; + } + if (typeof data === 'undefined') { + throw new Error('You must specify data to save, or null to remove'); + } + // potentially allow {} and "" as 'remove' flags + if (data === null || data === '') { + this.remove(instance); + } else { + this.persist(data, instance); + } + }, + }, +}); diff --git a/ui-v2/app/components/data-source/README.mdx b/ui-v2/app/components/data-source/README.mdx new file mode 100644 index 0000000000..63c485a3db --- /dev/null +++ b/ui-v2/app/components/data-source/README.mdx @@ -0,0 +1,61 @@ +## DataSource + +```handlebars + +``` + +### Arguments + +| Argument | Type | Default | Description | +| --- | --- | --- | --- | +| `src` | `String` | | The source to subscribe to updates to, this should map to a string based URI | +| `loading` | `String` | eager | Allows the browser to defer loading offscreen DataSources (`eager\|lazy`). Setting to `lazy` only loads the data when the DataSource is visible in the DOM (inc. `display: none\|block;`) | +| `onchange` | `Function` | | The action to fire when the data changes. Emits an Event-like object with a `data` property containing the data. | +| `onerror` | `Function` | | The action to fire when an error occurs. Emits ErrorEvent object with an `error` property containing the Error. | + +The component takes a `src` or an identifier (a uri) for some data and then emits `onchange` events whenever that data changes. If an error occurs whilst listening for data changes, an `onerror` event is emitted. + +Setting `@loading="lazy"` uses `IntersectionObserver` to activate/deactive event emitting until the `` element is displayed in the DOM. This means you can use CSS `display: none|block;` to control the loading and stopping loading of data for usage with CSS based tabs and such-like. + +Consuls HTTP API DataSources use Consul's blocking query support for live updating of data. + +Behind the scenes in the Consul UI we map URIs back to our `ember-data` backed `Repositories` meaning we can essentially redesign the URIs used for our data to more closely fit our needs. For example we currently require that **all** HTTP API URIs begin with `/dc/nspace/` values whether they require them or not. + +`DataSource` is not just restricted to HTTP API data, and can be configured to listen for data changes using a variety of methods and sources. For example we have also configured `DataSource` to listen to `LocalStorage` changes using the `settings://` pseudo-protocol in the URI (See examples below). + + +### Example + +Straightforward usage can use `mut` to easily update data within a template + +```handlebars + {{! listen for HTTP API changes}} + + {{! the value of items will change whenever the data changes}} + {{#each items as |item|}} + {{item.Name}} {{! < Prints the item name }} + {{/each}} + + {{! listen for Settings (local storage) changes}} + + {{! the value of token will change whenever the data changes}} + {{token.AccessorID}} {{! < Prints the token AccessorID }} +``` + +### See + +- [Component Source Code](./index.js) +- [Template Source Code](./index.hbs) + +--- diff --git a/ui-v2/app/components/data-source/index.hbs b/ui-v2/app/components/data-source/index.hbs new file mode 100644 index 0000000000..3e9f7250ec --- /dev/null +++ b/ui-v2/app/components/data-source/index.hbs @@ -0,0 +1,4 @@ +{{#if (eq loading "lazy")}} +{{! in order to use intersection observer we need a DOM element on the page}} +