Unverified Commit 69ce3643 authored by Marcos Iglesias's avatar Marcos Iglesias Committed by GitHub

refactor: Tested and refactored Stats component (#687)

Signed-off-by: 's avatarMarcos Iglesias Valle <golodhros@gmail.com>
parent 050f674c
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
import React from 'react';
import { storiesOf } from '@storybook/react';
import StorySection from 'components/common/StorySection';
import ColumnStats from '.';
import TestDataBuilder from './testDataBuilder';
const dataBuilder = new TestDataBuilder();
const { stats: fourStats } = dataBuilder.withFourStats().build();
const { stats: threeStats } = dataBuilder.withThreeStats().build();
const { stats: eightStats } = dataBuilder.withEightStats().build();
const stories = storiesOf('Components/ColumnStats', module);
stories.add('Column Stats', () => (
<>
<StorySection title="with four stats">
<ColumnStats stats={fourStats} />
</StorySection>
<StorySection title="with three stats">
<ColumnStats stats={threeStats} />
</StorySection>
<StorySection title="with eight stats">
<ColumnStats stats={eightStats} />
</StorySection>
</>
));
...@@ -2,126 +2,116 @@ ...@@ -2,126 +2,116 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
import * as React from 'react'; import * as React from 'react';
import { shallow } from 'enzyme'; import { mount } from 'enzyme';
import { ColumnStats, ColumnStatsProps } from '.';
describe('ColumnStats', () => { import ColumnStats, { ColumnStatsProps } from '.';
const setup = (propOverrides?: Partial<ColumnStatsProps>) => { import TestDataBuilder from './testDataBuilder';
const props = {
stats: [ const dataBuilder = new TestDataBuilder();
{
end_epoch: 1571616000, const setup = (propOverrides?: Partial<ColumnStatsProps>) => {
start_epoch: 1571616000, const props = {
stat_type: 'count', stats: [],
stat_val: '12345', ...propOverrides,
},
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'count_null',
stat_val: '123',
},
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'count_distinct',
stat_val: '22',
},
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'count_zero',
stat_val: '44',
},
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'max',
stat_val: '1237466454',
},
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'min',
stat_val: '856',
},
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'avg',
stat_val: '2356575',
},
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'stddev',
stat_val: '1234563',
},
],
...propOverrides,
};
const wrapper = shallow<ColumnStats>(<ColumnStats {...props} />);
return { props, wrapper };
}; };
const wrapper = mount<typeof ColumnStats>(<ColumnStats {...props} />);
const { wrapper, props } = setup(); return { props, wrapper };
const instance = wrapper.instance(); };
describe('getStatsInfoText', () => {
it('generates correct info text for a daily partition', () => {
const startEpoch = 1568160000;
const endEpoch = 1568160000;
const expectedInfoText = `Stats reflect data collected on Sep 11, 2019 only. (daily partition)`;
expect(instance.getStatsInfoText(startEpoch, endEpoch)).toBe(
expectedInfoText
);
});
it('generates correct info text for a date range', () => { describe('ColumnStats', () => {
const startEpoch = 1568160000; describe('render', () => {
const endEpoch = 1571616000; describe('when stats are empty', () => {
const expectedInfoText = `Stats reflect data collected between Sep 11, 2019 and Oct 21, 2019.`; const { stats } = dataBuilder.withEmptyStats().build();
expect(instance.getStatsInfoText(startEpoch, endEpoch)).toBe(
expectedInfoText
);
});
it('generates correct when no dates are given', () => { it('does not render the component', () => {
const expectedInfoText = `Stats reflect data collected over a recent period of time.`; const { wrapper } = setup({ stats });
const expected = stats.length;
const actual = wrapper.find('.column-stats').length;
expect(instance.getStatsInfoText()).toBe(expectedInfoText); expect(actual).toEqual(expected);
});
}); });
});
describe('renderColumnStat', () => { describe('when four stats are passed', () => {
it('renders a single column stat', () => { const { stats } = dataBuilder.withFourStats().build();
const columnStat = {
end_epoch: 1571616000, it('renders the component', () => {
start_epoch: 1571616000, const { wrapper } = setup({ stats });
stat_type: 'count', const expected = 1;
stat_val: '12345', const actual = wrapper.find('.column-stats').length;
};
const expectedStatType = columnStat.stat_type.toUpperCase(); expect(actual).toEqual(expected);
const expectedStatValue = columnStat.stat_val; });
const result = shallow(instance.renderColumnStat(columnStat));
expect(result.find('.stat-name').text()).toBe(expectedStatType); it('renders two columns', () => {
expect(result.find('.stat-value').text()).toBe(expectedStatValue); const { wrapper } = setup({ stats });
const expected = 2;
const actual = wrapper.find('.column-stats-column').length;
expect(actual).toEqual(expected);
});
it('renders the stats info text', () => {
const { wrapper } = setup({ stats });
const expected = 1;
const actual = wrapper.find('.stat-collection-info').length;
expect(actual).toEqual(expected);
});
it('renders four stat rows', () => {
const { wrapper } = setup({ stats });
const expected = stats.length;
const actual = wrapper.find('.column-stat-row').length;
expect(actual).toEqual(expected);
});
}); });
});
describe('render', () => { describe('when three stats are passed', () => {
it('calls the appropriate functions', () => { const { stats } = dataBuilder.withThreeStats().build();
const getStatsInfoTextSpy = jest.spyOn(instance, 'getStatsInfoText');
instance.render(); it('renders three stat rows', () => {
const { wrapper } = setup({ stats });
const expected = stats.length;
const actual = wrapper.find('.column-stat-row').length;
expect(actual).toEqual(expected);
});
it('renders two rows in the first column', () => {
const { wrapper } = setup({ stats });
const expected = 2;
const actual = wrapper
.find('.column-stats-column')
.first()
.find('.column-stat-row').length;
expect(actual).toEqual(expected);
});
expect(getStatsInfoTextSpy).toHaveBeenCalledWith(1571616000, 1571616000); it('renders one row in the second column', () => {
const { wrapper } = setup({ stats });
const expected = 1;
const actual = wrapper
.find('.column-stats-column')
.last()
.find('.column-stat-row').length;
expect(actual).toEqual(expected);
});
}); });
it('calls renderColumnStat with all of the stats', () => { describe('when eight stats are passed', () => {
const renderColumnStatSpy = jest.spyOn(instance, 'renderColumnStat'); const { stats } = dataBuilder.withEightStats().build();
instance.render();
props.stats.forEach((stat) => { it('renders eight stat rows', () => {
expect(renderColumnStatSpy).toHaveBeenCalledWith(stat); const { wrapper } = setup({ stats });
const expected = stats.length;
const actual = wrapper.find('.column-stat-row').length;
expect(actual).toEqual(expected);
}); });
}); });
}); });
......
...@@ -4,7 +4,9 @@ ...@@ -4,7 +4,9 @@
import * as React from 'react'; import * as React from 'react';
import { TableColumnStats } from 'interfaces/index'; import { TableColumnStats } from 'interfaces/index';
import { formatDate } from 'utils/dateUtils'; import { getStatsInfoText } from '../utils';
import { COLUMN_STATS_TITLE } from '../constants';
import './styles.scss'; import './styles.scss';
...@@ -12,70 +14,75 @@ export interface ColumnStatsProps { ...@@ -12,70 +14,75 @@ export interface ColumnStatsProps {
stats: TableColumnStats[]; stats: TableColumnStats[];
} }
export class ColumnStats extends React.Component<ColumnStatsProps> { type ColumnStatRowProps = {
getStatsInfoText = (startEpoch?: number, endEpoch?: number) => { stat_type: string;
const startDate = startEpoch stat_val: string;
? formatDate({ epochTimestamp: startEpoch }) };
: null;
const endDate = endEpoch ? formatDate({ epochTimestamp: endEpoch }) : null;
let infoText = 'Stats reflect data collected'; const ColumnStatRow: React.FC<ColumnStatRowProps> = ({
if (startDate && endDate) { stat_type,
if (startDate === endDate) { stat_val,
infoText = `${infoText} on ${startDate} only. (daily partition)`; }: ColumnStatRowProps) => {
} else { return (
infoText = `${infoText} between ${startDate} and ${endDate}.`; <div className="column-stat-row">
} <div className="stat-name body-3">{stat_type.toUpperCase()}</div>
} else { <div className="stat-value">{stat_val}</div>
infoText = `${infoText} over a recent period of time.`; </div>
} );
return infoText; };
};
renderColumnStat = (entry: TableColumnStats) => { const getStart = ({ start_epoch }) => start_epoch;
return ( const getEnd = ({ end_epoch }) => end_epoch;
<div className="column-stat-row" key={entry.stat_type}>
<div className="stat-name body-3">{entry.stat_type.toUpperCase()}</div>
<div className="stat-value">{entry.stat_val}</div>
</div>
);
};
render = () => { const ColumnStats: React.FC<ColumnStatsProps> = ({
const { stats } = this.props; stats,
if (stats.length === 0) { }: ColumnStatsProps) => {
return null; if (stats.length === 0) {
} return null;
}
const startEpoch = Math.min(...stats.map(getStart));
const endEpoch = Math.max(...stats.map(getEnd));
// TODO - Move map statements to separate functions for better testing return (
const startEpoch = Math.min(...stats.map((s) => s.start_epoch)); <article className="column-stats">
const endEpoch = Math.max(...stats.map((s) => s.end_epoch)); <div className="stat-collection-info">
<span className="stat-title">{COLUMN_STATS_TITLE} </span>
{getStatsInfoText(startEpoch, endEpoch)}
</div>
<div className="column-stats-table">
<div className="column-stats-column">
{stats.map((stat, index) => {
if (index % 2 === 0) {
return (
<ColumnStatRow
key={stat.stat_type}
stat_type={stat.stat_type}
stat_val={stat.stat_val}
/>
);
}
return ( return null;
<section className="column-stats"> })}
<div className="stat-collection-info">
<span className="title-3">Column Statistics&nbsp;</span>
{this.getStatsInfoText(startEpoch, endEpoch)}
</div> </div>
<div className="column-stats-table"> <div className="column-stats-column">
<div className="column-stats-column"> {stats.map((stat, index) => {
{stats.map((stat, index) => { if (index % 2 === 1) {
if (index % 2 === 0) { return (
return this.renderColumnStat(stat); <ColumnStatRow
} key={stat.stat_type}
})} stat_type={stat.stat_type}
</div> stat_val={stat.stat_val}
<div className="column-stats-column"> />
{this.props.stats.map((stat, index) => { );
if (index % 2 === 1) { }
return this.renderColumnStat(stat);
} return null;
})} })}
</div>
</div> </div>
</section> </div>
); </article>
}; );
} };
export default ColumnStats; export default ColumnStats;
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
@import 'variables'; @import 'variables';
@import 'typography';
.column-stats { .column-stats {
.stat-collection-info { .stat-collection-info {
...@@ -48,4 +49,8 @@ ...@@ -48,4 +49,8 @@
} }
} }
} }
.stat-title {
@extend %text-title-w3;
}
} }
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
const defaultConfig = {
stats: [],
};
/**
* Generates test data for the table data
* @example
* let testData = new TestDataBuilder()
* .withEightStats()
* .build();
*/
function TestDataBuilder(config = {}) {
this.Klass = TestDataBuilder;
this.config = {
...defaultConfig,
...config,
};
this.withEightStats = () => {
const attr = {
stats: [
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'count',
stat_val: '12345',
},
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'count_null',
stat_val: '123',
},
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'count_distinct',
stat_val: '22',
},
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'count_zero',
stat_val: '44',
},
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'max',
stat_val: '1237466454',
},
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'min',
stat_val: '856',
},
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'avg',
stat_val: '2356575',
},
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'stddev',
stat_val: '1234563',
},
],
};
return new this.Klass(attr);
};
this.withFourStats = () => {
const attr = {
stats: [
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'count',
stat_val: '12345',
},
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'count_null',
stat_val: '123',
},
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'avg',
stat_val: '2356575',
},
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'stddev',
stat_val: '1234563',
},
],
};
return new this.Klass(attr);
};
this.withThreeStats = () => {
const attr = {
stats: [
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'count',
stat_val: '12345',
},
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'avg',
stat_val: '2356575',
},
{
end_epoch: 1571616000,
start_epoch: 1571616000,
stat_type: 'stddev',
stat_val: '1234563',
},
],
};
return new this.Klass(attr);
};
this.withEmptyStats = () => {
const attr = { stats: [] };
return new this.Klass(attr);
};
this.build = () => this.config;
}
export default TestDataBuilder;
...@@ -12,12 +12,12 @@ import Table, { ...@@ -12,12 +12,12 @@ import Table, {
} from 'components/common/Table'; } from 'components/common/Table';
import { logAction } from 'ducks/utilMethods'; import { logAction } from 'ducks/utilMethods';
import { formatDate } from 'utils/dateUtils';
import { notificationsEnabled, getMaxLength } from 'config/config-utils'; import { notificationsEnabled, getMaxLength } from 'config/config-utils';
import { TableColumn, RequestMetadataType } from 'interfaces'; import { TableColumn, RequestMetadataType } from 'interfaces';
import ColumnType from './ColumnType'; import ColumnType from './ColumnType';
import ColumnDescEditableText from './ColumnDescEditableText'; import ColumnDescEditableText from './ColumnDescEditableText';
import { getStatsInfoText } from './utils';
import { import {
MORE_BUTTON_TEXT, MORE_BUTTON_TEXT,
...@@ -86,28 +86,6 @@ const handleRowExpand = (rowValues) => { ...@@ -86,28 +86,6 @@ const handleRowExpand = (rowValues) => {
}); });
}; };
// TODO: Move into utils
const getStatsInfoText = (startEpoch: number, endEpoch: number) => {
const startDate = startEpoch
? formatDate({ epochTimestamp: startEpoch })
: null;
const endDate = endEpoch ? formatDate({ epochTimestamp: endEpoch }) : null;
let infoText = 'Stats reflect data collected';
if (startDate && endDate) {
if (startDate === endDate) {
infoText = `${infoText} on ${startDate} only. (daily partition)`;
} else {
infoText = `${infoText} between ${startDate} and ${endDate}.`;
}
} else {
infoText = `${infoText} over a recent period of time.`;
}
return infoText;
};
// @ts-ignore // @ts-ignore
const ExpandedRowComponent: React.FC<ExpandedRowProps> = ( const ExpandedRowComponent: React.FC<ExpandedRowProps> = (
rowValue: FormattedDataType rowValue: FormattedDataType
......
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
import { getStatsInfoText } from './utils';
describe('ColumnList utils', () => {
describe('getStatsInfoText', () => {
it('generates correct info text for a daily partition', () => {
const startEpoch = 1568160000;
const endEpoch = 1568160000;
const expected = `Stats reflect data collected on Sep 11, 2019 only. (daily partition)`;
const actual = getStatsInfoText(startEpoch, endEpoch);
expect(actual).toEqual(expected);
});
it('generates correct info text for a date range', () => {
const startEpoch = 1568160000;
const endEpoch = 1571616000;
const expected = `Stats reflect data collected between Sep 11, 2019 and Oct 21, 2019.`;
const actual = getStatsInfoText(startEpoch, endEpoch);
expect(actual).toEqual(expected);
});
it('generates correct when no dates are given', () => {
const expected = `Stats reflect data collected over a recent period of time.`;
const actual = getStatsInfoText();
expect(actual).toEqual(expected);
});
});
});
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
import { formatDate } from 'utils/dateUtils';
export const getStatsInfoText = (startEpoch?: number, endEpoch?: number) => {
const startDate = startEpoch
? formatDate({ epochTimestamp: startEpoch })
: null;
const endDate = endEpoch ? formatDate({ epochTimestamp: endEpoch }) : null;
let infoText = 'Stats reflect data collected';
if (startDate && endDate) {
if (startDate === endDate) {
infoText = `${infoText} on ${startDate} only. (daily partition)`;
} else {
infoText = `${infoText} between ${startDate} and ${endDate}.`;
}
} else {
infoText = `${infoText} over a recent period of time.`;
}
return infoText;
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment