import Component from '@ember/component'; import { get, computed, setProperties, getProperties } from '@ember/object'; import { task } from 'ember-concurrency'; import ComputedProperty from '@ember/object/computed'; import { IChartDatum } from 'wherehows-web/typings/app/visualization/charts'; import { IHealthScore, IDatasetHealth } from 'wherehows-web/typings/api/datasets/health'; import { readDatasetHealthByUrn, getCategory } from 'wherehows-web/utils/api/datasets/health'; import { Tabs } from 'wherehows-web/constants/datasets/shared'; import { equal } from '@ember-decorators/object/computed'; import { IObject } from 'wherehows-web/typings/generic'; /** * Used for the dataset health tab, represents the fieldnames for the health score table */ export enum HealthDataFields { category = 'Category', severity = 'Severity', description = 'Description', score = 'Score' } /** * This is the container component for the dataset health tab. It should contain the health bar graphs and a table * depicting the detailed health scores. Aside from fetching the data, it also handles click interactions between * the graphs and the table in terms of filtering and displaying of data */ export default class DatasetHealthContainer extends Component { /** * The urn identifier for the dataset * @type {string} */ urn: string; /** * Sets the classes for the rendered html element for the component * @type {Array} */ 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} */ categoryMetrics: Array = []; /** * Raw fetched data for the category metrics * @type {Array} */ severityMetrics: Array = []; /** * Fetched data for the health score detailed data. * @type {Array} */ tableData: Array = []; /** * Passed in from the higher level component, we use this property in order to determine whether the dataset health * tab is the currently selected tab * @type {Tabs} */ tabSelected: Tabs; /** * Calculated from the currently selected tab to determine whether this container is the currently selected tab. * Note: Highcharts calculates size and other chart details upon initial render and doesn't do a good job of handling * rerenders. Because of this we want those calculations to take place when dataset health is the currently active tab, * otherwise we will insert elements off screen and size will default to 0 and we lose our charts * @type {ComputedProperty} */ @equal('tabSelected', Tabs.Health) isActiveTab: boolean; /** * Modified categoryMetrics to add properties that will help us render our actual charts without modifying the original * data * @type {ComputedProperty>} */ renderedCategories: ComputedProperty> = computed( 'categoryMetrics', 'currentCategoryFilter', function(this: DatasetHealthContainer): Array { 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>} */ renderedSeverity: ComputedProperty> = computed( 'severityMetrics', 'currentSeverityFilter', function(this: DatasetHealthContainer): Array { 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(); } didUpdateAttrs() { get(this, 'getContainerDataTask').perform(); } /** * An async parent task to group all data tasks for this container component * @type {Task>, (a?: any) => TaskInstance>>>} */ getContainerDataTask = task(function*(this: DatasetHealthContainer): IterableIterator> { const health: IDatasetHealth = yield readDatasetHealthByUrn(get(this, 'urn')); const details = health.validations || []; const total = details.length; const categories: IObject = {}; const severities: IObject = {}; // Go through the details and find the COUNT of severity and category groupings const tableData: Array = details.map(detail => { const category = getCategory(detail.validator); const severity = detail.tier || 'none'; categories[category] = (categories[category] || 0) + 1; severities[severity] = (severities[severity] || 0) + 1; return { category, severity, description: detail.description, score: detail.score * 100 }; }); const categoryMetrics: Array = Object.keys(categories).map(category => ({ name: category, value: Math.round((categories[category] / total) * 100) })); const severityMetrics: Array = Object.keys(severities).map(severity => ({ name: severity, value: Math.round((severities[severity] / total) * 100) })); setProperties(this, { categoryMetrics, severityMetrics, tableData }); }); /** * 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 || filterDatum.value.toString(); setProperties(this, { currentCategoryFilter: filterType === 'category' && newFilterName !== currentCategoryFilter ? newFilterName : '', currentSeverityFilter: filterType === 'severity' && newFilterName !== currentSeverityFilter ? newFilterName : '' }); } }