ui: Auth Methods List view (#9617)

* Create mock-api endpoints for auth-methods

* Implement auth-method endpoints and model with tests

* Create route and tab for auth-methods

* Create auth-method list and type components with styles

* Add JWT and OIDC svg logos to codebase

* Add brand translations

* Add SearchBar to Auth Methods

* Add acceptance test for Auth Methods UI

* Skip auth method repo test

* Changes from review notes

* Fixup auth-method modela and mock-data

* Update SearhBar with rebased changes

* Add filterBy source and sortBy max token ttl

* Update to SortBy MethodName

* Update UI acceptance tests

* Update mock data DisplayNames

* Skip repo test

* Fix to breaking serializer test

* Implement auth-method endpoints and model with tests

* Add acceptance test for Auth Methods UI

* Update SearhBar with rebased changes

* Add filterBy source and sortBy max token ttl

* Update to SortBy MethodName

* Update UI acceptance tests

* Update mock data DisplayNames

* Fix to breaking serializer test

* Update class for search

* Add auth-methods link to sidebar

* Fixup PR review notes

* Fixup review notes

* Only show OIDC filter with enterprise

* Update conditionals for MaxTokenTTL & TokenLocality

* Refactor
This commit is contained in:
Kenia 2021-02-17 13:56:56 -05:00 committed by GitHub
parent 4a8b1c2f0d
commit da8280f4c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 991 additions and 2 deletions

View File

@ -0,0 +1,28 @@
import Adapter from './application';
export default class AuthMethodAdapter extends Adapter {
requestForQuery(request, { dc, ns, index, id }) {
return request`
GET /v1/acl/auth-methods?${{ dc }}
${{
...this.formatNspace(ns),
index,
}}
`;
}
requestForQueryRecord(request, { dc, ns, index, id }) {
if (typeof id === 'undefined') {
throw new Error('You must specify an id');
}
return request`
GET /v1/acl/auth-method/${id}?${{ dc }}
${{
...this.formatNspace(ns),
index,
}}
`;
}
}

View File

@ -0,0 +1,9 @@
.consul-auth-method-list ul {
.consul-auth-method-type {
@extend %pill-200, %frame-gray-600;
}
.locality::before {
@extend %with-public-default-mask, %as-pseudo;
margin-right: 4px;
}
}

View File

@ -0,0 +1,31 @@
<ListCollection
class="consul-auth-method-list"
@items={{@items}}
as |item|>
<BlockSlot @name="header">
{{#if (not-eq item.DisplayName '')}}
<p data-test-auth-method>{{item.DisplayName}}</p>
{{else}}
<p data-test-auth-method>{{item.Name}}</p>
{{/if}}
</BlockSlot>
<BlockSlot @name="details">
<Consul::AuthMethod::Type @item={{item}} />
{{#if (not-eq item.DisplayName '')}}
<span data-test-display-name>{{item.Name}}</span>
{{/if}}
{{#if (eq item.TokenLocality 'global')}}
<span class="locality">creates global tokens</span>
{{/if}}
{{#if item.MaxTokenTTL}}
<dl class="ttl">
<dt>
<Tooltip>
Maximum Time to Live: the maximum life of any token created by this auth method
</Tooltip>
</dt>
<dd>{{item.MaxTokenTTL}}</dd>
</dl>
{{/if}}
</BlockSlot>
</ListCollection>

View File

@ -0,0 +1,7 @@
export default (collection, text) => () => {
return collection('.consul-auth-method-list [data-test-list-row]', {
name: text('[data-test-auth-method]'),
displayName: text('[data-test-display-name]'),
type: text('[data-test-type]'),
});
};

View File

@ -0,0 +1,148 @@
<SearchBar
class="consul-auth-method-search-bar"
...attributes
@filter={{@filter}}
>
<:status as |search|>
{{#let
(t (concat "components.consul.auth-method.search-bar." search.status.key ".name")
default=(array
(concat "common.search." search.status.key)
(concat "common.consul." search.status.key)
)
)
(t (concat "components.consul.auth-method.search-bar." search.status.key ".options." search.status.value)
default=(array
(concat "common.search." search.status.value)
(concat "common.consul." search.status.value)
(concat "common.brand." search.status.value)
)
)
as |key value|}}
<search.RemoveFilter
aria-label={{t "common.ui.remove" item=(concat key " " value)}}
>
<dl>
<dt>{{key}}</dt>
<dd>{{value}}</dd>
</dl>
</search.RemoveFilter>
{{/let}}
</:status>
<:search as |search|>
<search.Search
@onsearch={{action @onsearch}}
@value={{@search}}
@placeholder={{t "common.search.search"}}
>
<search.Select
class="type-search-properties"
@position="right"
@onchange={{action @filter.searchproperty.change}}
@multiple={{true}}
@required={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
{{t "common.search.searchproperty"}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
{{#each @filter.searchproperty.default as |prop|}}
<Option @value={{prop}} @selected={{contains prop @filter.searchproperty.value}}>
{{t (concat "common.consul." (lowercase prop))}}
</Option>
{{/each}}
{{/let}}
</BlockSlot>
</search.Select>
</search.Search>
</:search>
<:filter as |search|>
<search.Select
class="type-kind"
@position="left"
@onchange={{action @filter.kind.change}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
{{t "components.consul.auth-method.search-bar.kind.name"}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Option class="kubernetes" @value="kubernetes" @selected={{contains 'kubernetes' @filter.kind.value}}>Kubernetes</Option>
<Option class="jwt" @value="jwt" @selected={{contains 'jwt' @filter.kind.value}}>JWT</Option>
{{#if (env 'CONSUL_SSO_ENABLED')}}
<Option class="oidc" @value="oidc" @selected={{contains 'oidc' @filter.kind.value}}>OIDC</Option>
{{/if}}
{{/let}}
</BlockSlot>
</search.Select>
<search.Select
class="type-locality"
@position="left"
@onchange={{action @filter.source.change}}
@multiple={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
{{t "components.consul.auth-method.search-bar.locality.name"}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
{{#each (array "local" "global") as |option|}}
<Option class="{{option}}" @value={{option}} @selected={{contains option @filter.types}}>
{{t (concat "components.consul.auth-method.search-bar.locality.options." option)}}
</Option>
{{/each}}
{{/let}}
</BlockSlot>
</search.Select>
</:filter>
<:sort as |search|>
<search.Select
class="type-sort"
data-test-sort-control
@position="right"
@onchange={{action @sort.change}}
@multiple={{false}}
@required={{true}}
as |components|>
<BlockSlot @name="selected">
<span>
{{#let (from-entries (array
(array "MethodName:asc" (t "common.sort.alpha.asc"))
(array "MethodName:desc" (t "common.sort.alpha.desc"))
(array "MaxTokenTTL:desc" (t "common.sort.duration.asc"))
(array "MaxTokenTTL:asc" (t "common.sort.duration.desc"))
))
as |selectable|
}}
{{get selectable @sort.value}}
{{/let}}
</span>
</BlockSlot>
<BlockSlot @name="options">
{{#let components.Optgroup components.Option as |Optgroup Option|}}
<Optgroup @label={{t "common.ui.name"}}>
<Option @value="MethodName:asc" @selected={{eq "MethodName:asc" @sort.value}}>{{t "common.sort.alpha.asc"}}</Option>
<Option @value="MethodName:desc" @selected={{eq "MethodName:desc" @sort.value}}>{{t "common.sort.alpha.desc"}}</Option>
</Optgroup>
<Optgroup @label={{t "common.ui.maxttl"}}>
<Option @value="MaxTokenTTL:desc" @selected={{eq "MaxTokenTTL:desc" @sort.value}}>{{t "common.sort.duration.asc"}}</Option>
<Option @value="MaxTokenTTL:asc" @selected={{eq "MaxTokenTTL:asc" @sort.value}}>{{t "common.sort.duration.desc"}}</Option>
</Optgroup>
{{/let}}
</BlockSlot>
</search.Select>
</:sort>
</SearchBar>

View File

@ -0,0 +1,3 @@
<span class="consul-auth-method-type {{@item.Type}}" data-test-type={{@item.Type}}>
{{t (concat "common.brand." @item.Type)}}
</span>

View File

@ -126,6 +126,9 @@
<li data-test-main-nav-roles class={{if (is-href 'dc.acls.roles' @dc.Name) 'is-active'}}> <li data-test-main-nav-roles class={{if (is-href 'dc.acls.roles' @dc.Name) 'is-active'}}>
<a href={{href-to 'dc.acls.roles' @dc.Name}}>Roles</a> <a href={{href-to 'dc.acls.roles' @dc.Name}}>Roles</a>
</li> </li>
<li data-test-main-nav-auth-methods class={{if (is-href 'dc.acls.auth-methods' @dc.Name) 'is-active'}}>
<a href={{href-to 'dc.acls.auth-methods' @dc.Name}}>Auth Methods</a>
</li>
</ul> </ul>
{{/if}} {{/if}}

View File

@ -0,0 +1,11 @@
export default {
kind: {
kubernetes: (item, value) => item.Type === value,
jwt: (item, value) => item.Type === value,
oidc: (item, value) => item.Type === value,
},
source: {
local: (item, value) => item.TokenLocality === value,
global: (item, value) => item.TokenLocality === value,
},
};

View File

@ -99,7 +99,10 @@ export function initialize(container) {
register(container, index, indexed); register(container, index, indexed);
} }
} }
if (typeof route !== 'undefined') {
register(container, route, item); register(container, route, item);
}
}); });
} }
} }

View File

@ -0,0 +1,24 @@
import Model, { attr } from '@ember-data/model';
import { or } from '@ember/object/computed';
export const PRIMARY_KEY = 'uid';
export const SLUG_KEY = 'Name';
export default class AuthMethod extends Model {
@attr('string') uid;
@attr('string') Name;
@attr('string') Datacenter;
@attr('string') Namespace;
@attr('string', { defaultValue: () => '' }) Description;
@attr('string', { defaultValue: () => '' }) DisplayName;
@attr('string', { defaultValue: () => 'local' }) TokenLocality;
@attr('string') Type;
@or('DisplayName', 'Name') MethodName;
@attr() Config;
@attr('string') MaxTokenTTL;
@attr('number') CreateIndex;
@attr('number') ModifyIndex;
@attr() Datacenters; // string[]
@attr() meta; // {}
}

View File

@ -149,6 +149,12 @@ export const routes = {
_options: { path: '/create' }, _options: { path: '/create' },
}, },
}, },
'auth-methods': {
_options: { path: '/auth-methods' },
show: {
_options: { path: '/show' },
},
},
}, },
}, },
// Shows a datacenter picker. If you only have one // Shows a datacenter picker. If you only have one

View File

@ -0,0 +1,38 @@
import { inject as service } from '@ember/service';
import Route from 'consul-ui/routing/route';
import { hash } from 'rsvp';
export default class IndexRoute extends Route {
@service('repository/auth-method') repo;
queryParams = {
sortBy: 'sort',
source: 'source',
kind: 'kind',
searchproperty: {
as: 'searchproperty',
empty: [['Name', 'DisplayName']],
},
search: {
as: 'filter',
replace: true,
},
};
model(params) {
return hash({
...this.repo.status({
items: this.repo.findAllByDatacenter(
this.modelFor('dc').dc.Name,
this.modelFor('nspace').nspace.substr(1)
),
}),
searchProperties: this.queryParams.searchproperty.empty[0],
});
}
setupController(controller, model) {
super.setupController(...arguments);
controller.setProperties(model);
}
}

View File

@ -0,0 +1,4 @@
export default {
Name: item => item.Name,
DisplayName: item => item.DisplayName,
};

View File

@ -0,0 +1,7 @@
import Serializer from './application';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/auth-method';
export default class AuthMethodSerializer extends Serializer {
primaryKey = PRIMARY_KEY;
slugKey = SLUG_KEY;
}

View File

@ -10,12 +10,14 @@ import kv from 'consul-ui/filter/predicates/kv';
import intention from 'consul-ui/filter/predicates/intention'; import intention from 'consul-ui/filter/predicates/intention';
import token from 'consul-ui/filter/predicates/token'; import token from 'consul-ui/filter/predicates/token';
import policy from 'consul-ui/filter/predicates/policy'; import policy from 'consul-ui/filter/predicates/policy';
import authMethod from 'consul-ui/filter/predicates/auth-method';
const predicates = { const predicates = {
acl: andOr(acl), acl: andOr(acl),
service: andOr(service), service: andOr(service),
['service-instance']: andOr(serviceInstance), ['service-instance']: andOr(serviceInstance),
['health-check']: andOr(healthCheck), ['health-check']: andOr(healthCheck),
['auth-method']: andOr(authMethod),
node: andOr(node), node: andOr(node),
kv: andOr(kv), kv: andOr(kv),
intention: andOr(intention), intention: andOr(intention),

View File

@ -0,0 +1,26 @@
import RepositoryService from 'consul-ui/services/repository';
import statusFactory from 'consul-ui/utils/acls-status';
import isValidServerErrorFactory from 'consul-ui/utils/http/acl/is-valid-server-error';
import { PRIMARY_KEY, SLUG_KEY } from 'consul-ui/models/auth-method';
const isValidServerError = isValidServerErrorFactory();
const status = statusFactory(isValidServerError, Promise);
const MODEL_NAME = 'auth-method';
export default class AuthMethodService extends RepositoryService {
getModelName() {
return MODEL_NAME;
}
getPrimaryKey() {
return PRIMARY_KEY;
}
getSlugKey() {
return SLUG_KEY;
}
status(obj) {
return status(obj);
}
}

View File

@ -13,6 +13,7 @@ import kv from 'consul-ui/search/predicates/kv';
import token from 'consul-ui/search/predicates/token'; import token from 'consul-ui/search/predicates/token';
import role from 'consul-ui/search/predicates/role'; import role from 'consul-ui/search/predicates/role';
import policy from 'consul-ui/search/predicates/policy'; import policy from 'consul-ui/search/predicates/policy';
import authMethod from 'consul-ui/search/predicates/auth-method';
import nspace from 'consul-ui/search/predicates/nspace'; import nspace from 'consul-ui/search/predicates/nspace';
const predicates = { const predicates = {
@ -21,6 +22,7 @@ const predicates = {
['service-instance']: serviceInstance, ['service-instance']: serviceInstance,
['upstream-instance']: upstreamInstance, ['upstream-instance']: upstreamInstance,
['health-check']: healthCheck, ['health-check']: healthCheck,
['auth-method']: authMethod,
node: node, node: node,
kv: kv, kv: kv,
acl: acl, acl: acl,

View File

@ -9,6 +9,7 @@ import intention from 'consul-ui/sort/comparators/intention';
import token from 'consul-ui/sort/comparators/token'; import token from 'consul-ui/sort/comparators/token';
import role from 'consul-ui/sort/comparators/role'; import role from 'consul-ui/sort/comparators/role';
import policy from 'consul-ui/sort/comparators/policy'; import policy from 'consul-ui/sort/comparators/policy';
import authMethod from 'consul-ui/sort/comparators/auth-method';
import nspace from 'consul-ui/sort/comparators/nspace'; import nspace from 'consul-ui/sort/comparators/nspace';
import node from 'consul-ui/sort/comparators/node'; import node from 'consul-ui/sort/comparators/node';
@ -32,6 +33,7 @@ const comparators = {
['service-instance']: serviceInstance(options), ['service-instance']: serviceInstance(options),
['upstream-instance']: upstreamInstance(options), ['upstream-instance']: upstreamInstance(options),
['health-check']: healthCheck(options), ['health-check']: healthCheck(options),
['auth-method']: authMethod(options),
acl: acl(options), acl: acl(options),
kv: kv(options), kv: kv(options),
intention: intention(options), intention: intention(options),

View File

@ -0,0 +1,3 @@
export default ({ properties }) => (key = 'MethodName:asc') => {
return properties(['MethodName', 'MaxTokenTTL'])(key);
};

View File

@ -77,6 +77,7 @@ $history-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fil
$info-circle-fill-color-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10S2 17.543 2 12 6.486 2 12 2zm1.429 10.014a1.555 1.555 0 0 0-.443-.985c-.286-.272-.6-.429-.986-.443h-1.429c-.385.028-.685.185-.985.443a1.457 1.457 0 0 0-.443.985h1.428V16.3c.029.386.158.714.443.986.286.285.6.443.986.443h1.429c.385 0 .685-.158.985-.443.286-.272.429-.6.443-.986H13.43V12v.014zM11 7.73a1.345 1.345 0 0 1-.4-1c0-.4.129-.743.4-1 .271-.258.6-.4 1-.4s.743.128 1 .4c.257.271.4.6.4 1s-.129.742-.4 1a1.433 1.433 0 0 1-1 .428c-.4 0-.743-.157-1-.428z" fill="%231563ff" fill-rule="evenodd"/></svg>'); $info-circle-fill-color-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10S2 17.543 2 12 6.486 2 12 2zm1.429 10.014a1.555 1.555 0 0 0-.443-.985c-.286-.272-.6-.429-.986-.443h-1.429c-.385.028-.685.185-.985.443a1.457 1.457 0 0 0-.443.985h1.428V16.3c.029.386.158.714.443.986.286.285.6.443.986.443h1.429c.385 0 .685-.158.985-.443.286-.272.429-.6.443-.986H13.43V12v.014zM11 7.73a1.345 1.345 0 0 1-.4-1c0-.4.129-.743.4-1 .271-.258.6-.4 1-.4s.743.128 1 .4c.257.271.4.6.4 1s-.129.742-.4 1a1.433 1.433 0 0 1-1 .428c-.4 0-.743-.157-1-.428z" fill="%231563ff" fill-rule="evenodd"/></svg>');
$info-circle-fill-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10S2 17.543 2 12 6.486 2 12 2zm1.429 10.014a1.555 1.555 0 0 0-.443-.985c-.286-.272-.6-.429-.986-.443h-1.429c-.385.028-.685.185-.985.443a1.456 1.456 0 0 0-.443.985h1.428V16.3c.029.386.158.714.443.986.286.285.6.443.986.443h1.429c.385 0 .685-.158.985-.443.286-.272.429-.6.443-.986H13.43V12v.014zM11 7.73a1.345 1.345 0 0 1-.4-1c0-.4.129-.743.4-1 .271-.258.6-.4 1-.4s.743.128 1 .4c.257.271.4.6.4 1s-.129.742-.4 1a1.433 1.433 0 0 1-1 .428c-.4 0-.743-.157-1-.428z" fill="%23000"/></svg>'); $info-circle-fill-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10S2 17.543 2 12 6.486 2 12 2zm1.429 10.014a1.555 1.555 0 0 0-.443-.985c-.286-.272-.6-.429-.986-.443h-1.429c-.385.028-.685.185-.985.443a1.456 1.456 0 0 0-.443.985h1.428V16.3c.029.386.158.714.443.986.286.285.6.443.986.443h1.429c.385 0 .685-.158.985-.443.286-.272.429-.6.443-.986H13.43V12v.014zM11 7.73a1.345 1.345 0 0 1-.4-1c0-.4.129-.743.4-1 .271-.258.6-.4 1-.4s.743.128 1 .4c.257.271.4.6.4 1s-.129.742-.4 1a1.433 1.433 0 0 1-1 .428c-.4 0-.743-.157-1-.428z" fill="%23000"/></svg>');
$info-circle-outline-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10S2 17.543 2 12 6.486 2 12 2zm0 1.886c-4.486 0-8.143 3.628-8.143 8.114 0 4.486 3.657 8.143 8.143 8.143 4.486 0 8.143-3.643 8.143-8.143 0-4.5-3.657-8.129-8.143-8.129v.015zm1.429 8.128a1.555 1.555 0 0 0-.443-.985c-.286-.272-.6-.429-.986-.443h-1.429c-.385.028-.685.185-.985.443a1.456 1.456 0 0 0-.443.985h1.428V16.3c.029.386.158.714.443.986.286.285.6.443.986.443h1.429c.385 0 .685-.158.985-.443.286-.272.429-.6.443-.986H13.43V12v.014zM11 8.73a1.345 1.345 0 0 1-.4-1c0-.4.129-.743.4-1 .271-.258.6-.4 1-.4s.743.128 1 .4c.257.271.4.6.4 1s-.129.742-.4 1a1.433 1.433 0 0 1-1 .428c-.4 0-.743-.157-1-.428z" fill="%23000"/></svg>'); $info-circle-outline-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10S2 17.543 2 12 6.486 2 12 2zm0 1.886c-4.486 0-8.143 3.628-8.143 8.114 0 4.486 3.657 8.143 8.143 8.143 4.486 0 8.143-3.643 8.143-8.143 0-4.5-3.657-8.129-8.143-8.129v.015zm1.429 8.128a1.555 1.555 0 0 0-.443-.985c-.286-.272-.6-.429-.986-.443h-1.429c-.385.028-.685.185-.985.443a1.456 1.456 0 0 0-.443.985h1.428V16.3c.029.386.158.714.443.986.286.285.6.443.986.443h1.429c.385 0 .685-.158.985-.443.286-.272.429-.6.443-.986H13.43V12v.014zM11 8.73a1.345 1.345 0 0 1-.4-1c0-.4.129-.743.4-1 .271-.258.6-.4 1-.4s.743.128 1 .4c.257.271.4.6.4 1s-.129.742-.4 1a1.433 1.433 0 0 1-1 .428c-.4 0-.743-.157-1-.428z" fill="%23000"/></svg>');
$jwt-logo-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="12" height="13" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M6.863 3.805V.667h-1.75v3.138l.875 1.202.875-1.202zM5.113 9.195v3.138h1.75V9.195l-.875-1.202-.875 1.202z" fill="%23fff"/><path fill-rule="evenodd" clip-rule="evenodd" d="M6.863 9.195l1.844 2.543L10.13 10.7 8.275 8.168l-1.412-.466v1.493zM5.113 3.805L3.27 1.262 1.847 2.3l1.855 2.532 1.411.466V3.805z" fill="%2300F2E6"/><path fill-rule="evenodd" clip-rule="evenodd" d="M3.702 4.832L.715 3.863.167 5.532l2.986.968 1.424-.455-.875-1.213zM7.4 6.955l.875 1.213 2.987.969.548-1.669L8.823 6.5 7.4 6.955z" fill="%2300B9F1"/><path fill-rule="evenodd" clip-rule="evenodd" d="M8.823 6.5l2.987-.968-.548-1.669-2.987.969L7.4 6.045l1.423.455zM3.153 6.5l-2.986.968.548 1.669 2.987-.969.875-1.213L3.153 6.5z" fill="%23D63AFF"/><path fill-rule="evenodd" clip-rule="evenodd" d="M3.702 8.168L1.847 10.7l1.423 1.038 1.843-2.543V7.702l-1.411.466zM8.275 4.832L10.13 2.3 8.707 1.262 6.863 3.805v1.493l1.412-.466z" fill="%23FB015B"/></svg>');
$key-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M21 18v3h-2.969L17 20v-2h-2v-2h-2l-4-4 3.052-3L21 18zM10 6L8 4 5.003 5 4 8l2 2 4-4zm-4.217 7.839L1.132 9.188l1.702-6.354 6.354-1.702 4.65 4.65-1.702 6.354-6.353 1.703z" fill="%23000"/></svg>'); $key-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M21 18v3h-2.969L17 20v-2h-2v-2h-2l-4-4 3.052-3L21 18zM10 6L8 4 5.003 5 4 8l2 2 4-4zm-4.217 7.839L1.132 9.188l1.702-6.354 6.354-1.702 4.65 4.65-1.702 6.354-6.353 1.703z" fill="%23000"/></svg>');
$kubernetes-logo-color-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="21" height="20" xmlns="http://www.w3.org/2000/svg"><g stroke="%23FFF" fill="none"><path d="M10.21 1.002a1.241 1.241 0 0 0-.472.12L3.29 4.201a1.225 1.225 0 0 0-.667.83l-1.591 6.922a1.215 1.215 0 0 0 .238 1.035l4.463 5.55c.234.29.59.46.964.46l7.159-.002c.375 0 .73-.168.964-.459l4.462-5.55c.234-.292.322-.673.238-1.036L17.927 5.03a1.225 1.225 0 0 0-.667-.83l-6.45-3.08a1.242 1.242 0 0 0-.598-.12z" fill="%23326CE5"/><path d="M10.275 3.357c-.213 0-.386.192-.386.429v.11c.005.136.035.24.052.367.033.27.06.492.043.7a.421.421 0 0 1-.125.2l-.01.163a4.965 4.965 0 0 0-3.22 1.548 6.47 6.47 0 0 1-.138-.099c-.07.01-.139.03-.23-.022-.172-.117-.33-.277-.52-.47-.087-.093-.15-.181-.254-.27L5.4 5.944a.46.46 0 0 0-.269-.101.372.372 0 0 0-.307.136c-.133.167-.09.422.094.57l.006.003.08.065c.11.08.21.122.32.187.231.142.422.26.574.403.06.063.07.175.078.223l.123.11a4.995 4.995 0 0 0-.787 3.483l-.162.047c-.042.055-.103.141-.166.167-.198.063-.422.086-.692.114-.126.01-.236.004-.37.03-.03.005-.07.016-.103.023l-.003.001-.006.002c-.228.055-.374.264-.327.47.047.206.27.331.498.282h.006c.003-.001.005-.003.008-.003l.1-.022c.131-.036.227-.088.346-.133.255-.092.467-.168.673-.198.086-.007.177.053.222.078l.168-.029a5.023 5.023 0 0 0 2.226 2.78l-.07.168c.025.065.053.154.034.218-.075.195-.203.4-.35.628-.07.106-.142.188-.206.309l-.05.104c-.099.212-.026.456.165.548.191.092.43-.005.532-.218h.001v-.001c.015-.03.036-.07.048-.098.055-.126.073-.233.111-.354.102-.257.159-.526.3-.694.038-.046.1-.063.166-.08l.087-.159a4.987 4.987 0 0 0 3.562.01l.083.148c.066.021.138.032.197.12.105.179.177.391.265.648.038.121.057.229.112.354.012.029.033.069.048.099.102.213.341.311.533.219.19-.092.264-.337.164-.549l-.05-.104c-.064-.12-.136-.202-.207-.307-.146-.23-.267-.419-.342-.613-.032-.1.005-.163.03-.228-.015-.017-.047-.111-.065-.156a5.023 5.023 0 0 0 2.225-2.8l.165.03c.058-.039.112-.088.216-.08.206.03.418.106.673.198.12.045.215.098.347.133.028.008.068.015.1.022l.007.002.006.001c.229.05.45-.076.498-.282.047-.206-.1-.415-.327-.47l-.112-.027c-.134-.025-.243-.019-.37-.03-.27-.027-.494-.05-.692-.113-.081-.031-.139-.128-.167-.167l-.156-.046a4.997 4.997 0 0 0-.804-3.474l.137-.123c.006-.069.001-.142.073-.218.151-.143.343-.261.574-.404.11-.064.21-.106.32-.187.025-.018.06-.047.086-.068.185-.148.227-.403.094-.57-.133-.166-.39-.182-.575-.034-.027.02-.062.048-.086.068-.104.09-.168.178-.255.27-.19.194-.348.355-.52.471-.075.044-.185.029-.235.026l-.146.104A5.059 5.059 0 0 0 10.7 5.328a9.325 9.325 0 0 1-.009-.172c-.05-.048-.11-.09-.126-.193-.017-.208.011-.43.044-.7.018-.126.047-.23.053-.367l-.001-.11c0-.237-.173-.429-.386-.429zM9.79 6.351l-.114 2.025-.009.004a.34.34 0 0 1-.54.26l-.003.002-1.66-1.177A3.976 3.976 0 0 1 9.79 6.351zm.968 0a4.01 4.01 0 0 1 2.313 1.115l-1.65 1.17-.006-.003a.34.34 0 0 1-.54-.26h-.003L10.76 6.35zm-3.896 1.87l1.516 1.357-.002.008a.34.34 0 0 1-.134.585l-.001.006-1.944.561a3.975 3.975 0 0 1 .565-2.516zm6.813.001a4.025 4.025 0 0 1 .582 2.51l-1.954-.563-.001-.008a.34.34 0 0 1-.134-.585v-.004l1.507-1.35zm-3.712 1.46h.62l.387.483-.139.602-.557.268-.56-.269-.138-.602.387-.482zm1.99 1.652a.339.339 0 0 1 .08.005l.002-.004 2.01.34a3.98 3.98 0 0 1-1.609 2.022l-.78-1.885.002-.003a.34.34 0 0 1 .296-.475zm-3.375.008a.34.34 0 0 1 .308.474l.005.007-.772 1.866a3.997 3.997 0 0 1-1.604-2.007l1.993-.339.003.005a.345.345 0 0 1 .067-.006zm1.683.817a.338.338 0 0 1 .312.179h.008l.982 1.775a3.991 3.991 0 0 1-2.57-.002l.979-1.772h.001a.34.34 0 0 1 .288-.18z" stroke-width=".25" fill="%23FFF"/></g></svg>'); $kubernetes-logo-color-svg: url('data:image/svg+xml;charset=UTF-8,<svg width="21" height="20" xmlns="http://www.w3.org/2000/svg"><g stroke="%23FFF" fill="none"><path d="M10.21 1.002a1.241 1.241 0 0 0-.472.12L3.29 4.201a1.225 1.225 0 0 0-.667.83l-1.591 6.922a1.215 1.215 0 0 0 .238 1.035l4.463 5.55c.234.29.59.46.964.46l7.159-.002c.375 0 .73-.168.964-.459l4.462-5.55c.234-.292.322-.673.238-1.036L17.927 5.03a1.225 1.225 0 0 0-.667-.83l-6.45-3.08a1.242 1.242 0 0 0-.598-.12z" fill="%23326CE5"/><path d="M10.275 3.357c-.213 0-.386.192-.386.429v.11c.005.136.035.24.052.367.033.27.06.492.043.7a.421.421 0 0 1-.125.2l-.01.163a4.965 4.965 0 0 0-3.22 1.548 6.47 6.47 0 0 1-.138-.099c-.07.01-.139.03-.23-.022-.172-.117-.33-.277-.52-.47-.087-.093-.15-.181-.254-.27L5.4 5.944a.46.46 0 0 0-.269-.101.372.372 0 0 0-.307.136c-.133.167-.09.422.094.57l.006.003.08.065c.11.08.21.122.32.187.231.142.422.26.574.403.06.063.07.175.078.223l.123.11a4.995 4.995 0 0 0-.787 3.483l-.162.047c-.042.055-.103.141-.166.167-.198.063-.422.086-.692.114-.126.01-.236.004-.37.03-.03.005-.07.016-.103.023l-.003.001-.006.002c-.228.055-.374.264-.327.47.047.206.27.331.498.282h.006c.003-.001.005-.003.008-.003l.1-.022c.131-.036.227-.088.346-.133.255-.092.467-.168.673-.198.086-.007.177.053.222.078l.168-.029a5.023 5.023 0 0 0 2.226 2.78l-.07.168c.025.065.053.154.034.218-.075.195-.203.4-.35.628-.07.106-.142.188-.206.309l-.05.104c-.099.212-.026.456.165.548.191.092.43-.005.532-.218h.001v-.001c.015-.03.036-.07.048-.098.055-.126.073-.233.111-.354.102-.257.159-.526.3-.694.038-.046.1-.063.166-.08l.087-.159a4.987 4.987 0 0 0 3.562.01l.083.148c.066.021.138.032.197.12.105.179.177.391.265.648.038.121.057.229.112.354.012.029.033.069.048.099.102.213.341.311.533.219.19-.092.264-.337.164-.549l-.05-.104c-.064-.12-.136-.202-.207-.307-.146-.23-.267-.419-.342-.613-.032-.1.005-.163.03-.228-.015-.017-.047-.111-.065-.156a5.023 5.023 0 0 0 2.225-2.8l.165.03c.058-.039.112-.088.216-.08.206.03.418.106.673.198.12.045.215.098.347.133.028.008.068.015.1.022l.007.002.006.001c.229.05.45-.076.498-.282.047-.206-.1-.415-.327-.47l-.112-.027c-.134-.025-.243-.019-.37-.03-.27-.027-.494-.05-.692-.113-.081-.031-.139-.128-.167-.167l-.156-.046a4.997 4.997 0 0 0-.804-3.474l.137-.123c.006-.069.001-.142.073-.218.151-.143.343-.261.574-.404.11-.064.21-.106.32-.187.025-.018.06-.047.086-.068.185-.148.227-.403.094-.57-.133-.166-.39-.182-.575-.034-.027.02-.062.048-.086.068-.104.09-.168.178-.255.27-.19.194-.348.355-.52.471-.075.044-.185.029-.235.026l-.146.104A5.059 5.059 0 0 0 10.7 5.328a9.325 9.325 0 0 1-.009-.172c-.05-.048-.11-.09-.126-.193-.017-.208.011-.43.044-.7.018-.126.047-.23.053-.367l-.001-.11c0-.237-.173-.429-.386-.429zM9.79 6.351l-.114 2.025-.009.004a.34.34 0 0 1-.54.26l-.003.002-1.66-1.177A3.976 3.976 0 0 1 9.79 6.351zm.968 0a4.01 4.01 0 0 1 2.313 1.115l-1.65 1.17-.006-.003a.34.34 0 0 1-.54-.26h-.003L10.76 6.35zm-3.896 1.87l1.516 1.357-.002.008a.34.34 0 0 1-.134.585l-.001.006-1.944.561a3.975 3.975 0 0 1 .565-2.516zm6.813.001a4.025 4.025 0 0 1 .582 2.51l-1.954-.563-.001-.008a.34.34 0 0 1-.134-.585v-.004l1.507-1.35zm-3.712 1.46h.62l.387.483-.139.602-.557.268-.56-.269-.138-.602.387-.482zm1.99 1.652a.339.339 0 0 1 .08.005l.002-.004 2.01.34a3.98 3.98 0 0 1-1.609 2.022l-.78-1.885.002-.003a.34.34 0 0 1 .296-.475zm-3.375.008a.34.34 0 0 1 .308.474l.005.007-.772 1.866a3.997 3.997 0 0 1-1.604-2.007l1.993-.339.003.005a.345.345 0 0 1 .067-.006zm1.683.817a.338.338 0 0 1 .312.179h.008l.982 1.775a3.991 3.991 0 0 1-2.57-.002l.979-1.772h.001a.34.34 0 0 1 .288-.18z" stroke-width=".25" fill="%23FFF"/></g></svg>');
$layers-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.815 18.637l-.101.029c-.169.043-.34.064-.51.064H12.103l-.09-.007a1.74 1.74 0 01-.319-.054l-.061-.01-.163-.058c-.108-.043-3.833-1.667-6.675-2.907l-2.559 1.328a.422.422 0 00-.184.149.329.329 0 00-.044.256v.01c.011.042.03.08.055.117.006.008.006.018.013.026l.007.007c.018.021.042.038.066.056.025.021.049.041.078.057.002 0 .005.004.007.005.128.059 9.629 4.205 9.76 4.258.01.005.02.004.03.007a.506.506 0 00.315.012l.023-.008c.03-.01.062-.015.09-.03l9.319-4.833c.015-.006.023-.019.036-.026a.461.461 0 00.125-.115c.008-.011.02-.018.026-.03.007-.01.006-.023.012-.034a.368.368 0 00.029-.151.323.323 0 00-.013-.072.343.343 0 00-.03-.076c-.006-.011-.006-.023-.013-.034-.008-.01-.02-.018-.03-.028a.445.445 0 00-.134-.106c-.014-.007-.023-.018-.037-.024l-2.479-1.082-6.128 3.178a1.403 1.403 0 01-.321.126z" fill="%23000"/><path fill-rule="evenodd" clip-rule="evenodd" d="M12.819 13.873l-.095.028a2.104 2.104 0 01-.531.067h-.073l-.106-.008c-.1-.006-.22-.027-.342-.06l-.056-.01-.145-.05a2798 2798 0 01-6.674-2.909l-2.56 1.33a.423.423 0 00-.184.148.33.33 0 00-.044.257v.01c.011.041.03.079.055.116.006.008.006.017.013.026.002.004.005.005.007.007.018.021.042.039.066.057.025.02.049.041.078.056l.007.005c.128.06 9.629 4.206 9.76 4.26.01.003.02.002.03.006a.506.506 0 00.135.028l.033.001a.542.542 0 00.17-.026c.03-.01.062-.014.09-.03l9.319-4.832c.015-.008.023-.019.036-.027a.489.489 0 00.125-.114c.008-.012.02-.019.026-.03.007-.011.006-.023.012-.035a.362.362 0 00.029-.15.311.311 0 00-.013-.072.333.333 0 00-.03-.076c-.006-.012-.006-.023-.013-.034-.008-.011-.02-.018-.03-.028a.412.412 0 00-.134-.107c-.014-.007-.023-.017-.037-.024l-2.478-1.082-6.129 3.178a1.462 1.462 0 01-.317.124z" fill="%23000"/><path fill-rule="evenodd" clip-rule="evenodd" d="M12.029 2.045c-.018-.008-.036-.01-.053-.015A.45.45 0 0011.79 2a.44.44 0 00-.182.04c-.01.005-.023.005-.034.01-.123.064-8.853 4.87-9.338 5.141a.433.433 0 00-.184.157.37.37 0 00-.044.273v.01c.011.045.03.085.055.124.006.01.006.02.013.029l.007.006c.018.024.042.041.066.06.025.022.049.044.078.06a5453.121 5453.121 0 009.766 4.528c.011.004.021.004.031.007a.482.482 0 00.315.012l.023-.008c.03-.01.062-.016.09-.033l9.319-5.13c.015-.008.023-.02.036-.029a.464.464 0 00.125-.122c.008-.011.02-.019.026-.032.007-.011.006-.023.012-.035A.435.435 0 0022 6.907a.345.345 0 00-.013-.076.367.367 0 00-.03-.081c-.006-.012-.006-.024-.013-.035-.008-.013-.02-.02-.03-.03a.457.457 0 00-.134-.113c-.014-.008-.023-.02-.037-.026l-9.714-4.501z" fill="%23000"/></svg>'); $layers-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12.815 18.637l-.101.029c-.169.043-.34.064-.51.064H12.103l-.09-.007a1.74 1.74 0 01-.319-.054l-.061-.01-.163-.058c-.108-.043-3.833-1.667-6.675-2.907l-2.559 1.328a.422.422 0 00-.184.149.329.329 0 00-.044.256v.01c.011.042.03.08.055.117.006.008.006.018.013.026l.007.007c.018.021.042.038.066.056.025.021.049.041.078.057.002 0 .005.004.007.005.128.059 9.629 4.205 9.76 4.258.01.005.02.004.03.007a.506.506 0 00.315.012l.023-.008c.03-.01.062-.015.09-.03l9.319-4.833c.015-.006.023-.019.036-.026a.461.461 0 00.125-.115c.008-.011.02-.018.026-.03.007-.01.006-.023.012-.034a.368.368 0 00.029-.151.323.323 0 00-.013-.072.343.343 0 00-.03-.076c-.006-.011-.006-.023-.013-.034-.008-.01-.02-.018-.03-.028a.445.445 0 00-.134-.106c-.014-.007-.023-.018-.037-.024l-2.479-1.082-6.128 3.178a1.403 1.403 0 01-.321.126z" fill="%23000"/><path fill-rule="evenodd" clip-rule="evenodd" d="M12.819 13.873l-.095.028a2.104 2.104 0 01-.531.067h-.073l-.106-.008c-.1-.006-.22-.027-.342-.06l-.056-.01-.145-.05a2798 2798 0 01-6.674-2.909l-2.56 1.33a.423.423 0 00-.184.148.33.33 0 00-.044.257v.01c.011.041.03.079.055.116.006.008.006.017.013.026.002.004.005.005.007.007.018.021.042.039.066.057.025.02.049.041.078.056l.007.005c.128.06 9.629 4.206 9.76 4.26.01.003.02.002.03.006a.506.506 0 00.135.028l.033.001a.542.542 0 00.17-.026c.03-.01.062-.014.09-.03l9.319-4.832c.015-.008.023-.019.036-.027a.489.489 0 00.125-.114c.008-.012.02-.019.026-.03.007-.011.006-.023.012-.035a.362.362 0 00.029-.15.311.311 0 00-.013-.072.333.333 0 00-.03-.076c-.006-.012-.006-.023-.013-.034-.008-.011-.02-.018-.03-.028a.412.412 0 00-.134-.107c-.014-.007-.023-.017-.037-.024l-2.478-1.082-6.129 3.178a1.462 1.462 0 01-.317.124z" fill="%23000"/><path fill-rule="evenodd" clip-rule="evenodd" d="M12.029 2.045c-.018-.008-.036-.01-.053-.015A.45.45 0 0011.79 2a.44.44 0 00-.182.04c-.01.005-.023.005-.034.01-.123.064-8.853 4.87-9.338 5.141a.433.433 0 00-.184.157.37.37 0 00-.044.273v.01c.011.045.03.085.055.124.006.01.006.02.013.029l.007.006c.018.024.042.041.066.06.025.022.049.044.078.06a5453.121 5453.121 0 009.766 4.528c.011.004.021.004.031.007a.482.482 0 00.315.012l.023-.008c.03-.01.062-.016.09-.033l9.319-5.13c.015-.008.023-.02.036-.029a.464.464 0 00.125-.122c.008-.011.02-.019.026-.032.007-.011.006-.023.012-.035A.435.435 0 0022 6.907a.345.345 0 00-.013-.076.367.367 0 00-.03-.081c-.006-.012-.006-.024-.013-.035-.008-.013-.02-.02-.03-.03a.457.457 0 00-.134-.113c-.014-.008-.023-.02-.037-.026l-9.714-4.501z" fill="%23000"/></svg>');
@ -128,6 +129,7 @@ $nomad-logo-color-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 1
$notification-disabled-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M20 18.19L7.84 5.64 5.27 2.99 4 4.26l2.8 2.8v.01c-.52.99-.8 2.16-.8 3.42v5l-2 2v1h13.73l2 2L21 19.22l-1-1.03zm-8 3.31c1.11 0 2-.89 2-2h-4c0 1.11.89 2 2 2zm6-7.32V10.5c0-3.08-1.64-5.64-4.5-6.32V3.5c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68c-.15.03-.29.08-.42.12-.1.03-.2.07-.3.11h-.01c-.01 0-.01 0-.02.01-.23.09-.46.2-.68.31 0 0-.01 0-.01.01L18 14.18z" fill="%23000"/></svg>'); $notification-disabled-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M20 18.19L7.84 5.64 5.27 2.99 4 4.26l2.8 2.8v.01c-.52.99-.8 2.16-.8 3.42v5l-2 2v1h13.73l2 2L21 19.22l-1-1.03zm-8 3.31c1.11 0 2-.89 2-2h-4c0 1.11.89 2 2 2zm6-7.32V10.5c0-3.08-1.64-5.64-4.5-6.32V3.5c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68c-.15.03-.29.08-.42.12-.1.03-.2.07-.3.11h-.01c-.01 0-.01 0-.02.01-.23.09-.46.2-.68.31 0 0-.01 0-.01.01L18 14.18z" fill="%23000"/></svg>');
$notification-fill-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 21.5c1.1 0 2-.9 2-2h-4a2 2 0 0 0 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V3.5c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 4.86 6 7.42 6 10.5v5l-2 2v1h16v-1l-2-2z" fill="%23000"/></svg>'); $notification-fill-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 21.5c1.1 0 2-.9 2-2h-4a2 2 0 0 0 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V3.5c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 4.86 6 7.42 6 10.5v5l-2 2v1h16v-1l-2-2z" fill="%23000"/></svg>');
$notification-outline-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 21.5c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V3.5c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 4.86 6 7.42 6 10.5v5l-2 2v1h16v-1l-2-2zm-2 1H8v-6C8 8.02 9.51 6 12 6s4 2.02 4 4.5v6z" fill="%23000"/></svg>'); $notification-outline-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 21.5c1.1 0 2-.9 2-2h-4c0 1.1.9 2 2 2zm6-6v-5c0-3.07-1.63-5.64-4.5-6.32V3.5c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.64 4.86 6 7.42 6 10.5v5l-2 2v1h16v-1l-2-2zm-2 1H8v-6C8 8.02 9.51 6 12 6s4 2.02 4 4.5v6z" fill="%23000"/></svg>');
$oidc-logo-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewbox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12.833 8.375l-.291-2.625-.846.554c-.788-.496-1.78-.846-2.888-1.02 0 0-.554-.117-1.283-.117-.73 0-1.4.087-1.4.087-2.83.35-4.958 1.954-4.958 3.88 0 1.983 2.187 3.616 5.541 3.908v-1.138c-2.304-.32-3.762-1.4-3.762-2.77 0-1.284 1.341-2.363 3.179-2.713 0 0 1.43-.321 2.683.058.613.146 1.167.35 1.634.642l-1.109.67 3.5.584z" fill="%239E9E9E"/><path d="M6.708 2.833v10.209l1.75-.875V1.958l-1.75.875z" fill="%23000"/><path d="M6.708 2.833v10.209l1.75-.875V1.958l-1.75.875z" fill="%23FF9800"/></svg>');
$outline-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3 4c0-1.1.9-2 2-2h14c1.1 0 2 .9 2 2v16c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2V4zm2 0v16h14V4H5zm5.996 7.006v2h3v-2h-3zM15 15v2h-4v-2h4zm-4-6h6V7h-6v2zm-4 3.006a1 1 0 0 1 .998-.998 1 1 0 0 1 .998.998 1 1 0 0 1-.998.998A1 1 0 0 1 7 12.006zM7.998 15a1 1 0 0 0-.998.998 1 1 0 0 0 .998.998 1 1 0 0 0 .998-.998A1 1 0 0 0 7.998 15zM7 7.998A1 1 0 0 1 7.998 7a1 1 0 0 1 .998.998 1 1 0 0 1-.998.998A1 1 0 0 1 7 7.998z" fill="%23000"/></svg>'); $outline-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M3 4c0-1.1.9-2 2-2h14c1.1 0 2 .9 2 2v16c0 1.1-.9 2-2 2H5c-1.1 0-2-.9-2-2V4zm2 0v16h14V4H5zm5.996 7.006v2h3v-2h-3zM15 15v2h-4v-2h4zm-4-6h6V7h-6v2zm-4 3.006a1 1 0 0 1 .998-.998 1 1 0 0 1 .998.998 1 1 0 0 1-.998.998A1 1 0 0 1 7 12.006zM7.998 15a1 1 0 0 0-.998.998 1 1 0 0 0 .998.998 1 1 0 0 0 .998-.998A1 1 0 0 0 7.998 15zM7 7.998A1 1 0 0 1 7.998 7a1 1 0 0 1 .998.998 1 1 0 0 1-.998.998A1 1 0 0 1 7 7.998z" fill="%23000"/></svg>');
$page-outline-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19 2c1.05 0 1.918.82 1.994 1.851L21 4v16c0 1.05-.82 1.918-1.851 1.994L19 22H5c-1.05 0-1.918-.82-1.994-1.851L3 20V4c0-1.05.82-1.918 1.851-1.994L5 2h14zm0 2H5v16h14V4zM7.952 15.004a1 1 0 0 1 .998.998 1 1 0 0 1-.998.998 1 1 0 0 1-.998-.998 1 1 0 0 1 .998-.998zM15.944 15v2h-6v-2h6zm-2-4v2h-4v-2h4zm-5.992 0a1 1 0 0 1 .998.998 1 1 0 0 1-.998.998 1 1 0 0 1-.998-.998A1 1 0 0 1 7.952 11zm8.992-4v2h-7V7h7zm-8.992.004a1 1 0 0 1 .998.998A1 1 0 0 1 7.952 9a1 1 0 0 1-.998-.998 1 1 0 0 1 .998-.998z" fill="%23000"/></svg>'); $page-outline-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19 2c1.05 0 1.918.82 1.994 1.851L21 4v16c0 1.05-.82 1.918-1.851 1.994L19 22H5c-1.05 0-1.918-.82-1.994-1.851L3 20V4c0-1.05.82-1.918 1.851-1.994L5 2h14zm0 2H5v16h14V4zM7.952 15.004a1 1 0 0 1 .998.998 1 1 0 0 1-.998.998 1 1 0 0 1-.998-.998 1 1 0 0 1 .998-.998zM15.944 15v2h-6v-2h6zm-2-4v2h-4v-2h4zm-5.992 0a1 1 0 0 1 .998.998 1 1 0 0 1-.998.998 1 1 0 0 1-.998-.998A1 1 0 0 1 7.952 11zm8.992-4v2h-7V7h7zm-8.992.004a1 1 0 0 1 .998.998A1 1 0 0 1 7.952 9a1 1 0 0 1-.998-.998 1 1 0 0 1 .998-.998z" fill="%23000"/></svg>');
$partner-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M8.478 8.41l2.556-2.339H8.341L5 8.075H2v6.99h4.572l2.441 2.184c.913.779 1.473.779 2.028.54.61-.262.959-.699.959-.699l.613.517c.587.54 1.537.294 2.027-.192.745-.741.724-1.188.724-1.188l.517.43c.39.233.988-.025 1.187-.228.44-.446.54-.944.064-1.395l-4.124-3.669c-.545-.471-.562-.464-1.008-.094l-.491.445c-1.02.765-2.34.775-3.168-.128a2.25 2.25 0 0 1 .137-3.177zm7.813-2.045l-.707-.294H12.9a1 1 0 0 0-.675.263L9.153 9.145l-.005.006-.004.007a1.242 1.242 0 0 0-.066 1.749c.397.434 1.231.55 1.753.084l.007-.003.006-.003 2.497-2.287a.5.5 0 1 1 .675.737l-.816.747 4.797 4.216H22V8.07h-4.003L16.29 6.365z" fill="%23000"/></svg>'); $partner-svg: url('data:image/svg+xml;charset=UTF-8,<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M8.478 8.41l2.556-2.339H8.341L5 8.075H2v6.99h4.572l2.441 2.184c.913.779 1.473.779 2.028.54.61-.262.959-.699.959-.699l.613.517c.587.54 1.537.294 2.027-.192.745-.741.724-1.188.724-1.188l.517.43c.39.233.988-.025 1.187-.228.44-.446.54-.944.064-1.395l-4.124-3.669c-.545-.471-.562-.464-1.008-.094l-.491.445c-1.02.765-2.34.775-3.168-.128a2.25 2.25 0 0 1 .137-3.177zm7.813-2.045l-.707-.294H12.9a1 1 0 0 0-.675.263L9.153 9.145l-.005.006-.004.007a1.242 1.242 0 0 0-.066 1.749c.397.434 1.231.55 1.753.084l.007-.003.006-.003 2.497-2.287a.5.5 0 1 1 .675.737l-.816.747 4.797 4.216H22V8.07h-4.003L16.29 6.365z" fill="%23000"/></svg>');

View File

@ -778,6 +778,16 @@
mask-image: $info-circle-outline-svg; mask-image: $info-circle-outline-svg;
} }
%with-jwt-logo-icon {
@extend %with-icon;
background-image: $jwt-logo-svg;
}
%with-jwt-logo-mask {
@extend %with-mask;
-webkit-mask-image: $jwt-logo-svg;
mask-image: $jwt-logo-svg;
}
%with-key-icon { %with-key-icon {
@extend %with-icon; @extend %with-icon;
background-image: $key-svg; background-image: $key-svg;
@ -1316,6 +1326,16 @@
mask-image: $notification-outline-svg; mask-image: $notification-outline-svg;
} }
%with-oidc-logo-icon {
@extend %with-icon;
background-image: $oidc-logo-svg;
}
%with-oidc-logo-mask {
@extend %with-mask;
-webkit-mask-image: $oidc-logo-svg;
mask-image: $oidc-logo-svg;
}
%with-outline-icon { %with-outline-icon {
@extend %with-icon; @extend %with-icon;
background-image: $outline-svg; background-image: $outline-svg;

View File

@ -74,6 +74,7 @@
@import 'consul-ui/components/consul/kind'; @import 'consul-ui/components/consul/kind';
@import 'consul-ui/components/consul/intention'; @import 'consul-ui/components/consul/intention';
@import 'consul-ui/components/consul/lock-session/form'; @import 'consul-ui/components/consul/lock-session/form';
@import 'consul-ui/components/consul/auth-method';
@import 'consul-ui/components/role-selector'; @import 'consul-ui/components/role-selector';
@import 'consul-ui/components/topology-metrics'; @import 'consul-ui/components/topology-metrics';

View File

@ -33,3 +33,9 @@ span.policy-service-identity::before {
%pill.leader::before { %pill.leader::before {
@extend %with-star-outline-mask, %as-pseudo; @extend %with-star-outline-mask, %as-pseudo;
} }
%pill.jwt::before {
@extend %with-jwt-logo-icon, %as-pseudo;
}
%pill.oidc::before {
@extend %with-oidc-logo-icon, %as-pseudo;
}

View File

@ -49,6 +49,12 @@
%popover-select .kubernetes button::before { %popover-select .kubernetes button::before {
@extend %with-logo-kubernetes-color-icon, %as-pseudo; @extend %with-logo-kubernetes-color-icon, %as-pseudo;
} }
%popover-select .jwt button::before {
@extend %with-jwt-logo-icon, %as-pseudo;
}
%popover-select .oidc button::before {
@extend %with-oidc-logo-icon, %as-pseudo;
}
%popover-select .consul button::before { %popover-select .consul button::before {
@extend %with-logo-consul-color-icon, %as-pseudo; @extend %with-logo-consul-color-icon, %as-pseudo;
} }

View File

@ -0,0 +1,102 @@
{{#if isAuthorized }}
{{page-title 'Auth Methods'}}
{{else}}
{{page-title 'Access Controls'}}
{{/if}}
{{#let
(hash
value=(or sortBy "MethodName:asc")
change=(action (mut sortBy) value="target.selected")
)
(hash
kind=(hash
value=(if kind (split kind ',') undefined)
change=(action (mut kind) value="target.selectedItems")
)
source=(hash
value=(if source (split source ',') undefined)
change=(action (mut source) value="target.selectedItems")
)
searchproperty=(hash
value=(if (not-eq searchproperty undefined)
(split searchproperty ',')
searchProperties
)
change=(action (mut searchproperty) value="target.selectedItems")
default=searchProperties
)
)
items
as |sort filters items|}}
<AppView
@authorized={{isAuthorized}}
@enabled={{isEnabled}}
>
<BlockSlot @name="header">
<h1>
Access Controls
</h1>
</BlockSlot>
<BlockSlot @name="toolbar">
{{#if (gt items.length 0)}}
<Consul::AuthMethod::SearchBar
@search={{search}}
@onsearch={{action (mut search) value="target.value"}}
@sort={{sort}}
@filter={{filters}}
/>
{{/if}}
</BlockSlot>
<BlockSlot @name="content">
<DataCollection
@type="auth-method"
@sort={{sort.value}}
@filters={{filters}}
@search={{search}}
@items={{items}}
as |collection|>
<collection.Collection>
<Consul::AuthMethod::List @items={{collection.items}} />
</collection.Collection>
<collection.Empty>
<EmptyState @allowLogin={{true}}>
<BlockSlot @name="header">
<h2>
{{#if (gt items.length 0)}}
No auth methods found
{{else}}
Welcome to Auth Methods
{{/if}}
</h2>
</BlockSlot>
<BlockSlot @name="body">
<p>
{{#if (gt items.length 0)}}
No auth methods where found matching that search, or you may not have access to view the auth methods you are searching for.
{{else}}
There don't seem to be any auth methods, or you may not have access to view auth methods yet.
{{/if}}
</p>
</BlockSlot>
<BlockSlot @name="actions">
<li class="docs-link">
<a href="{{env 'CONSUL_DOCS_URL'}}/security/acl/auth-methods" rel="noopener noreferrer" target="_blank">Documentation on auth methods</a>
</li>
<li class="learn-link">
<a href="{{env 'CONSUL_DOCS_API_URL'}}/acl/auth-methods.html" rel="noopener noreferrer" target="_blank">Read the API Docs</a>
</li>
</BlockSlot>
</EmptyState>
</collection.Empty>
</DataCollection>
</BlockSlot>
</AppView>
{{/let}}

View File

@ -97,7 +97,7 @@ as |sort filters items|}}
<a href="{{env 'CONSUL_DOCS_URL'}}/commands/acl/role" rel="noopener noreferrer" target="_blank">Documentation on roles</a> <a href="{{env 'CONSUL_DOCS_URL'}}/commands/acl/role" rel="noopener noreferrer" target="_blank">Documentation on roles</a>
</li> </li>
<li class="learn-link"> <li class="learn-link">
<a href="{{env 'CONSUL_DOCS_API_URL'}}/acl/roles.html" rel="noopener noreferrer" target="_blank">Read the guide</a> <a href="{{env 'CONSUL_DOCS_API_URL'}}/acl/roles.html" rel="noopener noreferrer" target="_blank">Read the API Docs</a>
</li> </li>
</BlockSlot> </BlockSlot>
</EmptyState> </EmptyState>

View File

@ -0,0 +1,44 @@
${
[1].map(() => {
const type = `${fake.helpers.randomize(['kubernetes', 'jwt', 'oidc'])}`;
const fakeIP = `${fake.internet.ip()}`;
let config = {};
switch(type) {
case 'kubernetes':
config = {
Host: `https://${fake.internet.ip()}:8443`,
CACert: `-----BEGIN CERTIFICATE-----${fake.internet.password(1357)}-----END CERTIFICATE-----`,
ServiceAccountJWT: `eyJhbGciOiJ${fake.internet.password(25)}.eyJ${fake.internet.password(61)}.${fake.internet.password(32)}`
};
break;
case 'oidc':
config = {
OIDCDiscoveryURL: `https://${fake.internet.ip()}:8443`,
};
break;
case 'jwt':
config = {
JWTValidationPubKeys: `-----BEGIN CERTIFICATE-----${fake.internet.password(1357)}-----END CERTIFICATE-----`,
JWKSURL: `https://${fake.internet.ip()}:8443`,
OIDCDiscoveryURL: `https://${fake.internet.ip()}:8443`,
};
break;
}
return `{
"Name": "${location.pathname.get(3)}",
"Namespace": "${
typeof location.search.ns !== 'undefined' ? location.search.ns :
typeof http.body.Namespace !== 'undefined' ? http.body.Namespace : 'default'
}",
"Type": "${type}",
"Description": "${fake.lorem.sentence()}",
"DisplayName": "${fake.hacker.noun()}",
"MaxTokenTTL": "${fake.random.number({min: 0, max: 60})}m${fake.random.number({min: 0, max: 60})}s",
"TokenLocality": "${fake.helpers.randomize(['local', 'global', ''])}",
"Config": ${JSON.stringify(config)},
"CreateIndex": ${fake.random.number()},
"ModifyIndex": 10
}`
})
}

View File

@ -0,0 +1,42 @@
[
${
range(
env(
'CONSUL_AUTH_METHOD_COUNT',
Math.floor(
(
Math.random() * env('CONSUL_AUTH_METHOD_MAX', 10)
) + parseInt(env('CONSUL_AUTH_METHOD_MIN', 1))
)
)
).map(
function(item, i) {
return `
{
"Name": "${fake.hacker.noun()}-${i}",
${typeof location.search.ns !== 'undefined' ? `
"Namespace": "${location.search.ns}",
` : ``}
"Type": "${fake.helpers.randomize(['kubernetes', 'jwt', 'oidc'])}",
"Description": "${fake.lorem.sentence()}",
${i%2 ? `
"DisplayName": "${fake.hacker.noun()}-${i}",
` : `
"DisplayName": "",
`}
${i%2 ? `
"MaxTokenTTL": "${fake.random.number({min: 0, max: 60})}m${fake.random.number({min: 0, max: 60})}s",
` : `
`}
${i%2 ? `
"TokenLocality": "${fake.helpers.randomize(['local', 'global', ''])}",
` : `
`}
"CreateIndex": ${fake.random.number()},
"ModifyIndex": 10
}
`
}
)
}
]

View File

@ -0,0 +1,48 @@
@setupApplicationTest
Feature: dc / acls / auth-methods / index: ACL Auth Methods List
Scenario:
Given 1 datacenter model with the value "dc-1"
And 3 authMethod models
When I visit the authMethods page for yaml
---
dc: dc-1
---
Then the url should be /dc-1/acls/auth-methods
Then I see 3 authMethod models
And the title should be "Auth Methods - Consul"
Scenario: Searching the Auth Methods
Given 1 datacenter model with the value "dc-1"
And 3 authMethod models from yaml
---
- Name: kube
DisplayName: minikube
- Name: agent
DisplayName: ''
- Name: node
DisplayName: mininode
---
When I visit the authMethods page for yaml
---
dc: dc-1
---
Then the url should be /dc-1/acls/auth-methods
Then I see 3 authMethod models
Then I fill in with yaml
---
s: kube
---
And I see 1 authMethod model
And I see 1 authMethod model with the name "minikube"
Then I fill in with yaml
---
s: agent
---
And I see 1 authMethod model
And I see 1 authMethod model with the name "agent"
Then I fill in with yaml
---
s: ode
---
And I see 1 authMethod model
And I see 1 authMethod model with the name "mininode"

View File

@ -0,0 +1,39 @@
@setupApplicationTest
Feature: dc / acls / auth-methods / sorting
Scenario: Sorting Auth Methods
Given 1 datacenter model with the value "dc-1"
And 4 authMethod models from yaml
---
- Name: "system-A"
DisplayName: ''
- Name: "system-D"
DisplayName: ''
- Name: "system-C"
DisplayName: ''
- Name: "system-B"
DisplayName: ''
---
When I visit the authMethods page for yaml
---
dc: dc-1
---
Then the url should be /dc-1/acls/auth-methods
Then I see 4 authMethod models
When I click selected on the sort
When I click options.1.button on the sort
Then I see name on the authMethods vertically like yaml
---
- "system-D"
- "system-C"
- "system-B"
- "system-A"
---
When I click selected on the sort
When I click options.0.button on the sort
Then I see name on the authMethods vertically like yaml
---
- "system-A"
- "system-B"
- "system-C"
- "system-D"
---

View File

@ -0,0 +1,10 @@
import steps from '../../../steps';
// step definitions that are shared between features should be moved to the
// tests/acceptance/steps/steps.js file
export default function(assert) {
return steps(assert).then('I should find a file', function() {
assert.ok(true, this.step);
});
}

View File

@ -0,0 +1,10 @@
import steps from '../../../steps';
// step definitions that are shared between features should be moved to the
// tests/acceptance/steps/steps.js file
export default function(assert) {
return steps(assert).then('I should find a file', function() {
assert.ok(true, this.step);
});
}

View File

@ -42,6 +42,10 @@ export default function(type, value) {
key = 'CONSUL_TOKEN_COUNT'; key = 'CONSUL_TOKEN_COUNT';
obj['CONSUL_ACLS_ENABLE'] = 1; obj['CONSUL_ACLS_ENABLE'] = 1;
break; break;
case 'authMethod':
key = 'CONSUL_AUTH_METHOD_COUNT';
obj['CONSUL_ACLS_ENABLE'] = 1;
break;
case 'nspace': case 'nspace':
key = 'CONSUL_NSPACE_COUNT'; key = 'CONSUL_NSPACE_COUNT';
break; break;

View File

@ -35,6 +35,9 @@ export default function(type) {
case 'token': case 'token':
requests = ['/v1/acl/tokens', '/v1/acl/token/']; requests = ['/v1/acl/tokens', '/v1/acl/token/'];
break; break;
case 'authMethod':
requests = ['/v1/acl/auth-methods', '/v1/acl/auth-method/'];
break;
case 'nspace': case 'nspace':
requests = ['/v1/namespaces', '/v1/namespace/']; requests = ['/v1/namespaces', '/v1/namespace/'];
break; break;

View File

@ -0,0 +1,50 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import getNspaceRunner from 'consul-ui/tests/helpers/get-nspace-runner';
const nspaceRunner = getNspaceRunner('auth-method');
module('Integration | Adapter | auth-method', function(hooks) {
setupTest(hooks);
const dc = 'dc-1';
const id = 'slug';
test('requestForQueryRecord returns the correct url/method', function(assert) {
const adapter = this.owner.lookup('adapter:auth-method');
const client = this.owner.lookup('service:client/http');
const expected = `GET /v1/acl/auth-method/${id}?dc=${dc}`;
const actual = adapter.requestForQueryRecord(client.requestParams.bind(client), {
dc: dc,
id: id,
});
assert.equal(`${actual.method} ${actual.url}`, expected);
});
test("requestForQueryRecord throws if you don't specify an id", function(assert) {
const adapter = this.owner.lookup('adapter:auth-method');
const client = this.owner.lookup('service:client/http');
assert.throws(function() {
adapter.requestForQueryRecord(client.url, {
dc: dc,
});
});
});
test('requestForQueryRecord returns the correct body', function(assert) {
return nspaceRunner(
(adapter, serializer, client) => {
return adapter.requestForQueryRecord(client.body, {
id: id,
dc: dc,
ns: 'team-1',
index: 1,
});
},
{
index: 1,
ns: 'team-1',
},
{
index: 1,
},
this,
assert
);
});
});

View File

@ -0,0 +1,75 @@
import { module, test, skip } from 'qunit';
import { setupTest } from 'ember-qunit';
import { get } from 'consul-ui/tests/helpers/api';
import {
HEADERS_SYMBOL as META,
HEADERS_DATACENTER as DC,
HEADERS_NAMESPACE as NSPACE,
} from 'consul-ui/utils/http/consul';
module('Integration | Serializer | auth-method', function(hooks) {
setupTest(hooks);
const dc = 'dc-1';
const id = 'auth-method-name';
const undefinedNspace = 'default';
[undefinedNspace, 'team-1', undefined].forEach(nspace => {
test(`respondForQuery returns the correct data for list endpoint when nspace is ${nspace}`, function(assert) {
const serializer = this.owner.lookup('serializer:auth-method');
const request = {
url: `/v1/acl/auth-methods?dc=${dc}${typeof nspace !== 'undefined' ? `&ns=${nspace}` : ``}`,
};
return get(request.url).then(function(payload) {
const expected = payload.map(item =>
Object.assign({}, item, {
Datacenter: dc,
Namespace: item.Namespace || undefinedNspace,
uid: `["${item.Namespace || undefinedNspace}","${dc}","${item.Name}"]`,
})
);
const actual = serializer.respondForQuery(
function(cb) {
const headers = {};
const body = payload;
return cb(headers, body);
},
{
dc: dc,
ns: nspace,
}
);
assert.deepEqual(actual, expected);
});
});
skip(`respondForQueryRecord returns the correct data for item endpoint when nspace is ${nspace}`, function(assert) {
const serializer = this.owner.lookup('serializer:auth-method');
const request = {
url: `/v1/acl/auth-method/${id}?dc=${dc}${
typeof nspace !== 'undefined' ? `&ns=${nspace}` : ``
}`,
};
return get(request.url).then(function(payload) {
const expected = Object.assign({}, payload, {
Datacenter: dc,
[META]: {
[DC.toLowerCase()]: dc,
[NSPACE.toLowerCase()]: payload.Namespace || undefinedNspace,
},
Namespace: payload.Namespace || undefinedNspace,
uid: `["${payload.Namespace || undefinedNspace}","${dc}","${id}"]`,
});
const actual = serializer.respondForQueryRecord(
function(cb) {
const headers = {};
const body = payload;
return cb(headers, body);
},
{
dc: dc,
ns: nspace,
id: id,
}
);
assert.deepEqual(actual, expected);
});
});
});
});

View File

@ -0,0 +1,82 @@
import { moduleFor, test } from 'ember-qunit';
import repo from 'consul-ui/tests/helpers/repo';
import { skip } from 'qunit';
const NAME = 'auth-method';
moduleFor(`service:repository/${NAME}`, `Integration | Service | ${NAME}`, {
// Specify the other units that are required for this test.
integration: true,
});
const dc = 'dc-1';
const id = 'auth-method-name';
const undefinedNspace = 'default';
[undefinedNspace, 'team-1', undefined].forEach(nspace => {
test(`findAllByDatacenter returns the correct data for list endpoint when nspace is ${nspace}`, function(assert) {
return repo(
'auth-method',
'findAllByDatacenter',
this.subject(),
function retrieveStub(stub) {
return stub(
`/v1/acl/auth-methods?dc=${dc}${typeof nspace !== 'undefined' ? `&ns=${nspace}` : ``}`,
{
CONSUL_AUTH_METHOD_COUNT: '3',
}
);
},
function performTest(service) {
return service.findAllByDatacenter(dc, nspace || undefinedNspace);
},
function performAssertion(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
return payload.map(function(item) {
return Object.assign({}, item, {
Datacenter: dc,
Namespace: item.Namespace || undefinedNspace,
uid: `["${item.Namespace || undefinedNspace}","${dc}","${item.Name}"]`,
});
});
})
);
}
);
});
skip(`findBySlug returns the correct data for item endpoint when the nspace is ${nspace}`, function(assert) {
return repo(
'AuthMethod',
'findBySlug',
this.subject(),
function retrieveStub(stub) {
return stub(
`/v1/acl/auth-method/${id}?dc=${dc}${
typeof nspace !== 'undefined' ? `&ns=${nspace}` : ``
}`
);
},
function performTest(service) {
return service.findBySlug(id, dc, nspace || undefinedNspace);
},
function performAssertion(actual, expected) {
assert.deepEqual(
actual,
expected(function(payload) {
const item = payload;
return Object.assign({}, item, {
Datacenter: dc,
Namespace: item.Namespace || undefinedNspace,
uid: `["${item.Namespace || undefinedNspace}","${dc}","${item.Name}"]`,
meta: {
cacheControl: undefined,
cursor: undefined,
dc: dc,
nspace: item.Namespace || undefinedNspace,
},
});
})
);
}
);
});
});

View File

@ -42,6 +42,7 @@ import consulUpstreamInstanceListFactory from 'consul-ui/components/consul/upstr
import consulTokenListFactory from 'consul-ui/components/consul/token/list/pageobject'; import consulTokenListFactory from 'consul-ui/components/consul/token/list/pageobject';
import consulRoleListFactory from 'consul-ui/components/consul/role/list/pageobject'; import consulRoleListFactory from 'consul-ui/components/consul/role/list/pageobject';
import consulPolicyListFactory from 'consul-ui/components/consul/policy/list/pageobject'; import consulPolicyListFactory from 'consul-ui/components/consul/policy/list/pageobject';
import consulAuthMethodListFactory from 'consul-ui/components/consul/auth-method/list/pageobject';
import consulIntentionListFactory from 'consul-ui/components/consul/intention/list/pageobject'; import consulIntentionListFactory from 'consul-ui/components/consul/intention/list/pageobject';
import consulNspaceListFactory from 'consul-ui/components/consul/nspace/list/pageobject'; import consulNspaceListFactory from 'consul-ui/components/consul/nspace/list/pageobject';
import consulKvListFactory from 'consul-ui/components/consul/kv/list/pageobject'; import consulKvListFactory from 'consul-ui/components/consul/kv/list/pageobject';
@ -65,6 +66,7 @@ import roles from 'consul-ui/tests/pages/dc/acls/roles/index';
import role from 'consul-ui/tests/pages/dc/acls/roles/edit'; import role from 'consul-ui/tests/pages/dc/acls/roles/edit';
import tokens from 'consul-ui/tests/pages/dc/acls/tokens/index'; import tokens from 'consul-ui/tests/pages/dc/acls/tokens/index';
import token from 'consul-ui/tests/pages/dc/acls/tokens/edit'; import token from 'consul-ui/tests/pages/dc/acls/tokens/edit';
import authMethods from 'consul-ui/tests/pages/dc/acls/auth-methods/index';
import intentions from 'consul-ui/tests/pages/dc/intentions/index'; import intentions from 'consul-ui/tests/pages/dc/intentions/index';
import intention from 'consul-ui/tests/pages/dc/intentions/edit'; import intention from 'consul-ui/tests/pages/dc/intentions/edit';
import nspaces from 'consul-ui/tests/pages/dc/nspaces/index'; import nspaces from 'consul-ui/tests/pages/dc/nspaces/index';
@ -90,6 +92,7 @@ const emptyState = emptyStateFactory(isPresent);
const consulHealthCheckList = consulHealthCheckListFactory(collection, text); const consulHealthCheckList = consulHealthCheckListFactory(collection, text);
const consulUpstreamInstanceList = consulUpstreamInstanceListFactory(collection, text); const consulUpstreamInstanceList = consulUpstreamInstanceListFactory(collection, text);
const consulAuthMethodList = consulAuthMethodListFactory(collection, text);
const consulIntentionList = consulIntentionListFactory( const consulIntentionList = consulIntentionListFactory(
collection, collection,
clickable, clickable,
@ -191,6 +194,7 @@ export default {
token: create( token: create(
token(visitable, submitable, deletable, cancelable, clickable, policySelector, roleSelector) token(visitable, submitable, deletable, cancelable, clickable, policySelector, roleSelector)
), ),
authMethods: create(authMethods(visitable, creatable, consulAuthMethodList, popoverSelect)),
intentions: create( intentions: create(
intentions(visitable, creatable, clickable, consulIntentionList, popoverSelect) intentions(visitable, creatable, clickable, consulIntentionList, popoverSelect)
), ),

View File

@ -0,0 +1,7 @@
export default function(visitable, creatable, authMethods, popoverSelect) {
return creatable({
visit: visitable('/:dc/acls/auth-methods'),
authMethods: authMethods(),
sort: popoverSelect('[data-test-sort-control]'),
});
}

View File

@ -0,0 +1,12 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Adapter | auth-method', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let adapter = this.owner.lookup('adapter:auth-method');
assert.ok(adapter);
});
});

View File

@ -0,0 +1,14 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
import { run } from '@ember/runloop';
module('Unit | Model | auth-method', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let store = this.owner.lookup('service:store');
let model = run(() => store.createRecord('auth-method', {}));
assert.ok(model);
});
});

View File

@ -0,0 +1,23 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Serializer | auth-method', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let store = this.owner.lookup('service:store');
let serializer = store.serializerFor('auth-method');
assert.ok(serializer);
});
test('it serializes records', function(assert) {
let store = this.owner.lookup('service:store');
let record = store.createRecord('auth-method', {});
let serializedRecord = record.serialize();
assert.ok(serializedRecord);
});
});

View File

@ -0,0 +1,12 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Service | auth-method', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let service = this.owner.lookup('service:repository/auth-method');
assert.ok(service);
});
});

View File

@ -6,11 +6,14 @@ common:
vault: Vault vault: Vault
aws: AWS aws: AWS
kubernetes: Kubernetes kubernetes: Kubernetes
jwt: JWT
oidc: OIDC
ui: ui:
remove: Remove {item} remove: Remove {item}
filtered-by: Filtered by {item} filtered-by: Filtered by {item}
name: Name name: Name
creation: Creation creation: Creation
maxttl: Max TTL
consul: consul:
name: Name name: Name
passing: Passing passing: Passing
@ -35,6 +38,7 @@ common:
localbindport: Local Bind Port localbindport: Local Bind Port
destinationname: Destination Name destinationname: Destination Name
sourcename: Source Name sourcename: Source Name
displayname: Display Name
search: search:
search: Search search: Search
searchproperty: Search Across searchproperty: Search Across
@ -52,6 +56,9 @@ common:
age: age:
asc: Oldest to Newest asc: Oldest to Newest
desc: Newest to Oldest desc: Newest to Oldest
duration:
asc: Longest to shortest
desc: Shortest to longest
status: status:
asc: Unhealthy to Healthy asc: Unhealthy to Healthy
desc: Healthy to Unhealthy desc: Healthy to Unhealthy
@ -127,6 +134,15 @@ components:
options: options:
global-management: Global Management global-management: Global Management
standard: Standard standard: Standard
auth-method:
search-bar:
kind:
name: Type
locality:
name: Source
options:
local: Creates local tokens
global: Creates global tokens
kv: kv:
search-bar: search-bar:
kind: kind: