Unverified Commit 0bdb4ebf authored by Allison Suarez Miranda's avatar Allison Suarez Miranda Committed by GitHub

feat: surface column level badges (#706)

* adding badges to column and starting badges column work in table
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* tests and adding column badges logic
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* fixed test issues and fixed search bug by forcing badge text to be lower case when searching
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* lint
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* fixed method isse
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* betterer fix
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* lint
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* adding badges to column and starting badges column work in table
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* tests and adding column badges logic
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* fixed test issues and fixed search bug by forcing badge text to be lower case when searching
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* lint
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* build(deps): bump throttle-debounce in /amundsen_application/static (#711)
Signed-off-by: 's avatarMarcos Iglesias Valle <golodhros@gmail.com>

* build(deps-dev): bump @babel/preset-react (#710)
Signed-off-by: 's avatarMarcos Iglesias Valle <golodhros@gmail.com>

* build(deps-dev): bump enzyme-adapter-react-16 (#707)
Signed-off-by: 's avatarMarcos Iglesias Valle <golodhros@gmail.com>

* build(deps-dev): bump jest in /amundsen_application/static (#708)

Bumps [jest](https://github.com/facebook/jest) from 26.4.2 to 26.5.0.
- [Release notes](https://github.com/facebook/jest/releases)
- [Changelog](https://github.com/facebook/jest/blob/master/CHANGELOG.md)
- [Commits](https://github.com/facebook/jest/compare/v26.4.2...v26.5.0)
Signed-off-by: 's avatardependabot-preview[bot] <support@dependabot.com>
Co-authored-by: 's avatardependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>

* chore: Updates Storybook to version 6 (#712)

* Removing story.name
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Adding pre-push hook
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Updating Flag and Card
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Updating Storybook to version 6
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Updating betterer results
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Update eslint config
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Update eslint config
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* Removing pre-push hook as it runs on .src-custom
Signed-off-by: 's avatarMarcos Iglesias <miglesiasvalle@lyft.com>

* fixed method isse
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* betterer fix
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* lint
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* added new table with 4 columns to storybook
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* removed weird package file
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* removed ternary
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* refactored badges, need to clean up all leftover code
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* bits of cleanup and rename donClick to handleClick
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* lint fix
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* added text utils tests
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* got convertText out of Flag
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* lint fixes
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* removed important
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* oopsie
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* Update amundsen_application/static/js/utils/textUtils.ts
Co-authored-by: 's avatarMarcos Iglesias <190833+Golodhros@users.noreply.github.com>

* removed comments and renamed onClick
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>

* type issue
Signed-off-by: 's avatarAllison Suarez Miranda <asuarezmiranda@lyft.com>
Co-authored-by: 's avatardependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com>
Co-authored-by: 's avatarMarcos Iglesias <190833+Golodhros@users.noreply.github.com>
parent 86594b9a
...@@ -4,13 +4,12 @@ ...@@ -4,13 +4,12 @@
import customWebpackConfig from './webpack.config.js'; import customWebpackConfig from './webpack.config.js';
import MiniCssExtractPlugin from 'mini-css-extract-plugin'; import MiniCssExtractPlugin from 'mini-css-extract-plugin';
module.exports = { module.exports = {
stories: ['../js/**/*.story.tsx'], stories: ['../js/**/*.story.tsx'],
addons: [ addons: [
'@storybook/addon-actions', '@storybook/addon-actions',
'@storybook/addon-links', '@storybook/addon-links',
'@storybook/addon-knobs' '@storybook/addon-knobs',
], ],
webpackFinal: (config) => { webpackFinal: (config) => {
return { return {
...@@ -25,10 +24,10 @@ module.exports = { ...@@ -25,10 +24,10 @@ module.exports = {
}, },
plugins: [ plugins: [
new MiniCssExtractPlugin({ new MiniCssExtractPlugin({
filename: "[name].[contenthash].css", filename: '[name].[contenthash].css',
}), }),
...config.plugins ...config.plugins,
] ],
}; };
}, },
}; };
...@@ -3,11 +3,7 @@ ...@@ -3,11 +3,7 @@
import '../css/styles.scss'; import '../css/styles.scss';
const categoriesOrder = [ const categoriesOrder = ['Overview', 'Attributes', 'Components'];
'Overview',
'Attributes',
'Components',
];
export const parameters = { export const parameters = {
options: { options: {
......
...@@ -8,10 +8,8 @@ import configureStore from 'redux-mock-store'; ...@@ -8,10 +8,8 @@ import configureStore from 'redux-mock-store';
import { mocked } from 'ts-jest/utils'; import { mocked } from 'ts-jest/utils';
import { SortDirection } from 'interfaces'; import { SortDirection } from 'interfaces';
import { import { BadgeStyle } from 'config/config-types';
notificationsEnabled, import * as ConfigUtils from 'config/config-utils';
getTableSortCriterias,
} from 'config/config-utils';
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
import ColumnList, { ColumnListProps } from '.'; import ColumnList, { ColumnListProps } from '.';
...@@ -22,8 +20,14 @@ import TestDataBuilder from './testDataBuilder'; ...@@ -22,8 +20,14 @@ import TestDataBuilder from './testDataBuilder';
jest.mock('config/config-utils'); jest.mock('config/config-utils');
const mockedNotificationsEnabled = mocked(notificationsEnabled, true); const mockedNotificationsEnabled = mocked(
const mockedGetTableSortCriterias = mocked(getTableSortCriterias, true); ConfigUtils.notificationsEnabled,
true
);
const mockedGetTableSortCriterias = mocked(
ConfigUtils.getTableSortCriterias,
true
);
const dataBuilder = new TestDataBuilder(); const dataBuilder = new TestDataBuilder();
const middlewares = []; const middlewares = [];
const mockStore = configureStore(middlewares); const mockStore = configureStore(middlewares);
...@@ -232,7 +236,7 @@ describe('ColumnList', () => { ...@@ -232,7 +236,7 @@ describe('ColumnList', () => {
}); });
}); });
describe('when columns with serveral stats including usage are passed', () => { describe('when columns with several stats including usage are passed', () => {
const { columns } = dataBuilder.withSeveralStats().build(); const { columns } = dataBuilder.withSeveralStats().build();
it('should render the usage column', () => { it('should render the usage column', () => {
...@@ -277,5 +281,59 @@ describe('ColumnList', () => { ...@@ -277,5 +281,59 @@ describe('ColumnList', () => {
expect(actual).toEqual(expected); expect(actual).toEqual(expected);
}); });
}); });
describe('when columns with badges are passed', () => {
const { columns } = dataBuilder.withBadges().build();
const getBadgeConfigSpy = jest.spyOn(ConfigUtils, 'getBadgeConfig');
getBadgeConfigSpy.mockImplementation((badgeName: string) => {
return {
displayName: badgeName + ' test name',
style: BadgeStyle.PRIMARY,
};
});
it('should render the rows', () => {
const { wrapper } = setup({ columns });
const expected = columns.length;
const actual = wrapper.find('.table-detail-table .ams-table-row')
.length;
expect(actual).toEqual(expected);
});
it('should render the badge column', () => {
const { wrapper } = setup({ columns });
const expected = columns.length;
const actual = wrapper.find('.badge-list').length;
expect(actual).toEqual(expected);
});
describe('number of bages', () => {
it('should render no badges in the first cell', () => {
const { wrapper } = setup({ columns });
const expected = 0;
const actual = wrapper.find('.badge-list').at(0).find('.flag').length;
expect(actual).toEqual(expected);
});
it('should render one badge in the second cell', () => {
const { wrapper } = setup({ columns });
const expected = 1;
const actual = wrapper.find('.badge-list').at(1).find('.flag').length;
expect(actual).toEqual(expected);
});
it('should render three badges in the third cell', () => {
const { wrapper } = setup({ columns });
const expected = 3;
const actual = wrapper.find('.badge-list').at(2).find('.flag').length;
expect(actual).toEqual(expected);
});
});
});
}); });
}); });
...@@ -24,8 +24,10 @@ import { ...@@ -24,8 +24,10 @@ import {
RequestMetadataType, RequestMetadataType,
SortCriteria, SortCriteria,
SortDirection, SortDirection,
Badge,
} from 'interfaces'; } from 'interfaces';
import BadgeList from 'components/common/BadgeList';
import ColumnType from './ColumnType'; import ColumnType from './ColumnType';
import ColumnDescEditableText from './ColumnDescEditableText'; import ColumnDescEditableText from './ColumnDescEditableText';
import { getStatsInfoText } from './utils'; import { getStatsInfoText } from './utils';
...@@ -82,6 +84,7 @@ type FormattedDataType = { ...@@ -82,6 +84,7 @@ type FormattedDataType = {
name: string; name: string;
sort_order: string; sort_order: string;
isEditable: boolean; isEditable: boolean;
badges: Badge[];
}; };
type ExpandedRowProps = { type ExpandedRowProps = {
...@@ -122,6 +125,15 @@ const getSortingFunction = ( ...@@ -122,6 +125,15 @@ const getSortingFunction = (
: stringSortingFunction; : stringSortingFunction;
}; };
const hasColumnWithBadge = (columns: TableColumn[]) => {
return columns.some((col) => {
if (col.badges) {
return col.badges.length > 0;
}
return false;
});
};
const getUsageStat = (item) => { const getUsageStat = (item) => {
const hasItemStats = !!item.stats.length; const hasItemStats = !!item.stats.length;
...@@ -200,6 +212,7 @@ const ColumnList: React.FC<ColumnListProps> = ({ ...@@ -200,6 +212,7 @@ const ColumnList: React.FC<ColumnListProps> = ({
openRequestDescriptionDialog, openRequestDescriptionDialog,
sortBy = DEFAULT_SORTING, sortBy = DEFAULT_SORTING,
}: ColumnListProps) => { }: ColumnListProps) => {
const hasColumnBadges = hasColumnWithBadge(columns);
const formattedData: FormattedDataType[] = columns.map((item, index) => { const formattedData: FormattedDataType[] = columns.map((item, index) => {
const hasItemStats = !!item.stats.length; const hasItemStats = !!item.stats.length;
...@@ -216,6 +229,7 @@ const ColumnList: React.FC<ColumnListProps> = ({ ...@@ -216,6 +229,7 @@ const ColumnList: React.FC<ColumnListProps> = ({
sort_order: item.sort_order, sort_order: item.sort_order,
usage: getUsageStat(item), usage: getUsageStat(item),
stats: hasItemStats ? item.stats[0] : null, stats: hasItemStats ? item.stats[0] : null,
badges: hasColumnBadges ? item.badges : [],
action: item.name, action: item.name,
name: item.name, name: item.name,
isEditable: item.is_editable, isEditable: item.is_editable,
...@@ -274,6 +288,18 @@ const ColumnList: React.FC<ColumnListProps> = ({ ...@@ -274,6 +288,18 @@ const ColumnList: React.FC<ColumnListProps> = ({
]; ];
} }
if (hasColumnBadges) {
formattedColumns = [
...formattedColumns,
{
title: 'Badges',
field: 'badges',
horAlign: TextAlignmentValues.left,
component: (values) => <BadgeList badges={values} />,
},
];
}
if (notificationsEnabled()) { if (notificationsEnabled()) {
formattedColumns = [ formattedColumns = [
...formattedColumns, ...formattedColumns,
......
...@@ -268,6 +268,89 @@ function TestDataBuilder(config = {}) { ...@@ -268,6 +268,89 @@ function TestDataBuilder(config = {}) {
return new this.Klass(attr); return new this.Klass(attr);
}; };
this.withBadges = () => {
const attr = {
columns: [
{
col_type:
'struct<trigger_event:string,backfill:boolean,graphql_version:string>',
description: null,
is_editable: true,
name: 'complex_column_name_2',
sort_order: 1,
stats: [
{
end_epoch: 1600473600,
start_epoch: 1597881600,
stat_type: 'column_usage',
stat_val: '111',
},
],
badges: [],
},
{
col_type: 'struct<code:string,timezone:string>',
description: null,
is_editable: true,
name: 'complex_column_name_3',
sort_order: 2,
stats: [
{
end_epoch: 1600473600,
start_epoch: 1597881600,
stat_type: 'test_stat',
stat_val: '000',
},
{
end_epoch: 1600473600,
start_epoch: 1597881600,
stat_type: 'column_usage',
stat_val: '222',
},
],
badges: [
{
badge_name: 'Badge Name 1',
category: 'column',
},
],
},
{
col_type:
'struct<route_id:string,shift:struct<shift_id:string,started_at:timestamp,ended_at:timestamp>>',
description: null,
is_editable: true,
name: 'complex_column_name_4',
sort_order: 3,
stats: [
{
end_epoch: 1600473600,
start_epoch: 1597881600,
stat_type: 'column_usage',
stat_val: '333',
},
],
badges: [
{
badge_name: 'Badge Name 1',
category: 'column',
},
{
badge_name: 'Badge Name 2',
category: 'column',
},
{
badge_name: 'Badge Name 3',
category: 'column',
},
],
},
],
};
return new this.Klass(attr);
};
this.withEmptyColumns = () => { this.withEmptyColumns = () => {
const attr = { columns: [] }; const attr = { columns: [] };
......
...@@ -2,13 +2,57 @@ ...@@ -2,13 +2,57 @@
// 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 { Provider } from 'react-redux';
import { mount } from 'enzyme';
import configureStore from 'redux-mock-store';
import ClickableBadge from 'components/common/Badges'; import globalState from 'fixtures/globalState';
import Flag from 'components/common/Flag';
import { BadgeStyle } from 'config/config-types'; import { BadgeStyle } from 'config/config-types';
import * as ConfigUtils from 'config/config-utils'; import * as ConfigUtils from 'config/config-utils';
import { Badge } from 'interfaces/Badges'; import { Badge } from 'interfaces/Badges';
import BadgeList from '.'; import BadgeList, { BadgeListProps } from '.';
const columnBadges: Badge[] = [
{
badge_name: 'col badge 1',
category: 'column',
},
{
badge_name: 'col badge 2',
category: 'column',
},
];
const badges: Badge[] = [
{
badge_name: 'beta',
category: 'table_status',
},
{
badge_name: 'Core Concepts',
category: 'coco',
},
];
const middlewares = [];
const mockStore = configureStore(middlewares);
const setup = (propOverrides?: Partial<BadgeListProps>) => {
const props = {
badges: [],
...propOverrides,
};
const testState = globalState;
testState.tableMetadata.tableData.badges = badges;
const wrapper = mount<BadgeListProps>(
<Provider store={mockStore(testState)}>
<BadgeList {...props} />
</Provider>
);
return { props, wrapper };
};
describe('BadgeList', () => { describe('BadgeList', () => {
const getBadgeConfigSpy = jest.spyOn(ConfigUtils, 'getBadgeConfig'); const getBadgeConfigSpy = jest.spyOn(ConfigUtils, 'getBadgeConfig');
...@@ -19,37 +63,57 @@ describe('BadgeList', () => { ...@@ -19,37 +63,57 @@ describe('BadgeList', () => {
}; };
}); });
describe('BadgeList function component', () => { describe('when no badges are passed', () => {
const badges: Badge[] = [ it('renders a badge-list element', () => {
{ const { wrapper } = setup();
badge_name: 'beta', const expected = 1;
category: 'table_status', const actual = wrapper.find('.badge-list').length;
},
{ expect(actual).toEqual(expected);
badge_name: 'Core Concepts', });
category: 'coco',
},
];
const badgeList = shallow(<BadgeList badges={badges} />); it('does not render any badges', () => {
const { wrapper } = setup();
const actual = wrapper.find(Flag).length;
const expected = 0;
expect(actual).toEqual(expected);
});
});
describe('when badges are passed', () => {
it('renders a badge-list element', () => { it('renders a badge-list element', () => {
const container = badgeList.find('.badge-list'); const { wrapper } = setup({ badges });
expect(container.exists()).toBe(true); const expected = 1;
const actual = wrapper.find('.badge-list').length;
expect(actual).toEqual(expected);
}); });
it('renders a <ClickableBadge> for each badge in the input', () => { it('renders a .actionable-badge for each badge in the input', () => {
expect(badgeList.find(ClickableBadge).length).toEqual(badges.length); const { wrapper } = setup({ badges });
const expected = badges.length;
const actual = wrapper.find('.actionable-badge').length;
expect(actual).toEqual(expected);
}); });
});
describe('when badge category is column', () => {
it('renders a badge-list element', () => {
const { wrapper } = setup({ badges: columnBadges });
const expected = 1;
const actual = wrapper.find('.badge-list').length;
expect(actual).toEqual(expected);
});
it('renders a .static-badge for each badge in the input', () => {
const { wrapper } = setup({ badges: columnBadges });
const expected = 2;
const actual = wrapper.find('.static-badge').length;
it('passes the correct props to the Clickable Badge', () => { expect(actual).toEqual(expected);
badges.forEach((badge, index) => {
const clickableBadge = badgeList.childAt(index);
const clickableBadgeProps = clickableBadge.props();
const badgeConfig = ConfigUtils.getBadgeConfig(badge.badge_name);
expect(clickableBadgeProps.text).toEqual(badgeConfig.displayName);
expect(clickableBadgeProps.labelStyle).toEqual(badgeConfig.style);
});
}); });
}); });
}); });
...@@ -3,37 +3,129 @@ ...@@ -3,37 +3,129 @@
import * as React from 'react'; import * as React from 'react';
import ClickableBadge from 'components/common/Badges';
import { getBadgeConfig } from 'config/config-utils'; import { getBadgeConfig } from 'config/config-utils';
import { convertText, CaseType } from 'utils/textUtils';
import { Badge } from 'interfaces/Badges'; import { Badge } from 'interfaces/Badges';
import { BadgeStyle, BadgeStyleConfig } from 'config/config-types';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
export interface BadgeListProps { import { ResourceType } from 'interfaces';
import { updateSearchState } from 'ducks/search/reducer';
import { UpdateSearchStateRequest } from 'ducks/search/types';
import { logClick } from 'ducks/utilMethods';
import './styles.scss';
const COLUMN_BADGE_CATEGORY = 'column';
export interface ListProps {
badges: Badge[]; badges: Badge[];
} }
const BadgeList: React.FC<BadgeListProps> = ({ badges }: BadgeListProps) => { export interface DispatchFromProps {
searchBadge: (badgeText: string) => UpdateSearchStateRequest;
}
export interface ActionableBadgeProps {
style: BadgeStyle;
displayName: string;
action: any;
}
export type BadgeListProps = ListProps & DispatchFromProps;
const StaticBadge: React.FC<BadgeStyleConfig> = ({
style,
displayName,
}: BadgeStyleConfig) => {
return ( return (
<span className="badge-list"> <span className={`static-badge flag label label-${style}`}>
{badges.map((badge, index) => { <div className={`badge-overlay-${style}`}>{displayName}</div>
let badgeConfig;
// search badges with just name
if (badge.tag_name) {
badgeConfig = getBadgeConfig(badge.tag_name);
}
// metadata badges with name and category
else if (badge.badge_name) {
badgeConfig = getBadgeConfig(badge.badge_name);
}
return (
<ClickableBadge
text={badgeConfig.displayName}
labelStyle={badgeConfig.style}
key={`badge-${index}`}
/>
);
})}
</span> </span>
); );
}; };
export default BadgeList; const ActionableBadge: React.FC<ActionableBadgeProps> = ({
style,
displayName,
action,
}: ActionableBadgeProps) => {
return (
<span className="actionable-badge" onClick={action}>
<StaticBadge style={style} displayName={displayName} />
</span>
);
};
export class BadgeList extends React.Component<BadgeListProps> {
idx = 0;
handleClick = (e) => {
const badgeText = this.props.badges[this.idx].badge_name
? this.props.badges[this.idx].badge_name
: this.props.badges[this.idx].tag_name;
logClick(e, {
target_type: 'badge',
label: badgeText,
});
this.props.searchBadge(convertText(badgeText, CaseType.LOWER_CASE));
};
render() {
return (
<span className="badge-list">
{this.props.badges.map((badge, index) => {
let badgeConfig;
// search badges with just name
if (badge.tag_name) {
badgeConfig = getBadgeConfig(badge.tag_name);
}
// metadata badges with name and category
else if (badge.badge_name) {
badgeConfig = getBadgeConfig(badge.badge_name);
if (badge.category === COLUMN_BADGE_CATEGORY) {
return (
<StaticBadge
style={badgeConfig.style}
displayName={badgeConfig.displayName}
key={`badge-${index}`}
/>
);
}
}
if (badge.category !== COLUMN_BADGE_CATEGORY) {
return (
<ActionableBadge
displayName={badgeConfig.displayName}
style={badgeConfig.style}
action={this.handleClick}
key={`badge-${index}`}
/>
);
}
this.idx++;
})}
</span>
);
}
}
export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators(
{
searchBadge: (badgeText: string) =>
updateSearchState({
filters: {
[ResourceType.table]: { badges: badgeText },
},
submitSearch: true,
}),
},
dispatch
);
};
export default connect<null, DispatchFromProps, ListProps>(
null,
mapDispatchToProps
)(BadgeList);
...@@ -3,7 +3,21 @@ ...@@ -3,7 +3,21 @@
@import 'variables'; @import 'variables';
.clickable-badge { .flag {
border-radius: 5px;
display: inline-block;
font-size: $font-size-base;
height: $badge-height;
margin: 0 0 0 $spacer-1;
}
.label {
border-radius: 10px;
font-weight: normal;
}
.actionable-badge {
&:hover { &:hover {
cursor: pointer; cursor: pointer;
} }
...@@ -38,36 +52,30 @@ ...@@ -38,36 +52,30 @@
.badge-overlay-neutral, .badge-overlay-neutral,
.badge-overlay-positive, .badge-overlay-positive,
.badge-overlay-primary { .badge-overlay-primary {
&:hover, &:hover,
&:focus { &:focus {
background-color: rgba( background-color: rgba($color: $badge-overlay,
$color: $badge-overlay, $alpha: $badge-opacity-light);
$alpha: $badge-opacity-light
);
} }
&:active { &:active {
background-color: rgba( background-color: rgba($color: $badge-overlay,
$color: $badge-overlay, $alpha: $badge-pressed-light);
$alpha: $badge-pressed-light
);
} }
} }
.badge-overlay-warning { .badge-overlay-warning {
&:hover, &:hover,
&:focus { &:focus {
background-color: rgba( background-color: rgba($color: $badge-overlay,
$color: $badge-overlay, $alpha: $badge-opacity-dark);
$alpha: $badge-opacity-dark
);
} }
&:active { &:active {
background-color: rgba( background-color: rgba($color: $badge-overlay,
$color: $badge-overlay, $alpha: $badge-pressed-dark);
$alpha: $badge-pressed-dark
);
} }
} }
} }
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
import * as React from 'react';
import { shallow } from 'enzyme';
import Flag from 'components/common/Flag';
import { BadgeStyle } from 'config/config-types';
import { updateSearchState } from 'ducks/search/reducer';
import * as UtilMethods from 'ducks/utilMethods';
import { ClickableBadge, ClickableBadgeProps, mapDispatchToProps } from '.';
const logClickSpy = jest.spyOn(UtilMethods, 'logClick');
logClickSpy.mockImplementation(() => null);
jest.mock('ducks/search/reducer', () => ({
updateSearchState: jest.fn(),
}));
describe('ClickableBadge', () => {
const setup = (propOverrides?: Partial<ClickableBadgeProps>) => {
const props = {
text: 'test_badge',
labelStyle: BadgeStyle.PRIMARY,
searchBadge: jest.fn(),
...propOverrides,
};
const wrapper = shallow(<ClickableBadge {...props} />);
return { props, wrapper };
};
describe('onClick', () => {
let props;
let wrapper;
const mockEvent = {};
beforeAll(() => {
const setupResult = setup();
props = setupResult.props;
wrapper = setupResult.wrapper;
});
it('Calls the logClick utility function', () => {
logClickSpy.mockClear();
const expectedData = {
target_type: 'badge',
label: props.text,
};
wrapper.instance().onClick(mockEvent);
expect(logClickSpy).toHaveBeenCalledWith(mockEvent, expectedData);
});
it('it calls searchBadge', () => {
wrapper.instance().onClick(mockEvent);
expect(props.searchBadge).toHaveBeenCalledWith(props.text);
});
});
describe('render', () => {
let props;
let wrapper;
beforeAll(() => {
const setupResult = setup();
wrapper = setupResult.wrapper;
props = setupResult.props;
});
it('renders a <Flag> for the ClickableBadge', () => {
const flagPerBadge = 1;
expect(wrapper.find(Flag).length).toEqual(flagPerBadge);
});
it('renders with correct text', () => {
expect(wrapper.find(Flag).props().text).toEqual(props.text);
});
});
});
describe('mapDispatchToProps', () => {
let dispatch;
let result;
beforeAll(() => {
dispatch = jest.fn(() => Promise.resolve());
result = mapDispatchToProps(dispatch);
});
it('sets searchBadge on the props to trigger desired action', () => {
result.searchBadge();
expect(updateSearchState).toHaveBeenCalled();
});
});
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
import * as React from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import Flag, { FlagProps } from 'components/common/Flag';
import { ResourceType } from 'interfaces';
import { updateSearchState } from 'ducks/search/reducer';
import { UpdateSearchStateRequest } from 'ducks/search/types';
import { logClick } from 'ducks/utilMethods';
import './styles.scss';
export interface DispatchFromProps {
searchBadge: (badgeText: string) => UpdateSearchStateRequest;
}
export type ClickableBadgeProps = FlagProps & DispatchFromProps;
export class ClickableBadge extends React.Component<ClickableBadgeProps> {
onClick = (e) => {
const badgeText = this.props.text;
logClick(e, {
target_type: 'badge',
label: badgeText,
});
this.props.searchBadge(badgeText);
};
render() {
return (
<span className="clickable-badge" onClick={this.onClick}>
<Flag
caseType={this.props.caseType}
text={this.props.text}
labelStyle={this.props.labelStyle}
/>
</span>
);
}
}
export const mapDispatchToProps = (dispatch: any) => {
return bindActionCreators(
{
searchBadge: (badgeText: string) =>
updateSearchState({
filters: {
[ResourceType.table]: { badges: badgeText },
},
submitSearch: true,
}),
},
dispatch
);
};
export default connect<null, DispatchFromProps, FlagProps>(
null,
mapDispatchToProps
)(ClickableBadge);
...@@ -4,8 +4,9 @@ ...@@ -4,8 +4,9 @@
import React from 'react'; import React from 'react';
import { BadgeStyle } from 'config/config-types'; import { BadgeStyle } from 'config/config-types';
import { CaseType } from 'utils/textUtils';
import StorySection from '../StorySection'; import StorySection from '../StorySection';
import Flag, { CaseType } from '.'; import Flag from '.';
export default { export default {
title: 'Components/Flags', title: 'Components/Flags',
......
...@@ -6,7 +6,7 @@ import * as React from 'react'; ...@@ -6,7 +6,7 @@ import * as React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import { BadgeStyle } from 'config/config-types'; import { BadgeStyle } from 'config/config-types';
import Flag, { CaseType, FlagProps, convertText } from '.'; import Flag, { FlagProps } from '.';
describe('Flag', () => { describe('Flag', () => {
let props: FlagProps; let props: FlagProps;
...@@ -38,30 +38,4 @@ describe('Flag', () => { ...@@ -38,30 +38,4 @@ describe('Flag', () => {
expect(subject.find('span').text()).toEqual(props.text); expect(subject.find('span').text()).toEqual(props.text);
}); });
}); });
describe('convertText', () => {
let text;
beforeEach(() => {
text = 'RandOM teXT';
});
it('returns lowercase text if caseType=CaseType.LOWER_CASE', () => {
expect(convertText(text, CaseType.LOWER_CASE)).toEqual('random text');
});
it('returns UPPERCASE text if caseType=CaseType.UPPER_CASE', () => {
expect(convertText(text, CaseType.UPPER_CASE)).toEqual('RANDOM TEXT');
});
it('returns Sentence case text if caseType=CaseType.SENTENCE_CASE', () => {
expect(convertText(text, CaseType.SENTENCE_CASE)).toEqual('Random text');
});
it('returns text in defauilt case', () => {
expect(convertText(text, 'not a valid options')).toEqual(text);
});
it('returns empty strings for undefined values', () => {
expect(convertText(undefined, CaseType.SENTENCE_CASE)).toEqual('');
});
});
}); });
...@@ -4,34 +4,16 @@ ...@@ -4,34 +4,16 @@
import * as React from 'react'; import * as React from 'react';
import { BadgeStyle } from 'config/config-types'; import { BadgeStyle } from 'config/config-types';
import { convertText, CaseType } from 'utils/textUtils';
import './styles.scss'; import './styles.scss';
export enum CaseType {
LOWER_CASE = 'lowerCase',
SENTENCE_CASE = 'sentenceCase',
UPPER_CASE = 'upperCase',
}
export interface FlagProps { export interface FlagProps {
caseType?: string | null; caseType?: CaseType | null;
text: string; text: string;
labelStyle?: BadgeStyle; labelStyle?: BadgeStyle;
} }
export function convertText(str = '', caseType: string): string {
switch (caseType) {
case CaseType.LOWER_CASE:
return str.toLowerCase();
case CaseType.SENTENCE_CASE:
return `${str.charAt(0).toUpperCase()}${str.slice(1).toLowerCase()}`;
case CaseType.UPPER_CASE:
return str.toUpperCase();
default:
return str;
}
}
const Flag: React.FC<FlagProps> = ({ const Flag: React.FC<FlagProps> = ({
caseType = null, caseType = null,
text = '', text = '',
......
...@@ -27,29 +27,22 @@ $row-bottom-border-color: $gray10; ...@@ -27,29 +27,22 @@ $row-bottom-border-color: $gray10;
background-color: $table-header-background-color; background-color: $table-header-background-color;
color: $text-secondary; color: $text-secondary;
border-bottom: $table-header-border-width solid border-bottom: $table-header-border-width solid $table-header-bottom-border-color;
$table-header-bottom-border-color;
} }
.ams-table-heading-cell { .ams-table-heading-cell {
height: $table-header-height; height: $table-header-height;
text-transform: uppercase; text-transform: uppercase;
&:first-child {
padding-left: $spacer-3;
}
&:last-child {
padding-right: $spacer-3;
}
&.is-left-aligned { &.is-left-aligned {
text-align: left; text-align: left;
padding-right: $spacer-1; padding-right: $spacer-1;
padding-left: $spacer-1;
} }
&.is-right-aligned { &.is-right-aligned {
text-align: right; text-align: right;
padding-right: $spacer-1;
padding-left: $spacer-1; padding-left: $spacer-1;
} }
...@@ -58,6 +51,14 @@ $row-bottom-border-color: $gray10; ...@@ -58,6 +51,14 @@ $row-bottom-border-color: $gray10;
padding-left: $spacer-1; padding-left: $spacer-1;
padding-right: $spacer-1; padding-right: $spacer-1;
} }
&:first-child {
padding-left: $spacer-3;
}
&:last-child {
padding-right: $spacer-3;
}
} }
.ams-table-row { .ams-table-row {
...@@ -81,21 +82,15 @@ $row-bottom-border-color: $gray10; ...@@ -81,21 +82,15 @@ $row-bottom-border-color: $gray10;
.ams-table-cell { .ams-table-cell {
overflow: visible; overflow: visible;
&:first-child {
padding-left: $spacer-3;
}
&:last-child {
padding-right: $spacer-3;
}
&.is-left-aligned { &.is-left-aligned {
text-align: left; text-align: left;
padding-right: $spacer-1; padding-right: $spacer-1;
padding-left: $spacer-1;
} }
&.is-right-aligned { &.is-right-aligned {
text-align: right; text-align: right;
padding-right: $spacer-1;
padding-left: $spacer-1; padding-left: $spacer-1;
} }
...@@ -104,6 +99,15 @@ $row-bottom-border-color: $gray10; ...@@ -104,6 +99,15 @@ $row-bottom-border-color: $gray10;
padding-left: $spacer-1; padding-left: $spacer-1;
padding-right: $spacer-1; padding-right: $spacer-1;
} }
&:first-child {
padding-left: $spacer-3;
}
&:last-child {
padding-right: $spacer-3;
}
} }
.ams-table-expanding-button { .ams-table-expanding-button {
...@@ -119,6 +123,7 @@ $row-bottom-border-color: $gray10; ...@@ -119,6 +123,7 @@ $row-bottom-border-color: $gray10;
&:hover svg use { &:hover svg use {
fill: $gray60; fill: $gray60;
} }
// TODO: Fix this so it is accessible // TODO: Fix this so it is accessible
&:focus { &:focus {
outline: none; outline: none;
......
...@@ -17,6 +17,10 @@ const { ...@@ -17,6 +17,10 @@ const {
const { const {
columns: differentWidthColumns, columns: differentWidthColumns,
} = dataBuilder.withFixedWidthColumns().build(); } = dataBuilder.withFixedWidthColumns().build();
const {
columns: fourColumns,
data: fourColData,
} = dataBuilder.withFourColumns().build();
const { const {
columns: customColumns, columns: customColumns,
data: customColumnsData, data: customColumnsData,
...@@ -88,6 +92,13 @@ export const StyledTable = () => ( ...@@ -88,6 +92,13 @@ export const StyledTable = () => (
options={{ rowHeight: 50 }} options={{ rowHeight: 50 }}
/> />
</StorySection> </StorySection>
<StorySection title="with four columns">
<Table
columns={fourColumns}
data={fourColData}
options={{ rowHeight: 50 }}
/>
</StorySection>
</> </>
); );
......
...@@ -227,6 +227,40 @@ function TestDataBuilder(config = {}) { ...@@ -227,6 +227,40 @@ function TestDataBuilder(config = {}) {
return new this.Klass(attr); return new this.Klass(attr);
}; };
this.withFourColumns = () => {
const attr = {
data: [
{ name: 'rowName', type: 'rowType', value: 1, usage: 4 },
{ name: 'rowName2', type: 'rowType2', value: 2, usage: 12 },
{ name: 'rowName3', type: 'rowType3', value: 3, usage: 7 },
],
columns: [
{
title: 'Name',
field: 'name',
horAlign: 'left',
},
{
title: 'Type',
field: 'type',
horAlign: 'right',
},
{
title: 'Value',
field: 'value',
horAlign: 'left',
},
{
title: 'Usage',
field: 'usage',
horAlign: 'left',
},
],
};
return new this.Klass(attr);
};
this.withFixedWidthColumns = () => { this.withFixedWidthColumns = () => {
const attr = { const attr = {
data: [...this.config.data], data: [...this.config.data],
......
import AppConfig from 'config/config'; import AppConfig from 'config/config';
import { BadgeStyle, BadgeStyleConfig } from 'config/config-types'; import { BadgeStyle, BadgeStyleConfig } from 'config/config-types';
import { TableMetadata } from 'interfaces/TableMetadata'; import { TableMetadata } from 'interfaces/TableMetadata';
import { convertText, CaseType } from 'utils/textUtils';
import { FilterConfig, LinkConfig } from './config-types'; import { FilterConfig, LinkConfig } from './config-types';
...@@ -85,7 +86,7 @@ export function getBadgeConfig(badgeName: string): BadgeStyleConfig { ...@@ -85,7 +86,7 @@ export function getBadgeConfig(badgeName: string): BadgeStyleConfig {
return { return {
style: BadgeStyle.DEFAULT, style: BadgeStyle.DEFAULT,
displayName: badgeName, displayName: convertText(badgeName, CaseType.TITLE_CASE),
...config, ...config,
}; };
} }
......
...@@ -107,7 +107,7 @@ describe('getBadgeConfig', () => { ...@@ -107,7 +107,7 @@ describe('getBadgeConfig', () => {
}); });
it('Returns default badge config for unspecified badges', () => { it('Returns default badge config for unspecified badges', () => {
const badgeName = 'not_configured_badge'; const badgeName = 'Not_configured_badge';
const badgeConfig = ConfigUtils.getBadgeConfig(badgeName); const badgeConfig = ConfigUtils.getBadgeConfig(badgeName);
expect(badgeConfig.style).toEqual(BadgeStyle.DEFAULT); expect(badgeConfig.style).toEqual(BadgeStyle.DEFAULT);
expect(badgeConfig.displayName).toEqual(badgeName); expect(badgeConfig.displayName).toEqual(badgeName);
......
...@@ -39,6 +39,7 @@ export const tableMetadata: TableMetadata = { ...@@ -39,6 +39,7 @@ export const tableMetadata: TableMetadata = {
stat_val: '992487', stat_val: '992487',
}, },
], ],
badges: [],
}, },
{ {
col_type: 'string', col_type: 'string',
...@@ -48,6 +49,7 @@ export const tableMetadata: TableMetadata = { ...@@ -48,6 +49,7 @@ export const tableMetadata: TableMetadata = {
sort_order: '1', sort_order: '1',
name: 'ds', name: 'ds',
stats: [], stats: [],
badges: [],
}, },
{ {
col_type: 'string', col_type: 'string',
...@@ -93,6 +95,7 @@ export const tableMetadata: TableMetadata = { ...@@ -93,6 +95,7 @@ export const tableMetadata: TableMetadata = {
stat_val: '24', stat_val: '24',
}, },
], ],
badges: [],
}, },
], ],
database: 'hive', database: 'hive',
......
...@@ -63,6 +63,7 @@ export interface TableColumn { ...@@ -63,6 +63,7 @@ export interface TableColumn {
col_type: string; col_type: string;
sort_order: string; sort_order: string;
stats: TableColumnStats[]; stats: TableColumnStats[];
badges: Badge[];
} }
export interface TableOwners { export interface TableOwners {
......
...@@ -7,3 +7,4 @@ export * from './TableMetadata'; ...@@ -7,3 +7,4 @@ export * from './TableMetadata';
export * from './Tags'; export * from './Tags';
export * from './User'; export * from './User';
export * from './Issue'; export * from './Issue';
export * from './Badges';
...@@ -2,6 +2,51 @@ import { ResourceType } from 'interfaces/Resources'; ...@@ -2,6 +2,51 @@ import { ResourceType } from 'interfaces/Resources';
import * as DateUtils from './dateUtils'; import * as DateUtils from './dateUtils';
import * as LogUtils from './logUtils'; import * as LogUtils from './logUtils';
import * as NavigationUtils from './navigationUtils'; import * as NavigationUtils from './navigationUtils';
import * as TextUtils from './textUtils';
describe('textUtils', () => {
describe('convertText', () => {
it('converts to lower case', () => {
const actual = TextUtils.convertText(
'TESt LoWer cASe',
TextUtils.CaseType.LOWER_CASE
);
const expected = 'test lower case';
expect(actual).toEqual(expected);
});
it('converts to sentence case', () => {
const actual = TextUtils.convertText(
'tESt sentence cASe',
TextUtils.CaseType.SENTENCE_CASE
);
const expected = 'Test sentence case';
expect(actual).toEqual(expected);
});
it('converts to upper case', () => {
const actual = TextUtils.convertText(
'test upper cASe',
TextUtils.CaseType.UPPER_CASE
);
const expected = 'TEST UPPER CASE';
expect(actual).toEqual(expected);
});
it('converts to title case', () => {
const actual = TextUtils.convertText(
'tEST title cASe',
TextUtils.CaseType.TITLE_CASE
);
const expected = 'Test Title Case';
expect(actual).toEqual(expected);
});
});
});
describe('navigationUtils', () => { describe('navigationUtils', () => {
describe('updateSearchUrl', () => { describe('updateSearchUrl', () => {
......
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
export enum CaseType {
LOWER_CASE = 'lowerCase',
SENTENCE_CASE = 'sentenceCase',
UPPER_CASE = 'upperCase',
TITLE_CASE = 'titleCase',
}
export function convertText(str = '', caseType: CaseType): string {
switch (caseType) {
case CaseType.LOWER_CASE:
return str.toLowerCase();
case CaseType.SENTENCE_CASE:
return `${str.charAt(0).toUpperCase()}${str.slice(1).toLowerCase()}`;
case CaseType.UPPER_CASE:
return str.toUpperCase();
case CaseType.TITLE_CASE:
const splitStr = str.toLowerCase().split(' ');
for (let i = 0; i < splitStr.length; i++) {
splitStr[i] =
splitStr[i].charAt(0).toUpperCase() + splitStr[i].substring(1);
}
return splitStr.join(' ');
default:
return str;
}
}
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