Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ui: dashboard links #5704

Merged
merged 11 commits into from
May 1, 2019
73 changes: 73 additions & 0 deletions ui-v2/app/components/templated-anchor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import Component from '@ember/component';
import { get, set, computed } from '@ember/object';

const createWeak = function(wm = new WeakMap()) {
return {
get: function(ref, prop) {
let map = wm.get(ref);
if (map) {
return map[prop];
}
},
set: function(ref, prop, value) {
let map = wm.get(ref);
if (typeof map === 'undefined') {
map = {};
wm.set(ref, map);
}
map[prop] = value;
return map[prop];
},
};
};
const weak = createWeak();
// Covers alpha-capitalized dot separated API keys such as
// `{{Name}}`, `{{Service.Name}}` etc. but not `{{}}`
const templateRe = /{{([A-Za-z.0-9_-]+)}}/g;
export default Component.extend({
tagName: 'a',
attributeBindings: ['href', 'rel', 'target'],
rel: computed({
get: function(prop) {
return weak.get(this, prop);
},
set: function(prop, value) {
switch (value) {
case 'external':
value = `${value} noopener noreferrer`;
set(this, 'target', '_blank');
break;
}
return weak.set(this, prop, value);
},
}),
vars: computed({
get: function(prop) {
return weak.get(this, prop);
},
set: function(prop, value) {
weak.set(this, prop, value);
set(this, 'href', weak.get(this, 'template'));
},
}),
href: computed({
get: function(prop) {
return weak.get(this, prop);
},
set: function(prop, value) {
weak.set(this, 'template', value);
const vars = weak.get(this, 'vars');
if (typeof vars !== 'undefined' && typeof value !== 'undefined') {
value = value.replace(templateRe, function(match, group) {
try {
return get(vars, group) || '';
} catch (e) {
return '';
}
});
return weak.set(this, prop, value);
}
return '';
},
}),
});
14 changes: 14 additions & 0 deletions ui-v2/app/controllers/settings.js
Original file line number Diff line number Diff line change
@@ -6,6 +6,13 @@ export default Controller.extend({
repo: service('settings'),
dom: service('dom'),
actions: {
key: function(e) {
switch (true) {
case e.keyCode === 13:
// disable ENTER
e.preventDefault();
}
},
change: function(e, value, item) {
const event = get(this, 'dom').normalizeEvent(e, value);
// TODO: Switch to using forms like the rest of the app
@@ -23,6 +30,13 @@ export default Controller.extend({
set(this, 'item.client.blocking', !blocking);
this.send('update', get(this, 'item'));
break;
case 'urls[service]':
if (typeof get(this, 'item.urls') === 'undefined') {
set(this, 'item.urls', {});
}
set(this, 'item.urls.service', target.value);
this.send('update', get(this, 'item'));
break;
}
},
},
3 changes: 3 additions & 0 deletions ui-v2/app/routes/dc/services/show.js
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import { get } from '@ember/object';

export default Route.extend({
repo: service('repository/service'),
settings: service('settings'),
queryParams: {
s: {
as: 'filter',
@@ -13,8 +14,10 @@ export default Route.extend({
},
model: function(params) {
const repo = get(this, 'repo');
const settings = get(this, 'settings');
return hash({
item: repo.findBySlug(params.name, this.modelFor('dc').dc.Name),
urls: settings.findBySlug('urls'),
});
},
setupController: function(controller, model) {
3 changes: 3 additions & 0 deletions ui-v2/app/styles/components/anchors.scss
Original file line number Diff line number Diff line change
@@ -2,6 +2,9 @@
%main-content a {
color: $gray-900;
}
a[rel*='external'] {
@extend %with-exit;
}
%main-content label a[rel*='help'] {
color: $gray-400;
}
2 changes: 1 addition & 1 deletion ui-v2/app/styles/components/app-view.scss
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@ main {
}
@media #{$--lt-spacious-page-header} {
%app-view header .actions {
margin-top: 5px;
margin-top: 9px;
}
}
// TODO: This should be its own component
13 changes: 7 additions & 6 deletions ui-v2/app/styles/components/app-view/layout.scss
Original file line number Diff line number Diff line change
@@ -9,6 +9,7 @@
float: right;
display: flex;
align-items: flex-start;
margin-top: 9px;
}
%app-view header dl {
float: left;
@@ -28,12 +29,7 @@
}
%app-view h2 {
padding-bottom: 0.2em;
margin-bottom: 1.1em;
}
%app-view fieldset h2,
%app-view fieldset p {
padding-bottom: 0;
margin-bottom: 0;
margin-bottom: 0.2em;
}
%app-view header .actions > *:not(:last-child) {
margin-right: 12px;
@@ -64,3 +60,8 @@
min-height: 1em;
margin-bottom: 0.4em;
}
// TODO: Think about an %app-form or similar
%app-content fieldset {
padding-bottom: 0.3em;
margin-bottom: 2em;
}
6 changes: 4 additions & 2 deletions ui-v2/app/styles/components/app-view/skin.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
%app-view h2 {
%app-view h2,
%app-view fieldset {
border-bottom: $decor-border-200;
}
%app-view fieldset h2 {
@@ -16,7 +17,8 @@
}
%app-view header > div > div:last-child,
%app-view header h1,
%app-view h2 {
%app-view h2,
%app-view fieldset {
border-color: $gray-200;
}
// We know that any sibling navs might have a top border
9 changes: 9 additions & 0 deletions ui-v2/app/styles/components/icons/index.scss
Original file line number Diff line number Diff line change
@@ -149,6 +149,15 @@
height: 0.05em;
transform: rotate(45deg);
}
%with-exit::after {
@extend %pseudo-icon-bg-img;
top: 3px;
right: -8px;
background-image: $exit-svg;
background-color: $color-transparent;
width: 16px;
height: 16px;
}
/*TODO: All chevrons need merging */
%with-chevron-down::before {
@extend %pseudo-icon-bg-img;
1 change: 1 addition & 0 deletions ui-v2/app/styles/components/notice/layout.scss
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
position: relative;
padding: 1em;
padding-left: 45px;
margin-bottom: 1em;
}
%notice::before {
position: absolute;
19 changes: 14 additions & 5 deletions ui-v2/app/styles/core/typography.scss
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
main p,
%modal-window p {
%button {
font-family: $typo-family-sans;
}
main p:not(:last-child),
%modal-window p:not(:last-child) {
margin-bottom: 1em;
}
%button,
@@ -8,6 +11,7 @@ main p,
%form-element [type='password'] {
line-height: 1.5;
}
h3,
%radio-group label {
line-height: 1.25;
}
@@ -32,6 +36,7 @@ h1,
font-weight: $typo-weight-bold;
}
h2,
h3,
fieldset > header,
caption,
%header-nav,
@@ -77,18 +82,22 @@ td strong,
%footer > * {
font-size: inherit;
}

h1 {
font-size: $typo-header-100;
}
h2,
h2 {
font-size: $typo-header-200;
}
h3 {
font-size: $typo-header-300;
}
%healthcheck-info dt,
%header-drop-nav .is-active,
%app-view h1 em {
font-size: $typo-size-500;
}
body,
fieldset h2,
fieldset > header,
pre code,
input,
textarea,
2 changes: 1 addition & 1 deletion ui-v2/app/styles/routes/dc/acls/index.scss
Original file line number Diff line number Diff line change
@@ -10,7 +10,7 @@ td a.is-management::after {
.template-policy.template-list main header .actions,
.template-token.template-list main header .actions {
position: relative;
top: 50px;
top: 45px;
}
}

1 change: 1 addition & 0 deletions ui-v2/app/templates/components/templated-anchor.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{yield}}
34 changes: 17 additions & 17 deletions ui-v2/app/templates/dc/nodes/show.hbs
Original file line number Diff line number Diff line change
@@ -26,23 +26,23 @@
}}
{{/block-slot}}
{{#block-slot 'actions'}}
{{#feedback-dialog type='inline'}}
{{#block-slot 'action' as |success error|}}
{{#copy-button success=(action success) error=(action error) clipboardText=item.Address title='copy IP address to clipboard'}}
{{item.Address}}
{{/copy-button}}
{{/block-slot}}
{{#block-slot 'success' as |transition|}}
<p class={{transition}}>
Copied IP Address!
</p>
{{/block-slot}}
{{#block-slot 'error' as |transition|}}
<p class={{transition}}>
Sorry, something went wrong!
</p>
{{/block-slot}}
{{/feedback-dialog}}
{{#feedback-dialog type='inline'}}
{{#block-slot 'action' as |success error|}}
{{#copy-button success=(action success) error=(action error) clipboardText=item.Address title='copy IP address to clipboard'}}
{{item.Address}}
{{/copy-button}}
{{/block-slot}}
{{#block-slot 'success' as |transition|}}
<p class={{transition}}>
Copied IP Address!
</p>
{{/block-slot}}
{{#block-slot 'error' as |transition|}}
<p class={{transition}}>
Sorry, something went wrong!
</p>
{{/block-slot}}
{{/feedback-dialog}}
{{/block-slot}}
{{#block-slot 'content'}}
{{#each
5 changes: 5 additions & 0 deletions ui-v2/app/templates/dc/services/show.hbs
Original file line number Diff line number Diff line change
@@ -32,6 +32,11 @@
selected=selectedTab
}}
{{/block-slot}}
{{#block-slot 'actions'}}
{{#if urls.service}}
{{#templated-anchor href=urls.service vars=(hash Service=(hash Name=item.Service.Service)) rel="external"}}Open Dashboard{{/templated-anchor}}
{{/if}}
{{/block-slot}}
{{#block-slot 'content'}}
{{#each
(compact
62 changes: 38 additions & 24 deletions ui-v2/app/templates/settings.hbs
Original file line number Diff line number Diff line change
@@ -1,26 +1,40 @@
{{#hashicorp-consul id="wrapper" dcs=dcs dc=dc}}
{{#app-view class="settings show"}}
{{#block-slot 'header'}}
<h1>
Settings
</h1>
{{/block-slot}}
{{#block-slot 'content'}}
<p>
These settings are specific to the Consul web UI. They are saved to local storage and persist through browser usage and visits.
</p>
<form>
<fieldset>
<h2>Blocking Queries</h2>
<p>Automatically get updated catalog information without refreshing the page. Any changes made to services and nodes would be reflected in real time.</p>
<div class="type-toggle">
<label>
<input type="checkbox" name="client[blocking]" checked={{if item.client.blocking 'checked' }} onchange={{action 'change'}} />
<span>{{if item.client.blocking 'On' 'Off' }}</span>
</label>
</div>
</fieldset>
</form>
{{/block-slot}}
{{/app-view}}
{{#app-view class="settings show"}}
{{#block-slot 'header'}}
<h1>
Settings
</h1>
{{/block-slot}}
{{#block-slot 'content'}}
<div class="notice info">
<h3>Local Storage</h3>
<p>
These settings are immediately saved to local storage and persisted through browser usage.
</p>
</div>
<form>
<fieldset>
<h2>Dashboard Links</h2>
<p>
Add a link to the service detail page in the UI to get quick access to a service-wide metrics dashboard. Enter the dashboard URL into the field below. You can use the placeholder {{Service.Name}} which will be replaced with the name of the service currently being viewed.
</p>
<label class="type-text">
<span>Link template for services</span>
<input type="text" name="urls[service]" value={{item.urls.service}} onchange={{action 'change'}} onkeypress={{action 'key'}} onkeydown={{action 'key'}} />
<em>e.g. https://grafana.example.com/d/1/consul-service-mesh&amp;orgid=1&amp;service-name={{ '{{Service.Name}}' }}</em>
</label>
</fieldset>
<fieldset>
<h2>Blocking Queries</h2>
<p>Keep catalog info up-to-date without refreshing the page. Any changes made to services and nodes would be reflected in real time.</p>
<div class="type-toggle">
<label>
<input type="checkbox" name="client[blocking]" checked={{if item.client.blocking 'checked' }} onchange={{action 'change'}} />
<span>{{if item.client.blocking 'On' 'Off' }}</span>
</label>
</div>
</fieldset>
</form>
{{/block-slot}}
{{/app-view}}
{{/hashicorp-consul}}
98 changes: 98 additions & 0 deletions ui-v2/tests/integration/components/templated-anchor-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { moduleForComponent, test } from 'ember-qunit';
import hbs from 'htmlbars-inline-precompile';

moduleForComponent('templated-anchor', 'Integration | Component | templated anchor', {
integration: true,
});

test('it renders', function(assert) {
[
{
href: 'http://localhost/?={{Name}}/{{ID}}',
vars: {
Name: 'name',
ID: 'id',
},
result: 'http://localhost/?=name/id',
},
{
href: 'http://localhost/?={{Name}}/{{ID}}',
vars: {
Name: '{{Name}}',
ID: '{{ID}}',
},
result: 'http://localhost/?={{Name}}/{{ID}}',
},
{
href: 'http://localhost/?={{deep.Name}}/{{deep.ID}}',
vars: {
deep: {
Name: '{{Name}}',
ID: '{{ID}}',
},
},
result: 'http://localhost/?={{Name}}/{{ID}}',
},
{
href: 'http://localhost/?={{}}/{{}}',
vars: {
Name: 'name',
ID: 'id',
},
result: 'http://localhost/?={{}}/{{}}',
},
{
href: 'http://localhost/?={{Service_Name}}/{{Meta-Key}}',
vars: {
Service_Name: 'name',
['Meta-Key']: 'id',
},
result: 'http://localhost/?=name/id',
},
{
href: 'http://localhost/?={{Service_Name}}/{{Meta-Key}}',
vars: {
WrongPropertyName: 'name',
['Meta-Key']: 'id',
},
result: 'http://localhost/?=/id',
},
{
href: 'http://localhost/?={{.Name}}',
vars: {
['.Name']: 'name',
},
result: 'http://localhost/?=',
},
{
href: 'http://localhost/?={{.}}',
vars: {
['.']: 'name',
},
result: 'http://localhost/?=',
},
{
href: 'http://localhost/?={{deep..Name}}',
vars: {
deep: {
Name: 'Name',
ID: 'ID',
},
},
result: 'http://localhost/?=',
},
].forEach(item => {
this.set('item', item);
this.render(hbs`
{{#templated-anchor href=item.href vars=item.vars}}
Dashboard link
{{/templated-anchor}}
`);
assert.equal(
this.$()
.find('a')
.attr('href'),
item.result
);
});
});
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I could be missing something but it looks like this doesn't actually test the variable replacement? Do we know that regexp etc. works?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So these are just the auto generated tests when you use an ember generator to create your stubs. They generally just test the thing you generate can be instantiated.

I've added some integration tests with some basic usecases which cover the regex.

2 changes: 1 addition & 1 deletion ui-v2/tests/unit/routes/dc/services/show-test.js
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { moduleFor, test } from 'ember-qunit';

moduleFor('route:dc/services/show', 'Unit | Route | dc/services/show', {
// Specify the other units that are required for this test.
needs: ['service:repository/service'],
needs: ['service:repository/service', 'service:settings'],
});

test('it exists', function(assert) {