mirror of
https://github.com/status-im/consul.git
synced 2025-01-22 03:29:43 +00:00
ui: DistributionMeter Component (#12452)
This commit is contained in:
parent
c46bdbd600
commit
55851c784f
@ -0,0 +1,83 @@
|
||||
---
|
||||
type: custom-element
|
||||
---
|
||||
<!-- START component-docs:@tagName -->
|
||||
# DistributionMeter
|
||||
<!-- END component-docs:@tagName -->
|
||||
|
||||
<!-- START component-docs:@description -->
|
||||
A meter-like component to show a distribution of values.
|
||||
<!-- END component-docs:@description -->
|
||||
|
||||
```hbs preview-template
|
||||
<figure>
|
||||
<figcaption>
|
||||
Provide a widget so we can try switching between all types of meter
|
||||
</figcaption>
|
||||
<select
|
||||
onchange={{action (mut this.type) value="target.value"}}
|
||||
>
|
||||
<option>linear</option>
|
||||
<option>radial</option>
|
||||
<option>circular</option>
|
||||
</select>
|
||||
</figure>
|
||||
<figure>
|
||||
|
||||
<DataSource
|
||||
@src={{uri '/partition/namespace/dc-1/services'}}
|
||||
as |source|>
|
||||
{{#let
|
||||
(group-by "MeshStatus" (or source.data (array)))
|
||||
as |grouped|}}
|
||||
<DistributionMeter type={{or this.type 'linear'}} as |meter|>
|
||||
{{#each (array 'passing' 'warning' 'critical') as |status|}}
|
||||
{{#let
|
||||
(concat (percentage-of (get grouped (concat status '.length')) source.data.length) '%')
|
||||
as |percentage|}}
|
||||
<meter.Meter
|
||||
description={{capitalize status}}
|
||||
percentage={{percentage}}
|
||||
class={{class-map
|
||||
status
|
||||
}}
|
||||
as |meter|></meter.Meter>
|
||||
{{/let}}
|
||||
{{/each}}
|
||||
</DistributionMeter>
|
||||
{{/let}}
|
||||
</DataSource>
|
||||
</figure>
|
||||
```
|
||||
|
||||
## Attributes
|
||||
|
||||
<!-- START component-docs:@attrs -->
|
||||
| Attribute | Type | Default | Description |
|
||||
| :-------- | :--------------------------------- | :------ | :------------------------------------ |
|
||||
| type | "linear" \| "radial" \| "circular" | linear | The type of distribution meter to use |
|
||||
|
||||
<!-- END component-docs:@attrs -->
|
||||
|
||||
## Contextual Components
|
||||
|
||||
<!-- START component-docs:@components -->
|
||||
|
||||
### DistributionMeter::Meter
|
||||
|
||||
#### Attributes
|
||||
|
||||
| Attribute | Type | Default | Description |
|
||||
| :---------- | :----- | :------ | :----------------------------------------- |
|
||||
| percentage | number | 0 | The percentage to be used for the meter |
|
||||
| description | string | | Textual value to describe the meters value |
|
||||
|
||||
|
||||
#### CSS Properties
|
||||
|
||||
| Property | Type | Tracks | Description |
|
||||
| :---------------------- | :--------- | :----------- | :---------------------------------------------------------------- |
|
||||
| --percentage | percentage | [percentage] | Read-only alias of the percentage attribute |
|
||||
| --aggregated-percentage | percentage | | Aggregated percentage of all meters within the distribution meter |
|
||||
|
||||
<!-- END component-docs:@components -->
|
@ -0,0 +1,32 @@
|
||||
export default (css) => {
|
||||
return css`
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
dl {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
:host([type='linear']) {
|
||||
height: 3px;
|
||||
}
|
||||
:host([type='radial']),
|
||||
:host([type='circular']) {
|
||||
height: 300px;
|
||||
}
|
||||
:host([type='linear']) dl {
|
||||
background-color: currentColor;
|
||||
color: rgb(var(--tone-gray-100));
|
||||
border-radius: var(--decor-radius-999);
|
||||
transition-property: transform;
|
||||
transition-timing-function: ease-out;
|
||||
transition-duration: .1s;
|
||||
}
|
||||
:host([type='linear']) dl:hover {
|
||||
transform: scaleY(3);
|
||||
box-shadow: var(--decor-elevation-200);
|
||||
}
|
||||
`;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
<CustomElement
|
||||
@element="distribution-meter"
|
||||
@description="A meter-like component to show a distribution of values."
|
||||
@attrs={{array
|
||||
(array 'type' '"linear" | "radial" | "circular"' 'linear'
|
||||
'The type of distribution meter to use'
|
||||
)
|
||||
}}
|
||||
as |custom element|>
|
||||
<distribution-meter
|
||||
{{did-insert custom.connect}}
|
||||
{{will-destroy custom.disconnect}}
|
||||
...attributes
|
||||
>
|
||||
<custom.Template
|
||||
@styles={{css-map
|
||||
(require './index.css' from='/components/distribution-meter')
|
||||
}}
|
||||
>
|
||||
<dl>
|
||||
<slot></slot>
|
||||
</dl>
|
||||
</custom.Template>
|
||||
{{yield (hash
|
||||
Meter=(component 'distribution-meter/meter'
|
||||
type=element.attrs.type
|
||||
)
|
||||
)}}
|
||||
</distribution-meter>
|
||||
</CustomElement>
|
@ -0,0 +1,26 @@
|
||||
const parseFloatWithDefault = (val, d = 0) => {
|
||||
const num = parseFloat(val);
|
||||
return isNaN(num) ? d : num;
|
||||
}
|
||||
|
||||
export default (Component) => {
|
||||
return class extends Component {
|
||||
attributeChangedCallback(name, prev, value) {
|
||||
const target = this;
|
||||
switch(name) {
|
||||
case 'percentage': {
|
||||
let prevSibling = target;
|
||||
while(prevSibling) {
|
||||
const nextSibling = prevSibling.nextElementSibling;
|
||||
const aggregatedPercentage = nextSibling ? parseFloatWithDefault(nextSibling.style.getPropertyValue('--aggregated-percentage')) : 0;
|
||||
const perc = parseFloatWithDefault(prevSibling.getAttribute('percentage')) + aggregatedPercentage;
|
||||
prevSibling.style.setProperty('--aggregated-percentage', perc);
|
||||
prevSibling.setAttribute('aggregated-percentage', perc);
|
||||
prevSibling = prevSibling.previousElementSibling;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,79 @@
|
||||
export default (css) => {
|
||||
return css`
|
||||
/*@import '~/styles/base/decoration/visually-hidden.css';*/
|
||||
|
||||
:host(.critical) {
|
||||
color: rgb(var(--tone-red-500));
|
||||
}
|
||||
:host(.warning) {
|
||||
color: rgb(var(--tone-orange-500));
|
||||
}
|
||||
:host(.passing) {
|
||||
color: rgb(var(--tone-green-500));
|
||||
}
|
||||
|
||||
:host {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
|
||||
transition-timing-function: ease-out;
|
||||
transition-duration: .5s;
|
||||
}
|
||||
dt, dd meter {
|
||||
animation-name: visually-hidden;
|
||||
animation-fill-mode: forwards;
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
:host(.type-linear) {
|
||||
transition-property: width;
|
||||
width: calc(var(--aggregated-percentage) * 1%);
|
||||
height: 100%;
|
||||
background-color: currentColor;
|
||||
border-radius: var(--decor-radius-999);
|
||||
}
|
||||
|
||||
:host svg {
|
||||
height: 100%;
|
||||
}
|
||||
:host(.type-radial),
|
||||
:host(.type-circular) {
|
||||
transition-property: none;
|
||||
}
|
||||
:host(.type-radial) dd,
|
||||
:host(.type-circular) dd {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
:host(.type-radial) circle,
|
||||
:host(.type-circular) circle {
|
||||
transition-timing-function: ease-out;
|
||||
transition-duration: .5s;
|
||||
pointer-events: stroke;
|
||||
transition-property: stroke-dashoffset, stroke-width;
|
||||
transform: rotate(-90deg);
|
||||
transform-origin: 50%;
|
||||
fill: transparent;
|
||||
stroke: currentColor;
|
||||
stroke-dasharray: 100, 100;
|
||||
stroke-dashoffset: calc(calc(100 - var(--aggregated-percentage)) * 1px);
|
||||
}
|
||||
:host([aggregated-percentage='100']) circle {
|
||||
stroke-dasharray: 0 !important;
|
||||
}
|
||||
:host([aggregated-percentage='0']) circle {
|
||||
stroke-dasharray: 0, 100 !important;
|
||||
}
|
||||
:host(.type-radial) circle,
|
||||
:host(.type-circular]) svg {
|
||||
pointer-events: none;
|
||||
}
|
||||
:host(.type-radial) circle {
|
||||
stroke-width: 32;
|
||||
}
|
||||
:host(.type-circular) circle {
|
||||
stroke-width: 14;
|
||||
}
|
||||
`;
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
<CustomElement
|
||||
@element="distribution-meter-meter"
|
||||
@class={{require './element'
|
||||
from='/components/distribution-meter/meter'}}
|
||||
|
||||
@attrs={{array
|
||||
(array 'percentage' 'number' 0
|
||||
'The percentage to be used for the meter'
|
||||
)
|
||||
(array 'description' 'string' ''
|
||||
'Textual value to describe the meters value'
|
||||
)
|
||||
}}
|
||||
|
||||
@cssprops={{array
|
||||
(array '--percentage' 'percentage' '[percentage]'
|
||||
'Read-only alias of the percentage attribute'
|
||||
)
|
||||
(array '--aggregated-percentage' 'percentage' undefined
|
||||
'Aggregated percentage of all meters within the distribution meter'
|
||||
)
|
||||
}}
|
||||
as |custom element|>
|
||||
|
||||
<distribution-meter-meter
|
||||
{{did-insert custom.connect}}
|
||||
{{will-destroy custom.disconnect}}
|
||||
class={{class-map
|
||||
(array (concat 'type-' @type) @type)
|
||||
}}
|
||||
...attributes
|
||||
>
|
||||
<custom.Template
|
||||
@styles={{css-map
|
||||
(require '/styles/base/decoration/visually-hidden.css'
|
||||
from='/components/distribution-meter/meter')
|
||||
(require './index.css'
|
||||
from='/components/distribution-meter/meter')
|
||||
}}
|
||||
>
|
||||
<dt>{{element.attrs.description}}</dt>
|
||||
<dd aria-label={{concat element.attrs.percentage '%'}}>
|
||||
<meter min="0" max="100" value={{element.attrs.percentage}}>
|
||||
<slot>{{concat element.attrs.percentage '%'}}</slot>
|
||||
</meter>
|
||||
{{#if (or (eq @type 'circular') (eq @type 'radial'))}}
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 32 32"
|
||||
clip-path="circle()"
|
||||
>
|
||||
<circle
|
||||
r="16"
|
||||
cx="16"
|
||||
cy="16"
|
||||
/>
|
||||
</svg>
|
||||
{{/if}}
|
||||
</dd>
|
||||
</custom.Template>
|
||||
</distribution-meter-meter>
|
||||
|
||||
</CustomElement>
|
||||
|
9
ui/packages/consul-ui/app/helpers/percentage-of.js
Normal file
9
ui/packages/consul-ui/app/helpers/percentage-of.js
Normal file
@ -0,0 +1,9 @@
|
||||
import { helper } from '@ember/component/helper';
|
||||
|
||||
export default helper(function([of, num], hash) {
|
||||
const perc = (of / num * 100);
|
||||
if(isNaN(perc)) {
|
||||
return 0;
|
||||
}
|
||||
return perc.toFixed(2);
|
||||
});
|
38
ui/packages/consul-ui/app/helpers/require.js
Normal file
38
ui/packages/consul-ui/app/helpers/require.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { helper } from '@ember/component/helper';
|
||||
|
||||
import { css } from '@lit/reactive-element';
|
||||
|
||||
import resolve from 'consul-ui/utils/path/resolve';
|
||||
|
||||
import distributionMeter from 'consul-ui/components/distribution-meter/index.css';
|
||||
import distributionMeterMeter from 'consul-ui/components/distribution-meter/meter/index.css';
|
||||
import distributionMeterMeterElement from 'consul-ui/components/distribution-meter/meter/element';
|
||||
import visuallyHidden from 'consul-ui/styles/base/decoration/visually-hidden.css';
|
||||
|
||||
const fs = {
|
||||
['/components/distribution-meter/index.css']: distributionMeter,
|
||||
['/components/distribution-meter/meter/index.css']: distributionMeterMeter,
|
||||
['/components/distribution-meter/meter/element']: distributionMeterMeterElement,
|
||||
['/styles/base/decoration/visually-hidden.css']: visuallyHidden
|
||||
};
|
||||
|
||||
const container = new Map();
|
||||
|
||||
// `css` already has a caching mechanism under the hood so rely on that, plus
|
||||
// we get the advantage of laziness here, i.e. we only call css as and when we
|
||||
// need to
|
||||
export default helper(([path = ''], { from }) => {
|
||||
const fullPath = resolve(from, path);
|
||||
switch(true) {
|
||||
case fullPath.endsWith('.css'):
|
||||
return fs[fullPath](css)
|
||||
default: {
|
||||
if(container.has(fullPath)) {
|
||||
return container.get(fullPath);
|
||||
}
|
||||
const module = fs[fullPath](HTMLElement);
|
||||
container.set(fullPath, module);
|
||||
return module;
|
||||
}
|
||||
}
|
||||
});
|
@ -4,6 +4,7 @@
|
||||
--decor-radius-100: 2px;
|
||||
--decor-radius-200: 4px;
|
||||
--decor-radius-300: 7px;
|
||||
--decor-radius-999: 9999px;
|
||||
--decor-radius-full: 100%;
|
||||
|
||||
--decor-border-000: none;
|
||||
|
@ -0,0 +1,18 @@
|
||||
export default (css) => {
|
||||
/*%visually-hidden {*/
|
||||
return css`
|
||||
@keyframes visually-hidden {
|
||||
100% {
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
clip: rect(0 0 0 0);
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
}
|
||||
`;
|
||||
/*}*/
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user