ui: DistributionMeter Component (#12452)

This commit is contained in:
John Cowen 2022-03-09 08:28:34 +00:00 committed by GitHub
parent c46bdbd600
commit 55851c784f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 380 additions and 0 deletions

View File

@ -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 -->

View File

@ -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);
}
`;
}

View File

@ -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>

View File

@ -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;
}
}
}
}
}

View File

@ -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;
}
`;
}

View File

@ -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>

View 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);
});

View 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;
}
}
});

View File

@ -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;

View File

@ -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;
}
}
`;
/*}*/
}