Merge pull request #8013 from hashicorp/ui-staging

ui: UI Release Merge (1.8-beta-3: ui-staging merge)
This commit is contained in:
John Cowen 2020-06-03 18:46:20 +01:00 committed by GitHub
commit 1cc5a2445d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
157 changed files with 1523 additions and 1561 deletions

View file

@ -1,17 +0,0 @@
import Adapter from './application';
export default Adapter.extend({
requestForQueryRecord: function(request, { dc, ns, index, id }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
return request`
GET /v1/internal/ui/gateway-services-nodes/${id}?${{ dc }}
${{
...this.formatNspace(ns),
index,
}}
`;
},
});

View file

@ -1,15 +1,26 @@
import Adapter from './application';
// TODO: Update to use this.formatDatacenter()
export default Adapter.extend({
requestForQuery: function(request, { dc, ns, index }) {
return request`
GET /v1/internal/ui/services?${{ dc }}
requestForQuery: function(request, { dc, ns, index, gateway }) {
if (typeof gateway !== 'undefined') {
return request`
GET /v1/internal/ui/gateway-services-nodes/${gateway}?${{ dc }}
${{
...this.formatNspace(ns),
index,
}}
${{
...this.formatNspace(ns),
index,
}}
`;
} else {
return request`
GET /v1/internal/ui/services?${{ dc }}
${{
...this.formatNspace(ns),
index,
}}
`;
}
},
requestForQueryRecord: function(request, { dc, ns, index, id }) {
if (typeof id === 'undefined') {

View file

@ -1,4 +0,0 @@
{{!<form>}}
<FreetextFilter @searchable={{searchable}} @value={{search}} @placeholder="Search by name/token" />
<RadioGroup @keyboardAccess={{true}} @name="type" @value={{type}} @items={{filters}} @onchange={{action onchange}} />
{{!</form>}}

View file

@ -1,8 +0,0 @@
import Component from '@ember/component';
export default Component.extend({
tagName: 'form',
classNames: ['filter-bar'],
'data-test-acl-filter': true,
onchange: function() {},
});

View file

@ -1,14 +0,0 @@
{{! action groups are block only components, you MUST specify a list of actions in the component body }}
{{! therefore if you call this component as an inline component, nothing is produced }}
{{#if hasBlock }}
<input type="radio" name="actions" id="actions_{{index}}" checked={{if (eq checked 'checked') 'checked'}} onchange={{action onchange}} value={{index}} />
<label for="actions_{{index}}">
<span>Open</span>
</label>
<label for="actions_close">
<span>Close</span>
</label>
<div>
{{yield}}
</div>
{{/if}}

View file

@ -1,6 +0,0 @@
import Component from '@ember/component';
export default Component.extend({
classNames: ['action-group'],
onchange: function() {},
});

View file

@ -43,7 +43,7 @@
<input
{{ref this 'input'}}
disabled={{state-matches state "loading"}}
type="password"
type={{inputType}}
name="auth[SecretID]"
placeholder="SecretID"
value={{secret}}
@ -93,7 +93,7 @@
@nspace={{or value.Namespace nspace}}
@type={{if value.Name 'oidc' 'secret'}}
@value={{if value.Name value.Name value}}
@onchange={{action onsubmit}}
@onchange={{queue (action dispatch "RESET") (action onsubmit)}}
@onerror={{queue (action (mut error) value="error.errors.firstObject") (action dispatch "ERROR")}}
/>
</State>

View file

@ -1,4 +1,6 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import Ember from 'ember';
import chart from './chart.xstate';
@ -6,6 +8,12 @@ export default Component.extend({
tagName: '',
onsubmit: function(e) {},
onchange: function(e) {},
// Blink/Webkit based seem to leak password inputs
// this will only occur during acceptance testing so
// turn them into text inputs during acceptance testing
inputType: computed(function() {
return Ember.testing ? 'text' : 'password';
}),
init: function() {
this._super(...arguments);
this.chart = chart;

View file

@ -1,4 +0,0 @@
{{!<form>}}
<FreetextFilter @searchable={{searchable}} @value={{search}} @placeholder="Search" />
<RadioGroup @keyboardAccess={{true}} @name="status" @value={{status}} @items={{array (hash label="All (Any Status)" value="") (hash label="Critical Checks" value="critical") (hash label="Warning Checks" value="warning") (hash label="Passing Checks" value="passing")}} @onchange={{action onchange}} />
{{!</form>}}

View file

@ -1,8 +0,0 @@
import Component from '@ember/component';
export default Component.extend({
tagName: 'form',
classNames: ['filter-bar'],
'data-test-catalog-filter': true,
onchange: function() {},
});

View file

@ -1,10 +0,0 @@
<form class="catalog-toolbar" data-test-catalog-toolbar>
<FreetextFilter @searchable={{searchable}} @value={{value}} @placeholder="Search" />
<PopoverSelect
data-popover-select
@selected={{selected}}
@options={{options}}
@onchange={{onchange}}
@title='Sort By'
/>
</form>

View file

@ -1,19 +1,28 @@
import Component from '@ember/component';
import { get, set } from '@ember/object';
import SlotsMixin from 'block-slots';
import WithListeners from 'consul-ui/mixins/with-listeners';
import { inject as service } from '@ember/service';
import Slotted from 'block-slots';
export default Component.extend(WithListeners, SlotsMixin, {
export default Component.extend(Slotted, {
tagName: '',
dom: service('dom'),
init: function() {
this._super(...arguments);
this._listeners = this.dom.listeners();
},
willDestroyElement: function() {
this._listeners.remove();
this._super(...arguments);
},
didReceiveAttrs: function() {
this._super(...arguments);
this.removeListeners();
const dispatcher = this.dispatcher;
if (dispatcher) {
this.listen(dispatcher, 'change', e => {
set(this, 'items', e.target.data);
if (this.items !== this.dispatcher.data) {
this._listeners.remove();
this._listeners.add(this.dispatcher, {
change: e => set(this, 'items', e.target.data),
});
set(this, 'items', get(dispatcher, 'data'));
set(this, 'items', get(this.dispatcher, 'data'));
}
this.dispatcher.search(this.terms);
},
});

View file

@ -8,11 +8,24 @@
{{yield}}
{{/yield-slot}}
</header>
<p>
{{#yield-slot name="body"}}
{{#yield-slot name="body"}}
<div>
{{yield}}
{{/yield-slot}}
</p>
{{#if (and (env 'CONSUL_ACLS_ENABLED') allowLogin)}}
<label for="login-toggle">
<DataSource
@src="settings://consul:token"
@onchange={{action (mut token) value="data"}}
/>
{{#if token.AccessorID}}
Log in with a different token
{{else}}
Log in
{{/if}}
</label>
{{/if}}
</div>
{{/yield-slot}}
{{#yield-slot name="actions"}}
<ul>
{{yield}}

View file

@ -1,6 +1,6 @@
{{!<fieldset>}}
<fieldset class="freetext-filter">
<label class="type-search">
<span>Search</span>
<input type="search" onsearch={{action onchange}} oninput={{action onchange}} name="s" value="{{value}}" placeholder="{{placeholder}}" autofocus="autofocus" />
<input type="search" onsearch={{action "change"}} oninput={{action "change"}} onkeydown={{action "keydown"}} name="s" value={{value}} placeholder={{placeholder}} autofocus="autofocus" />
</label>
{{!</fieldset>}}
</fieldset>

View file

@ -1,14 +1,19 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
const ENTER = 13;
export default Component.extend({
tagName: 'fieldset',
classNames: ['freetext-filter'],
onchange: function(e) {
let searchable = this.searchable;
if (!Array.isArray(searchable)) {
searchable = [searchable];
}
searchable.forEach(function(item) {
item.search(e.target.value);
});
dom: service('dom'),
tagName: '',
actions: {
change: function(e) {
this.onsearch(
this.dom.setEventTargetProperty(e, 'value', value => (value === '' ? undefined : value))
);
},
keydown: function(e) {
if (e.keyCode === ENTER) {
e.preventDefault();
}
},
},
});

View file

@ -127,14 +127,15 @@
<AuthDialog
@dc={{dc.Name}}
@nspace={{nspace.Name}}
@onchange={{action onchange}} as |authDialog components|
@onchange={{action "reauthorize"}} as |authDialog components|
>
{{#let components.AuthForm components.AuthProfile as |AuthForm AuthProfile|}}
<BlockSlot @name="unauthorized">
<label tabindex="0" for="login-toggle" onkeypress={{action 'keypressClick'}}>
<span>Log in</span>
</label>
<ModalDialog @name="login-toggle" @onclose={{action 'close'}} @onopen={{action 'open'}}>
<ModalDialog @name="login-toggle" @onclose={{action 'close'}} @onopen={{action 'open'}} as |api|>
<Ref @target={{this}} @name="modal" @value={{api}} />
<BlockSlot @name="header">
<h2>Log in to Consul</h2>
</BlockSlot>
@ -151,6 +152,22 @@
</ModalDialog>
</BlockSlot>
<BlockSlot @name="authorized">
<ModalDialog @name="login-toggle" @onclose={{action 'close'}} @onopen={{action 'open'}} as |api|>
<Ref @target={{this}} @name="modal" @value={{api}} />
<BlockSlot @name="header">
<h2>Log in with a different token</h2>
</BlockSlot>
<BlockSlot @name="body">
<AuthForm as |api|>
<Ref @target={{this}} @name="authForm" @value={{api}} />
</AuthForm>
</BlockSlot>
<BlockSlot @name="actions" as |close|>
<button type="button" onclick={{action close}}>
Continue without logging in
</button>
</BlockSlot>
</ModalDialog>
<PopoverMenu @position="right">
<BlockSlot @name="trigger">
Logout

View file

@ -28,6 +28,10 @@ export default Component.extend({
close: function() {
this.authForm.reset();
},
reauthorize: function(e) {
this.modal.close();
this.onchange(e);
},
change: function(e) {
const win = this.dom.viewport();
const $root = this.dom.root();

View file

@ -1,4 +0,0 @@
{{!<form>}}
<FreetextFilter @searchable={{searchable}} @value={{search}} @placeholder="Search by Source or Destination" />
<RadioGroup @keyboardAccess={{true}} @name="currentFilter" @value={{selected}} @items={{filters}} @onchange={{action onchange}} />
{{!</form>}}

View file

@ -1,8 +0,0 @@
import Component from '@ember/component';
export default Component.extend({
tagName: 'form',
classNames: ['filter-bar'],
'data-test-intention-filter': true,
onchange: function() {},
});

View file

@ -1,6 +1,15 @@
<EmberNativeScrollable @tagName="ul" @content-size={{_contentSize}} @scroll-left={{_scrollLeft}} @scroll-top={{_scrollTop}} @scrollChange={{action "scrollChange"}} @clientSizeChange={{action "clientSizeChange"}}>
<EmberNativeScrollable
@tagName="ul"
@content-size={{_contentSize}}
@scroll-left={{_scrollLeft}}
@scroll-top={{_scrollTop}}
@scrollChange={{action "scrollChange"}}
@clientSizeChange={{action "clientSizeChange"}}
>
<li></li>
{{~#each _cells as |cell|~}}
<li onclick={{action 'click'}} style={{{cell.style}}}>{{yield cell.item cell.index }}</li>
<li onclick={{action 'click'}} style={{{cell.style}}} class={{if (service/exists cell.item) 'linkable' }}>
{{yield cell.item cell.index }}
</li>
{{~/each~}}
</EmberNativeScrollable>

View file

@ -6,13 +6,25 @@
<div>
<header>
<label for="modal_close">Close</label>
<YieldSlot @name="header">{{yield}}</YieldSlot>
<YieldSlot @name="header">
{{yield (hash
close=(action "close")
)}}
</YieldSlot>
</header>
<div>
<YieldSlot @name="body">{{yield}}</YieldSlot>
<YieldSlot @name="body">
{{yield (hash
close=(action "close")
)}}
</YieldSlot>
</div>
<footer>
<YieldSlot @name="actions" @params={{block-params (action "close")}}>{{yield}}</YieldSlot>
<YieldSlot @name="actions" @params={{block-params (action "close")}}>
{{yield (hash
close=(action "close")
)}}
</YieldSlot>
</footer>
</div>
</div>

View file

@ -1,11 +0,0 @@
<ul>
{{#each value as |item index|}}
<li>
<button type="button" {{action 'remove' index}}>Remove</button>{{item}}
</li>
{{/each}}
</ul>
<label class="type-search">
<span>Search</span>
<input {{ref this "input"}} onchange={{action 'add'}} onsearch={{action 'add'}} oninput={{action 'input'}} onkeydown={{action 'keydown'}} placeholder={{if (eq value.length 0) placeholder}} value={{item}} type="search" name="s" autofocus="autofocus" />
</label>

View file

@ -1,62 +0,0 @@
import Component from '@ember/component';
import { set } from '@ember/object';
import { inject as service } from '@ember/service';
export default Component.extend({
dom: service('dom'),
classNames: ['phrase-editor'],
item: '',
onchange: function(e) {},
search: function(e) {
// TODO: Temporarily continue supporting `searchable`
let searchable = this.searchable;
if (searchable) {
if (!Array.isArray(searchable)) {
searchable = [searchable];
}
searchable.forEach(item => {
item.search(this.value);
});
}
this.onchange(e);
},
oninput: function(e) {},
onkeydown: function(e) {},
actions: {
keydown: function(e) {
switch (e.keyCode) {
case 8: // backspace
if (e.target.value == '' && this.value.length > 0) {
this.actions.remove.bind(this)(this.value.length - 1);
}
break;
case 27: // escape
set(this, 'value', []);
this.search({ target: this });
break;
}
this.onkeydown({ target: this });
},
input: function(e) {
set(this, 'item', e.target.value);
this.oninput({ target: this });
},
remove: function(index, e) {
this.value.removeAt(index, 1);
this.search({ target: this });
this.input.focus();
},
add: function(e) {
const item = this.item.trim();
if (item !== '') {
set(this, 'item', '');
const currentItems = this.value || [];
const items = new Set(currentItems).add(item);
if (items.size > currentItems.length) {
set(this, 'value', [...items]);
this.search({ target: this });
}
}
},
},
});

View file

@ -1,19 +1,12 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
export default Component.extend({
tagName: '',
dom: service('dom'),
actions: {
change: function(option, e) {
// We fake an event here, which could be a bit of a footbun if we treat
// it completely like an event, we should be abe to avoid doing this
// when we move to glimmer components (this.args.selected vs this.selected)
this.onchange({
target: {
selected: option,
},
// make this vaguely event like to avoid
// having a separate property
preventDefault: function(e) {},
});
this.onchange(this.dom.setEventTargetProperty(e, 'selected', selected => option));
},
},
});

View file

@ -1,10 +1,12 @@
<fieldset>
<div role="radiogroup" id="radiogroup_{{name}}" data-test-radiogroup={{name}}>{{! menu?}}
{{#each items as |item|}}
<label tabindex={{if keyboardAccess '0'}} onkeydown={{if keyboardAccess (action 'keydown')}} class="type-radio value-{{item.value}}" data-test-radiobutton="{{name}}_{{item.value}}"> {{! slugify }}
<input type="radio" name={{name}} value={{item.value}} checked={{if (eq (concat value) item.value) 'checked'}} onchange={{action onchange}} />
<span>{{item.label}}</span>
{{#let (if (not-eq item.key undefined) item.key item.value) (or item.label item.value) as |_key _value|}}
<label tabindex={{if keyboardAccess '0'}} onkeydown={{if keyboardAccess (action 'keydown')}} class="type-radio value-{{_key}}" data-test-radiobutton="{{name}}_{{_key}}"> {{! slugify }}
<input type="radio" name={{name}} value={{_key}} checked={{if (eq (concat value) _key) 'checked'}} onchange={{action "change"}} />
<span>{{_value}}</span>
</label>
{{/let}}
{{/each}}
</div>
</fieldset>

View file

@ -1,14 +1,25 @@
import Component from '@ember/component';
import { inject as service } from '@ember/service';
const ENTER = 13;
export default Component.extend({
tagName: '',
keyboardAccess: false,
dom: service('dom'),
init: function() {
this._super(...arguments);
this.name = this.dom.guid(this);
},
actions: {
keydown: function(e) {
if (e.keyCode === ENTER) {
e.target.dispatchEvent(new MouseEvent('click'));
}
},
change: function(e) {
this.onchange(
this.dom.setEventTargetProperty(e, 'value', value => (value === '' ? undefined : value))
);
},
},
});

View file

@ -0,0 +1,61 @@
## SearchBar
```handlebars
<SearchBar
@value={{"search term"}}
@onsearch={{action "search"}}
/>
```
### Arguments
| Argument | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `String` | | The string `value` of the freetext search bar |
| `onsearch` | `Function` | | The action to fire when the freetext search bar changes. Emits a native event with a `target.value` property containing the text typed into the search bar |
| `options` | `Array` | | An array of Key/Values pairs to use for options for either a filter interface or a sort interface |
| `selected` | `Object` | | An object containing a Key/Value pair of the currently selected option |
| `onchange` | `Function` | | The action to fire when the filter/sort changes. Emits an Event-like object, when filtering this has a `target.value` property containg the key of the selected filter, when sorting this has a `target.selected` property containing the selected Key/Value pair |
| `secondary` | `string` | | String identifier to signify what type of secondary filter to show. Currently only value here is 'sort' |
`SearchBar` is used for a variety of searching behaviours, freetext searching, filtering and sorting. It is also slot based to enable you to completely overwrite the secondary search if need be.
### Examples
```handlebars
{{! Freetext only search bar}}
<SearchBar
@value={{"search term"}}
@onsearch={{action "search"}}
/>
```
```handlebars
{{! Freetext and filter search bar}}
<SearchBar
@value={{search}}
@onsearch={{action (mut search) value='target.value'}}
@selected={{filter.selected}}
@options={{filter.items}}
@onchange={{action (mut filterBy) value='target.value'}}
/>
```
```handlebars
{{! Freetext and sort search bar}}
<SearchBar
@value={{search}}
@onsearch={{action (mut search) value='target.value'}}
@secondary="sort"
@selected={{sort.selected}}
@options={{sort.items}}
@onchange={{action (mut sortBy) value='target.selected.key'}}
/>
```
### See
- [Component Source Code](./index.js)
- [Template Source Code](./index.hbs)
---

View file

@ -0,0 +1,32 @@
{{yield}}
<form class={{concat 'filter-bar' (if (eq secondary 'sort') ' with-sort')}} ...attributes>
<FreetextFilter
@onsearch={{action onsearch}}
@value={{value}}
@placeholder={{or placeholder 'Search'}}
/>
{{#yield-slot name="secondary"}}
{{yield}}
{{else}}
{{#if options}}
{{#if (eq secondary 'sort')}}
<fieldset>
<PopoverSelect
data-popover-select
@selected={{selected}}
@options={{options}}
@onchange={{action onchange}}
@title="Sort By"
/>
</fieldset>
{{else}}
<RadioGroup
@keyboardAccess={{true}}
@value={{selected.key}}
@items={{options}}
@onchange={{action onchange}}
/>
{{/if}}
{{/if}}
{{/yield-slot}}
</form>

View file

@ -0,0 +1,6 @@
import Component from '@ember/component';
import Slotted from 'block-slots';
export default Component.extend(Slotted, {
tagName: '',
});

View file

@ -0,0 +1,3 @@
<EmberTooltip @popperContainer=".app-view">
{{yield}}
</EmberTooltip>

View file

@ -6,9 +6,6 @@ import transitionable from 'consul-ui/utils/routing/transitionable';
export default Controller.extend({
router: service('router'),
http: service('repository/type/event-source'),
dataSource: service('data-source/service'),
client: service('client/http'),
store: service('store'),
feedback: service('feedback'),
actions: {
@ -23,12 +20,11 @@ export default Controller.extend({
// used for the feedback service.
this.feedback.execute(
() => {
// TODO: Centralize this elsewhere
this.client.abort();
this.http.resetCache();
this.dataSource.resetCache();
this.store.init();
// TODO: Currently we clear cache from the ember-data store
// ideally this would be a static method of the abstract Repository class
// once we move to proper classes for services take another look at this.
this.store.clear();
//
const params = {};
if (e.data) {
const token = e.data;
@ -42,22 +38,24 @@ export default Controller.extend({
}
}
}
const container = getOwner(this);
const routeName = this.router.currentRoute.name;
const route = getOwner(this).lookup(`route:${routeName}`);
const router = this.router;
const route = container.lookup(`route:${routeName}`);
// Refresh the application route
return getOwner(this)
return container
.lookup('route:application')
.refresh()
.promise.then(() => {
// We use transitionable here as refresh doesn't work if you are on an error page
// which is highly likely to happen here (403s)
if (routeName !== router.currentRouteName || typeof params.nspace !== 'undefined') {
.promise.then(res => {
// Use transitionable if we need to change a section of the URL
if (
routeName !== this.router.currentRouteName ||
typeof params.nspace !== 'undefined'
) {
return route.transitionTo(
...transitionable(router.currentRoute, params, getOwner(this))
...transitionable(this.router.currentRoute, params, container)
);
} else {
return route.refresh();
return res;
}
});
},

View file

@ -1,47 +1,14 @@
import Controller from '@ember/controller';
import { computed, get } from '@ember/object';
import WithFiltering from 'consul-ui/mixins/with-filtering';
import WithSearching from 'consul-ui/mixins/with-searching';
import ucfirst from 'consul-ui/utils/ucfirst';
const countType = function(items, type) {
return type === '' ? get(items, 'length') : items.filterBy('Type', type).length;
};
export default Controller.extend(WithSearching, WithFiltering, {
export default Controller.extend({
queryParams: {
type: {
filterBy: {
as: 'type',
},
s: {
search: {
as: 'filter',
replace: true,
},
},
init: function() {
this.searchParams = {
acl: 's',
};
this._super(...arguments);
},
searchable: computed('filtered', function() {
return get(this, 'searchables.acl')
.add(get(this, 'filtered'))
.search(get(this, this.searchParams.acl));
}),
typeFilters: computed('items', function() {
const items = get(this, 'items');
return ['', 'management', 'client'].map(function(item) {
return {
label: `${item === '' ? 'All' : ucfirst(item)} (${countType(
items,
item
).toLocaleString()})`,
value: item,
};
});
}),
filter: function(item, { type = '' }) {
return type === '' || get(item, 'Type') === type;
},
actions: {
sendClone: function(item) {
this.send('clone', item);

View file

@ -1,23 +1,9 @@
import Controller from '@ember/controller';
import { get, computed } from '@ember/object';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithSearching, {
export default Controller.extend({
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},
},
init: function() {
this.searchParams = {
policy: 's',
};
this._super(...arguments);
},
searchable: computed('items', function() {
return get(this, 'searchables.policy')
.add(this.items)
.search(get(this, this.searchParams.policy));
}),
actions: {},
});

View file

@ -1,23 +1,9 @@
import Controller from '@ember/controller';
import { get, computed } from '@ember/object';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithSearching, {
export default Controller.extend({
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},
},
init: function() {
this.searchParams = {
role: 's',
};
this._super(...arguments);
},
searchable: computed('items', function() {
return get(this, 'searchables.role')
.add(this.items)
.search(get(this, this.searchParams.role));
}),
actions: {},
});

View file

@ -1,24 +1,11 @@
import Controller from '@ember/controller';
import { computed, get } from '@ember/object';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithSearching, {
export default Controller.extend({
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},
},
init: function() {
this.searchParams = {
token: 's',
};
this._super(...arguments);
},
searchable: computed('items', function() {
return get(this, 'searchables.token')
.add(get(this, 'items'))
.search(get(this, this.searchParams.token));
}),
actions: {
sendClone: function(item) {
this.send('clone', item);

View file

@ -1,52 +1,15 @@
import Controller from '@ember/controller';
import { computed, get } from '@ember/object';
import WithFiltering from 'consul-ui/mixins/with-filtering';
import WithSearching from 'consul-ui/mixins/with-searching';
import WithEventSource from 'consul-ui/mixins/with-event-source';
import ucfirst from 'consul-ui/utils/ucfirst';
// TODO: DRY out in acls at least
const createCounter = function(prop) {
return function(items, val) {
return val === '' ? get(items, 'length') : items.filterBy(prop, val).length;
};
};
const countAction = createCounter('Action');
export default Controller.extend(WithSearching, WithFiltering, WithEventSource, {
export default Controller.extend(WithEventSource, {
queryParams: {
currentFilter: {
filterBy: {
as: 'action',
},
s: {
search: {
as: 'filter',
replace: true,
},
},
init: function() {
this.searchParams = {
intention: 's',
};
this._super(...arguments);
},
searchable: computed('filtered', function() {
return get(this, 'searchables.intention')
.add(get(this, 'filtered'))
.search(get(this, this.searchParams.intention));
}),
actionFilters: computed('items', function() {
const items = get(this, 'items');
return ['', 'allow', 'deny'].map(function(item) {
return {
label: `${item === '' ? 'All' : ucfirst(item)} (${countAction(
items,
item
).toLocaleString()})`,
value: item,
};
});
}),
filter: function(item, { s = '', currentFilter = '' }) {
return currentFilter === '' || get(item, 'Action') === currentFilter;
},
actions: {
route: function() {
this.send(...arguments);

View file

@ -1,22 +1,9 @@
import Controller from '@ember/controller';
import { get, computed } from '@ember/object';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithSearching, {
export default Controller.extend({
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},
},
init: function() {
this.searchParams = {
kv: 's',
};
this._super(...arguments);
},
searchable: computed('items', function() {
return get(this, 'searchables.kv')
.add(this.items)
.search(get(this, this.searchParams.kv));
}),
});

View file

@ -1,38 +1,28 @@
import Controller from '@ember/controller';
import { computed } from '@ember/object';
import WithEventSource from 'consul-ui/mixins/with-event-source';
import WithHealthFiltering from 'consul-ui/mixins/with-health-filtering';
import WithSearching from 'consul-ui/mixins/with-searching';
import { get } from '@ember/object';
export default Controller.extend(WithEventSource, WithSearching, WithHealthFiltering, {
init: function() {
this.searchParams = {
healthyNode: 's',
unhealthyNode: 's',
};
this._super(...arguments);
export default Controller.extend(WithEventSource, {
queryParams: {
filterBy: {
as: 'status',
},
search: {
as: 'filter',
replace: true,
},
},
searchableHealthy: computed('healthy', function() {
return get(this, 'searchables.healthyNode')
.add(this.healthy)
.search(get(this, this.searchParams.healthyNode));
}),
searchableUnhealthy: computed('unhealthy', function() {
return get(this, 'searchables.unhealthyNode')
.add(this.unhealthy)
.search(get(this, this.searchParams.unhealthyNode));
}),
unhealthy: computed('filtered', function() {
return this.filtered.filter(function(item) {
return get(item, 'isUnhealthy');
});
}),
healthy: computed('filtered', function() {
return this.filtered.filter(function(item) {
return get(item, 'isHealthy');
});
}),
filter: function(item, { s = '', status = '' }) {
return item.hasStatus(status);
actions: {
hasStatus: function(status, checks) {
if (status === '') {
return true;
}
return checks.some(item => item.Status === status);
},
isHealthy: function(checks) {
return !this.actions.isUnhealthy.apply(this, [checks]);
},
isUnhealthy: function(checks) {
return checks.some(item => item.Status === 'critical' || item.Status === 'warning');
},
},
});

View file

@ -1,27 +1,12 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
import { get, computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithSearching, {
dom: service('dom'),
export default Controller.extend({
items: alias('item.Services'),
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},
},
init: function() {
this.searchParams = {
nodeservice: 's',
};
this._super(...arguments);
},
searchable: computed('items', function() {
return get(this, 'searchables.nodeservice')
.add(this.items)
.search(get(this, this.searchParams.nodeservice));
}),
});

View file

@ -1,23 +1,10 @@
import Controller from '@ember/controller';
import { get, computed } from '@ember/object';
import WithEventSource from 'consul-ui/mixins/with-event-source';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithEventSource, WithSearching, {
export default Controller.extend(WithEventSource, {
queryParams: {
s: {
search: {
as: 'filter',
},
},
init: function() {
this.searchParams = {
nspace: 's',
};
this._super(...arguments);
},
searchable: computed('items.[]', function() {
return get(this, 'searchables.nspace')
.add(this.items)
.search(get(this, this.searchParams.nspace));
}),
});

View file

@ -1,25 +1,13 @@
import Controller from '@ember/controller';
import { get, computed } from '@ember/object';
import { computed } from '@ember/object';
import WithEventSource from 'consul-ui/mixins/with-event-source';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithEventSource, WithSearching, {
export default Controller.extend(WithEventSource, {
queryParams: {
sortBy: 'sort',
s: {
search: {
as: 'filter',
},
},
init: function() {
this.searchParams = {
service: 's',
};
this._super(...arguments);
},
searchable: computed('services.[]', function() {
return get(this, 'searchables.service')
.add(this.services)
.search(this.terms);
}),
services: computed('items.[]', function() {
return this.items.filter(function(item) {
return item.Kind !== 'connect-proxy';

View file

@ -1,29 +1,15 @@
import Controller from '@ember/controller';
import { get, computed } from '@ember/object';
import { computed } from '@ember/object';
import { alias } from '@ember/object/computed';
import { inject as service } from '@ember/service';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithSearching, {
dom: service('dom'),
export default Controller.extend({
items: alias('item.Nodes'),
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},
},
init: function() {
this.searchParams = {
serviceInstance: 's',
};
this._super(...arguments);
},
searchable: computed('items', function() {
return get(this, 'searchables.serviceInstance')
.add(this.items)
.search(get(this, this.searchParams.serviceInstance));
}),
keyedProxies: computed('proxies.[]', function() {
const proxies = {};
this.proxies.forEach(item => {

View file

@ -1,25 +1,15 @@
import Controller from '@ember/controller';
import { get, computed } from '@ember/object';
import WithSearching from 'consul-ui/mixins/with-searching';
export default Controller.extend(WithSearching, {
export default Controller.extend({
queryParams: {
s: {
filterBy: {
as: 'action',
},
search: {
as: 'filter',
replace: true,
},
},
init: function() {
this.searchParams = {
intention: 's',
};
this._super(...arguments);
},
searchable: computed('intentions', function() {
return get(this, 'searchables.intention')
.add(this.intentions)
.search(get(this, this.searchParams.intention));
}),
actions: {
route: function() {
this.send(...arguments);

View file

@ -0,0 +1,9 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
export default Helper.extend({
sort: service('sort'),
compute([type, key], hash) {
return this.sort.comparator(type)(key);
},
});

View file

@ -0,0 +1,9 @@
import Helper from '@ember/component/helper';
import { inject as service } from '@ember/service';
export default Helper.extend({
search: service('search'),
compute([type, items], hash) {
return this.search.searchable(type).add(items);
},
});

View file

@ -0,0 +1,11 @@
import { helper } from '@ember/component/helper';
export function serviceExists([item], hash) {
if (typeof item.InstanceCount === 'undefined') {
return false;
}
return item.InstanceCount > 0;
}
export default helper(serviceExists);

View file

@ -0,0 +1,18 @@
import service from 'consul-ui/sort/comparators/service';
export function initialize(container) {
// Service-less injection using private properties at a per-project level
const Sort = container.resolveRegistration('service:sort');
const comparators = {
service: service(),
};
Sort.reopen({
comparator: function(type) {
return comparators[type];
},
});
}
export default {
initialize,
};

View file

@ -1,49 +0,0 @@
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':
value = el.value === 'on' ? '' : el.value;
break;
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.filters,
...obj,
});
},
},
});

View file

@ -1,13 +0,0 @@
import Mixin from '@ember/object/mixin';
import WithFiltering from 'consul-ui/mixins/with-filtering';
export default Mixin.create(WithFiltering, {
queryParams: {
status: {
as: 'status',
},
s: {
as: 'filter',
},
},
});

View file

@ -1,32 +0,0 @@
import Mixin from '@ember/object/mixin';
import { inject as service } from '@ember/service';
import { set } from '@ember/object';
import WithListeners from 'consul-ui/mixins/with-listeners';
/**
* WithSearching mostly depends on a `searchParams` object which must be set
* inside the `init` function. The naming and usage of this is modelled on
* `queryParams` but in contrast cannot _yet_ be 'hung' of the Controller
* object, it MUST be set in the `init` method.
* Reasons: As well as producing a eslint error, it can also be 'shared' amongst
* child Classes of the component. It is not clear _yet_ whether mixing this in
* avoids this and is something to be looked at in future to slightly improve DX
* Please also see:
* https://emberjs.com/api/ember/2.12/classes/Ember.Object/properties?anchor=mergedProperties
*
*/
export default Mixin.create(WithListeners, {
builder: service('search'),
init: function() {
this._super(...arguments);
const params = this.searchParams || {};
this.searchables = {};
Object.keys(params).forEach(type => {
const key = params[type];
this.searchables[type] = this.builder.searchable(type);
this.listen(this.searchables[type], 'change', e => {
const value = e.target.value;
set(this, key, value === '' ? null : value);
});
});
},
});

View file

@ -1,12 +0,0 @@
import Model from 'ember-data/model';
import attr from 'ember-data/attr';
export const PRIMARY_KEY = 'uid';
export const SLUG_KEY = 'Name';
export default Model.extend({
[PRIMARY_KEY]: attr('string'),
[SLUG_KEY]: attr('string'),
Datacenter: attr('string'),
Namespace: attr('string'),
Services: attr(),
});

View file

@ -1,8 +1,5 @@
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';
@ -23,13 +20,4 @@ export default Model.extend({
Coord: attr(),
SyncTime: attr('number'),
meta: 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;
}),
});

View file

@ -17,6 +17,7 @@ export default Model.extend({
ProxyFor: attr(),
Kind: attr('string'),
ExternalSources: attr(),
GatewayConfig: attr(),
Meta: attr(),
Address: attr('string'),
TaggedAddresses: attr(),

View file

@ -49,9 +49,6 @@ export const routes = {
addresses: {
_options: { path: '/addresses' },
},
tags: {
_options: { path: '/tags' },
},
metadata: {
_options: { path: '/metadata' },
},

View file

@ -1,38 +1,3 @@
import Route from '@ember/routing/route';
import { get } from '@ember/object';
import { inject as service } from '@ember/service';
import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions';
export default Route.extend(WithBlockingActions, {
router: service('router'),
settings: service('settings'),
feedback: service('feedback'),
repo: service('repository/token'),
actions: {
authorize: function(secret, nspace) {
const dc = this.modelFor('dc').dc.Name;
return this.repo
.self(secret, dc)
.then(item => {
return this.settings.persist({
token: {
Namespace: get(item, 'Namespace'),
AccessorID: get(item, 'AccessorID'),
SecretID: secret,
},
});
})
.catch(e => {
this.feedback.execute(
() => {
return Promise.resolve();
},
'authorize',
function(type, e) {
return 'error';
},
{}
);
});
},
},
});
export default Route.extend(WithBlockingActions, {});

View file

@ -9,7 +9,7 @@ export default Route.extend(WithAclActions, {
repo: service('repository/acl'),
settings: service('settings'),
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},

View file

@ -7,7 +7,7 @@ import WithPolicyActions from 'consul-ui/mixins/policy/with-actions';
export default Route.extend(WithPolicyActions, {
repo: service('repository/policy'),
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},

View file

@ -7,7 +7,7 @@ import WithRoleActions from 'consul-ui/mixins/role/with-actions';
export default Route.extend(WithRoleActions, {
repo: service('repository/role'),
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},

View file

@ -7,7 +7,7 @@ export default Route.extend(WithTokenActions, {
repo: service('repository/token'),
settings: service('settings'),
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},

View file

@ -7,10 +7,10 @@ import WithIntentionActions from 'consul-ui/mixins/intention/with-actions';
export default Route.extend(WithIntentionActions, {
repo: service('repository/intention'),
queryParams: {
currentFilter: {
filterBy: {
as: 'action',
},
s: {
search: {
as: 'filter',
replace: true,
},

View file

@ -7,7 +7,7 @@ import WithKvActions from 'consul-ui/mixins/kv/with-actions';
export default Route.extend(WithKvActions, {
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},

View file

@ -5,7 +5,7 @@ import { hash } from 'rsvp';
export default Route.extend({
repo: service('repository/node'),
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},

View file

@ -1,6 +1,12 @@
import Route from '@ember/routing/route';
export default Route.extend({
queryParams: {
search: {
as: 'filter',
replace: true,
},
},
model: function() {
const parent = this.routeName
.split('.')

View file

@ -6,7 +6,7 @@ import WithNspaceActions from 'consul-ui/mixins/nspace/with-actions';
export default Route.extend(WithNspaceActions, {
repo: service('repository/nspace'),
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},

View file

@ -5,7 +5,7 @@ import { hash } from 'rsvp';
export default Route.extend({
repo: service('repository/service'),
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},

View file

@ -1,14 +0,0 @@
import Route from '@ember/routing/route';
export default Route.extend({
model: function() {
const parent = this.routeName
.split('.')
.slice(0, -1)
.join('.');
return this.modelFor(parent);
},
setupController: function(controller, model) {
controller.setProperties(model);
},
});

View file

@ -8,7 +8,6 @@ export default Route.extend({
intentionRepo: service('repository/intention'),
chainRepo: service('repository/discovery-chain'),
proxyRepo: service('repository/proxy'),
gatewayRepo: service('repository/gateway'),
settings: service('settings'),
model: function(params, transition = {}) {
const dc = this.modelFor('dc').dc.Name;
@ -55,7 +54,7 @@ export default Route.extend({
.then(model => {
return ['ingress-gateway', 'terminating-gateway'].includes(get(model, 'item.Service.Kind'))
? hash({
gateway: this.gatewayRepo.findBySlug(params.name, dc, nspace),
gatewayServices: this.repo.findGatewayBySlug(params.name, dc, nspace),
...model,
})
: model;

View file

@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
export default Route.extend({
queryParams: {
s: {
search: {
as: 'filter',
replace: true,
},

View file

@ -3,6 +3,12 @@ import { inject as service } from '@ember/service';
import WithIntentionActions from 'consul-ui/mixins/intention/with-actions';
export default Route.extend(WithIntentionActions, {
queryParams: {
search: {
as: 'filter',
replace: true,
},
},
repo: service('repository/intention'),
model: function() {
const parent = this.routeName

View file

@ -1,17 +0,0 @@
import Serializer from './application';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/gateway';
export default Serializer.extend({
primaryKey: PRIMARY_KEY,
slugKey: SLUG_KEY,
respondForQueryRecord: function(respond, query) {
return this._super(function(cb) {
return respond(function(headers, body) {
return cb(headers, {
Name: query.id,
Services: body,
});
});
}, query);
},
});

View file

@ -69,25 +69,38 @@ export default Service.extend({
settings: service('settings'),
init: function() {
this._super(...arguments);
this._listeners = this.dom.listeners();
const maxConnections = env('CONSUL_HTTP_MAX_CONNECTIONS');
set(this, 'connections', getObjectPool(dispose, maxConnections));
if (typeof maxConnections !== 'undefined') {
set(this, 'maxConnections', maxConnections);
const doc = this.dom.document();
// when the user hides the tab, abort all connections
doc.addEventListener('visibilitychange', e => {
if (e.target.hidden) {
this.connections.purge();
}
this._listeners.add(this.dom.document(), {
visibilitychange: e => {
if (e.target.hidden) {
this.connections.purge();
}
},
});
}
},
willDestroy: function() {
this._listeners.remove();
this.connections.purge();
set(this, 'connections', undefined);
this._super(...arguments);
},
url: function() {
return url(...arguments);
},
body: function(strs, ...values) {
let body = {};
const doubleBreak = strs.reduce(function(prev, item, i) {
// Ensure each line has no whitespace either end, including empty lines
item = item
.split('\n')
.map(item => item.trim())
.join('\n');
if (item.indexOf('\n\n') !== -1) {
return i;
}
@ -235,14 +248,18 @@ export default Service.extend({
this.connections.purge();
},
whenAvailable: function(e) {
const doc = this.dom.document();
// if we are using a connection limited protocol and the user has hidden the tab (hidden browser/tab switch)
// any aborted errors should restart
const doc = this.dom.document();
if (typeof this.maxConnections !== 'undefined' && doc.hidden) {
return new Promise(function(resolve) {
doc.addEventListener('visibilitychange', function listen(event) {
doc.removeEventListener('visibilitychange', listen);
resolve(e);
return new Promise(resolve => {
const remove = this._listeners.add(doc, {
visibilitychange: function(event) {
remove();
// we resolve with the event that comes from
// whenAvailable not visibilitychange
resolve(e);
},
});
});
}

View file

@ -20,7 +20,7 @@ export default Service.extend({
// always be complete, they should never have things like '///model'
let find;
const repo = this[model];
if (typeof repo.reconcile === 'function') {
if (repo.shouldReconcile(src)) {
configuration.createEvent = function(result = {}, configuration) {
const event = {
type: 'message',

View file

@ -18,22 +18,17 @@ export default Service.extend({
init: function() {
this._super(...arguments);
if (cache === null) {
this.resetCache();
}
this._listeners = this.dom.listeners();
},
resetCache: function() {
Object.entries(sources || {}).forEach(function([key, item]) {
item.close();
});
cache = new Map();
sources = new Map();
usage = new MultiMap(Set);
this._listeners = this.dom.listeners();
},
resetCache: function() {
cache = new Map();
},
willDestroy: function() {
this._listeners.remove();
Object.entries(sources || {}).forEach(function([key, item]) {
sources.forEach(function(item) {
item.close();
});
cache = null;
@ -61,10 +56,15 @@ export default Service.extend({
close: e => {
const source = e.target;
source.removeEventListener('close', close);
cache.set(uri, {
currentEvent: source.getCurrentEvent(),
cursor: source.configuration.cursor,
});
const event = source.getCurrentEvent();
const cursor = source.configuration.cursor;
// only cache data if we have any
if (typeof event !== 'undefined' && typeof cursor !== 'undefined') {
cache.set(uri, {
currentEvent: source.getCurrentEvent(),
cursor: source.configuration.cursor,
});
}
// the data is cached delete the EventSource
sources.delete(uri);
},

View file

@ -51,6 +51,24 @@ export default Service.extend({
sibling: sibling,
isOutside: isOutside,
normalizeEvent: normalizeEvent,
setEventTargetProperty: function(e, property, cb) {
const target = e.target;
return new Proxy(e, {
get: function(obj, prop, receiver) {
if (prop === 'target') {
return new Proxy(target, {
get: function(obj, prop, receiver) {
if (prop === property) {
return cb(e.target[property]);
}
return target[prop];
},
});
}
return Reflect.get(...arguments);
},
});
},
listeners: createListeners,
root: function() {
return this.doc.documentElement;

View file

@ -15,6 +15,9 @@ export default Service.extend({
},
//
store: service('store'),
shouldReconcile: function(method) {
return true;
},
reconcile: function(meta = {}) {
// unload anything older than our current sync date/time
if (typeof meta.date !== 'undefined') {

View file

@ -1,8 +0,0 @@
import RepositoryService from 'consul-ui/services/repository';
const modelName = 'gateway';
export default RepositoryService.extend({
getModelName: function() {
return modelName;
},
});

View file

@ -5,6 +5,13 @@ export default RepositoryService.extend({
getModelName: function() {
return modelName;
},
shouldReconcile: function(method) {
switch (method) {
case 'findGatewayBySlug':
return false;
}
return this._super(...arguments);
},
findBySlug: function(slug, dc) {
return this._super(...arguments).then(function(item) {
// TODO: Move this to the Serializer
@ -69,4 +76,15 @@ export default RepositoryService.extend({
throw e;
});
},
findGatewayBySlug: function(slug, dc, nspace, configuration = {}) {
const query = {
dc: dc,
ns: nspace,
gateway: slug,
};
if (typeof configuration.cursor !== 'undefined') {
query.index = configuration.cursor;
}
return this.store.query(this.getModelName(), query);
},
});

View file

@ -10,15 +10,13 @@ const createProxy = function(repo, find, settings, cache, serialize = JSON.strin
const client = this.client;
// custom createEvent, here used to reconcile the ember-data store for each tick
let createEvent;
if (typeof repo.reconcile === 'function') {
if (repo.shouldReconcile(find)) {
createEvent = function(result = {}, configuration) {
const event = {
type: 'message',
data: result,
};
if (repo.reconcile === 'function') {
repo.reconcile(get(event, 'data.meta'));
}
repo.reconcile(get(event, 'data.meta'));
return event;
};
}

View file

@ -0,0 +1,4 @@
import Service from '@ember/service';
export default Service.extend({
comparator: function(type) {},
});

View file

@ -1,6 +1,21 @@
import Store from 'ember-data/store';
import { inject as service } from '@ember/service';
export default Store.extend({
// TODO: This should eventually go on a static method
// of the abstract Repository class
http: service('repository/type/event-source'),
dataSource: service('data-source/service'),
client: service('client/http'),
clear: function() {
// Aborting the client will close all open http type sources
this.client.abort();
// once they are closed clear their caches
this.http.resetCache();
this.dataSource.resetCache();
this.init();
},
//
// TODO: These only exist for ACLs, should probably make sure they fail
// nicely if you aren't on ACLs for good DX
// cloning immediately refreshes the view

View file

@ -0,0 +1,37 @@
export default () => key => {
if (key.startsWith('Status:')) {
return function(serviceA, serviceB) {
const [, dir] = key.split(':');
let a, b;
if (dir === 'asc') {
b = serviceA;
a = serviceB;
} else {
a = serviceA;
b = serviceB;
}
switch (true) {
case a.ChecksCritical > b.ChecksCritical:
return 1;
case a.ChecksCritical < b.ChecksCritical:
return -1;
default:
switch (true) {
case a.ChecksWarning > b.ChecksWarning:
return 1;
case a.ChecksWarning < b.ChecksWarning:
return -1;
default:
switch (true) {
case a.ChecksPassing < b.ChecksPassing:
return 1;
case a.ChecksPassing > b.ChecksPassing:
return -1;
}
}
return 0;
}
};
}
return key;
};

View file

@ -58,16 +58,7 @@ $cyan-600: #009fd9;
$cyan-700: #0077a3;
$cyan-800: #005574;
$cyan-900: #003346;
$gray-1: #191a1c;
$gray-2: #323538;
$gray-3: #4c4f54;
$gray-4: #656a70;
$gray-5: #7f858d;
$gray-6: #9a9ea5;
$gray-7: #b4b8bc;
$gray-8: #d0d2d5;
$gray-9: #ebecee;
$gray-10: #f3f4f6;
$gray-010: #fbfbfc;
$gray-050: #f7f8fa;
$gray-100: #ebeef2;
$gray-200: #dce0e6;

View file

@ -29,5 +29,5 @@
border-bottom: 1px solid $gray-200;
}
%tab-section section > h3 {
margin: 24px 0;
margin: 24px 0 12px 0;
}

View file

@ -50,3 +50,8 @@
%tooltip-bottom::after {
bottom: -12px;
}
// Ember Tooltips
.ember-tooltip {
padding: 12px;
}

View file

@ -24,6 +24,15 @@
border-bottom-width: 18px;
}
%tooltip-bubble {
border-radius: $decor-radius-200;
border-radius: $decor-radius-100;
box-shadow: $decor-elevation-400;
}
// Ember Tooltips
.ember-tooltip {
background-color: $gray-700;
border-radius: $decor-radius-100;
}
.ember-tooltip[x-placement^='top'] .ember-tooltip-arrow {
border-top-color: $gray-700;
}

File diff suppressed because one or more lines are too long

View file

@ -998,6 +998,16 @@
mask-image: $logo-gitlab-monochrome-svg;
}
%with-logo-google-color-icon {
@extend %with-icon;
background-image: $logo-google-color-svg;
}
%with-logo-google-color-mask {
@extend %with-mask;
-webkit-mask-image: $logo-google-color-svg;
mask-image: $logo-google-color-svg;
}
%with-logo-kubernetes-color-icon {
@extend %with-icon;
background-image: $logo-kubernetes-color-svg;
@ -1018,6 +1028,16 @@
mask-image: $logo-kubernetes-monochrome-svg;
}
%with-logo-microsoft-color-icon {
@extend %with-icon;
background-image: $logo-microsoft-color-svg;
}
%with-logo-microsoft-color-mask {
@extend %with-mask;
-webkit-mask-image: $logo-microsoft-color-svg;
mask-image: $logo-microsoft-color-svg;
}
%with-logo-okta-color-icon {
@extend %with-icon;
background-image: $logo-okta-color-svg;

View file

@ -73,7 +73,7 @@
}
/* */
/* TODO: Think about an %app-form or similar */
%app-view-content fieldset:not(.freetext-filter) {
%app-view-content form:not(.filter-bar) fieldset {
padding-bottom: 0.3em;
margin-bottom: 2em;
}

View file

@ -6,7 +6,7 @@
@extend %composite-row;
}
/* hoverable rows */
.consul-upstream-list > ul > li:not(:first-child),
%composite-row.linkable,
.consul-gateway-service-list > ul > li:not(:first-child),
.consul-service-instance-list > ul > li:not(:first-child),
.consul-service-list > ul > li:not(:first-child) {
@ -22,7 +22,7 @@
// In this case we do not need a background on the icon
background-color: $transparent !important;
}
.proxy-upstreams > ul,
.proxy-exposed-paths > ul {
.proxy-exposed-paths > ul,
.proxy-upstreams > ul {
border-top: 1px solid $gray-200;
}

View file

@ -16,3 +16,6 @@
%empty-state > ul > li {
@extend %with-popover-menu;
}
%empty-state label {
@extend %primary-button;
}

View file

@ -1,15 +1,28 @@
%empty-state,
%empty-state > div {
display: flex;
flex-direction: column;
}
%empty-state-header {
padding: 0;
margin: 0;
}
%empty-state {
width: 320px;
margin-top: 0 !important;
padding-bottom: 2.8em;
}
%empty-state > * {
width: 370px;
margin: 0 auto;
}
%empty-state label {
margin: 0 auto !important;
}
%empty-state-header {
margin-bottom: -3px;
}
%empty-state header {
margin-top: 1.8em;
margin-bottom: 0.5em;
}
%empty-state > ul {

View file

@ -1,5 +1,6 @@
%empty-state {
color: $gray-500;
background-color: $gray-010;
}
%empty-state > ul {
border-color: $gray-300;
@ -34,12 +35,16 @@
%empty-state[class*='status-5'] header::before {
@extend %with-alert-circle-outline-mask;
}
%empty-state .docs-link > *::before {
@extend %with-docs-mask, %as-pseudo;
%empty-state li[class*='-link'] > *::after {
@extend %as-pseudo;
margin-left: 5px;
}
%empty-state .back-link > *::before {
@extend %with-chevron-left-mask, %as-pseudo;
%empty-state .docs-link > *::after {
@extend %with-docs-mask;
}
%empty-state .learn-link > *::before {
@extend %with-learn-mask, %as-pseudo;
%empty-state .back-link > *::after {
@extend %with-chevron-left-mask;
}
%empty-state .learn-link > *::after {
@extend %with-learn-mask;
}

View file

@ -1,3 +1,8 @@
%expanded-single-select {
border: $decor-border-100;
border-color: $gray-300;
border-radius: $decor-radius-100;
}
%expanded-single-select label {
cursor: pointer;
}

View file

@ -3,11 +3,8 @@
.filter-bar {
@extend %filter-bar;
}
.catalog-toolbar {
@extend %catalog-toolbar;
}
%catalog-toolbar {
@extend %filter-bar;
%filter-bar:not(.with-sort) {
@extend %filter-bar-reversed;
}
%filter-bar [role='radiogroup'] {
@extend %expanded-single-select;

View file

@ -1,45 +1,38 @@
%filter-bar {
padding: 4px;
display: block;
margin-bottom: 8px;
display: flex;
justify-content: space-between;
padding: 4px 8px;
margin-top: 0 !important;
margin-bottom: -12px;
}
%filter-bar + :not(.notice) {
margin-top: 1.8em;
}
%catalog-toolbar {
padding: 4px 8px;
display: flex;
margin-top: 0 !important;
margin-bottom: -12px !important;
border-bottom: 1px solid $gray-200;
%filter-bar-reversed {
flex-direction: row-reverse;
padding: 4px;
margin-bottom: 8px !important;
}
@media #{$--horizontal-filters} {
%filter-bar {
display: flex;
flex-direction: row-reverse;
justify-content: space-between;
}
%catalog-toolbar {
flex-direction: row;
}
%filter-bar > *:first-child {
margin-left: 12px;
}
%catalog-toolbar > *:first-child {
margin-left: 0px;
}
%filter-bar fieldset {
min-width: 210px;
width: auto;
}
%catalog-toolbar fieldset {
min-width: none;
width: 100%;
}
%filter-bar fieldset {
flex: 0 1 auto;
width: auto;
}
%filter-bar fieldset:first-child:not(:last-child) {
flex: 1 1 auto;
}
%filter-bar-reversed fieldset:first-child:not(:last-child) {
flex: 0 1 auto;
margin-left: auto;
}
%filter-bar-reversed fieldset {
min-width: 210px;
width: auto;
}
%filter-bar-reversed > *:first-child {
margin-left: 12px;
}
@media #{$--lt-horizontal-filters} {
%filter-bar > *:first-child {
margin: 2px 0;
%filter-bar-reversed > *:first-child {
margin-left: 0;
}
}

View file

@ -1,10 +1,10 @@
// decoration/color
%filter-bar > * {
border: $decor-border-100;
border-radius: $decor-radius-100;
%filter-bar {
border-bottom: $decor-border-100;
border-color: $gray-200;
}
%catalog-toolbar > div {
border: none;
%filter-bar-reversed {
border-bottom: none;
}
// TODO: Move this elsewhere
@media #{$--horizontal-selects} {

View file

@ -1,5 +1,8 @@
%freetext-filter {
cursor: pointer;
border: $decor-border-100;
border-color: $gray-200;
border-radius: $decor-radius-100;
}
%freetext-filter input {
-webkit-appearance: none;

View file

@ -23,6 +23,9 @@
display: grid;
grid-auto-rows: 12px;
}
%card-grid li.empty {
grid-column: 1 / -1;
}
@media #{$--fixed-grid} {
%card-grid > ul,
%card-grid > ol {

View file

@ -1,5 +1,4 @@
%oidc-select [class$='-oidc-provider']::before {
@extend %as-pseudo;
width: 22px;
height: 22px;
/* this is to prevent resizing in an inline-flex context */
@ -8,23 +7,14 @@
margin-right: 10px;
}
%oidc-select .auth0-oidc-provider::before {
@extend %with-logo-auth0-color-icon;
@extend %with-logo-auth0-color-icon, %as-pseudo;
}
%oidc-select .okta-oidc-provider::before {
@extend %with-logo-okta-color-icon;
@extend %with-logo-okta-color-icon, %as-pseudo;
}
%oidc-select .gitlab-oidc-provider::before {
@extend %with-logo-gitlab-color-icon;
%oidc-select .google-oidc-provider::before {
@extend %with-logo-google-color-icon, %as-pseudo;
}
%oidc-select .aws-oidc-provider::before {
@extend %with-logo-aws-color-icon;
}
%oidc-select .azure-oidc-provider::before {
@extend %with-logo-azure-color-icon;
}
%oidc-select .bitbucket-oidc-provider::before {
@extend %with-logo-bitbucket-color-icon;
}
%oidc-select .gcp-oidc-provider::before {
@extend %with-logo-gcp-color-icon;
%oidc-select .microsoft-oidc-provider::before {
@extend %with-logo-microsoft-color-icon, %as-pseudo;
}

Some files were not shown because too many files have changed in this diff Show more