UI V2 (#4086)
* Move settings to use the same service/route API as the rest of the app * Put some ideas down for unit testing on adapters * Favour `Model` over `Entity` * Move away from using `reopen` to using Mixins * Amend messages, comment/document some usage * Make sure the returns are consistent in normalizePayload, also Add some todo's in to remind me to think consider this further at a later date. For example, is normalizePayload to be a hook or an overridable method * Start stripping back the HTML to semantics * Use a variable rather than chaining * Remove unused helpers * Start picking through the new designs, start with listing pages * First draft HTML for every page * Making progress on the CSS * Keep plugging away at the catalog css * Looking at scrolling * Wire up filtering * Sort out filter counting, more or less done a few outstanding * Start knocking the forms into shape * Add in codemirror * Keep moving forwards with the form like layouts * Start looking at ACL editing page, add footer in * Pull the filters back in, look at an autoresizer for scroll views * First draft toggles * 2nd draft healthcheck icons * Tweak node healthcheck icons * Looking at healthcheck detail icons * Tweak the filter-bar and add selections to the in content tabs * Add ACL create, pill-like acl type highlight * Tweaking the main nav some more * Working on the filter-bar and freetext-filter * Masonry layout * Stick with `checks` instead of healthy/unhealthy * Fix up the filter numbers/counts * Use the thead for a measure * First draft tomography back in * First draft DC dropdown * Add a temporary create buttong to kv's * Move KV and ACL to use a create page * Move tags * Run through old tests * Injectable server * Start adding test attributes * Add some page objects * More test attributes and pages * Acl filter objects * Add a page.. page object * Clickable items in lists * Add rest/spread babel plugin, remove mirage for now * Add fix for ember-collection * Keep track of acl filters * ember-cli-page-object * ember-test-selectors * ui: update version of ui compile deps * Update static assets * Centralize radiogroup helper * Rejig KV's and begin to clean it up * Work around lack of Tags for the moment.. * Some little css tweaks and start to remove possibles * Working on the dc page and incidentals 1. Sort the datacenter-picker list 2. Add a selected state to the datacenter-picker 3. Make dc an {Name: dc} 4. Add an env helper to get to 'env vars' from within templates * Click outside stuff for the datacenter-picker, is-active on nav * Make sure the dropdown CTA can be active * Bump ember add pluralize helper * Little try at sass based custom queries * Rejig tablular collection so it deals with resizing, actions 1. WIP: start building actions dropdowns 2. Move tabular collection to deal with resizing to rule out differences * First draft actions dropdowns * Add ports, selectable IP's * Flash messages, plus general cleanup/consistency 1. Add ember-cli-flash for flash messages 2. Move everything to get() instead of item.get 3. Spotted a few things that weren't consistent * DOn't go lower than zero * First draft vertical menu * Missed a get, tweak dropmenu tick * Big cleanup 1. this.get(), this.set() > get(), set() 2. assign > {...{}, ...{}} 3. Seperator > separator * WIP: settings * Moved things into a ui-v2 folder * Decide on a way to do the settings page whilst maintaining the url + dc's * Start some error pages * Remove base64 polyfill * Tie in settings, fix atob bug, tweak layout css * Centralize confirmations into a component * Allow switching between the old and new UI with the CONSUL_UI_BETA env var Currently all the assets are packaged into a single AssetFS and a prefix is configured to switch between the two. * Attempt at some updates to integrate the v2 ui build into the main infrastructure * Add redirect to index.html for unknown paths * Allow redictor to /index.html for new ui when using -ui-dir * Take ACLs to the correct place on save * First pass breadcrumbs * Remove datacenter selector on the index page * Tweak overall layout * Make buttons 'resets' * Tweak last DC stuff * Validations plus kv keyname viewing tweaks * Pull sessions back in * Tweak the env vars to be more reusable * Move isAnon to the view * No items and disabled acl css * ACL and KV details 1. Unauthorized page 2. Make sure the ACL is always selected when it needs it 3. Check record deletion with a changeset * Few more acl tweaks/corrections * Add no items view to node > services * Tags for node > services * Make sure we have tags * Fix up the labels on the tomography graph * Add node link (agent) to kv sessions * Duplicate up `create` for KV 'root creation' * Safety check for health checks * Fix up the grids * Truncate td a's, fix kv columns * Watch for spaces in KV id's * Move actions to their own mixins for now at least * Link reset to settings incase I want to type it in * Tweak error page * Cleanup healthcheck icons in service listing * Centralize errors and make getting back easier * Nice numbers * Compact buttons * Some incidental css cleanups * Use 'Key / Value' for root * Tweak tomography layout * Fix single healthcheck unhealthy resource * Get loading screen ready * Fix healthy healthcheck tick * Everything in header starts white * First draft loader * Refactor the entire backend to use proper unique keys, plus.. 1. Make unique keys form dc + slug (uid) 2. Fun with errors... * Tweak header colors * Add noopener noreferrer to external links * Add supers to setupController * Implement cloning, using ember-data... * Move the more expensive down the switch order * First draft empty record cleanup.. * Add the cusomt store test * Temporarily use the htmlSafe prototype to remove the console warning * Encode hashes in urls * Go back to using title for errors for now * Start removing unused bulma * Lint * WIP: Start looking at failing tests * Remove single redirect test * Finish off error message styling * Add full ember-data cache invalidation to avoid stale data... * Add uncolorable warning icons * More info icon * Rearrange single service, plus tag printing * Logo * No quotes * Add a simple startup logo * Tweak healthcheck statuses * Fix border-color for healthchecks * Tweak node tabs * Catch 401 ACL errors and rethrow with the provided error message * Remove old acl unauth and error routes * Missed a super * Make 'All' refer to number of checks, not services * Remove ember-resizer, add autoprefixer * Don't show tomography if its not worth it, viewify it more also * Little model cleanup * Chevrons * Find a way to reliably set the class of html from the view * Consistent html * Make sure session id's are visible as long as possible * Fix single service check count * Add filters and searchs to the query string * Don't remember the selected tab * Change text * Eror tweaking * Use chevrons on all breadcrumbs even in kv's * Clean up a file * Tweak some messaging * Makesure the footer overlays whats in the page * Tweak KV errors * Move json toggle over to the right * feedback-dialog along with copy buttons * Better confirmation dialogs * Add git sha comment * Same title as old UI * Allow defaults * Make sure value is a string * WIP: Scrolling dropdowns/confirmations * Add to kv's * Remove set * First pass trace * Better table rows * Pull over the hashi code editor styles * Editor tweaks * Responsive tabs * Add number formatting to tomography * Review whats left todo * Lint * Add a coordinate ember data triplet * Bump in a v2.0.0 * Update old tests * Get coverage working again * Make sure query keys are also encoded * Don't test console.error * Unit test some more utils * Tweak the size of the tabular collections * Clean up gitignore * Fix copy button rollovers * Get healthcheck 'icon icons' onto the text baseline * Tweak healthcheck padding and alignment * Make sure commas kick in in rtt, probably never get to that * Improve vertical menu * Tweak dropdown active state to not have a bg * Tweak paddings * Search entire string not just 'startsWith' * Button states * Most buttons have 1px border * More button tweaks * You can only view kv folders * CSS cleanup reduction * Form input states and little cleanup * More CSS reduction * Sort checks by importance * Fix click outside on datacenter picker * Make sure table th's also auto calculate properly * Make sure `json` isn't remembered in KV editing * Fix recursive deletion in KV's * Centralize size * Catch updateRecord * Don't double envode * model > item consistency * Action loading and ACL tweaks * Add settings dependencies to acl tests * Better loading * utf-8 base64 encode/decode * Don't hang off a prototype for htmlSafe * Missing base64 files... * Get atob/btoa polyfill right * Shadowy rollovers * Disabled button styling for primaries * autofocuses only onload for now * Fix footer centering * Beginning of 'notices' * Remove the isLocked disabling as we are letting you do what the API does * Don't forget the documentation link for sessions * Updates are more likely * Use exported constant * Dont export redirectFS and a few other PR updates * Remove the old bootstrap config which was used for the old UI skin * Use curlies for multiple properties
This commit is contained in:
parent
4fffd3b658
commit
ca15998b51
|
@ -121,8 +121,7 @@ ui:
|
|||
# also run as part of the release build script when it verifies that there are no
|
||||
# changes to the UI assets that aren't checked in.
|
||||
static-assets:
|
||||
@go-bindata-assetfs -pkg agent -prefix pkg ./pkg/web_ui/...
|
||||
@mv bindata.go agent/bindata_assetfs.go
|
||||
@go-bindata-assetfs -pkg agent -prefix pkg -o agent/bindata_assetfs.go ./pkg/web_ui/...
|
||||
$(MAKE) format
|
||||
|
||||
tools:
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -7,6 +7,7 @@ import (
|
|||
"net/http"
|
||||
"net/http/pprof"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -41,6 +42,18 @@ type HTTPServer struct {
|
|||
proto string
|
||||
}
|
||||
|
||||
type redirectFS struct {
|
||||
fs http.FileSystem
|
||||
}
|
||||
|
||||
func (fs *redirectFS) Open(name string) (http.File, error) {
|
||||
file, err := fs.fs.Open(name)
|
||||
if err != nil {
|
||||
file, err = fs.fs.Open("/index.html")
|
||||
}
|
||||
return file, err
|
||||
}
|
||||
|
||||
// endpoint is a Consul-specific HTTP handler that takes the usual arguments in
|
||||
// but returns a response object and error, both of which are handled in a
|
||||
// common manner by Consul's HTTP server.
|
||||
|
@ -135,11 +148,32 @@ func (s *HTTPServer) handler(enableDebug bool) http.Handler {
|
|||
handleFuncMetrics("/debug/pprof/symbol", pprof.Symbol)
|
||||
}
|
||||
|
||||
// Use the custom UI dir if provided.
|
||||
if s.agent.config.UIDir != "" {
|
||||
mux.Handle("/ui/", http.StripPrefix("/ui/", http.FileServer(http.Dir(s.agent.config.UIDir))))
|
||||
} else if s.agent.config.EnableUI {
|
||||
mux.Handle("/ui/", http.StripPrefix("/ui/", http.FileServer(assetFS())))
|
||||
if s.IsUIEnabled() {
|
||||
new_ui, err := strconv.ParseBool(os.Getenv("CONSUL_UI_BETA"))
|
||||
if err != nil {
|
||||
new_ui = false
|
||||
}
|
||||
var uifs http.FileSystem;
|
||||
|
||||
// Use the custom UI dir if provided.
|
||||
if s.agent.config.UIDir != "" {
|
||||
uifs = http.Dir(s.agent.config.UIDir)
|
||||
} else {
|
||||
fs := assetFS()
|
||||
|
||||
if new_ui {
|
||||
fs.Prefix += "/v2/"
|
||||
} else {
|
||||
fs.Prefix += "/v1/"
|
||||
}
|
||||
uifs = fs
|
||||
}
|
||||
|
||||
if new_ui {
|
||||
uifs = &redirectFS{fs:uifs}
|
||||
}
|
||||
|
||||
mux.Handle("/ui/", http.StripPrefix("/ui/", http.FileServer(uifs)))
|
||||
}
|
||||
|
||||
// Wrap the whole mux with a handler that bans URLs with non-printable
|
||||
|
|
|
@ -11,8 +11,12 @@ RUN apt-get update -y && \
|
|||
ruby \
|
||||
ruby-dev \
|
||||
zip \
|
||||
zlib1g-dev && \
|
||||
gem install bundler
|
||||
zlib1g-dev \
|
||||
nodejs \
|
||||
npm && \
|
||||
gem install bundler && \
|
||||
npm install --global yarn && \
|
||||
npm install --global ember-cli
|
||||
|
||||
RUN mkdir /goroot && \
|
||||
mkdir /gopath && \
|
||||
|
|
|
@ -18,6 +18,11 @@ bundle
|
|||
make dist
|
||||
popd
|
||||
|
||||
pushd ui-v2
|
||||
yarn install
|
||||
make dist
|
||||
popd
|
||||
|
||||
# Fixup the timestamps to match what's checked in. This will allow us to cleanly
|
||||
# verify that the checked-in content is up to date without spurious diffs of the
|
||||
# file mod times.
|
||||
|
|
|
@ -13,11 +13,18 @@ cd $DIR
|
|||
make tools
|
||||
|
||||
# Build the web assets.
|
||||
echo "Building the V1 UI"
|
||||
pushd ui
|
||||
bundle
|
||||
make dist
|
||||
popd
|
||||
|
||||
echo "Building the V2 UI"
|
||||
pushd ui-v2
|
||||
yarn install
|
||||
make dist
|
||||
popd
|
||||
|
||||
# Make the static assets using the container version of the builder
|
||||
make static-assets
|
||||
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
module.exports = {
|
||||
extends: ['./.eslintrc.js'],
|
||||
rules: {
|
||||
'no-console': 'warn',
|
||||
'no-unused-vars': ['error', { args: 'none' }],
|
||||
'ember/routes-segments-snake-case': 'warn',
|
||||
},
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{js,scss}]
|
||||
insert_final_newline = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.hbs]
|
||||
insert_final_newline = false
|
||||
|
||||
[*.{diff,md}]
|
||||
trim_trailing_whitespace = false
|
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
/**
|
||||
Ember CLI sends analytics information by default. The data is completely
|
||||
anonymous, but there are times when you might want to disable this behavior.
|
||||
|
||||
Setting `disableAnalytics` to true will prevent any data from being sent.
|
||||
*/
|
||||
"disableAnalytics": false
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
module.exports = {
|
||||
root: true,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2017,
|
||||
sourceType: 'module',
|
||||
ecmaFeatures: {
|
||||
experimentalObjectRestSpread: true
|
||||
}
|
||||
},
|
||||
plugins: ['ember'],
|
||||
extends: ['eslint:recommended', 'plugin:ember/recommended'],
|
||||
env: {
|
||||
browser: true,
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': ['error', { args: 'none' }]
|
||||
},
|
||||
overrides: [
|
||||
// node files
|
||||
{
|
||||
files: ['testem.js', 'ember-cli-build.js', 'config/**/*.js'],
|
||||
parserOptions: {
|
||||
sourceType: 'script',
|
||||
ecmaVersion: 2015,
|
||||
},
|
||||
env: {
|
||||
browser: false,
|
||||
node: true,
|
||||
},
|
||||
},
|
||||
|
||||
// test files
|
||||
{
|
||||
files: ['tests/**/*.js'],
|
||||
excludedFiles: ['tests/dummy/**/*.js'],
|
||||
env: {
|
||||
embertest: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
|
@ -0,0 +1,10 @@
|
|||
/dist
|
||||
|
||||
/tmp
|
||||
/node_modules
|
||||
|
||||
/coverage/*
|
||||
/npm-debug.log*
|
||||
/yarn-error.log
|
||||
/testem.log
|
||||
|
|
@ -0,0 +1 @@
|
|||
8
|
|
@ -0,0 +1,3 @@
|
|||
printWidth: 100
|
||||
singleQuote: true
|
||||
trailingComma: es5
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"ignore_dirs": ["tmp", "dist"]
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
ROOT:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
|
||||
|
||||
server:
|
||||
yarn run start
|
||||
|
||||
dist:
|
||||
yarn run build
|
||||
mv dist ../pkg/web_ui/v2
|
||||
|
||||
lint:
|
||||
yarn run lint:js
|
||||
format:
|
||||
yarn run format:js
|
||||
|
||||
.PHONY: server dist lint format
|
|
@ -0,0 +1,49 @@
|
|||
# consul-ui
|
||||
|
||||
|
||||
## Prerequisites
|
||||
|
||||
You will need the following things properly installed on your computer.
|
||||
|
||||
* [Git](https://git-scm.com/)
|
||||
* [Node.js](https://nodejs.org/) (with npm)
|
||||
* [yarn](https://yarnpkg.com)
|
||||
* [Ember CLI](https://ember-cli.com/)
|
||||
* [Google Chrome](https://google.com/chrome/)
|
||||
|
||||
## Installation
|
||||
|
||||
* `git clone <repository-url>` this repository
|
||||
* `cd ui`
|
||||
* `yarn install`
|
||||
|
||||
## Running / Development
|
||||
|
||||
* `yarn run start`
|
||||
* Visit your app at [http://localhost:4200](http://localhost:4200).
|
||||
* Visit your tests at [http://localhost:4200/tests](http://localhost:4200/tests).
|
||||
|
||||
### Code Generators
|
||||
|
||||
Make use of the many generators for code, try `ember help generate` for more details
|
||||
|
||||
### Running Tests
|
||||
|
||||
* `ember test`
|
||||
* `ember test --server`
|
||||
|
||||
### Building
|
||||
|
||||
* `ember build` (development)
|
||||
* `ember build --environment production` (production)
|
||||
|
||||
### Deploying
|
||||
|
||||
|
||||
## Further Reading / Useful Links
|
||||
|
||||
* [ember.js](https://emberjs.com/)
|
||||
* [ember-cli](https://ember-cli.com/)
|
||||
* Development Browser Extensions
|
||||
* [ember inspector for chrome](https://chrome.google.com/webstore/detail/ember-inspector/bmdblncegkenkacieihfhpjfppoconhi)
|
||||
* [ember inspector for firefox](https://addons.mozilla.org/en-US/firefox/addon/ember-inspector/)
|
|
@ -0,0 +1,162 @@
|
|||
import Adapter, {
|
||||
REQUEST_CREATE,
|
||||
REQUEST_UPDATE,
|
||||
REQUEST_DELETE,
|
||||
DATACENTER_KEY as API_DATACENTER_KEY,
|
||||
} from './application';
|
||||
import EmberError from '@ember/error';
|
||||
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/acl';
|
||||
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
|
||||
import { PUT as HTTP_PUT } from 'consul-ui/utils/http/method';
|
||||
import { OK as HTTP_OK, UNAUTHORIZED as HTTP_UNAUTHORIZED } from 'consul-ui/utils/http/status';
|
||||
|
||||
import makeAttrable from 'consul-ui/utils/makeAttrable';
|
||||
const REQUEST_CLONE = 'cloneRecord';
|
||||
export default Adapter.extend({
|
||||
urlForQuery: function(query, modelName) {
|
||||
// https://www.consul.io/api/acl.html#list-acls
|
||||
return this.appendURL('acl/list', [], this.cleanQuery(query));
|
||||
},
|
||||
urlForQueryRecord: function(query, modelName) {
|
||||
// https://www.consul.io/api/acl.html#read-acl-token
|
||||
return this.appendURL('acl/info', [query.id], this.cleanQuery(query));
|
||||
},
|
||||
urlForCreateRecord: function(modelName, snapshot) {
|
||||
// https://www.consul.io/api/acl.html#create-acl-token
|
||||
return this.appendURL('acl/create', [], {
|
||||
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
|
||||
});
|
||||
},
|
||||
urlForUpdateRecord: function(id, modelName, snapshot) {
|
||||
// the id is in the payload, don't add it in here
|
||||
// https://www.consul.io/api/acl.html#update-acl-token
|
||||
return this.appendURL('acl/update', [], {
|
||||
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
|
||||
});
|
||||
},
|
||||
urlForDeleteRecord: function(id, modelName, snapshot) {
|
||||
// https://www.consul.io/api/acl.html#delete-acl-token
|
||||
return this.appendURL('acl/destroy', [snapshot.attr(SLUG_KEY)], {
|
||||
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
|
||||
});
|
||||
},
|
||||
urlForCloneRecord: function(modelName, snapshot) {
|
||||
// https://www.consul.io/api/acl.html#clone-acl-token
|
||||
return this.appendURL('acl/clone', [snapshot.attr(SLUG_KEY)], {
|
||||
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
|
||||
});
|
||||
},
|
||||
urlForRequest: function({ type, snapshot, requestType }) {
|
||||
switch (requestType) {
|
||||
case 'cloneRecord':
|
||||
return this.urlForCloneRecord(type.modelName, snapshot);
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
clone: function(store, modelClass, id, snapshot) {
|
||||
const params = {
|
||||
store: store,
|
||||
type: modelClass,
|
||||
id: id,
|
||||
snapshot: snapshot,
|
||||
requestType: 'cloneRecord',
|
||||
};
|
||||
// _requestFor is private... but these methods aren't, until they disappear..
|
||||
const request = {
|
||||
method: this.methodForRequest(params),
|
||||
url: this.urlForRequest(params),
|
||||
headers: this.headersForRequest(params),
|
||||
data: this.dataForRequest(params),
|
||||
};
|
||||
// TODO: private..
|
||||
return this._makeRequest(request);
|
||||
},
|
||||
dataForRequest: function(params) {
|
||||
const data = this._super(...arguments);
|
||||
switch (params.requestType) {
|
||||
case REQUEST_UPDATE:
|
||||
case REQUEST_CREATE:
|
||||
return data.acl;
|
||||
}
|
||||
return data;
|
||||
},
|
||||
methodForRequest: function(params) {
|
||||
switch (params.requestType) {
|
||||
case REQUEST_DELETE:
|
||||
case REQUEST_CREATE:
|
||||
case REQUEST_CLONE:
|
||||
return HTTP_PUT;
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
isCreateRecord: function(url) {
|
||||
return (
|
||||
url.pathname ===
|
||||
this.parseURL(this.urlForCreateRecord('acl', makeAttrable({ [DATACENTER_KEY]: '' }))).pathname
|
||||
);
|
||||
},
|
||||
isCloneRecord: function(url) {
|
||||
return (
|
||||
url.pathname ===
|
||||
this.parseURL(
|
||||
this.urlForCloneRecord(
|
||||
'acl',
|
||||
makeAttrable({ [SLUG_KEY]: this.slugFromURL(url), [DATACENTER_KEY]: '' })
|
||||
)
|
||||
).pathname
|
||||
);
|
||||
},
|
||||
isUpdateRecord: function(url) {
|
||||
return (
|
||||
url.pathname ===
|
||||
this.parseURL(this.urlForUpdateRecord(null, 'acl', makeAttrable({ [DATACENTER_KEY]: '' })))
|
||||
.pathname
|
||||
);
|
||||
},
|
||||
handleResponse: function(status, headers, payload, requestData) {
|
||||
let response = payload;
|
||||
if (status === HTTP_OK) {
|
||||
const url = this.parseURL(requestData.url);
|
||||
switch (true) {
|
||||
case response === true:
|
||||
response = {
|
||||
[PRIMARY_KEY]: this.uidForURL(url),
|
||||
};
|
||||
break;
|
||||
case this.isQueryRecord(url):
|
||||
response = {
|
||||
...response[0],
|
||||
...{
|
||||
[PRIMARY_KEY]: this.uidForURL(url),
|
||||
},
|
||||
};
|
||||
break;
|
||||
case this.isUpdateRecord(url):
|
||||
case this.isCreateRecord(url):
|
||||
case this.isCloneRecord(url):
|
||||
response = {
|
||||
...response,
|
||||
...{
|
||||
[PRIMARY_KEY]: this.uidForURL(url, response[SLUG_KEY]),
|
||||
},
|
||||
};
|
||||
break;
|
||||
default:
|
||||
response = response.map((item, i, arr) => {
|
||||
return {
|
||||
...item,
|
||||
...{
|
||||
[PRIMARY_KEY]: this.uidForURL(url, item[SLUG_KEY]),
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
} else if (status === HTTP_UNAUTHORIZED) {
|
||||
const e = new EmberError();
|
||||
e.code = status;
|
||||
e.message = payload;
|
||||
throw e;
|
||||
}
|
||||
return this._super(status, headers, response, requestData);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,82 @@
|
|||
import Adapter from 'ember-data/adapters/rest';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
import URL from 'url';
|
||||
import createURL from 'consul-ui/utils/createURL';
|
||||
|
||||
export const REQUEST_CREATE = 'createRecord';
|
||||
export const REQUEST_READ = 'queryRecord';
|
||||
export const REQUEST_UPDATE = 'updateRecord';
|
||||
export const REQUEST_DELETE = 'deleteRecord';
|
||||
// export const REQUEST_READ_MULTIPLE = 'query';
|
||||
|
||||
export const DATACENTER_KEY = 'dc';
|
||||
|
||||
export default Adapter.extend({
|
||||
namespace: 'v1',
|
||||
repo: service('settings'),
|
||||
headersForRequest: function(params) {
|
||||
return {
|
||||
...this.get('repo').findHeaders(),
|
||||
...this._super(...arguments),
|
||||
};
|
||||
},
|
||||
cleanQuery: function(_query) {
|
||||
delete _query.id;
|
||||
const query = { ..._query };
|
||||
delete _query[DATACENTER_KEY];
|
||||
return query;
|
||||
},
|
||||
isQueryRecord: function(url) {
|
||||
// this is ONLY if ALL api's using it
|
||||
// follow the 'last part of the url is the id' rule
|
||||
const pathname = url.pathname
|
||||
.split('/') // unslashify
|
||||
// remove the last
|
||||
.slice(0, -1)
|
||||
// add and empty to ensure a trailing slash
|
||||
.concat([''])
|
||||
// slashify
|
||||
.join('/');
|
||||
// compare with empty id against empty id
|
||||
return pathname === this.parseURL(this.urlForQueryRecord({ id: '' })).pathname;
|
||||
},
|
||||
getHost: function() {
|
||||
return this.host || `${location.protocol}//${location.host}`;
|
||||
},
|
||||
slugFromURL: function(url) {
|
||||
// follow the 'last part of the url is the id' rule
|
||||
return decodeURIComponent(url.pathname.split('/').pop());
|
||||
},
|
||||
parseURL: function(str) {
|
||||
return new URL(str, this.getHost());
|
||||
},
|
||||
uidForURL: function(url, _slug = '') {
|
||||
const dc = url.searchParams.get(DATACENTER_KEY) || '';
|
||||
const slug = _slug === '' ? this.slugFromURL(url) : _slug;
|
||||
if (dc.length < 1) {
|
||||
throw new Error('Unable to create unique id, missing datacenter');
|
||||
}
|
||||
if (slug.length < 1) {
|
||||
throw new Error('Unable to create unique id, missing slug');
|
||||
}
|
||||
// TODO: we could use a URL here? They are unique AND useful
|
||||
// but probably slower to create?
|
||||
return JSON.stringify([dc, slug]);
|
||||
},
|
||||
|
||||
// appendURL in turn calls createURL
|
||||
// createURL ensures that all `parts` are URL encoded
|
||||
// and all `query` values are URL encoded
|
||||
|
||||
// `this.buildURL()` with no arguments will give us `${host}/${namespace}`
|
||||
// `path` is the user configurable 'urlsafe' string to append on `buildURL`
|
||||
// `parts` is an array of possibly non 'urlsafe parts' to be encoded and
|
||||
// appended onto the url
|
||||
// `query` will populate the query string. Again the values of which will be
|
||||
// url encoded
|
||||
|
||||
appendURL: function(path, parts = [], query = {}) {
|
||||
return createURL([this.buildURL(), path], parts, query);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,27 @@
|
|||
import ApplicationAdapter from './application';
|
||||
|
||||
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/coordinate';
|
||||
|
||||
import { OK as HTTP_OK } from 'consul-ui/utils/http/status';
|
||||
|
||||
export default ApplicationAdapter.extend({
|
||||
urlForQuery: function(query, modelName) {
|
||||
// https://www.consul.io/api/coordinate.html#read-lan-coordinates-for-all-nodes
|
||||
return this.appendURL('coordinate/nodes', [], this.cleanQuery(query));
|
||||
},
|
||||
handleResponse: function(status, headers, payload, requestData) {
|
||||
let response = payload;
|
||||
if (status === HTTP_OK) {
|
||||
const url = this.parseURL(requestData.url);
|
||||
response = response.map((item, i, arr) => {
|
||||
return {
|
||||
...item,
|
||||
...{
|
||||
[PRIMARY_KEY]: this.uidForURL(url, item[SLUG_KEY]),
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
return this._super(status, headers, response, requestData);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
import Adapter from './application';
|
||||
|
||||
export default Adapter.extend({
|
||||
urlForFindAll: function() {
|
||||
return this.appendURL('catalog/datacenters');
|
||||
},
|
||||
});
|
|
@ -0,0 +1,141 @@
|
|||
import Adapter, {
|
||||
REQUEST_CREATE,
|
||||
REQUEST_UPDATE,
|
||||
REQUEST_DELETE,
|
||||
DATACENTER_KEY as API_DATACENTER_KEY,
|
||||
} from './application';
|
||||
import isFolder from 'consul-ui/utils/isFolder';
|
||||
import injectableRequestToJQueryAjaxHash from 'consul-ui/utils/injectableRequestToJQueryAjaxHash';
|
||||
import { typeOf } from '@ember/utils';
|
||||
import { get } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
import keyToArray from 'consul-ui/utils/keyToArray';
|
||||
|
||||
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/kv';
|
||||
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
|
||||
import { PUT as HTTP_PUT, DELETE as HTTP_DELETE } from 'consul-ui/utils/http/method';
|
||||
import { OK as HTTP_OK } from 'consul-ui/utils/http/status';
|
||||
|
||||
const API_KEYS_KEY = 'keys';
|
||||
const stringify = function(obj) {
|
||||
if (typeOf(obj) === 'string') {
|
||||
return obj;
|
||||
}
|
||||
return JSON.stringify(obj);
|
||||
};
|
||||
export default Adapter.extend({
|
||||
// There is no code path that can avoid the payload of a PUT request from
|
||||
// going via JSON.stringify.
|
||||
// Therefore a string payload of 'foobar' will always be encoded to '"foobar"'
|
||||
//
|
||||
// This means we have no other choice but rewriting the entire codepath or
|
||||
// overwriting the private `_requestToJQueryAjaxHash` method
|
||||
//
|
||||
// The `injectableRequestToJQueryAjaxHash` function makes the JSON object
|
||||
// injectable, meaning we can copy letter for letter the sourcecode of
|
||||
// `_requestToJQueryAjaxHash`, which means we can compare it with the original
|
||||
// private method within a test (`tests/unit/utils/injectableRequestToJQueryAjaxHash.js`).
|
||||
// This means, if `_requestToJQueryAjaxHash` changes between Ember versions
|
||||
// we will know about it
|
||||
|
||||
_requestToJQueryAjaxHash: injectableRequestToJQueryAjaxHash({
|
||||
stringify: stringify,
|
||||
}),
|
||||
decoder: service('atob'),
|
||||
urlForQuery: function(query, modelName) {
|
||||
// append keys here otherwise query.keys will add an '='
|
||||
return this.appendURL('kv', keyToArray(query.id), {
|
||||
...{
|
||||
[API_KEYS_KEY]: null,
|
||||
},
|
||||
...this.cleanQuery(query),
|
||||
});
|
||||
},
|
||||
urlForQueryRecord: function(query, modelName) {
|
||||
return this.appendURL('kv', keyToArray(query.id), this.cleanQuery(query));
|
||||
},
|
||||
urlForCreateRecord: function(modelName, snapshot) {
|
||||
return this.appendURL('kv', keyToArray(snapshot.attr(SLUG_KEY)), {
|
||||
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
|
||||
});
|
||||
},
|
||||
urlForUpdateRecord: function(id, modelName, snapshot) {
|
||||
return this.appendURL('kv', keyToArray(snapshot.attr(SLUG_KEY)), {
|
||||
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
|
||||
});
|
||||
},
|
||||
urlForDeleteRecord: function(id, modelName, snapshot) {
|
||||
const query = {
|
||||
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
|
||||
};
|
||||
if (isFolder(snapshot.attr(SLUG_KEY))) {
|
||||
query.recurse = null;
|
||||
}
|
||||
return this.appendURL('kv', keyToArray(snapshot.attr(SLUG_KEY)), query);
|
||||
},
|
||||
slugFromURL: function(url) {
|
||||
// keys don't follow the 'last part of the url' rule as they contain slashes
|
||||
return decodeURIComponent(
|
||||
url.pathname
|
||||
.split('/')
|
||||
.splice(3)
|
||||
.join('/')
|
||||
);
|
||||
},
|
||||
isQueryRecord: function(url) {
|
||||
return !url.searchParams.has(API_KEYS_KEY);
|
||||
},
|
||||
handleResponse: function(status, headers, payload, requestData) {
|
||||
let response = payload;
|
||||
if (status === HTTP_OK) {
|
||||
const url = this.parseURL(requestData.url);
|
||||
switch (true) {
|
||||
case response === true:
|
||||
response = {
|
||||
[PRIMARY_KEY]: this.uidForURL(url),
|
||||
};
|
||||
break;
|
||||
case this.isQueryRecord(url):
|
||||
response = {
|
||||
...response[0],
|
||||
...{
|
||||
[PRIMARY_KEY]: this.uidForURL(url),
|
||||
},
|
||||
};
|
||||
break;
|
||||
default:
|
||||
// isQuery
|
||||
response = response.map((item, i, arr) => {
|
||||
return {
|
||||
[PRIMARY_KEY]: this.uidForURL(url, item),
|
||||
[SLUG_KEY]: item,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
return this._super(status, headers, response, requestData);
|
||||
},
|
||||
dataForRequest: function(params) {
|
||||
const data = this._super(...arguments);
|
||||
let value = '';
|
||||
switch (params.requestType) {
|
||||
case REQUEST_UPDATE:
|
||||
case REQUEST_CREATE:
|
||||
value = data.kv.Value;
|
||||
if (typeof value === 'string') {
|
||||
return get(this, 'decoder').execute(value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
},
|
||||
methodForRequest: function(params) {
|
||||
switch (params.requestType) {
|
||||
case REQUEST_DELETE:
|
||||
return HTTP_DELETE;
|
||||
case REQUEST_CREATE:
|
||||
return HTTP_PUT;
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
import Adapter from './application';
|
||||
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/node';
|
||||
import { OK as HTTP_OK } from 'consul-ui/utils/http/status';
|
||||
export default Adapter.extend({
|
||||
urlForQuery: function(query, modelName) {
|
||||
return this.appendURL('internal/ui/nodes', [], this.cleanQuery(query));
|
||||
},
|
||||
urlForQueryRecord: function(query, modelName) {
|
||||
return this.appendURL('internal/ui/node', [query.id], this.cleanQuery(query));
|
||||
},
|
||||
handleResponse: function(status, headers, payload, requestData) {
|
||||
let response = payload;
|
||||
if (status === HTTP_OK) {
|
||||
const url = this.parseURL(requestData.url);
|
||||
switch (true) {
|
||||
case this.isQueryRecord(url):
|
||||
response = {
|
||||
...response,
|
||||
...{
|
||||
[PRIMARY_KEY]: this.uidForURL(url),
|
||||
},
|
||||
};
|
||||
break;
|
||||
default:
|
||||
response = response.map((item, i, arr) => {
|
||||
return {
|
||||
...item,
|
||||
...{
|
||||
[PRIMARY_KEY]: this.uidForURL(url, item[SLUG_KEY]),
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
return this._super(status, headers, response, requestData);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
import Adapter from './application';
|
||||
import { PRIMARY_KEY } from 'consul-ui/models/service';
|
||||
import { OK as HTTP_OK } from 'consul-ui/utils/http/status';
|
||||
export default Adapter.extend({
|
||||
urlForQuery: function(query, modelName) {
|
||||
return this.appendURL('internal/ui/services', [], this.cleanQuery(query));
|
||||
},
|
||||
urlForQueryRecord: function(query, modelName) {
|
||||
return this.appendURL('health/service', [query.id], this.cleanQuery(query));
|
||||
},
|
||||
handleResponse: function(status, headers, payload, requestData) {
|
||||
let response = payload;
|
||||
if (status === HTTP_OK) {
|
||||
const url = this.parseURL(requestData.url);
|
||||
switch (true) {
|
||||
case this.isQueryRecord(url):
|
||||
response = {
|
||||
[PRIMARY_KEY]: this.uidForURL(url),
|
||||
Nodes: response,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
response = response.map((item, i, arr) => {
|
||||
return {
|
||||
...item,
|
||||
...{
|
||||
[PRIMARY_KEY]: this.uidForURL(url, item.Name),
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
return this._super(status, headers, response, requestData);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
import Adapter, { REQUEST_DELETE, DATACENTER_KEY as API_DATACENTER_KEY } from './application';
|
||||
|
||||
import { FOREIGN_KEY as DATACENTER_KEY } from 'consul-ui/models/dc';
|
||||
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/session';
|
||||
import { PUT as HTTP_PUT } from 'consul-ui/utils/http/method';
|
||||
import { OK as HTTP_OK } from 'consul-ui/utils/http/status';
|
||||
|
||||
export default Adapter.extend({
|
||||
urlForQuery: function(query, modelName) {
|
||||
return this.appendURL('session/node', [query.id], this.cleanQuery(query));
|
||||
},
|
||||
urlForQueryRecord: function(query, modelName) {
|
||||
return this.appendURL('session/info', [query.id], this.cleanQuery(query));
|
||||
},
|
||||
urlForDeleteRecord: function(id, modelName, snapshot) {
|
||||
const query = {
|
||||
[API_DATACENTER_KEY]: snapshot.attr(DATACENTER_KEY),
|
||||
};
|
||||
return this.appendURL('session/destroy', [snapshot.attr(SLUG_KEY)], query);
|
||||
},
|
||||
methodForRequest: function(params) {
|
||||
switch (params.requestType) {
|
||||
case REQUEST_DELETE:
|
||||
return HTTP_PUT;
|
||||
}
|
||||
return this._super(...arguments);
|
||||
},
|
||||
handleResponse: function(status, headers, payload, requestData) {
|
||||
let response = payload;
|
||||
if (status === HTTP_OK) {
|
||||
const url = this.parseURL(requestData.url);
|
||||
switch (true) {
|
||||
case response === true:
|
||||
response = {
|
||||
[PRIMARY_KEY]: this.uidForURL(url),
|
||||
};
|
||||
break;
|
||||
case this.isQueryRecord(url):
|
||||
response = {
|
||||
...response[0],
|
||||
...{
|
||||
[PRIMARY_KEY]: this.uidForURL(url),
|
||||
},
|
||||
};
|
||||
break;
|
||||
default:
|
||||
response = response.map((item, i, arr) => {
|
||||
return {
|
||||
...item,
|
||||
...{
|
||||
[PRIMARY_KEY]: this.uidForURL(url, item[SLUG_KEY]),
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
return this._super(status, headers, response, requestData);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import Application from '@ember/application';
|
||||
import Resolver from './resolver';
|
||||
import loadInitializers from 'ember-load-initializers';
|
||||
import config from './config/environment';
|
||||
|
||||
const App = Application.extend({
|
||||
modulePrefix: config.modulePrefix,
|
||||
podModulePrefix: config.podModulePrefix,
|
||||
Resolver,
|
||||
});
|
||||
|
||||
loadInitializers(App, config.modulePrefix);
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,8 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'form',
|
||||
classNames: ['filter-bar'],
|
||||
'data-test-acl-filter': true,
|
||||
onchange: function() {},
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ['action-group'],
|
||||
onchange: function() {},
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
import Component from '@ember/component';
|
||||
import SlotsMixin from 'ember-block-slots';
|
||||
import { get } from '@ember/object';
|
||||
const $html = document.documentElement;
|
||||
const templatize = function(arr = []) {
|
||||
return arr.map(item => `template-${item}`);
|
||||
};
|
||||
export default Component.extend(SlotsMixin, {
|
||||
loading: false,
|
||||
classNames: ['app-view'],
|
||||
didReceiveAttrs: function() {
|
||||
let cls = get(this, 'class') || '';
|
||||
if (get(this, 'loading')) {
|
||||
cls += ' loading';
|
||||
} else {
|
||||
$html.classList.remove(...templatize(['loading']));
|
||||
}
|
||||
if (cls) {
|
||||
$html.classList.add(...templatize(cls.split(' ')));
|
||||
}
|
||||
},
|
||||
didInsertElement: function() {
|
||||
this.didReceiveAttrs();
|
||||
},
|
||||
didDestroyElement: function() {
|
||||
const cls = get(this, 'class') + ' loading';
|
||||
if (cls) {
|
||||
$html.classList.remove(...templatize(cls.split(' ')));
|
||||
}
|
||||
},
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'form',
|
||||
classNames: ['filter-bar'],
|
||||
'data-test-catalog-filter': true,
|
||||
onchange: function() {},
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
mode: 'application/json',
|
||||
onkeyup: function() {},
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
/*eslint ember/closure-actions: "warn"*/
|
||||
import Component from '@ember/component';
|
||||
|
||||
import SlotsMixin from 'ember-block-slots';
|
||||
import { get, set } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
const cancel = function() {
|
||||
set(this, 'confirming', false);
|
||||
};
|
||||
const execute = function() {
|
||||
this.sendAction(...['actionName', ...get(this, 'arguments')]);
|
||||
};
|
||||
const confirm = function() {
|
||||
const [action, ...args] = arguments;
|
||||
set(this, 'actionName', action);
|
||||
set(this, 'arguments', args);
|
||||
if (this._isRegistered('dialog')) {
|
||||
set(this, 'confirming', true);
|
||||
} else {
|
||||
get(this, 'confirm')
|
||||
.execute(get(this, 'message'))
|
||||
.then(confirmed => {
|
||||
if (confirmed) {
|
||||
this.execute();
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
return get(this, 'error').execute(...arguments);
|
||||
});
|
||||
}
|
||||
};
|
||||
export default Component.extend(SlotsMixin, {
|
||||
classNameBindings: ['confirming'],
|
||||
confirm: service('confirm'),
|
||||
error: service('error'),
|
||||
classNames: ['with-confirmation'],
|
||||
message: 'Are you sure?',
|
||||
confirming: false,
|
||||
permanent: false,
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this.cancel = cancel.bind(this);
|
||||
this.execute = execute.bind(this);
|
||||
this.confirm = confirm.bind(this);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
import Component from '@ember/component';
|
||||
import WithClickOutside from 'consul-ui/mixins/click-outside';
|
||||
|
||||
export default Component.extend(WithClickOutside, {
|
||||
tagName: 'ul',
|
||||
onchange: function() {},
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
import Component from '@ember/component';
|
||||
import { get, set } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
import SlotsMixin from 'ember-block-slots';
|
||||
const STATE_READY = 'ready';
|
||||
const STATE_SUCCESS = 'success';
|
||||
const STATE_ERROR = 'error';
|
||||
export default Component.extend(SlotsMixin, {
|
||||
wait: service('timeout'),
|
||||
interval: null,
|
||||
classNames: ['with-feedback'],
|
||||
state: STATE_READY,
|
||||
permanent: true,
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this.success = this._success.bind(this);
|
||||
this.error = this._error.bind(this);
|
||||
},
|
||||
_success: function() {
|
||||
set(this, 'state', STATE_SUCCESS);
|
||||
get(this, 'wait')
|
||||
.execute(3000, interval => {
|
||||
clearInterval(get(this, 'interval'));
|
||||
set(this, 'interval', interval);
|
||||
})
|
||||
.then(() => {
|
||||
set(this, 'state', STATE_READY);
|
||||
});
|
||||
},
|
||||
_error: function() {
|
||||
set(this, 'state', STATE_ERROR);
|
||||
get(this, 'wait')
|
||||
.execute(3000, interval => {
|
||||
clearInterval(get(this, 'interval'));
|
||||
set(this, 'interval', interval);
|
||||
})
|
||||
.then(() => {
|
||||
set(this, 'state', STATE_READY);
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'fieldset',
|
||||
classNames: ['freetext-filter'],
|
||||
onchange: function(){}
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
import Component from '@ember/component';
|
||||
import { get, set } from '@ember/object';
|
||||
const $html = document.documentElement;
|
||||
const $body = document.body;
|
||||
export default Component.extend({
|
||||
isDropdownVisible: false,
|
||||
didInsertElement: function() {
|
||||
$html.classList.remove('template-with-vertical-menu');
|
||||
},
|
||||
actions: {
|
||||
dropdown: function(e) {
|
||||
if (get(this, 'dcs.length') > 0) {
|
||||
set(this, 'isDropdownVisible', !get(this, 'isDropdownVisible'));
|
||||
}
|
||||
},
|
||||
change: function(e) {
|
||||
if (e.target.checked) {
|
||||
$html.classList.add('template-with-vertical-menu');
|
||||
$body.style.height = $html.style.height = window.innerHeight + 'px';
|
||||
} else {
|
||||
$html.classList.remove('template-with-vertical-menu');
|
||||
$body.style.height = $html.style.height = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ['healthcheck-status'],
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
import Component from '@ember/component';
|
||||
import { computed, get } from '@ember/object';
|
||||
import style from 'ember-computed-style';
|
||||
export default Component.extend({
|
||||
classNames: ['healthchecked-resource'],
|
||||
attributeBindings: ['style'],
|
||||
style: style('gridRowEnd'),
|
||||
unhealthy: computed.filter(`checks.@each.Status`, function(item) {
|
||||
const status = get(item, 'Status');
|
||||
return status === 'critical' || status === 'warning';
|
||||
}),
|
||||
healthy: computed.filter(`checks.@each.Status`, function(item) {
|
||||
const status = get(item, 'Status');
|
||||
return status === 'passing';
|
||||
}),
|
||||
gridRowEnd: computed('UnhealthyChecks', function() {
|
||||
let spans = 3;
|
||||
if (get(this, 'healthy.length') > 0) {
|
||||
spans++;
|
||||
}
|
||||
return {
|
||||
gridRow: `auto / span ${spans + (get(this, 'unhealthy.length') || 0)}`,
|
||||
};
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import Component from 'ember-collection/components/ember-collection';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'div',
|
||||
classNames: ['list-collection'],
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
tagName: 'fieldset',
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
name: 'tab',
|
||||
tagName: 'nav',
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
import Component from '@ember/component';
|
||||
import { computed } from '@ember/object';
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ['tab-section'],
|
||||
'data-test-radiobutton': computed('name,id', function() {
|
||||
return `${this.get('name')}_${this.get('id')}`;
|
||||
}),
|
||||
name: 'tab',
|
||||
onchange: function() {},
|
||||
});
|
|
@ -0,0 +1,130 @@
|
|||
import Component from 'ember-collection/components/ember-collection';
|
||||
import needsRevalidate from 'ember-collection/utils/needs-revalidate';
|
||||
import Grid from 'ember-collection/layouts/grid';
|
||||
import SlotsMixin from 'ember-block-slots';
|
||||
import style from 'ember-computed-style';
|
||||
|
||||
import { computed, get, set } from '@ember/object';
|
||||
|
||||
const $$ = document.querySelectorAll.bind(document);
|
||||
const createSizeEvent = function(detail) {
|
||||
return {
|
||||
detail: { width: window.innerWidth, height: window.innerHeight },
|
||||
};
|
||||
};
|
||||
class ZIndexedGrid extends Grid {
|
||||
formatItemStyle(index, w, h) {
|
||||
let style = super.formatItemStyle(...arguments);
|
||||
style += 'z-index: ' + (10000 - index);
|
||||
return style;
|
||||
}
|
||||
}
|
||||
// TODO instead of degrading gracefully
|
||||
// add a while polyfill for closest
|
||||
const closest = function(sel, el) {
|
||||
try {
|
||||
return el.closest(sel);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
};
|
||||
const change = function(e) {
|
||||
if (e instanceof MouseEvent) {
|
||||
return;
|
||||
}
|
||||
// TODO: Why am I getting a jQuery event here?!
|
||||
if (e instanceof Event) {
|
||||
const value = e.currentTarget.value;
|
||||
if (value != get(this, 'checked')) {
|
||||
set(this, 'checked', value);
|
||||
} else {
|
||||
set(this, 'checked', null);
|
||||
}
|
||||
} else if (e.detail && e.detail.index) {
|
||||
if (e.detail.confirming) {
|
||||
this.confirming.push(e.detail.index);
|
||||
} else {
|
||||
const pos = this.confirming.indexOf(e.detail.index);
|
||||
if (pos !== -1) {
|
||||
this.confirming.splice(pos, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
export default Component.extend(SlotsMixin, {
|
||||
tagName: 'table',
|
||||
attributeBindings: ['style'],
|
||||
width: 1150,
|
||||
height: 500,
|
||||
style: style('getStyle'),
|
||||
checked: null,
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this.change = change.bind(this);
|
||||
this.confirming = [];
|
||||
// TODO: This should auto calculate properly from the CSS
|
||||
this['cell-layout'] = new ZIndexedGrid(get(this, 'width'), 50);
|
||||
this.handler = () => {
|
||||
this.resize(createSizeEvent());
|
||||
};
|
||||
},
|
||||
getStyle: computed('height', function() {
|
||||
return {
|
||||
height: get(this, 'height'),
|
||||
};
|
||||
}),
|
||||
willRender: function() {
|
||||
this._super(...arguments);
|
||||
this.set('hasActions', this._isRegistered('actions'));
|
||||
},
|
||||
didInsertElement: function() {
|
||||
this._super(...arguments);
|
||||
window.addEventListener('resize', this.handler);
|
||||
this.handler();
|
||||
},
|
||||
willDestroyElement: function() {
|
||||
window.removeEventListener('resize', this.handler);
|
||||
},
|
||||
resize: function(e) {
|
||||
const $footer = [...$$('#wrapper > footer')][0];
|
||||
const $thead = [...$$('main > div')][0];
|
||||
if ($thead) {
|
||||
// TODO: This should auto calculate properly from the CSS
|
||||
this.set('height', Math.max(0, new Number(e.detail.height - ($footer.clientHeight + 218))));
|
||||
this['cell-layout'] = new ZIndexedGrid($thead.clientWidth, 50);
|
||||
this.updateItems();
|
||||
this.updateScrollPosition();
|
||||
}
|
||||
},
|
||||
_needsRevalidate: function() {
|
||||
if (this.isDestroyed || this.isDestroying) {
|
||||
return;
|
||||
}
|
||||
if (this._isGlimmer2()) {
|
||||
this.rerender();
|
||||
} else {
|
||||
needsRevalidate(this);
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
click: function(e) {
|
||||
const name = e.target.nodeName.toLowerCase();
|
||||
switch (name) {
|
||||
case 'input':
|
||||
case 'label':
|
||||
case 'a':
|
||||
case 'button':
|
||||
return;
|
||||
}
|
||||
const $a = closest('tr', e.target).querySelector('a');
|
||||
if ($a) {
|
||||
const click = new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
view: window,
|
||||
});
|
||||
$a.dispatchEvent(click);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,57 @@
|
|||
import Component from '@ember/component';
|
||||
import { computed, set, get } from '@ember/object';
|
||||
|
||||
const size = 336;
|
||||
const insetSize = size / 2 - 8;
|
||||
const inset = function(num) {
|
||||
return insetSize * num;
|
||||
};
|
||||
const milliseconds = function(num, max) {
|
||||
return max > 0 ? parseInt(max * num) / 100 : 0;
|
||||
};
|
||||
export default Component.extend({
|
||||
size: size,
|
||||
tomography: 0,
|
||||
max: -999999999,
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this.circle = [inset(1), inset(0.25), inset(0.5), inset(0.75), inset(1)];
|
||||
this.labels = [inset(-0.25), inset(-0.5), inset(-0.75), inset(-1)];
|
||||
},
|
||||
milliseconds: computed('distances', 'max', function() {
|
||||
const max = get(this, 'max');
|
||||
return [
|
||||
milliseconds(25, max),
|
||||
milliseconds(50, max),
|
||||
milliseconds(75, max),
|
||||
milliseconds(100, max),
|
||||
];
|
||||
}),
|
||||
distances: computed('tomography', function() {
|
||||
const tomography = this.get('tomography');
|
||||
let distances = tomography.distances || [];
|
||||
distances.forEach((d, i) => {
|
||||
if (d.distance > get(this, 'max')) {
|
||||
set(this, 'max', d.distance);
|
||||
}
|
||||
});
|
||||
if (tomography.n > 360) {
|
||||
let n = distances.length;
|
||||
// We have more nodes than we want to show, take a random sampling to keep
|
||||
// the number around 360.
|
||||
const sampling = 360 / tomography.n;
|
||||
distances = distances.filter(function(_, i) {
|
||||
return i == 0 || i == n - 1 || Math.random() < sampling;
|
||||
});
|
||||
}
|
||||
return distances.map((d, i) => {
|
||||
return {
|
||||
rotate: i * 360 / distances.length,
|
||||
y2: -insetSize * (d.distance / get(this, 'max')),
|
||||
node: d.node,
|
||||
distance: d.distance,
|
||||
segment: d.segment,
|
||||
};
|
||||
});
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
import Controller from '@ember/controller';
|
||||
|
||||
export default Controller.extend({});
|
|
@ -0,0 +1,2 @@
|
|||
import Controller from './edit';
|
||||
export default Controller.extend();
|
|
@ -0,0 +1,30 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { set } from '@ember/object';
|
||||
import Changeset from 'ember-changeset';
|
||||
import validations from 'consul-ui/validations/acl';
|
||||
import lookupValidator from 'ember-changeset-validations';
|
||||
|
||||
export default Controller.extend({
|
||||
setProperties: function(model) {
|
||||
this.changeset = new Changeset(model.item, lookupValidator(validations), validations);
|
||||
this._super({
|
||||
...model,
|
||||
...{
|
||||
item: this.changeset,
|
||||
},
|
||||
});
|
||||
},
|
||||
actions: {
|
||||
change: function(e) {
|
||||
const target = e.target || { name: 'Rules', value: e };
|
||||
switch (target.name) {
|
||||
case 'Type':
|
||||
set(this.changeset, target.name, target.value);
|
||||
break;
|
||||
case 'Rules':
|
||||
set(this, 'item.Rules', target.value);
|
||||
break;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { computed, get } from '@ember/object';
|
||||
import WithFiltering from 'consul-ui/mixins/with-filtering';
|
||||
import ucfirst from 'consul-ui/utils/ucfirst';
|
||||
import numeral from 'numeral';
|
||||
const countType = function(items, type) {
|
||||
return type === '' ? get(items, 'length') : items.filterBy('Type', type).length;
|
||||
};
|
||||
export default Controller.extend(WithFiltering, {
|
||||
queryParams: {
|
||||
type: {
|
||||
as: 'type',
|
||||
},
|
||||
s: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
typeFilters: computed('items', function() {
|
||||
const items = get(this, 'items');
|
||||
return ['', 'management', 'client'].map(function(item) {
|
||||
return {
|
||||
label: `${item === '' ? 'All' : ucfirst(item)} (${numeral(
|
||||
countType(items, item)
|
||||
).format()})`,
|
||||
value: item,
|
||||
};
|
||||
});
|
||||
}),
|
||||
filter: function(item, { s = '', type = '' }) {
|
||||
return (
|
||||
get(item, 'Name')
|
||||
.toLowerCase()
|
||||
.indexOf(s.toLowerCase()) !== -1 &&
|
||||
(type === '' || get(item, 'Type') === type)
|
||||
);
|
||||
},
|
||||
actions: {
|
||||
sendClone: function(item) {
|
||||
this.send('clone', item);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,2 @@
|
|||
import Controller from './edit';
|
||||
export default Controller.extend();
|
|
@ -0,0 +1,40 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { get, set } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
|
||||
import Changeset from 'ember-changeset';
|
||||
import validations from 'consul-ui/validations/kv';
|
||||
import lookupValidator from 'ember-changeset-validations';
|
||||
export default Controller.extend({
|
||||
json: false,
|
||||
encoder: service('btoa'),
|
||||
setProperties: function(model) {
|
||||
// TODO: Potentially save whether json has been clicked to the model
|
||||
set(this, 'json', false);
|
||||
this.changeset = new Changeset(model.item, lookupValidator(validations), validations);
|
||||
this._super({
|
||||
...model,
|
||||
...{
|
||||
item: this.changeset,
|
||||
},
|
||||
});
|
||||
},
|
||||
actions: {
|
||||
change: function(e) {
|
||||
const target = e.target || { name: 'value', value: e };
|
||||
var parent;
|
||||
switch (target.name) {
|
||||
case 'additional':
|
||||
parent = get(this, 'parent.Key');
|
||||
set(this.changeset, 'Key', `${parent !== '/' ? parent : ''}${target.value}`);
|
||||
break;
|
||||
case 'json':
|
||||
set(this, 'json', !get(this, 'json'));
|
||||
break;
|
||||
case 'value':
|
||||
set(this, 'item.Value', get(this, 'encoder').execute(target.value));
|
||||
break;
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,2 @@
|
|||
import Controller from './create';
|
||||
export default Controller.extend();
|
|
@ -0,0 +1,27 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { computed } from '@ember/object';
|
||||
import WithHealthFiltering from 'consul-ui/mixins/with-health-filtering';
|
||||
import { get } from '@ember/object';
|
||||
export default Controller.extend(WithHealthFiltering, {
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this.columns = [25, 25, 25, 25];
|
||||
},
|
||||
unhealthy: computed('filtered', function() {
|
||||
return get(this, 'filtered').filter(function(item) {
|
||||
return get(item, 'isUnhealthy');
|
||||
});
|
||||
}),
|
||||
healthy: computed('filtered', function() {
|
||||
return get(this, 'filtered').filter(function(item) {
|
||||
return get(item, 'isHealthy');
|
||||
});
|
||||
}),
|
||||
filter: function(item, { s = '', status = '' }) {
|
||||
return (
|
||||
get(item, 'Node')
|
||||
.toLowerCase()
|
||||
.indexOf(s.toLowerCase()) !== -1 && item.hasStatus(status)
|
||||
);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,53 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { get, set } from '@ember/object';
|
||||
import WithFiltering from 'consul-ui/mixins/with-filtering';
|
||||
|
||||
export default Controller.extend(WithFiltering, {
|
||||
queryParams: {
|
||||
s: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
setProperties: function() {
|
||||
this._super(...arguments);
|
||||
set(this, 'selectedTab', 'health-checks');
|
||||
},
|
||||
filter: function(item, { s = '' }) {
|
||||
return (
|
||||
get(item, 'Service')
|
||||
.toLowerCase()
|
||||
.indexOf(s.toLowerCase()) !== -1
|
||||
);
|
||||
},
|
||||
actions: {
|
||||
sortChecksByImportance: function(a, b) {
|
||||
const statusA = get(a, 'Status');
|
||||
const statusB = get(b, 'Status');
|
||||
switch (statusA) {
|
||||
case 'passing':
|
||||
// a = passing
|
||||
// unless b is also passing then a is less important
|
||||
return statusB === 'passing' ? 0 : 1;
|
||||
case 'critical':
|
||||
// a = critical
|
||||
// unless b is also critical then a is more important
|
||||
return statusB === 'critical' ? 0 : -1;
|
||||
case 'warning':
|
||||
// a = warning
|
||||
switch (statusB) {
|
||||
// b is passing so a is more important
|
||||
case 'passing':
|
||||
return -1;
|
||||
// b is critical so a is less important
|
||||
case 'critical':
|
||||
return 1;
|
||||
// a and b are both warning, therefore equal
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,65 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { get, computed } from '@ember/object';
|
||||
import { htmlSafe } from '@ember/string';
|
||||
import WithHealthFiltering from 'consul-ui/mixins/with-health-filtering';
|
||||
const max = function(arr, prop) {
|
||||
return arr.reduce(function(prev, item) {
|
||||
return Math.max(prev, get(item, prop));
|
||||
}, 0);
|
||||
};
|
||||
const chunk = function(str, size) {
|
||||
const num = Math.ceil(str.length / size);
|
||||
const chunks = new Array(num);
|
||||
for (let i = 0, o = 0; i < num; ++i, o += size) {
|
||||
chunks[i] = str.substr(o, size);
|
||||
}
|
||||
return chunks;
|
||||
};
|
||||
const width = function(num) {
|
||||
const str = num.toString();
|
||||
const len = str.length;
|
||||
const commas = chunk(str, 3).length - 1;
|
||||
return commas * 4 + len * 10;
|
||||
};
|
||||
const widthDeclaration = function(num) {
|
||||
return htmlSafe(`width: ${num}px`);
|
||||
};
|
||||
export default Controller.extend(WithHealthFiltering, {
|
||||
filter: function(item, { s = '', status = '' }) {
|
||||
return (
|
||||
get(item, 'Name')
|
||||
.toLowerCase()
|
||||
.indexOf(s.toLowerCase()) !== -1 && item.hasStatus(status)
|
||||
);
|
||||
},
|
||||
totalWidth: computed('{maxPassing,maxWarning,maxCritical}', function() {
|
||||
const PADDING = 32 * 3 + 13;
|
||||
return ['maxPassing', 'maxWarning', 'maxCritical'].reduce((prev, item) => {
|
||||
return prev + width(get(this, item));
|
||||
}, PADDING);
|
||||
}),
|
||||
thWidth: computed('totalWidth', function() {
|
||||
return widthDeclaration(get(this, 'totalWidth'));
|
||||
}),
|
||||
remainingWidth: computed('totalWidth', function() {
|
||||
return htmlSafe(`width: calc(50% - ${Math.round(get(this, 'totalWidth') / 2)}px)`);
|
||||
}),
|
||||
maxPassing: computed('items', function() {
|
||||
return max(get(this, 'items'), 'ChecksPassing');
|
||||
}),
|
||||
maxWarning: computed('items', function() {
|
||||
return max(get(this, 'items'), 'ChecksWarning');
|
||||
}),
|
||||
maxCritical: computed('items', function() {
|
||||
return max(get(this, 'items'), 'ChecksCritical');
|
||||
}),
|
||||
passingWidth: computed('maxPassing', function() {
|
||||
return widthDeclaration(width(get(this, 'maxPassing')));
|
||||
}),
|
||||
warningWidth: computed('maxWarning', function() {
|
||||
return widthDeclaration(width(get(this, 'maxWarning')));
|
||||
}),
|
||||
criticalWidth: computed('maxCritical', function() {
|
||||
return widthDeclaration(width(get(this, 'maxCritical')));
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
import Controller from '@ember/controller';
|
||||
import { get } from '@ember/object';
|
||||
import { computed } from '@ember/object';
|
||||
import sumOfUnhealthy from 'consul-ui/utils/sumOfUnhealthy';
|
||||
import hasStatus from 'consul-ui/utils/hasStatus';
|
||||
import WithHealthFiltering from 'consul-ui/mixins/with-health-filtering';
|
||||
export default Controller.extend(WithHealthFiltering, {
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this.columns = [25, 25, 25, 25];
|
||||
},
|
||||
unhealthy: computed('filtered', function() {
|
||||
return get(this, 'filtered').filter(function(item) {
|
||||
return sumOfUnhealthy(item.Checks) > 0;
|
||||
});
|
||||
}),
|
||||
healthy: computed('filtered', function() {
|
||||
return get(this, 'filtered').filter(function(item) {
|
||||
return sumOfUnhealthy(item.Checks) === 0;
|
||||
});
|
||||
}),
|
||||
filter: function(item, { s = '', status = '' }) {
|
||||
return (
|
||||
get(item, 'Node.Node')
|
||||
.toLowerCase()
|
||||
.indexOf(s.toLowerCase()) !== -1 && hasStatus(get(item, 'Checks'), status)
|
||||
);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,5 @@
|
|||
import { helper } from '@ember/component/helper';
|
||||
import atob from 'consul-ui/utils/atob';
|
||||
export default helper(function([str = '']) {
|
||||
return atob(str);
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
import { helper } from '@ember/component/helper';
|
||||
import $ from 'consul-ui/config/environment';
|
||||
export function env([name, def = ''], hash) {
|
||||
return $[name] != null ? $[name] : def;
|
||||
}
|
||||
|
||||
export default helper(env);
|
|
@ -0,0 +1,13 @@
|
|||
import Helper from '@ember/component/helper';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { observer } from '@ember/object';
|
||||
|
||||
export default Helper.extend({
|
||||
router: service('router'),
|
||||
compute(params) {
|
||||
return this.get('router').isActive(...params);
|
||||
},
|
||||
onURLChange: observer('router.currentURL', function() {
|
||||
this.recompute();
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
import { helper } from '@ember/component/helper';
|
||||
|
||||
export function last([obj = ''], hash) {
|
||||
switch (true) {
|
||||
case typeof obj === 'string':
|
||||
return obj.substr(-1);
|
||||
}
|
||||
}
|
||||
|
||||
export default helper(last);
|
|
@ -0,0 +1,7 @@
|
|||
import { helper } from '@ember/component/helper';
|
||||
|
||||
export function leftTrim([str = '', search = ''], hash) {
|
||||
return str.indexOf(search) === 0 ? str.substr(search.length) : str;
|
||||
}
|
||||
|
||||
export default helper(leftTrim);
|
|
@ -0,0 +1,8 @@
|
|||
import { helper } from '@ember/component/helper';
|
||||
|
||||
export function rightTrim([str = '', search = ''], hash) {
|
||||
const pos = str.length - search.length;
|
||||
return str.indexOf(search) === pos ? str.substr(0, pos) : str;
|
||||
}
|
||||
|
||||
export default helper(rightTrim);
|
|
@ -0,0 +1,9 @@
|
|||
import { helper } from '@ember/component/helper';
|
||||
|
||||
// TODO: Currently I'm only using this for hardcoded values
|
||||
// so ' ' to '-' replacement is sufficient for the moment
|
||||
export function slugify([str = ''], hash) {
|
||||
return str.replace(/ /g, '-').toLowerCase();
|
||||
}
|
||||
|
||||
export default helper(slugify);
|
|
@ -0,0 +1,7 @@
|
|||
import { helper } from '@ember/component/helper';
|
||||
|
||||
export function split([array = [], separator = ','], hash) {
|
||||
return array.split(separator);
|
||||
}
|
||||
|
||||
export default helper(split);
|
|
@ -0,0 +1,33 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="ember-loading">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<title>Consul by HashiCorp</title>
|
||||
<meta name="description" content="">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
{{content-for "head"}}
|
||||
<link rel="icon" type="image/png" href="{{rootURL}}assets/favicon-32x32.png" sizes="32x32">
|
||||
<link rel="icon" type="image/png" href="{{rootURL}}assets/favicon-16x16.png" sizes="16x16">
|
||||
|
||||
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/vendor.css">
|
||||
<link integrity="" rel="stylesheet" href="{{rootURL}}assets/consul-ui.css">
|
||||
|
||||
{{content-for "head-footer"}}
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
<div style="margin: 0 auto;">
|
||||
<h2>JavaScript Required</h2>
|
||||
<p>Please enable JavaScript in your web browser to use Consul UI.</p>
|
||||
</div>
|
||||
</noscript>
|
||||
{{content-for "body"}}
|
||||
<svg width="168" height="53" xmlns="http://www.w3.org/2000/svg"><g fill="#919FA8" fill-rule="evenodd"><path d="M26.078 32.12a5.586 5.586 0 1 1 5.577-5.599 5.577 5.577 0 0 1-5.577 5.6M37.009 29.328a2.56 2.56 0 1 1 2.56-2.56 2.551 2.551 0 0 1-2.56 2.56M46.916 31.669a2.56 2.56 0 1 1 .051-.21c-.028.066-.028.13-.051.21M44.588 25.068a2.565 2.565 0 0 1-2.672-.992 2.558 2.558 0 0 1-.102-2.845 2.564 2.564 0 0 1 4.676.764c.072.328.081.667.027 1a2.463 2.463 0 0 1-1.925 2.073M53.932 31.402a2.547 2.547 0 0 1-2.95 2.076 2.559 2.559 0 0 1-2.064-2.965 2.547 2.547 0 0 1 2.948-2.077 2.57 2.57 0 0 1 2.128 2.716.664.664 0 0 0-.05.228M51.857 25.103a2.56 2.56 0 1 1 2.108-2.945c.034.218.043.439.027.658a2.547 2.547 0 0 1-2.135 2.287M49.954 40.113a2.56 2.56 0 1 1 .314-1.037c-.02.366-.128.721-.314 1.037M48.974 16.893a2.56 2.56 0 1 1 .97-3.487c.264.446.375.965.317 1.479a2.56 2.56 0 0 1-1.287 2.008"/><path d="M26.526 52.603c-14.393 0-26.06-11.567-26.06-25.836C.466 12.498 12.133.931 26.526.931a25.936 25.936 0 0 1 15.836 5.307l-3.167 4.117A20.962 20.962 0 0 0 17.304 8.23C10.194 11.713 5.7 18.9 5.714 26.763c-.014 7.862 4.48 15.05 11.59 18.534a20.962 20.962 0 0 0 21.89-2.127l3.168 4.123a25.981 25.981 0 0 1-15.836 5.31zM61 30.15V17.948c0-4.962 2.845-7.85 9.495-7.85 2.484 0 5.048.326 7.252.895l-.561 4.433c-2.164-.406-4.688-.691-6.53-.691-3.486 0-4.608 1.22-4.608 4.108v10.412c0 2.888 1.122 4.108 4.607 4.108 1.843 0 4.367-.284 6.53-.691l.562 4.433c-2.204.57-4.768.895-7.252.895C63.845 38 61 35.112 61 30.15zm36.808.04c0 4.068-1.802 7.81-8.493 7.81-6.69 0-8.494-3.742-8.494-7.81v-5.002c0-4.067 1.803-7.81 8.494-7.81 6.69 0 8.493 3.743 8.493 7.81v5.003zm-4.887-5.165c0-2.237-1.002-3.416-3.606-3.416s-3.606 1.18-3.606 3.416v5.328c0 2.237 1.002 3.417 3.606 3.417s3.606-1.18 3.606-3.417v-5.328zm25.79 12.568h-4.887V23.764c0-1.057-.44-1.586-1.563-1.586-1.201 0-3.325.732-5.088 1.668v13.747h-4.887V17.785h3.726l.48 1.668c2.444-1.22 5.53-2.074 7.813-2.074 3.245 0 4.407 2.318 4.407 5.857v14.357zm18.26-5.775c0 3.823-1.162 6.182-7.052 6.182-2.083 0-4.927-.488-6.73-1.139l.68-3.782c1.643.488 3.807.854 5.81.854 2.164 0 2.484-.488 2.484-1.993 0-1.22-.24-1.83-3.405-2.603-4.768-1.18-5.329-2.4-5.329-6.223 0-3.986 1.723-5.735 7.292-5.735 1.803 0 4.166.244 5.85.691l-.482 3.945c-1.482-.284-3.846-.569-5.368-.569-2.124 0-2.484.488-2.484 1.708 0 1.587.12 1.709 2.764 2.4 5.449 1.464 5.97 2.196 5.97 6.264zm4.357-14.033h4.887v13.83c0 1.057.441 1.586 1.563 1.586 1.202 0 3.325-.733 5.088-1.668V17.785h4.888v19.808h-3.726l-.481-1.667c-2.444 1.22-5.529 2.074-7.812 2.074-3.246 0-4.407-2.318-4.407-5.857V17.785zM168 37.593h-4.888V9.691L168 9v28.593z"/></g></svg>
|
||||
<script src="{{rootURL}}assets/vendor.js"></script>
|
||||
<script src="{{rootURL}}assets/consul-ui.js"></script>
|
||||
|
||||
{{content-for "body-footer"}}
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,92 @@
|
|||
import Mixin from '@ember/object/mixin';
|
||||
import { get } from '@ember/object';
|
||||
import { inject as service } from '@ember/service';
|
||||
import WithFeedback from 'consul-ui/mixins/with-feedback';
|
||||
|
||||
export default Mixin.create(WithFeedback, {
|
||||
settings: service('settings'),
|
||||
actions: {
|
||||
create: function(item) {
|
||||
get(this, 'feedback').execute(
|
||||
() => {
|
||||
return get(this, 'repo')
|
||||
.persist(item)
|
||||
.then(item => {
|
||||
return this.transitionTo('dc.acls');
|
||||
});
|
||||
},
|
||||
`Your ACL token has been added.`,
|
||||
`There was an error adding your ACL token.`
|
||||
);
|
||||
},
|
||||
update: function(item) {
|
||||
get(this, 'feedback').execute(
|
||||
() => {
|
||||
return get(this, 'repo')
|
||||
.persist(item)
|
||||
.then(() => {
|
||||
return this.transitionTo('dc.acls');
|
||||
});
|
||||
},
|
||||
`Your ACL token was saved.`,
|
||||
`There was an error saving your ACL token.`
|
||||
);
|
||||
},
|
||||
delete: function(item) {
|
||||
get(this, 'feedback').execute(
|
||||
() => {
|
||||
return (
|
||||
get(this, 'repo')
|
||||
// ember-changeset doesn't support `get`
|
||||
// and `data` returns an object not a model
|
||||
.remove(item)
|
||||
.then(() => {
|
||||
switch (this.routeName) {
|
||||
case 'dc.acls.index':
|
||||
return this.refresh();
|
||||
default:
|
||||
return this.transitionTo('dc.acls');
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
`Your ACL token was deleted.`,
|
||||
`There was an error deleting your ACL token.`
|
||||
);
|
||||
},
|
||||
cancel: function(item) {
|
||||
this.transitionTo('dc.acls');
|
||||
},
|
||||
use: function(item) {
|
||||
get(this, 'feedback').execute(
|
||||
() => {
|
||||
return get(this, 'settings')
|
||||
.persist({ token: get(item, 'ID') })
|
||||
.then(() => {
|
||||
this.transitionTo('dc.services');
|
||||
});
|
||||
},
|
||||
`Now using new ACL token`,
|
||||
`There was an error using that ACL token`
|
||||
);
|
||||
},
|
||||
clone: function(item) {
|
||||
get(this, 'feedback').execute(
|
||||
() => {
|
||||
return get(this, 'repo')
|
||||
.clone(item)
|
||||
.then(item => {
|
||||
switch (this.routeName) {
|
||||
case 'dc.acls.index':
|
||||
return this.refresh();
|
||||
default:
|
||||
return this.transitionTo('dc.acls');
|
||||
}
|
||||
});
|
||||
},
|
||||
`Your ACL token was cloned.`,
|
||||
`There was an error cloning your ACL token.`
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
import Mixin from '@ember/object/mixin';
|
||||
|
||||
import { next } from '@ember/runloop';
|
||||
import { get } from '@ember/object';
|
||||
const isOutside = function(element, e) {
|
||||
const isRemoved = !e.target || !document.contains(e.target);
|
||||
const isInside = element === e.target || element.contains(e.target);
|
||||
return !isRemoved && !isInside;
|
||||
};
|
||||
const handler = function(e) {
|
||||
const el = get(this, 'element');
|
||||
if (isOutside(el, e)) {
|
||||
this.onblur(e);
|
||||
}
|
||||
};
|
||||
export default Mixin.create({
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
this.handler = handler.bind(this);
|
||||
},
|
||||
onchange: function() {},
|
||||
onblur: function() {},
|
||||
didInsertElement: function() {
|
||||
this._super(...arguments);
|
||||
next(this, () => {
|
||||
document.addEventListener('click', this.handler);
|
||||
});
|
||||
},
|
||||
willDestroyElement: function() {
|
||||
this._super(...arguments);
|
||||
document.removeEventListener('click', this.handler);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,79 @@
|
|||
import Mixin from '@ember/object/mixin';
|
||||
import { get, set } from '@ember/object';
|
||||
import WithFeedback from 'consul-ui/mixins/with-feedback';
|
||||
|
||||
const transitionToList = function(key, transitionTo) {
|
||||
if (key === '/') {
|
||||
return transitionTo('dc.kv.index');
|
||||
} else {
|
||||
return transitionTo('dc.kv.folder', key);
|
||||
}
|
||||
};
|
||||
|
||||
export default Mixin.create(WithFeedback, {
|
||||
actions: {
|
||||
create: function(item, parent) {
|
||||
get(this, 'feedback').execute(
|
||||
() => {
|
||||
return get(this, 'repo')
|
||||
.persist(item)
|
||||
.then(item => {
|
||||
return transitionToList(get(parent, 'Key'), this.transitionTo.bind(this));
|
||||
});
|
||||
},
|
||||
`Your key has been added.`,
|
||||
`There was an error adding your key.`
|
||||
);
|
||||
},
|
||||
update: function(item, parent) {
|
||||
get(this, 'feedback').execute(
|
||||
() => {
|
||||
return get(this, 'repo')
|
||||
.persist(item)
|
||||
.then(() => {
|
||||
return transitionToList(get(parent, 'Key'), this.transitionTo.bind(this));
|
||||
});
|
||||
},
|
||||
`Your key has been saved.`,
|
||||
`There was an error saving your key.`
|
||||
);
|
||||
},
|
||||
delete: function(item, parent) {
|
||||
get(this, 'feedback').execute(
|
||||
() => {
|
||||
return get(this, 'repo')
|
||||
.remove(item)
|
||||
.then(() => {
|
||||
switch (this.routeName) {
|
||||
case 'dc.kv.index':
|
||||
return this.refresh();
|
||||
default:
|
||||
return transitionToList(get(parent, 'Key'), this.transitionTo.bind(this));
|
||||
}
|
||||
});
|
||||
},
|
||||
`Your key was deleted.`,
|
||||
`There was an error deleting your key.`
|
||||
);
|
||||
},
|
||||
cancel: function(item, parent) {
|
||||
return transitionToList(get(parent, 'Key'), this.transitionTo.bind(this));
|
||||
},
|
||||
invalidateSession: function(item) {
|
||||
const controller = this.controller;
|
||||
const repo = get(this, 'sessionRepo');
|
||||
get(this, 'feedback').execute(
|
||||
() => {
|
||||
return repo.remove(item).then(() => {
|
||||
const item = get(controller, 'item');
|
||||
set(item, 'Session', null);
|
||||
delete item.Session;
|
||||
set(controller, 'session', null);
|
||||
});
|
||||
},
|
||||
`The session was invalidated.`,
|
||||
`There was an error invalidating the session.`
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,13 @@
|
|||
import Mixin from '@ember/object/mixin';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { get, set } from '@ember/object';
|
||||
|
||||
export default Mixin.create({
|
||||
feedback: service('feedback'),
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
set(this, 'feedback', {
|
||||
execute: get(this, 'feedback').execute.bind(this),
|
||||
});
|
||||
},
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
import Mixin from '@ember/object/mixin';
|
||||
import { computed, get, set } from '@ember/object';
|
||||
|
||||
const toKeyValue = function(el) {
|
||||
const key = el.name;
|
||||
let value = '';
|
||||
switch (el.type) {
|
||||
case 'radio':
|
||||
case 'search':
|
||||
case 'text':
|
||||
value = el.value;
|
||||
break;
|
||||
}
|
||||
return { [key]: value };
|
||||
};
|
||||
export default Mixin.create({
|
||||
filters: {},
|
||||
filtered: computed('items', 'filters', function() {
|
||||
const filters = get(this, 'filters');
|
||||
return get(this, 'items').filter(item => {
|
||||
return this.filter(item, filters);
|
||||
});
|
||||
}),
|
||||
setProperties: function() {
|
||||
this._super(...arguments);
|
||||
const query = get(this, 'queryParams');
|
||||
query.forEach((item, i, arr) => {
|
||||
const filters = get(this, 'filters');
|
||||
Object.keys(item).forEach(key => {
|
||||
set(filters, key, get(this, key));
|
||||
});
|
||||
set(this, 'filters', filters);
|
||||
});
|
||||
},
|
||||
actions: {
|
||||
filter: function(e) {
|
||||
const obj = toKeyValue(e.target);
|
||||
Object.keys(obj).forEach((key, i, arr) => {
|
||||
set(this, key, obj[key] != '' ? obj[key] : null);
|
||||
});
|
||||
set(this, 'filters', {
|
||||
...this.get('filters'),
|
||||
...obj,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,50 @@
|
|||
import Mixin from '@ember/object/mixin';
|
||||
import WithFiltering from 'consul-ui/mixins/with-filtering';
|
||||
import { computed, get } from '@ember/object';
|
||||
import ucfirst from 'consul-ui/utils/ucfirst';
|
||||
import numeral from 'numeral';
|
||||
|
||||
const countStatus = function(items, status) {
|
||||
if (status === '') {
|
||||
return get(items, 'length');
|
||||
}
|
||||
const key = `Checks${ucfirst(status)}`;
|
||||
return items.reduce(function(prev, item, i, arr) {
|
||||
const num = get(item, key);
|
||||
return (
|
||||
prev +
|
||||
(typeof num !== 'undefined'
|
||||
? num
|
||||
: get(item, 'Checks').filter(function(item) {
|
||||
return item.Status === status;
|
||||
}).length) || 0
|
||||
);
|
||||
}, 0);
|
||||
};
|
||||
export default Mixin.create(WithFiltering, {
|
||||
queryParams: {
|
||||
status: {
|
||||
as: 'status',
|
||||
},
|
||||
s: {
|
||||
as: 'filter',
|
||||
},
|
||||
},
|
||||
healthFilters: computed('items', function() {
|
||||
const items = get(this, 'items');
|
||||
const objs = ['', 'passing', 'warning', 'critical'].map(function(item) {
|
||||
const count = countStatus(items, item);
|
||||
return {
|
||||
count: count,
|
||||
label: `${item === '' ? 'All' : ucfirst(item)} (${numeral(count).format()})`,
|
||||
value: item,
|
||||
};
|
||||
});
|
||||
objs[0].label = `All (${numeral(
|
||||
objs.slice(1).reduce(function(prev, item, i, arr) {
|
||||
return prev + item.count;
|
||||
}, 0)
|
||||
).format()})`;
|
||||
return objs;
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
import Model from 'ember-data/model';
|
||||
import attr from 'ember-data/attr';
|
||||
|
||||
export const PRIMARY_KEY = 'uid';
|
||||
export const SLUG_KEY = 'ID';
|
||||
|
||||
export default Model.extend({
|
||||
[PRIMARY_KEY]: attr('string'),
|
||||
[SLUG_KEY]: attr('string'),
|
||||
Name: attr('string'),
|
||||
Type: attr('string'),
|
||||
Rules: attr('string'),
|
||||
CreateIndex: attr('number'),
|
||||
ModifyIndex: attr('number'),
|
||||
Datacenter: attr('string'),
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
import Model from 'ember-data/model';
|
||||
import attr from 'ember-data/attr';
|
||||
|
||||
export const PRIMARY_KEY = 'uid';
|
||||
export const SLUG_KEY = 'Node';
|
||||
|
||||
export default Model.extend({
|
||||
[PRIMARY_KEY]: attr('string'),
|
||||
[SLUG_KEY]: attr('string'),
|
||||
Coord: attr(),
|
||||
Segment: attr('string'),
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
import Model from 'ember-data/model';
|
||||
import attr from 'ember-data/attr';
|
||||
import { hasMany } from 'ember-data/relationships';
|
||||
|
||||
export const PRIMARY_KEY = 'uid';
|
||||
export const FOREIGN_KEY = 'Datacenter';
|
||||
export const SLUG_KEY = 'Name';
|
||||
|
||||
export default Model.extend({
|
||||
[PRIMARY_KEY]: attr('string'),
|
||||
[SLUG_KEY]: attr('string'),
|
||||
Services: hasMany('service'),
|
||||
Nodes: hasMany('node'),
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
import Model from 'ember-data/model';
|
||||
import attr from 'ember-data/attr';
|
||||
import { computed, get } from '@ember/object';
|
||||
import isFolder from 'consul-ui/utils/isFolder';
|
||||
|
||||
export const PRIMARY_KEY = 'uid';
|
||||
// not really a slug as it contains slashes but all intents and purposes
|
||||
// its my 'slug'
|
||||
export const SLUG_KEY = 'Key';
|
||||
|
||||
export default Model.extend({
|
||||
[PRIMARY_KEY]: attr('string'),
|
||||
[SLUG_KEY]: attr('string'),
|
||||
LockIndex: attr('number'),
|
||||
Flags: attr('number'),
|
||||
Value: attr('string'),
|
||||
CreateIndex: attr('string'),
|
||||
ModifyIndex: attr('string'),
|
||||
Session: attr('string'),
|
||||
Datacenter: attr('string'),
|
||||
|
||||
isFolder: computed('Key', function() {
|
||||
return isFolder(get(this, 'Key') || '');
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
import Model from 'ember-data/model';
|
||||
import attr from 'ember-data/attr';
|
||||
import { computed, get } from '@ember/object';
|
||||
import sumOfUnhealthy from 'consul-ui/utils/sumOfUnhealthy';
|
||||
import hasStatus from 'consul-ui/utils/hasStatus';
|
||||
|
||||
export const PRIMARY_KEY = 'uid';
|
||||
export const SLUG_KEY = 'ID';
|
||||
|
||||
export default Model.extend({
|
||||
[PRIMARY_KEY]: attr('string'),
|
||||
[SLUG_KEY]: attr('string'),
|
||||
Address: attr('string'),
|
||||
Node: attr('string'),
|
||||
Meta: attr(),
|
||||
Services: attr(),
|
||||
Checks: attr(),
|
||||
CreateIndex: attr('number'),
|
||||
ModifyIndex: attr('number'),
|
||||
TaggedAddresses: attr(),
|
||||
Datacenter: attr('string'),
|
||||
Segment: attr(),
|
||||
Coord: attr(),
|
||||
hasStatus: function(status) {
|
||||
return hasStatus(get(this, 'Checks'), status);
|
||||
},
|
||||
isHealthy: computed('Checks', function() {
|
||||
return sumOfUnhealthy(get(this, 'Checks')) === 0;
|
||||
}),
|
||||
isUnhealthy: computed('Checks', function() {
|
||||
return sumOfUnhealthy(get(this, 'Checks')) > 0;
|
||||
}),
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
import Model from 'ember-data/model';
|
||||
import attr from 'ember-data/attr';
|
||||
import { computed, get } from '@ember/object';
|
||||
|
||||
export const PRIMARY_KEY = 'uid';
|
||||
export const SLUG_KEY = 'Name';
|
||||
|
||||
export default Model.extend({
|
||||
[PRIMARY_KEY]: attr('string'),
|
||||
[SLUG_KEY]: attr('string'),
|
||||
Tags: attr({
|
||||
defaultValue: function() {
|
||||
return [];
|
||||
},
|
||||
}),
|
||||
Address: attr('string'),
|
||||
Port: attr('number'),
|
||||
EnableTagOverride: attr('boolean'),
|
||||
CreateIndex: attr('number'),
|
||||
ModifyIndex: attr('number'),
|
||||
ChecksPassing: attr(),
|
||||
ChecksCritical: attr(),
|
||||
ChecksWarning: attr(),
|
||||
Nodes: attr(),
|
||||
Datacenter: attr('string'),
|
||||
Node: attr(),
|
||||
Service: attr(),
|
||||
Checks: attr(),
|
||||
passing: computed('ChecksPassing', 'Checks', function() {
|
||||
let num = 0;
|
||||
// TODO: use typeof
|
||||
if (get(this, 'ChecksPassing') !== undefined) {
|
||||
num = get(this, 'ChecksPassing');
|
||||
} else {
|
||||
num = get(get(this, 'Checks').filterBy('Status', 'passing'), 'length');
|
||||
}
|
||||
return {
|
||||
length: num,
|
||||
};
|
||||
}),
|
||||
hasStatus: function(status) {
|
||||
let num = 0;
|
||||
switch (status) {
|
||||
case 'passing':
|
||||
num = get(this, 'ChecksPassing');
|
||||
break;
|
||||
case 'critical':
|
||||
num = get(this, 'ChecksCritical');
|
||||
break;
|
||||
case 'warning':
|
||||
num = get(this, 'ChecksWarning');
|
||||
break;
|
||||
case '': // all
|
||||
num = 1;
|
||||
break;
|
||||
}
|
||||
return num > 0;
|
||||
},
|
||||
});
|
|
@ -0,0 +1,23 @@
|
|||
import Model from 'ember-data/model';
|
||||
import attr from 'ember-data/attr';
|
||||
|
||||
export const PRIMARY_KEY = 'uid';
|
||||
export const SLUG_KEY = 'ID';
|
||||
|
||||
export default Model.extend({
|
||||
[PRIMARY_KEY]: attr('string'),
|
||||
[SLUG_KEY]: attr('string'),
|
||||
Name: attr('string'),
|
||||
Node: attr('string'),
|
||||
CreateIndex: attr('number'),
|
||||
ModifyIndex: attr('number'),
|
||||
LockDelay: attr('number'),
|
||||
Behavior: attr('string'),
|
||||
TTL: attr('number'),
|
||||
Checks: attr({
|
||||
defaultValue: function() {
|
||||
return [];
|
||||
},
|
||||
}),
|
||||
Datacenter: attr('string'),
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
import Resolver from 'ember-resolver';
|
||||
|
||||
export default Resolver;
|
|
@ -0,0 +1,45 @@
|
|||
import EmberRouter from '@ember/routing/router';
|
||||
import config from './config/environment';
|
||||
|
||||
const Router = EmberRouter.extend({
|
||||
location: config.locationType,
|
||||
rootURL: config.rootURL,
|
||||
});
|
||||
Router.map(function() {
|
||||
// Our parent datacenter resource sets the namespace
|
||||
// for the entire application
|
||||
this.route('dc', { path: '/:dc' }, function() {
|
||||
// Services represent a consul service
|
||||
this.route('services', { path: '/services' }, function() {
|
||||
// Show an individual service
|
||||
this.route('show', { path: '/*name' });
|
||||
});
|
||||
// Nodes represent a consul node
|
||||
this.route('nodes', { path: '/nodes' }, function() {
|
||||
// Show an individual node
|
||||
this.route('show', { path: '/:name' });
|
||||
});
|
||||
// Key/Value
|
||||
this.route('kv', { path: '/kv' }, function() {
|
||||
this.route('folder', { path: '/*key' });
|
||||
this.route('edit', { path: '/*key/edit' });
|
||||
this.route('create', { path: '/*key/create' });
|
||||
this.route('root-create', { path: '/create' });
|
||||
});
|
||||
// ACLs
|
||||
this.route('acls', { path: '/acls' }, function() {
|
||||
this.route('edit', { path: '/:id' });
|
||||
this.route('create', { path: '/create' });
|
||||
});
|
||||
});
|
||||
|
||||
// Shows a datacenter picker. If you only have one
|
||||
// it just redirects you through.
|
||||
this.route('index', { path: '/' });
|
||||
|
||||
// The settings page is global.
|
||||
this.route('settings', { path: '/settings' });
|
||||
this.route('notfound', { path: '/*path' });
|
||||
});
|
||||
|
||||
export default Router;
|
|
@ -0,0 +1,66 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get } from '@ember/object';
|
||||
import { next } from '@ember/runloop';
|
||||
const $html = document.documentElement;
|
||||
export default Route.extend({
|
||||
init: function() {
|
||||
this._super(...arguments);
|
||||
},
|
||||
repo: service('dc'),
|
||||
actions: {
|
||||
loading: function(transition, originRoute) {
|
||||
let dc = null;
|
||||
if (originRoute.routeName !== 'dc') {
|
||||
const model = this.modelFor('dc') || { dcs: null, dc: { Name: null } };
|
||||
dc = get(this, 'repo').getActive(model.dc.Name, model.dcs);
|
||||
}
|
||||
hash({
|
||||
loading: !$html.classList.contains('ember-loading'),
|
||||
dc: dc,
|
||||
}).then(model => {
|
||||
next(() => {
|
||||
const controller = this.controllerFor('application');
|
||||
controller.setProperties(model);
|
||||
transition.promise.finally(function() {
|
||||
$html.classList.remove('ember-loading');
|
||||
controller.setProperties({
|
||||
loading: false,
|
||||
dc: model.dc,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
return true;
|
||||
},
|
||||
error: function(e, transition) {
|
||||
let error = {
|
||||
status: e.code || '',
|
||||
message: e.message || e.detail || 'Error',
|
||||
};
|
||||
if (e.errors && e.errors[0]) {
|
||||
error = e.errors[0];
|
||||
error.message = error.title || error.detail || 'Error';
|
||||
}
|
||||
if (error.status === '') {
|
||||
error.message = 'Error';
|
||||
}
|
||||
hash({
|
||||
error: error,
|
||||
dc: error.status.toString().indexOf('5') !== 0 ? get(this, 'repo').getActive() : null,
|
||||
})
|
||||
.then(model => {
|
||||
next(() => {
|
||||
this.controllerFor('error').setProperties(model);
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
next(() => {
|
||||
this.controllerFor('error').setProperties({ error: error });
|
||||
});
|
||||
});
|
||||
return true;
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,25 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get } from '@ember/object';
|
||||
export default Route.extend({
|
||||
repo: service('dc'),
|
||||
settings: service('settings'),
|
||||
model: function(params) {
|
||||
const repo = get(this, 'repo');
|
||||
return hash({
|
||||
dcs: repo.findAll(),
|
||||
}).then(function(model) {
|
||||
return hash({
|
||||
...model,
|
||||
...{
|
||||
dc: repo.findBySlug(params.dc, model.dcs),
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
this._super(...arguments);
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get, set } from '@ember/object';
|
||||
|
||||
import WithAclActions from 'consul-ui/mixins/acl/with-actions';
|
||||
|
||||
export default Route.extend(WithAclActions, {
|
||||
templateName: 'dc/acls/edit',
|
||||
repo: service('acls'),
|
||||
beforeModel: function() {
|
||||
get(this, 'repo').invalidate();
|
||||
},
|
||||
model: function(params) {
|
||||
this.item = get(this, 'repo').create();
|
||||
set(this.item, 'Datacenter', this.modelFor('dc').dc.Name);
|
||||
return hash({
|
||||
create: true,
|
||||
isLoading: false,
|
||||
item: this.item,
|
||||
types: ['management', 'client'],
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
this._super(...arguments);
|
||||
controller.setProperties(model);
|
||||
},
|
||||
deactivate: function() {
|
||||
if (get(this.item, 'isNew')) {
|
||||
this.item.destroyRecord();
|
||||
}
|
||||
},
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
import WithAclActions from 'consul-ui/mixins/acl/with-actions';
|
||||
|
||||
export default Route.extend(WithAclActions, {
|
||||
repo: service('acls'),
|
||||
settings: service('settings'),
|
||||
model: function(params) {
|
||||
return hash({
|
||||
isLoading: false,
|
||||
item: get(this, 'repo').findBySlug(params.id, this.modelFor('dc').dc.Name),
|
||||
types: ['management', 'client'],
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
this._super(...arguments);
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,26 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
import WithAclActions from 'consul-ui/mixins/acl/with-actions';
|
||||
|
||||
export default Route.extend(WithAclActions, {
|
||||
repo: service('acls'),
|
||||
queryParams: {
|
||||
s: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
model: function(params) {
|
||||
return hash({
|
||||
isLoading: false,
|
||||
items: get(this, 'repo').findAllByDatacenter(this.modelFor('dc').dc.Name),
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
this._super(...arguments);
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,35 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get, set } from '@ember/object';
|
||||
import WithKvActions from 'consul-ui/mixins/kv/with-actions';
|
||||
|
||||
export default Route.extend(WithKvActions, {
|
||||
templateName: 'dc/kv/edit',
|
||||
repo: service('kv'),
|
||||
beforeModel: function() {
|
||||
get(this, 'repo').invalidate();
|
||||
},
|
||||
model: function(params) {
|
||||
const key = params.key || '/';
|
||||
const repo = get(this, 'repo');
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
this.item = repo.create();
|
||||
set(this.item, 'Datacenter', dc);
|
||||
return hash({
|
||||
create: true,
|
||||
isLoading: false,
|
||||
item: this.item,
|
||||
parent: repo.findBySlug(key, dc),
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
this._super(...arguments);
|
||||
controller.setProperties(model);
|
||||
},
|
||||
deactivate: function() {
|
||||
if (get(this.item, 'isNew')) {
|
||||
this.item.destroyRecord();
|
||||
}
|
||||
},
|
||||
});
|
|
@ -0,0 +1,38 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get } from '@ember/object';
|
||||
import WithKvActions from 'consul-ui/mixins/kv/with-actions';
|
||||
|
||||
import ascend from 'consul-ui/utils/ascend';
|
||||
|
||||
export default Route.extend(WithKvActions, {
|
||||
repo: service('kv'),
|
||||
sessionRepo: service('session'),
|
||||
model: function(params) {
|
||||
const key = params.key;
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
const repo = get(this, 'repo');
|
||||
return hash({
|
||||
isLoading: false,
|
||||
parent: repo.findBySlug(ascend(key, 1) || '/', dc),
|
||||
item: repo.findBySlug(key, dc),
|
||||
}).then(model => {
|
||||
// TODO: Consider loading this after initial page load
|
||||
const session = get(model.item, 'Session');
|
||||
if (session) {
|
||||
return hash({
|
||||
...model,
|
||||
...{
|
||||
session: get(this, 'sessionRepo').findByKey(session, dc),
|
||||
},
|
||||
});
|
||||
}
|
||||
return model;
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
this._super(...arguments);
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,12 @@
|
|||
// import Route from '@ember/routing/route';
|
||||
import Route from './index';
|
||||
|
||||
export default Route.extend({
|
||||
templateName: 'dc/kv/index',
|
||||
beforeModel: function(transition) {
|
||||
const params = this.paramsFor('dc.kv.folder');
|
||||
if (params.key === '/' || params.key == null) {
|
||||
this.transitionTo('dc.kv.index');
|
||||
}
|
||||
},
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get } from '@ember/object';
|
||||
import WithKvActions from 'consul-ui/mixins/kv/with-actions';
|
||||
|
||||
export default Route.extend(WithKvActions, {
|
||||
repo: service('kv'),
|
||||
model: function(params) {
|
||||
const key = params.key || '/';
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
const repo = get(this, 'repo');
|
||||
return hash({
|
||||
isLoading: false,
|
||||
parent: repo.findBySlug(key, dc),
|
||||
})
|
||||
.then(function(model) {
|
||||
return hash({
|
||||
...model,
|
||||
...{
|
||||
items: repo.findAllBySlug(get(model.parent, 'Key'), dc),
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.errors && e.errors[0] && e.errors[0].status == '404') {
|
||||
this.transitionTo('dc.kv.index');
|
||||
return;
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
this._super(...arguments);
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,3 @@
|
|||
import Route from './create';
|
||||
|
||||
export default Route.extend();
|
|
@ -0,0 +1,23 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
export default Route.extend({
|
||||
repo: service('nodes'),
|
||||
queryParams: {
|
||||
s: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
model: function(params) {
|
||||
return hash({
|
||||
items: get(this, 'repo').findAllByDatacenter(this.modelFor('dc').dc.Name),
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
this._super(...arguments);
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,66 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get, set } from '@ember/object';
|
||||
|
||||
import distance from 'consul-ui/utils/distance';
|
||||
import tomographyFactory from 'consul-ui/utils/tomography';
|
||||
import WithFeedback from 'consul-ui/mixins/with-feedback';
|
||||
|
||||
const tomography = tomographyFactory(distance);
|
||||
|
||||
export default Route.extend(WithFeedback, {
|
||||
repo: service('nodes'),
|
||||
sessionRepo: service('session'),
|
||||
queryParams: {
|
||||
s: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
model: function(params) {
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
const repo = get(this, 'repo');
|
||||
const sessionRepo = get(this, 'sessionRepo');
|
||||
return hash({
|
||||
item: repo.findBySlug(params.name, dc),
|
||||
}).then(function(model) {
|
||||
// TODO: Consider loading this after initial page load
|
||||
const coordinates = get(model.item, 'Coordinates');
|
||||
return hash({
|
||||
...model,
|
||||
...{
|
||||
tomography:
|
||||
get(coordinates, 'length') > 1
|
||||
? tomography(params.name, coordinates.map(item => get(item, 'data')))
|
||||
: null,
|
||||
items: get(model.item, 'Services'),
|
||||
sessions: sessionRepo.findByNode(get(model.item, 'Node'), dc),
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
this._super(...arguments);
|
||||
controller.setProperties(model);
|
||||
},
|
||||
actions: {
|
||||
invalidateSession: function(item) {
|
||||
const dc = this.modelFor('dc').dc.Name;
|
||||
const controller = this.controller;
|
||||
const repo = get(this, 'sessionRepo');
|
||||
get(this, 'feedback').execute(
|
||||
() => {
|
||||
const node = get(item, 'Node');
|
||||
return repo.remove(item).then(() => {
|
||||
return repo.findByNode(node, dc).then(function(sessions) {
|
||||
set(controller, 'sessions', sessions);
|
||||
});
|
||||
});
|
||||
},
|
||||
`The session was invalidated.`,
|
||||
`There was an error invalidating the session.`
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,24 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
export default Route.extend({
|
||||
repo: service('services'),
|
||||
queryParams: {
|
||||
s: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
model: function(params) {
|
||||
const repo = get(this, 'repo');
|
||||
return hash({
|
||||
items: repo.findAllByDatacenter(this.modelFor('dc').dc.Name),
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
this._super(...arguments);
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
export default Route.extend({
|
||||
repo: service('services'),
|
||||
queryParams: {
|
||||
s: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
},
|
||||
model: function(params) {
|
||||
const repo = get(this, 'repo');
|
||||
return hash({
|
||||
item: repo.findBySlug(params.name, this.modelFor('dc').dc.Name),
|
||||
}).then(function(model) {
|
||||
return {
|
||||
...model,
|
||||
...{
|
||||
items: model.item.Nodes,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
this._super(...arguments);
|
||||
controller.setProperties(model);
|
||||
},
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
export default Route.extend({
|
||||
repo: service('dc'),
|
||||
model: function(params) {
|
||||
return hash({
|
||||
item: get(this, 'repo').getActive(),
|
||||
});
|
||||
},
|
||||
afterModel: function({ item }, transition) {
|
||||
this.transitionTo('dc.services', get(item, 'Name'));
|
||||
},
|
||||
});
|
|
@ -0,0 +1,10 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import Error from '@ember/error';
|
||||
|
||||
export default Route.extend({
|
||||
beforeModel: function() {
|
||||
const e = new Error('Page not found');
|
||||
e.code = 404;
|
||||
throw e;
|
||||
},
|
||||
});
|
|
@ -0,0 +1,47 @@
|
|||
import Route from '@ember/routing/route';
|
||||
import { inject as service } from '@ember/service';
|
||||
import { hash } from 'rsvp';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
import WithFeedback from 'consul-ui/mixins/with-feedback';
|
||||
export default Route.extend(WithFeedback, {
|
||||
dcRepo: service('dc'),
|
||||
repo: service('settings'),
|
||||
model: function(params) {
|
||||
return hash({
|
||||
item: get(this, 'repo').findAll(),
|
||||
dcs: get(this, 'dcRepo').findAll(),
|
||||
}).then(model => {
|
||||
return hash({
|
||||
...model,
|
||||
...{
|
||||
dc: get(this, 'dcRepo').getActive(null, model.dcs),
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
setupController: function(controller, model) {
|
||||
this._super(...arguments);
|
||||
controller.setProperties(model);
|
||||
},
|
||||
actions: {
|
||||
update: function(item) {
|
||||
get(this, 'feedback').execute(
|
||||
() => {
|
||||
return get(this, 'repo').persist(item);
|
||||
},
|
||||
`Your settings were saved.`,
|
||||
`There was an error saving your settings.`
|
||||
);
|
||||
},
|
||||
delete: function(key) {
|
||||
get(this, 'feedback').execute(
|
||||
() => {
|
||||
return get(this, 'repo').remove(key);
|
||||
},
|
||||
`You settings have been reset.`,
|
||||
`There was an error resetting your settings.`
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import Serializer from './application';
|
||||
import { PRIMARY_KEY } from 'consul-ui/models/acl';
|
||||
|
||||
export default Serializer.extend({
|
||||
primaryKey: PRIMARY_KEY,
|
||||
});
|
|
@ -0,0 +1,21 @@
|
|||
import Serializer from 'ember-data/serializers/rest';
|
||||
|
||||
export default Serializer.extend({
|
||||
// this could get confusing if you tried to override
|
||||
// say `normalizeQueryResponse`
|
||||
// TODO: consider creating a method for each one of the `normalize...Response` family
|
||||
normalizeResponse: function(store, primaryModelClass, payload, id, requestType) {
|
||||
return this._super(
|
||||
store,
|
||||
primaryModelClass,
|
||||
{
|
||||
[primaryModelClass.modelName]: this.normalizePayload(payload, id, requestType),
|
||||
},
|
||||
id,
|
||||
requestType
|
||||
);
|
||||
},
|
||||
normalizePayload: function(payload, id, requestType) {
|
||||
return payload;
|
||||
},
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import Serializer from './application';
|
||||
import { PRIMARY_KEY } from 'consul-ui/models/coordinate';
|
||||
|
||||
export default Serializer.extend({
|
||||
primaryKey: PRIMARY_KEY,
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
import Serializer from './application';
|
||||
import { get } from '@ember/object';
|
||||
|
||||
export default Serializer.extend({
|
||||
primaryKey: 'Name',
|
||||
normalizePayload: function(payload, id, requestType) {
|
||||
switch (requestType) {
|
||||
case 'findAll':
|
||||
return payload.map(item => {
|
||||
return {
|
||||
[get(this, 'primaryKey')]: item,
|
||||
};
|
||||
});
|
||||
}
|
||||
return payload;
|
||||
},
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import Serializer from './application';
|
||||
import { PRIMARY_KEY } from 'consul-ui/models/kv';
|
||||
|
||||
export default Serializer.extend({
|
||||
primaryKey: PRIMARY_KEY,
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue