mirror of
https://github.com/datahub-project/datahub.git
synced 2025-11-01 11:19:05 +00:00
Merge pull request #1287 from cptran777/metadata-health-header
Metadata health header
This commit is contained in:
commit
b8d2f3f799
@ -1,6 +1,10 @@
|
||||
import Component from '@ember/component';
|
||||
import { get } from '@ember/object';
|
||||
import { get, computed, setProperties, getProperties } from '@ember/object';
|
||||
import { task, TaskInstance } from 'ember-concurrency';
|
||||
import ComputedProperty from '@ember/object/computed';
|
||||
import { IChartDatum } from 'wherehows-web/typings/app/visualization/charts';
|
||||
import healthCategories from 'wherehows-web/mirage/fixtures/health-categories';
|
||||
import healthSeverity from 'wherehows-web/mirage/fixtures/health-severity';
|
||||
|
||||
/**
|
||||
* This is the container component for the dataset health tab. It should contain the health bar graphs and a table
|
||||
@ -14,6 +18,84 @@ export default class DatasetHealthContainer extends Component {
|
||||
*/
|
||||
urn: string;
|
||||
|
||||
/**
|
||||
* Sets the classes for the rendered html element for the component
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
classNames = ['dataset-health'];
|
||||
|
||||
/**
|
||||
* The current filter for the category chart. Clicking on a bar in the chart changes this value and will cause
|
||||
* the table component to filter out certain rows. Since we only allow one filter at a time, only this or
|
||||
* currentSeverityFilter should have a truthy value at any given point.
|
||||
* @type {string}
|
||||
*/
|
||||
currentCategoryFilter = '';
|
||||
|
||||
/**
|
||||
* The current filter for the severity chart. Clicking on a bar in the chart changes this value and will cause
|
||||
* the table component to filter out certain rows. Since we only allow one filter at a time, only this or
|
||||
* currentCategoryFilter should have a truthy value at any given point.
|
||||
* @type {string}
|
||||
*/
|
||||
currentSeverityFilter = '';
|
||||
|
||||
/**
|
||||
* Raw fetched data for the category metrics
|
||||
* @type {Array<pending>}
|
||||
*/
|
||||
categoryMetrics: Array<IChartDatum> = [];
|
||||
|
||||
/**
|
||||
* Raw fetched data for the category metrics
|
||||
* @type {Array<pending>}
|
||||
*/
|
||||
severityMetrics: Array<IChartDatum> = [];
|
||||
|
||||
/**
|
||||
* Modified categoryMetrics to add properties that will help us render our actual charts without modifying the original
|
||||
* data
|
||||
* @type {ComputedProperty<Array<IChartDatum>>}
|
||||
*/
|
||||
renderedCategories: ComputedProperty<Array<IChartDatum>> = computed(
|
||||
'categoryMetrics',
|
||||
'currentCategoryFilter',
|
||||
function(this: DatasetHealthContainer): Array<IChartDatum> {
|
||||
const { categoryMetrics, currentCategoryFilter } = getProperties(
|
||||
this,
|
||||
'categoryMetrics',
|
||||
'currentCategoryFilter'
|
||||
);
|
||||
|
||||
return categoryMetrics.map(category => ({
|
||||
...category,
|
||||
isFaded: !!currentCategoryFilter && category.name !== currentCategoryFilter
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Modified severityMetrics to add properties that will help us render our actual charts
|
||||
* @type {ComputedProperty<Array<IChartDatum>>}
|
||||
*/
|
||||
renderedSeverity: ComputedProperty<Array<IChartDatum>> = computed(
|
||||
'severityMetrics',
|
||||
'currentSeverityFilter',
|
||||
function(this: DatasetHealthContainer): Array<IChartDatum> {
|
||||
const { severityMetrics, currentSeverityFilter } = getProperties(
|
||||
this,
|
||||
'severityMetrics',
|
||||
'currentSeverityFilter'
|
||||
);
|
||||
|
||||
return severityMetrics.map(severity => ({
|
||||
...severity,
|
||||
isFaded: !!currentSeverityFilter && severity.name !== currentSeverityFilter,
|
||||
customColorClass: `severity-chart__bar--${severity.name.toLowerCase()}`
|
||||
}));
|
||||
}
|
||||
);
|
||||
|
||||
didInsertElement() {
|
||||
get(this, 'getContainerDataTask').perform();
|
||||
}
|
||||
@ -27,9 +109,36 @@ export default class DatasetHealthContainer extends Component {
|
||||
* @type {Task<TaskInstance<Promise<any>>, (a?: any) => TaskInstance<TaskInstance<Promise<any>>>>}
|
||||
*/
|
||||
getContainerDataTask = task(function*(this: DatasetHealthContainer): IterableIterator<TaskInstance<Promise<any>>> {
|
||||
// Do something in the future
|
||||
// Pretend like we're getting data from somehwere
|
||||
const healthData = {
|
||||
categories: healthCategories,
|
||||
severity: healthSeverity
|
||||
};
|
||||
|
||||
setProperties(this, {
|
||||
categoryMetrics: healthData.categories,
|
||||
severityMetrics: healthData.severity
|
||||
});
|
||||
});
|
||||
|
||||
// Mock data for testing demo purposes, to be deleted once we have actual data and further development
|
||||
testSeries = [{ name: 'Test1', value: 10 }, { name: 'Test2', value: 5 }, { name: 'Test3', value: 3 }];
|
||||
/**
|
||||
* Triggered when the user clicks on one of the bars in the summary charts child component, will trigger
|
||||
* a filter for whatever bar they select, unless it already is one in which case we will remove the filter
|
||||
* @param this - Explicit this declaration for typescript
|
||||
* @param filterType - Whether we are filtering by category or severity
|
||||
* @param filterDatum - Passed in to the action by the child component, contains the tag to be filtered for
|
||||
*/
|
||||
onFilterSelect(this: DatasetHealthContainer, filterType: string, filterDatum: IChartDatum): void {
|
||||
const { currentCategoryFilter, currentSeverityFilter } = getProperties(
|
||||
this,
|
||||
'currentCategoryFilter',
|
||||
'currentSeverityFilter'
|
||||
);
|
||||
const newFilterName = filterDatum.name;
|
||||
|
||||
setProperties(this, {
|
||||
currentCategoryFilter: filterType === 'category' && newFilterName !== currentCategoryFilter ? newFilterName : '',
|
||||
currentSeverityFilter: filterType === 'severity' && newFilterName !== currentSeverityFilter ? newFilterName : ''
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,35 @@
|
||||
import Component from '@ember/component';
|
||||
import { IChartDatum } from 'wherehows-web/typings/app/visualization/charts';
|
||||
import { setProperties } from '@ember/object';
|
||||
import { noop } from 'wherehows-web/utils/helpers/functions';
|
||||
|
||||
export default class DatasetsHealthMetricsCharts extends Component {
|
||||
/**
|
||||
* Sets the classes for the rendered html element for the compoennt
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
classNames = ['dataset-health__metrics-charts'];
|
||||
|
||||
/**
|
||||
* Pass through function meant to come from the dataset-health container that handles the selection of
|
||||
* a filter, triggered when the user clicks on one of the bars of the bar chart.
|
||||
* @param {IChartDatum} datum - the actual chart datum object so we know what was clicked
|
||||
*/
|
||||
onCategorySelect: (datum: IChartDatum) => void;
|
||||
|
||||
/**
|
||||
* Pass through function meant to come from the dataset-health container that handles the selection of
|
||||
* a filter, triggered when the user clicks on one of the bars of the bar chart.
|
||||
* @param {IChartDatum} datum - the actual chart datum object so we know what was clicked
|
||||
*/
|
||||
onSeveritySelect: (datum: IChartDatum) => void;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
setProperties(this, {
|
||||
onCategorySelect: this.onCategorySelect || noop,
|
||||
onSeveritySelect: this.onSeveritySelect || noop
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -2,6 +2,7 @@ import Component from '@ember/component';
|
||||
import { IChartDatum } from 'wherehows-web/typings/app/visualization/charts';
|
||||
import { computed, get, set, setProperties } from '@ember/object';
|
||||
import ComputedProperty from '@ember/object/computed';
|
||||
import { noop } from 'wherehows-web/utils/helpers/functions';
|
||||
|
||||
interface IBarSeriesDatum extends IChartDatum {
|
||||
yOffset: number;
|
||||
@ -34,7 +35,7 @@ export default class HorizontalBarChart extends Component {
|
||||
* Sets the classes for the rendered html element for the component
|
||||
* @type {Array<string>}
|
||||
*/
|
||||
classNames = ['vz-chart', 'viz-bar-chart', 'single-series'];
|
||||
classNames = ['viz-chart', 'viz-bar-chart', 'single-series'];
|
||||
|
||||
/**
|
||||
* Represents the series of data needed to power our chart. Format is
|
||||
@ -155,13 +156,22 @@ export default class HorizontalBarChart extends Component {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Expected to be optionally passed in from the containing component, this function handles the action to
|
||||
* be taken if a user selects an individual bar from the chart.
|
||||
* @param {string} name - the "category" or "tag" that goes with each legend label
|
||||
* @param {number} value - the value associated with each series datum
|
||||
*/
|
||||
onBarSelect: (name: string, value: number) => void;
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
// Applying passed in properties or setting to default values
|
||||
setProperties(this, {
|
||||
labelTagProperty: this.labelTagProperty || 'name',
|
||||
labelAppendTag: this.labelAppendTag || '',
|
||||
labelAppendValue: this.labelAppendValue || ''
|
||||
labelAppendValue: this.labelAppendValue || '',
|
||||
onBarSelect: this.onBarSelect || noop
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
@import 'dataset-fabric/all';
|
||||
@import 'dataset-relationships/all';
|
||||
@import 'visualization/all';
|
||||
@import 'dataset-health/all';
|
||||
|
||||
@import 'nacho/nacho-button';
|
||||
@import 'nacho/nacho-global-search';
|
||||
|
||||
@ -0,0 +1 @@
|
||||
@import 'metrics-charts';
|
||||
@ -0,0 +1,32 @@
|
||||
.dataset-health {
|
||||
&__metrics-charts {
|
||||
@include nacho-container;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
&__chart-container {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.severity-chart {
|
||||
&__bar {
|
||||
&--minor {
|
||||
$green3: get-color(green3);
|
||||
fill: $green3;
|
||||
stroke: $green3;
|
||||
}
|
||||
&--warning {
|
||||
$orange3: get-color(orange3);
|
||||
fill: $orange3;
|
||||
stroke: $orange3;
|
||||
}
|
||||
&--critical {
|
||||
$red3: get-color(red3);
|
||||
fill: $red3;
|
||||
stroke: $red3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -22,4 +22,22 @@
|
||||
font-size: 15px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
&__bar {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
transform: scale(1.02, 1);
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
&--faded {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
&__label {
|
||||
&--faded {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
Coming Soon!
|
||||
<div style="width: 50%">
|
||||
{{visualization/charts/horizontal-bar-chart
|
||||
title="Test Chart"
|
||||
series=testSeries
|
||||
labelAppendValue="%"}}
|
||||
</div>
|
||||
<h3>Metadata Health</h3>
|
||||
{{datasets/health/metrics-charts
|
||||
categoryData=renderedCategories
|
||||
severityData=renderedSeverity
|
||||
onCategorySelect=(action onFilterSelect "category")
|
||||
onSeveritySelect=(action onFilterSelect "severity")}}
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
<div class="dataset-health__chart-container">
|
||||
{{#if categoryData}}
|
||||
{{visualization/charts/horizontal-bar-chart
|
||||
title="Categories"
|
||||
series=categoryData
|
||||
labelAppendValue="%"
|
||||
onBarSelect=(action onCategorySelect)}}
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="dataset-health__chart-container">
|
||||
{{#if severityData}}
|
||||
{{visualization/charts/horizontal-bar-chart
|
||||
class="severity-chart"
|
||||
title="Severity"
|
||||
series=severityData
|
||||
labelAppendValue="%"
|
||||
onBarSelect=(action onSeveritySelect)}}
|
||||
{{/if}}
|
||||
</div>
|
||||
@ -3,12 +3,15 @@
|
||||
<g class="highcharts-series-group">
|
||||
<g class="highcharts-series highcharts-series-0 highcharts-bar-series highcharts-tracker highcharts-series-hover">
|
||||
{{#each seriesData as |datum index|}}
|
||||
<rect x="0" y="{{datum.yOffset}}" height="16" width="{{datum.barLength}}" class="highcharts-color-{{index}}" rx="2px" ry="2px"></rect>
|
||||
<rect x="0" y="{{datum.yOffset}}" height="16" width="{{datum.barLength}}" rx="2px" ry="2px"
|
||||
class="highcharts-color-{{index}} viz-bar-chart__bar {{if datum.isFaded "viz-bar-chart__bar--faded"}} {{datum.customColorClass}}"
|
||||
{{action onBarSelect datum}}>
|
||||
</rect>
|
||||
{{/each}}
|
||||
</g>
|
||||
<g class="highcharts-data-labels highcharts-series-0 highcharts-bar-series highcharts-color-0 highcharts-tracker">
|
||||
{{#each seriesData as |datum index|}}
|
||||
<g class="highcharts-label highcharts-data-label">
|
||||
<g class="highcharts-label highcharts-data-label viz-bar-chart__label {{if datum.isFaded "viz-bar-chart__label--faded"}}">
|
||||
<text x="0" y="{{datum.labelOffset}}">
|
||||
<tspan class="highcharts-emphasized">{{datum.value}}{{labelAppendValue}}</tspan>
|
||||
<tspan> | {{get datum labelTagProperty}}</tspan>
|
||||
|
||||
@ -4,4 +4,6 @@
|
||||
export interface IChartDatum {
|
||||
name: string;
|
||||
value: number;
|
||||
isFaded?: boolean;
|
||||
customColorClass?: string;
|
||||
}
|
||||
|
||||
1
wherehows-web/mirage/fixtures/health-categories.ts
Normal file
1
wherehows-web/mirage/fixtures/health-categories.ts
Normal file
@ -0,0 +1 @@
|
||||
export default [{ name: 'Compliance', value: 60 }, { name: 'Ownership', value: 40 }];
|
||||
1
wherehows-web/mirage/fixtures/health-severity.ts
Normal file
1
wherehows-web/mirage/fixtures/health-severity.ts
Normal file
@ -0,0 +1 @@
|
||||
export default [{ name: 'Minor', value: 50 }, { name: 'Warning', value: 30 }, { name: 'Critical', value: 25 }];
|
||||
@ -0,0 +1,45 @@
|
||||
import { moduleForComponent, test } from 'ember-qunit';
|
||||
import { run } from '@ember/runloop';
|
||||
import hbs from 'htmlbars-inline-precompile';
|
||||
|
||||
moduleForComponent('datasets/health/metrics-charts', 'Integration | Component | datasets/health/metrics-charts', {
|
||||
integration: true
|
||||
});
|
||||
|
||||
const chartClass = '.viz-bar-chart';
|
||||
const labelClass = '.highcharts-label';
|
||||
const labelValueClass = `${labelClass} .highcharts-emphasized`;
|
||||
const barClass = `${chartClass}__bar`;
|
||||
|
||||
test('it renders', async function(assert) {
|
||||
this.render(hbs`{{datasets/health/metrics-charts}}`);
|
||||
assert.ok(this.$(), 'Renders without errors');
|
||||
});
|
||||
|
||||
test('it provides the correct information', async function(assert) {
|
||||
const categoryData = [{ name: 'Compliance', value: 60 }, { name: 'Ownership', value: 40 }];
|
||||
const severityData = [
|
||||
{ name: 'Minor', value: 50, customColorClass: 'severity-chart__bar--minor' },
|
||||
{ name: 'Warning', value: 30, customColorClass: 'severity-chart__bar--warning' },
|
||||
{ name: 'Critical', value: 25, customColorClass: 'severity-chart__bar--critical' }
|
||||
];
|
||||
|
||||
this.setProperties({
|
||||
categoryData,
|
||||
severityData
|
||||
});
|
||||
|
||||
this.render(hbs`{{datasets/health/metrics-charts
|
||||
categoryData=categoryData
|
||||
severityData=severityData}}`);
|
||||
|
||||
assert.ok(this.$(), 'Still renders without errors');
|
||||
assert.equal(this.$(chartClass).length, 2, 'Renders 2 charts');
|
||||
assert.equal(
|
||||
this.$(`${labelValueClass}:eq(0)`)
|
||||
.text()
|
||||
.trim(),
|
||||
'60%',
|
||||
'Renders correct value for labels'
|
||||
);
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user