Merge pull request #8013 from hashicorp/ui-staging
ui: UI Release Merge (1.8-beta-3: ui-staging merge)
This commit is contained in:
commit
1cc5a2445d
|
@ -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,
|
||||
}}
|
||||
`;
|
||||
},
|
||||
});
|
|
@ -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') {
|
||||
|
|
|
@ -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>}}
|
|
@ -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() {},
|
||||
});
|
|
@ -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}}
|
|
@ -1,6 +0,0 @@
|
|||
import Component from '@ember/component';
|
||||
|
||||
export default Component.extend({
|
||||
classNames: ['action-group'],
|
||||
onchange: function() {},
|
||||
});
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>}}
|
|
@ -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() {},
|
||||
});
|
|
@ -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>
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -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>
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>}}
|
|
@ -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() {},
|
||||
});
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
|
@ -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));
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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))
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
61
ui-v2/app/components/search-bar/README.mdx
Normal file
61
ui-v2/app/components/search-bar/README.mdx
Normal 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)
|
||||
|
||||
---
|
32
ui-v2/app/components/search-bar/index.hbs
Normal file
32
ui-v2/app/components/search-bar/index.hbs
Normal 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>
|
6
ui-v2/app/components/search-bar/index.js
Normal file
6
ui-v2/app/components/search-bar/index.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
import Component from '@ember/component';
|
||||
import Slotted from 'block-slots';
|
||||
|
||||
export default Component.extend(Slotted, {
|
||||
tagName: '',
|
||||
});
|
3
ui-v2/app/components/tooltip/index.hbs
Normal file
3
ui-v2/app/components/tooltip/index.hbs
Normal file
|
@ -0,0 +1,3 @@
|
|||
<EmberTooltip @popperContainer=".app-view">
|
||||
{{yield}}
|
||||
</EmberTooltip>
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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: {},
|
||||
});
|
||||
|
|
|
@ -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: {},
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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);
|
||||
|
|
9
ui-v2/app/helpers/comparator.js
Normal file
9
ui-v2/app/helpers/comparator.js
Normal 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);
|
||||
},
|
||||
});
|
9
ui-v2/app/helpers/searchable.js
Normal file
9
ui-v2/app/helpers/searchable.js
Normal 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);
|
||||
},
|
||||
});
|
11
ui-v2/app/helpers/service/exists.js
Normal file
11
ui-v2/app/helpers/service/exists.js
Normal 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);
|
18
ui-v2/app/initializers/sort.js
Normal file
18
ui-v2/app/initializers/sort.js
Normal 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,
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
|
@ -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(),
|
||||
});
|
|
@ -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;
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -17,6 +17,7 @@ export default Model.extend({
|
|||
ProxyFor: attr(),
|
||||
Kind: attr('string'),
|
||||
ExternalSources: attr(),
|
||||
GatewayConfig: attr(),
|
||||
Meta: attr(),
|
||||
Address: attr('string'),
|
||||
TaggedAddresses: attr(),
|
||||
|
|
|
@ -49,9 +49,6 @@ export const routes = {
|
|||
addresses: {
|
||||
_options: { path: '/addresses' },
|
||||
},
|
||||
tags: {
|
||||
_options: { path: '/tags' },
|
||||
},
|
||||
metadata: {
|
||||
_options: { path: '/metadata' },
|
||||
},
|
||||
|
|
|
@ -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, {});
|
||||
|
|
|
@ -9,7 +9,7 @@ export default Route.extend(WithAclActions, {
|
|||
repo: service('repository/acl'),
|
||||
settings: service('settings'),
|
||||
queryParams: {
|
||||
s: {
|
||||
search: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -7,7 +7,7 @@ export default Route.extend(WithTokenActions, {
|
|||
repo: service('repository/token'),
|
||||
settings: service('settings'),
|
||||
queryParams: {
|
||||
s: {
|
||||
search: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@ import { hash } from 'rsvp';
|
|||
export default Route.extend({
|
||||
repo: service('repository/node'),
|
||||
queryParams: {
|
||||
s: {
|
||||
search: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
|
|
|
@ -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('.')
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@ import { hash } from 'rsvp';
|
|||
export default Route.extend({
|
||||
repo: service('repository/service'),
|
||||
queryParams: {
|
||||
s: {
|
||||
search: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
|
@ -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;
|
||||
|
|
|
@ -2,7 +2,7 @@ import Route from '@ember/routing/route';
|
|||
|
||||
export default Route.extend({
|
||||
queryParams: {
|
||||
s: {
|
||||
search: {
|
||||
as: 'filter',
|
||||
replace: true,
|
||||
},
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
import RepositoryService from 'consul-ui/services/repository';
|
||||
|
||||
const modelName = 'gateway';
|
||||
export default RepositoryService.extend({
|
||||
getModelName: function() {
|
||||
return modelName;
|
||||
},
|
||||
});
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
|
4
ui-v2/app/services/sort.js
Normal file
4
ui-v2/app/services/sort.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import Service from '@ember/service';
|
||||
export default Service.extend({
|
||||
comparator: function(type) {},
|
||||
});
|
|
@ -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
|
||||
|
|
37
ui-v2/app/sort/comparators/service.js
Normal file
37
ui-v2/app/sort/comparators/service.js
Normal 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;
|
||||
};
|
|
@ -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;
|
||||
|
|
|
@ -29,5 +29,5 @@
|
|||
border-bottom: 1px solid $gray-200;
|
||||
}
|
||||
%tab-section section > h3 {
|
||||
margin: 24px 0;
|
||||
margin: 24px 0 12px 0;
|
||||
}
|
||||
|
|
|
@ -50,3 +50,8 @@
|
|||
%tooltip-bottom::after {
|
||||
bottom: -12px;
|
||||
}
|
||||
|
||||
// Ember Tooltips
|
||||
.ember-tooltip {
|
||||
padding: 12px;
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -16,3 +16,6 @@
|
|||
%empty-state > ul > li {
|
||||
@extend %with-popover-menu;
|
||||
}
|
||||
%empty-state label {
|
||||
@extend %primary-button;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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} {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue