Unverified Commit 36aebd28 authored by Marcos Iglesias's avatar Marcos Iglesias Committed by GitHub

feat: Adds sortable table detail page (#691)

Signed-off-by: 's avatarMarcos Iglesias Valle <golodhros@gmail.com>
parent 7a14f5f8
...@@ -99,8 +99,8 @@ exports[`strict null compilation`] = { ...@@ -99,8 +99,8 @@ exports[`strict null compilation`] = {
[255, 6, 11, "No overload matches this call.\\n The last overload gave the following error.\\n Type \'(() => SubmitSearchRequest) | null\' is not assignable to type \'ActionCreator<any>\'.\\n Type \'null\' is not assignable to type \'ActionCreator<any>\'.", "2296208050"], [255, 6, 11, "No overload matches this call.\\n The last overload gave the following error.\\n Type \'(() => SubmitSearchRequest) | null\' is not assignable to type \'ActionCreator<any>\'.\\n Type \'null\' is not assignable to type \'ActionCreator<any>\'.", "2296208050"],
[270, 4, 18, "No overload matches this call.\\n The last overload gave the following error.\\n Argument of type \'(dispatch: any, ownProps: any) => ActionCreator<unknown>\' is not assignable to parameter of type \'DispatchFromProps\'.\\n Type \'(dispatch: any, ownProps: any) => ActionCreator<unknown>\' is missing the following properties from type \'DispatchFromProps\': submitSearch, onInputChange, onSelectInlineResult", "2926224796"] [270, 4, 18, "No overload matches this call.\\n The last overload gave the following error.\\n Argument of type \'(dispatch: any, ownProps: any) => ActionCreator<unknown>\' is not assignable to parameter of type \'DispatchFromProps\'.\\n Type \'(dispatch: any, ownProps: any) => ActionCreator<unknown>\' is missing the following properties from type \'DispatchFromProps\': submitSearch, onInputChange, onSelectInlineResult", "2926224796"]
], ],
"js/components/common/Table/index.tsx:3110699653": [ "js/components/common/Table/index.tsx:489040313": [
[220, 22, 13, "Type \'unknown\' is not assignable to type \'ReactNode\'.\\n Type \'unknown\' is not assignable to type \'ReactPortal\'.", "971959308"] [235, 22, 13, "Type \'unknown\' is not assignable to type \'ReactNode\'.\\n Type \'unknown\' is not assignable to type \'ReactPortal\'.", "971959308"]
], ],
"js/components/common/Tags/TagInput/index.tsx:3754832290": [ "js/components/common/Tags/TagInput/index.tsx:3754832290": [
[63, 22, 6, "Type \'undefined\' is not assignable to type \'GetAllTagsRequest\'.", "1979467425"], [63, 22, 6, "Type \'undefined\' is not assignable to type \'GetAllTagsRequest\'.", "1979467425"],
...@@ -124,10 +124,10 @@ exports[`strict null compilation`] = { ...@@ -124,10 +124,10 @@ exports[`strict null compilation`] = {
[93, 4, 11, "Type \'Tag[]\' is not assignable to type \'never[]\'.", "255414113"], [93, 4, 11, "Type \'Tag[]\' is not assignable to type \'never[]\'.", "255414113"],
[98, 4, 9, "Type \'Tag[]\' is not assignable to type \'never[]\'.", "3803340896"] [98, 4, 9, "Type \'Tag[]\' is not assignable to type \'never[]\'.", "3803340896"]
], ],
"js/config/config-utils.ts:1027600130": [ "js/config/config-utils.ts:1798174672": [
[86, 4, 25, "\'style\' is specified more than once, so this usage will be overwritten.", "1214862559"] [86, 4, 25, "\'style\' is specified more than once, so this usage will be overwritten.", "1214862559"]
], ],
"js/config/index.spec.ts:1695312294": [ "js/config/index.spec.ts:3155903042": [
[17, 6, 61, "Object is possibly \'undefined\'.", "1496333578"], [17, 6, 61, "Object is possibly \'undefined\'.", "1496333578"],
[51, 6, 61, "Object is possibly \'undefined\'.", "1496333578"] [51, 6, 61, "Object is possibly \'undefined\'.", "1496333578"]
], ],
...@@ -246,12 +246,6 @@ exports[`strict null compilation`] = { ...@@ -246,12 +246,6 @@ exports[`strict null compilation`] = {
"js/fixtures/globalState.ts:3931474038": [ "js/fixtures/globalState.ts:3931474038": [
[69, 4, 12, "Type \'null\' is not assignable to type \'string\'.", "124336133"] [69, 4, 12, "Type \'null\' is not assignable to type \'string\'.", "124336133"]
], ],
"js/fixtures/metadata/table.ts:2912527294": [
[52, 6, 11, "Type \'null\' is not assignable to type \'string\'.", "1848305091"],
[171, 4, 11, "Type \'null\' is not assignable to type \'string\'.", "1848305091"],
[183, 4, 11, "Type \'null\' is not assignable to type \'string\'.", "1848305091"],
[195, 4, 11, "Type \'null\' is not assignable to type \'string\'.", "1848305091"]
],
"js/fixtures/mockRouter.ts:3563515077": [ "js/fixtures/mockRouter.ts:3563515077": [
[30, 6, 4, "Type \'null\' is not assignable to type \'{ (path: string, state?: {} | null | undefined): void; (location: LocationDescriptorObject<{} | null | undefined>): void; }\'.", "2088074939"], [30, 6, 4, "Type \'null\' is not assignable to type \'{ (path: string, state?: {} | null | undefined): void; (location: LocationDescriptorObject<{} | null | undefined>): void; }\'.", "2088074939"],
[31, 6, 7, "Type \'null\' is not assignable to type \'{ (path: string, state?: {} | null | undefined): void; (location: LocationDescriptorObject<{} | null | undefined>): void; }\'.", "1364993353"], [31, 6, 7, "Type \'null\' is not assignable to type \'{ (path: string, state?: {} | null | undefined): void; (location: LocationDescriptorObject<{} | null | undefined>): void; }\'.", "1364993353"],
...@@ -329,8 +323,8 @@ exports[`strict null compilation`] = { ...@@ -329,8 +323,8 @@ exports[`strict null compilation`] = {
"js/pages/TableDetailPage/SourceLink/index.spec.tsx:4194369848": [ "js/pages/TableDetailPage/SourceLink/index.spec.tsx:4194369848": [
[42, 21, 100, "Object is possibly \'null\'.", "1316242242"] [42, 21, 100, "Object is possibly \'null\'.", "1316242242"]
], ],
"js/pages/TableDetailPage/TableDashboardResourceList/index.tsx:3147978263": [ "js/pages/TableDetailPage/TableDashboardResourceList/index.tsx:3276822301": [
[64, 2, 15, "No overload matches this call.\\n The last overload gave the following error.\\n Argument of type \'(state: GlobalState) => { dashboards: DashboardResource[]; isLoading: boolean; errorText: string | undefined; }\' is not assignable to parameter of type \'MapStateToPropsParam<StateFromProps, OwnProps, {}>\'.\\n Type \'(state: GlobalState) => { dashboards: DashboardResource[]; isLoading: boolean; errorText: string | undefined; }\' is not assignable to type \'MapStateToPropsFactory<StateFromProps, OwnProps, {}>\'.\\n Type \'{ dashboards: DashboardResource[]; isLoading: boolean; errorText: string | undefined; }\' is not assignable to type \'MapStateToProps<StateFromProps, OwnProps, {}>\'.\\n Type \'{ dashboards: DashboardResource[]; isLoading: boolean; errorText: string | undefined; }\' provides no match for the signature \'(state: {}, ownProps: OwnProps): StateFromProps\'.", "1389821531"] [65, 2, 15, "No overload matches this call.\\n The last overload gave the following error.\\n Argument of type \'(state: GlobalState) => { dashboards: DashboardResource[]; isLoading: boolean; errorText: string | undefined; }\' is not assignable to parameter of type \'MapStateToPropsParam<StateFromProps, OwnProps, {}>\'.\\n Type \'(state: GlobalState) => { dashboards: DashboardResource[]; isLoading: boolean; errorText: string | undefined; }\' is not assignable to type \'MapStateToPropsFactory<StateFromProps, OwnProps, {}>\'.\\n Type \'{ dashboards: DashboardResource[]; isLoading: boolean; errorText: string | undefined; }\' is not assignable to type \'MapStateToProps<StateFromProps, OwnProps, {}>\'.\\n Type \'{ dashboards: DashboardResource[]; isLoading: boolean; errorText: string | undefined; }\' provides no match for the signature \'(state: {}, ownProps: OwnProps): StateFromProps\'.", "1389821531"]
], ],
"js/pages/TableDetailPage/TableOwnerEditor/index.spec.tsx:3400494524": [ "js/pages/TableDetailPage/TableOwnerEditor/index.spec.tsx:3400494524": [
[28, 10, 6, "Type \'{ [x: string]: { manager_id: null; manager_fullname: null; manager_email: null; profile_url: string; role_name: null; display_name: null; github_username: null; team_name: null; last_name: null; full_name: null; ... 6 more ...; user_id: string; }; }\' is not assignable to type \'OwnerDict\'.\\n Index signatures are incompatible.\\n Type \'{ manager_id: null; manager_fullname: null; manager_email: null; profile_url: string; role_name: null; display_name: null; github_username: null; team_name: null; last_name: null; full_name: null; ... 6 more ...; user_id: string; }\' is not assignable to type \'User\'.", "1719502071"] [28, 10, 6, "Type \'{ [x: string]: { manager_id: null; manager_fullname: null; manager_email: null; profile_url: string; role_name: null; display_name: null; github_username: null; team_name: null; last_name: null; full_name: null; ... 6 more ...; user_id: string; }; }\' is not assignable to type \'OwnerDict\'.\\n Index signatures are incompatible.\\n Type \'{ manager_id: null; manager_fullname: null; manager_email: null; profile_url: string; role_name: null; display_name: null; github_username: null; team_name: null; last_name: null; full_name: null; ... 6 more ...; user_id: string; }\' is not assignable to type \'User\'.", "1719502071"]
...@@ -341,16 +335,13 @@ exports[`strict null compilation`] = { ...@@ -341,16 +335,13 @@ exports[`strict null compilation`] = {
"js/pages/TableDetailPage/WatermarkLabel/index.tsx:1354016727": [ "js/pages/TableDetailPage/WatermarkLabel/index.tsx:1354016727": [
[80, 34, 3, "Argument of type \'string | null\' is not assignable to parameter of type \'string\'.\\n Type \'null\' is not assignable to type \'string\'.", "193412913"] [80, 34, 3, "Argument of type \'string | null\' is not assignable to parameter of type \'string\'.\\n Type \'null\' is not assignable to type \'string\'.", "193412913"]
], ],
"js/pages/TableDetailPage/index.spec.tsx:3148704474": [ "js/pages/TableDetailPage/index.spec.tsx:120018363": [
[32, 4, 8, "Argument of type \'Partial<Location<{} | null | undefined>> | undefined\' is not assignable to parameter of type \'Partial<Location<{} | null | undefined>>\'.\\n Type \'undefined\' is not assignable to type \'Partial<Location<{} | null | undefined>>\'.", "2700611480"] [33, 4, 8, "Argument of type \'Partial<Location<{} | null | undefined>> | undefined\' is not assignable to parameter of type \'Partial<Location<{} | null | undefined>>\'.\\n Type \'undefined\' is not assignable to type \'Partial<Location<{} | null | undefined>>\'.", "2700611480"]
], ],
"js/pages/TableDetailPage/index.tsx:3027031293": [ "js/pages/TableDetailPage/index.tsx:987689418": [
[160, 10, 13, "Type \'null\' is not assignable to type \'((newValue: string, onSuccess?: (() => any) | undefined, onFailure?: (() => any) | undefined) => void) | undefined\'.", "67794331"], [181, 10, 13, "Type \'null\' is not assignable to type \'((newValue: string, onSuccess?: (() => any) | undefined, onFailure?: (() => any) | undefined) => void) | undefined\'.", "67794331"],
[199, 11, 26, "Type \'{ itemsPerPage: number; source: string; }\' is missing the following properties from type \'Readonly<Pick<TableDashboardResourceListProps, \\"source\\" | \\"isLoading\\" | \\"dashboards\\" | \\"itemsPerPage\\" | \\"errorText\\"> & OwnProps>\': isLoading, dashboards, errorText", "2224258167"], [236, 11, 26, "Type \'{ itemsPerPage: number; source: string; }\' is missing the following properties from type \'Readonly<Pick<TableDashboardResourceListProps, \\"source\\" | \\"isLoading\\" | \\"dashboards\\" | \\"itemsPerPage\\" | \\"errorText\\"> & OwnProps>\': isLoading, dashboards, errorText", "2224258167"],
[279, 16, 7, "Type \'string | null\' is not assignable to type \'string | undefined\'.\\n Type \'null\' is not assignable to type \'string | undefined\'.", "3817619378"], [325, 16, 7, "Type \'string | null\' is not assignable to type \'string | undefined\'.\\n Type \'null\' is not assignable to type \'string | undefined\'.", "3817619378"]
[327, 20, 35, "Argument of type \'ProgrammaticDescription[] | undefined\' is not assignable to parameter of type \'ProgrammaticDescription[]\'.\\n Type \'undefined\' is not assignable to type \'ProgrammaticDescription[]\'.", "4249007202"],
[341, 20, 36, "Argument of type \'ProgrammaticDescription[] | undefined\' is not assignable to parameter of type \'ProgrammaticDescription[]\'.\\n Type \'undefined\' is not assignable to type \'ProgrammaticDescription[]\'.", "2770872537"],
[346, 16, 36, "Argument of type \'ProgrammaticDescription[] | undefined\' is not assignable to parameter of type \'ProgrammaticDescription[]\'.\\n Type \'undefined\' is not assignable to type \'ProgrammaticDescription[]\'.", "2776557981"]
], ],
"js/utils/navigationUtils.ts:1127210474": [ "js/utils/navigationUtils.ts:1127210474": [
[19, 50, 21, "Type \'undefined\' cannot be used as an index type.", "602535635"] [19, 50, 21, "Type \'undefined\' cannot be used as an index type.", "602535635"]
......
...@@ -7,7 +7,11 @@ import { Provider } from 'react-redux'; ...@@ -7,7 +7,11 @@ import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store'; import configureStore from 'redux-mock-store';
import { mocked } from 'ts-jest/utils'; import { mocked } from 'ts-jest/utils';
import { notificationsEnabled } from 'config/config-utils'; import { SortDirection } from 'interfaces';
import {
notificationsEnabled,
getTableSortCriterias,
} from 'config/config-utils';
import globalState from 'fixtures/globalState'; import globalState from 'fixtures/globalState';
import ColumnList, { ColumnListProps } from '.'; import ColumnList, { ColumnListProps } from '.';
...@@ -19,6 +23,7 @@ import TestDataBuilder from './testDataBuilder'; ...@@ -19,6 +23,7 @@ import TestDataBuilder from './testDataBuilder';
jest.mock('config/config-utils'); jest.mock('config/config-utils');
const mockedNotificationsEnabled = mocked(notificationsEnabled, true); const mockedNotificationsEnabled = mocked(notificationsEnabled, true);
const mockedGetTableSortCriterias = mocked(getTableSortCriterias, true);
const dataBuilder = new TestDataBuilder(); const dataBuilder = new TestDataBuilder();
const middlewares = []; const middlewares = [];
const mockStore = configureStore(middlewares); const mockStore = configureStore(middlewares);
...@@ -46,6 +51,18 @@ const setup = (propOverrides?: Partial<ColumnListProps>) => { ...@@ -46,6 +51,18 @@ const setup = (propOverrides?: Partial<ColumnListProps>) => {
}; };
describe('ColumnList', () => { describe('ColumnList', () => {
mockedGetTableSortCriterias.mockReturnValue({
sort_order: {
name: 'Table Default',
key: 'sort_order',
direction: SortDirection.ascending,
},
usage: {
name: 'Usage Count',
key: 'usage',
direction: SortDirection.descending,
},
});
mockedNotificationsEnabled.mockReturnValue(true); mockedNotificationsEnabled.mockReturnValue(true);
describe('render', () => { describe('render', () => {
...@@ -96,6 +113,69 @@ describe('ColumnList', () => { ...@@ -96,6 +113,69 @@ describe('ColumnList', () => {
expect(actual).toEqual(expected); expect(actual).toEqual(expected);
}); });
describe('when usage sorting is passed', () => {
it('should sort the data by that value', () => {
const { wrapper } = setup({
columns,
sortBy: {
name: 'Usage',
key: 'usage',
direction: SortDirection.descending,
},
});
const expected = 'simple_column_name_timestamp';
const actual = wrapper
.find('.table-detail-table .ams-table-row')
.at(0)
.find('.column-name')
.text();
expect(actual).toEqual(expected);
});
});
describe('when default sorting is passed', () => {
it('should sort the data by that value', () => {
const { wrapper } = setup({
columns,
sortBy: {
name: 'Default',
key: 'sort_order',
direction: SortDirection.ascending,
},
});
const expected = 'simple_column_name_string';
const actual = wrapper
.find('.table-detail-table .ams-table-row')
.at(0)
.find('.column-name')
.text();
expect(actual).toEqual(expected);
});
});
describe('when name sorting is passed', () => {
it('should sort the data by name', () => {
const { wrapper } = setup({
columns,
sortBy: {
name: 'Name',
key: 'name',
direction: SortDirection.descending,
},
});
const expected = 'simple_column_name_bigint';
const actual = wrapper
.find('.table-detail-table .ams-table-row')
.at(0)
.find('.column-name')
.text();
expect(actual).toEqual(expected);
});
});
}); });
describe('when complex type columns are passed', () => { describe('when complex type columns are passed', () => {
...@@ -152,6 +232,39 @@ describe('ColumnList', () => { ...@@ -152,6 +232,39 @@ describe('ColumnList', () => {
}); });
}); });
describe('when columns with serveral stats including usage are passed', () => {
const { columns } = dataBuilder.withSeveralStats().build();
it('should render the usage column', () => {
const { wrapper } = setup({ columns });
const expected = columns.length;
const actual = wrapper.find('.table-detail-table .usage-value').length;
expect(actual).toEqual(expected);
});
describe('when usage sorting is passed', () => {
it('should sort the data by that value', () => {
const { wrapper } = setup({
columns,
sortBy: {
name: 'Usage',
key: 'usage',
direction: SortDirection.ascending,
},
});
const expected = 'complex_column_name_2';
const actual = wrapper
.find('.table-detail-table .ams-table-row')
.at(0)
.find('.column-name')
.text();
expect(actual).toEqual(expected);
});
});
});
describe('when notifications are not enabled', () => { describe('when notifications are not enabled', () => {
const { columns } = dataBuilder.build(); const { columns } = dataBuilder.build();
......
...@@ -9,11 +9,22 @@ import { OpenRequestAction } from 'ducks/notification/types'; ...@@ -9,11 +9,22 @@ import { OpenRequestAction } from 'ducks/notification/types';
import EditableSection from 'components/common/EditableSection'; import EditableSection from 'components/common/EditableSection';
import Table, { import Table, {
TableColumn as ReusableTableColumn, TableColumn as ReusableTableColumn,
TextAlignmentValues,
} from 'components/common/Table'; } from 'components/common/Table';
import { logAction } from 'ducks/utilMethods'; import { logAction } from 'ducks/utilMethods';
import { notificationsEnabled, getMaxLength } from 'config/config-utils'; import {
import { TableColumn, RequestMetadataType } from 'interfaces'; notificationsEnabled,
getMaxLength,
getTableSortCriterias,
} from 'config/config-utils';
import {
TableColumn,
RequestMetadataType,
SortCriteria,
SortDirection,
} from 'interfaces';
import ColumnType from './ColumnType'; import ColumnType from './ColumnType';
import ColumnDescEditableText from './ColumnDescEditableText'; import ColumnDescEditableText from './ColumnDescEditableText';
...@@ -38,6 +49,7 @@ export interface ColumnListProps { ...@@ -38,6 +49,7 @@ export interface ColumnListProps {
database: string; database: string;
editText?: string; editText?: string;
editUrl?: string; editUrl?: string;
sortBy?: SortCriteria;
} }
type ContentType = { type ContentType = {
...@@ -61,12 +73,14 @@ type StatType = { ...@@ -61,12 +73,14 @@ type StatType = {
type FormattedDataType = { type FormattedDataType = {
content: ContentType; content: ContentType;
type: DatatypeType; type: DatatypeType;
usage: string | null; usage: number | null;
stats: StatType | null; stats: StatType | null;
action: string; action: string;
editText?: string; editText?: string;
editUrl?: string; editUrl?: string;
index: number; index: number;
name: string;
sort_order: string;
isEditable: boolean; isEditable: boolean;
}; };
...@@ -75,7 +89,52 @@ type ExpandedRowProps = { ...@@ -75,7 +89,52 @@ type ExpandedRowProps = {
index: number; index: number;
}; };
// TODO: Move this into the configuration once we have more info about the rest of stats
const USAGE_STAT_TYPE = 'column_usage';
const SHOW_STATS_THRESHOLD = 1; const SHOW_STATS_THRESHOLD = 1;
const DEFAULT_SORTING: SortCriteria = {
name: 'Table Default',
key: 'sort_order',
direction: SortDirection.ascending,
};
const getSortingFunction = (
formattedData: FormattedDataType[],
sortBy: SortCriteria
) => {
const numberSortingFunction = (a, b) => {
return b[sortBy.key] - a[sortBy.key];
};
const stringSortingFunction = (a, b) => {
if (a[sortBy.key] && b[sortBy.key]) {
return a[sortBy.key].localeCompare(b[sortBy.key]);
}
return null;
};
if (!formattedData.length) {
return numberSortingFunction;
}
return Number.isInteger(formattedData[0][sortBy.key])
? numberSortingFunction
: stringSortingFunction;
};
const getUsageStat = (item) => {
const hasItemStats = !!item.stats.length;
if (hasItemStats) {
const usageStat = item.stats.find((s) => {
return s.stat_type === USAGE_STAT_TYPE;
});
return usageStat ? +usageStat.stat_val : null;
}
return null;
};
const handleRowExpand = (rowValues) => { const handleRowExpand = (rowValues) => {
logAction({ logAction({
...@@ -139,6 +198,7 @@ const ColumnList: React.FC<ColumnListProps> = ({ ...@@ -139,6 +198,7 @@ const ColumnList: React.FC<ColumnListProps> = ({
editText, editText,
editUrl, editUrl,
openRequestDescriptionDialog, openRequestDescriptionDialog,
sortBy = DEFAULT_SORTING,
}: ColumnListProps) => { }: ColumnListProps) => {
const formattedData: FormattedDataType[] = columns.map((item, index) => { const formattedData: FormattedDataType[] = columns.map((item, index) => {
const hasItemStats = !!item.stats.length; const hasItemStats = !!item.stats.length;
...@@ -153,9 +213,11 @@ const ColumnList: React.FC<ColumnListProps> = ({ ...@@ -153,9 +213,11 @@ const ColumnList: React.FC<ColumnListProps> = ({
name: item.name, name: item.name,
database, database,
}, },
usage: hasItemStats ? item.stats[0].stat_val : '', sort_order: item.sort_order,
usage: getUsageStat(item),
stats: hasItemStats ? item.stats[0] : null, stats: hasItemStats ? item.stats[0] : null,
action: item.name, action: item.name,
name: item.name,
isEditable: item.is_editable, isEditable: item.is_editable,
editText, editText,
editUrl, editUrl,
...@@ -163,7 +225,14 @@ const ColumnList: React.FC<ColumnListProps> = ({ ...@@ -163,7 +225,14 @@ const ColumnList: React.FC<ColumnListProps> = ({
}; };
}); });
const statsCount = formattedData.filter((item) => !!item.stats).length; const statsCount = formattedData.filter((item) => !!item.stats).length;
const hasStats = statsCount >= SHOW_STATS_THRESHOLD; const hasUsageStat =
getTableSortCriterias().usage && statsCount >= SHOW_STATS_THRESHOLD;
let formattedAndOrderedData = formattedData.sort(
getSortingFunction(formattedData, sortBy)
);
if (sortBy.direction === SortDirection.ascending) {
formattedAndOrderedData = formattedAndOrderedData.reverse();
}
let formattedColumns: ReusableTableColumn[] = [ let formattedColumns: ReusableTableColumn[] = [
{ {
...@@ -191,13 +260,13 @@ const ColumnList: React.FC<ColumnListProps> = ({ ...@@ -191,13 +260,13 @@ const ColumnList: React.FC<ColumnListProps> = ({
}, },
]; ];
if (hasStats) { if (hasUsageStat) {
formattedColumns = [ formattedColumns = [
...formattedColumns, ...formattedColumns,
{ {
title: 'Usage', title: 'Usage',
field: 'usage', field: 'usage',
horAlign: 'right', horAlign: TextAlignmentValues.right,
component: (usage) => ( component: (usage) => (
<p className="resource-type usage-value">{usage}</p> <p className="resource-type usage-value">{usage}</p>
), ),
...@@ -212,7 +281,7 @@ const ColumnList: React.FC<ColumnListProps> = ({ ...@@ -212,7 +281,7 @@ const ColumnList: React.FC<ColumnListProps> = ({
title: '', title: '',
field: 'action', field: 'action',
width: 80, width: 80,
horAlign: 'right', horAlign: TextAlignmentValues.right,
component: (name, index) => ( component: (name, index) => (
<div className="actions"> <div className="actions">
<Dropdown <Dropdown
...@@ -246,7 +315,7 @@ const ColumnList: React.FC<ColumnListProps> = ({ ...@@ -246,7 +315,7 @@ const ColumnList: React.FC<ColumnListProps> = ({
return ( return (
<Table <Table
columns={formattedColumns} columns={formattedColumns}
data={formattedData} data={formattedAndOrderedData}
options={{ options={{
rowHeight: 72, rowHeight: 72,
emptyMessage: EMPTY_MESSAGE, emptyMessage: EMPTY_MESSAGE,
......
...@@ -99,7 +99,6 @@ $description-max-width-small: 300px; ...@@ -99,7 +99,6 @@ $description-max-width-small: 300px;
} }
.usage-value { .usage-value {
font-variant-numeric: tabular-nums; font-family: $font-family-monospace-code;
font-family: $text-heading-font-family;
} }
} }
...@@ -206,6 +206,68 @@ function TestDataBuilder(config = {}) { ...@@ -206,6 +206,68 @@ function TestDataBuilder(config = {}) {
return new this.Klass(attr); return new this.Klass(attr);
}; };
this.withSeveralStats = () => {
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',
},
],
},
{
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',
},
],
},
{
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',
},
],
},
],
};
return new this.Klass(attr);
};
this.withEmptyColumns = () => { this.withEmptyColumns = () => {
const attr = { columns: [] }; const attr = { columns: [] };
......
...@@ -8,8 +8,14 @@ import { UpIcon, DownIcon } from '../SVGIcons'; ...@@ -8,8 +8,14 @@ import { UpIcon, DownIcon } from '../SVGIcons';
import './styles.scss'; import './styles.scss';
type TextAlignmentValues = 'left' | 'right' | 'center'; // export type SortDirection = 'asc' | 'desc';
// export type SortCriteria = { key: string; direction: SortDirection };
export enum TextAlignmentValues {
left = 'left',
right = 'right',
center = 'center',
}
export interface TableColumn { export interface TableColumn {
title: string; title: string;
field: string; field: string;
...@@ -41,7 +47,7 @@ const EXPAND_ROW_TEXT = 'Expand Row'; ...@@ -41,7 +47,7 @@ const EXPAND_ROW_TEXT = 'Expand Row';
const DEFAULT_LOADING_ITEMS = 3; const DEFAULT_LOADING_ITEMS = 3;
const DEFAULT_ROW_HEIGHT = 30; const DEFAULT_ROW_HEIGHT = 30;
const EXPANDING_CELL_WIDTH = '70px'; const EXPANDING_CELL_WIDTH = '70px';
const DEFAULT_TEXT_ALIGNMENT = 'left'; const DEFAULT_TEXT_ALIGNMENT = TextAlignmentValues.left;
const DEFAULT_CELL_WIDTH = 'auto'; const DEFAULT_CELL_WIDTH = 'auto';
const ALIGNEMENT_TO_CLASS_MAP = { const ALIGNEMENT_TO_CLASS_MAP = {
left: 'is-left-aligned', left: 'is-left-aligned',
...@@ -201,9 +207,9 @@ const Table: React.FC<TableProps> = ({ ...@@ -201,9 +207,9 @@ const Table: React.FC<TableProps> = ({
) : null} ) : null}
{Object.entries(item) {Object.entries(item)
.filter(([key]) => fields.includes(key)) .filter(([key]) => fields.includes(key))
.map(([key, value], index) => { .map(([key, value], rowIndex) => {
const columnInfo = columns.find(({ field }) => field === key); const columnInfo = columns.find(({ field }) => field === key);
const horAlign = columnInfo const horAlign: TextAlignmentValues = columnInfo
? columnInfo.horAlign || DEFAULT_TEXT_ALIGNMENT ? columnInfo.horAlign || DEFAULT_TEXT_ALIGNMENT
: DEFAULT_TEXT_ALIGNMENT; : DEFAULT_TEXT_ALIGNMENT;
const width = const width =
...@@ -216,7 +222,7 @@ const Table: React.FC<TableProps> = ({ ...@@ -216,7 +222,7 @@ const Table: React.FC<TableProps> = ({
// TODO: Improve the typing of this // TODO: Improve the typing of this
let cellContent: React.ReactNode | typeof value = value; let cellContent: React.ReactNode | typeof value = value;
if (columnInfo && columnInfo.component) { if (columnInfo && columnInfo.component) {
cellContent = columnInfo.component(value, index); cellContent = columnInfo.component(value, rowIndex);
} }
return ( return (
...@@ -224,7 +230,7 @@ const Table: React.FC<TableProps> = ({ ...@@ -224,7 +230,7 @@ const Table: React.FC<TableProps> = ({
className={`ams-table-cell ${getCellAlignmentClass( className={`ams-table-cell ${getCellAlignmentClass(
horAlign horAlign
)}`} )}`}
key={`index:${index}`} key={`index:${rowIndex}`}
style={cellStyle} style={cellStyle}
> >
{cellContent} {cellContent}
......
import { AppConfig } from './config-types'; import { AppConfig } from './config-types';
import { FilterType, ResourceType } from '../interfaces'; import { FilterType, ResourceType, SortDirection } from '../interfaces';
const configDefault: AppConfig = { const configDefault: AppConfig = {
badges: {}, badges: {},
...@@ -162,6 +162,18 @@ const configDefault: AppConfig = { ...@@ -162,6 +162,18 @@ const configDefault: AppConfig = {
type: FilterType.INPUT_SELECT, type: FilterType.INPUT_SELECT,
}, },
], ],
sortCriterias: {
sort_order: {
name: 'Table Default',
key: 'sort_order',
direction: SortDirection.ascending,
},
name: {
name: 'Alphabetical',
key: 'name',
direction: SortDirection.descending,
},
},
supportedDescriptionSources: { supportedDescriptionSources: {
github: { github: {
displayName: 'Github', displayName: 'Github',
......
import { FilterType, ResourceType } from '../interfaces'; import { FilterType, ResourceType, SortCriteria } from '../interfaces';
/** /**
* AppConfig and AppConfigCustom should share the same definition, except each field in AppConfigCustom * AppConfig and AppConfigCustom should share the same definition, except each field in AppConfigCustom
...@@ -130,6 +130,13 @@ type DescriptionSourceConfig = { ...@@ -130,6 +130,13 @@ type DescriptionSourceConfig = {
[id: string]: { displayName: string; iconPath: string }; [id: string]: { displayName: string; iconPath: string };
}; };
/**
* Shows criterias to sort tables
*/
type SortCriteriaConfig = {
[key: string]: SortCriteria;
};
/** /**
* Base interface for all possible ResourceConfig objects * Base interface for all possible ResourceConfig objects
* *
...@@ -144,6 +151,7 @@ interface BaseResourceConfig { ...@@ -144,6 +151,7 @@ interface BaseResourceConfig {
interface TableResourceConfig extends BaseResourceConfig { interface TableResourceConfig extends BaseResourceConfig {
supportedDescriptionSources?: DescriptionSourceConfig; supportedDescriptionSources?: DescriptionSourceConfig;
sortCriterias?: SortCriteriaConfig;
} }
export enum BadgeStyle { export enum BadgeStyle {
......
...@@ -146,6 +146,19 @@ export function getCuratedTags(): string[] { ...@@ -146,6 +146,19 @@ export function getCuratedTags(): string[] {
return AppConfig.browse.curatedTags; return AppConfig.browse.curatedTags;
} }
/**
* Returns a list of table sort options
*/
export function getTableSortCriterias() {
const config = AppConfig.resourceConfig[ResourceType.table];
if (config && config.sortCriterias) {
return config.sortCriterias;
}
return {};
}
/** /**
* Checks if nav links are active * Checks if nav links are active
*/ */
......
...@@ -78,6 +78,15 @@ describe('getFilterConfigByResource', () => { ...@@ -78,6 +78,15 @@ describe('getFilterConfigByResource', () => {
}); });
}); });
describe('getTableSortCriterias', () => {
it('returns the sorting criterias for tables', () => {
const expectedValue =
AppConfig.resourceConfig[ResourceType.table].sortCriterias;
expect(ConfigUtils.getTableSortCriterias()).toBe(expectedValue);
});
});
describe('getBadgeConfig', () => { describe('getBadgeConfig', () => {
AppConfig.badges = { AppConfig.badges = {
test_1: { test_1: {
......
...@@ -18,6 +18,7 @@ export const tableMetadata: TableMetadata = { ...@@ -18,6 +18,7 @@ export const tableMetadata: TableMetadata = {
col_type: 'bigint', col_type: 'bigint',
description: 'Test Value', description: 'Test Value',
is_editable: true, is_editable: true,
sort_order: '0',
name: 'ride_id', name: 'ride_id',
stats: [ stats: [
{ {
...@@ -45,13 +46,15 @@ export const tableMetadata: TableMetadata = { ...@@ -45,13 +46,15 @@ export const tableMetadata: TableMetadata = {
description: description:
'ds will be the date part of requested_at ds will be the date part of requested_at ds will be the date part of requested_at ds will be the date part of requested_at ds will be the date part of requested_at ds will be the date part of requested_at ds w', 'ds will be the date part of requested_at ds will be the date part of requested_at ds will be the date part of requested_at ds will be the date part of requested_at ds will be the date part of requested_at ds will be the date part of requested_at ds w',
is_editable: true, is_editable: true,
sort_order: '1',
name: 'ds', name: 'ds',
stats: [], stats: [],
}, },
{ {
col_type: 'string', col_type: 'string',
description: null, description: 'Route_id Description',
is_editable: true, is_editable: true,
sort_order: '2',
name: 'route_id', name: 'route_id',
stats: [ stats: [
{ {
...@@ -169,7 +172,7 @@ export const tableMetadata: TableMetadata = { ...@@ -169,7 +172,7 @@ export const tableMetadata: TableMetadata = {
export const relatedDashboards: DashboardResource[] = [ export const relatedDashboards: DashboardResource[] = [
{ {
group_name: 'Test Group 1', group_name: 'Test Group 1',
description: null, description: 'Test Group 1 Description',
cluster: 'gold', cluster: 'gold',
group_url: 'https://app.mode.com/testCompany/spaces/1234', group_url: 'https://app.mode.com/testCompany/spaces/1234',
uri: 'mode_dashboard://gold.1234/23445asb', uri: 'mode_dashboard://gold.1234/23445asb',
...@@ -181,7 +184,7 @@ export const relatedDashboards: DashboardResource[] = [ ...@@ -181,7 +184,7 @@ export const relatedDashboards: DashboardResource[] = [
}, },
{ {
group_name: 'Test Group 2', group_name: 'Test Group 2',
description: null, description: 'Test Group 2 Description',
cluster: 'gold', cluster: 'gold',
group_url: 'https://app.mode.com/testCompany/spaces/345asd', group_url: 'https://app.mode.com/testCompany/spaces/345asd',
uri: 'mode_dashboard://gold.345asd/asdfas001', uri: 'mode_dashboard://gold.345asd/asdfas001',
...@@ -193,7 +196,7 @@ export const relatedDashboards: DashboardResource[] = [ ...@@ -193,7 +196,7 @@ export const relatedDashboards: DashboardResource[] = [
}, },
{ {
group_name: 'Test Group 3', group_name: 'Test Group 3',
description: null, description: 'Test Group 3 Description',
cluster: 'gold', cluster: 'gold',
group_url: 'https://app.mode.com/testCompany/spaces/casdg80', group_url: 'https://app.mode.com/testCompany/spaces/casdg80',
uri: 'mode_dashboard://gold.casdg80/123566', uri: 'mode_dashboard://gold.casdg80/123566',
......
...@@ -42,6 +42,16 @@ export interface TableResource extends Resource { ...@@ -42,6 +42,16 @@ export interface TableResource extends Resource {
badges?: any[]; // TODO replace with new badges later @allisonsuarez badges?: any[]; // TODO replace with new badges later @allisonsuarez
} }
export enum SortDirection {
ascending = 'asc',
descending = 'desc',
}
export interface SortCriteria {
name: string;
key: string;
direction: SortDirection;
}
export interface UserResource extends Resource, PeopleUser { export interface UserResource extends Resource, PeopleUser {
type: ResourceType.user; type: ResourceType.user;
} }
......
...@@ -61,6 +61,7 @@ export interface TableColumn { ...@@ -61,6 +61,7 @@ export interface TableColumn {
description: string; description: string;
is_editable: boolean; is_editable: boolean;
col_type: string; col_type: string;
sort_order: string;
stats: TableColumnStats[]; stats: TableColumnStats[];
} }
......
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
import * as React from 'react';
import { Dropdown } from 'react-bootstrap';
import { mount } from 'enzyme';
import { SortDirection } from 'interfaces';
import ListSortingDropdown, { ListSortingDropdownProps } from '.';
const DEFAULT_SORTING = {
sort_order: {
name: 'Table Default',
key: 'sort_order',
direction: SortDirection.ascending,
},
};
const USAGE_SORTING = {
usage: {
name: 'Usage Count',
key: 'usage',
direction: SortDirection.descending,
},
};
const setup = (propOverrides?: Partial<ListSortingDropdownProps>) => {
const props = {
options: {},
...propOverrides,
};
const wrapper = mount<typeof ListSortingDropdown>(
<ListSortingDropdown {...props} />
);
return { props, wrapper };
};
describe('ListSortingDropdown', () => {
describe('render', () => {
it('renders without issues', () => {
expect(() => {
setup();
}).not.toThrow();
});
describe('when no options are passed', () => {
it('does not render the component', () => {
const { wrapper } = setup();
const expected = 0;
const actual = wrapper.find('.list-sorting-dropdown').length;
expect(actual).toEqual(expected);
});
});
describe('when one option is passed', () => {
it('renders a DropDown component', () => {
const { wrapper } = setup({ options: DEFAULT_SORTING });
const expected = 1;
const actual = wrapper.find(Dropdown).length;
expect(actual).toEqual(expected);
});
it('renders one item', () => {
const { wrapper } = setup({ options: DEFAULT_SORTING });
const expected = 1;
const actual = wrapper.find('.list-sorting-dropdown .radio-label')
.length;
expect(actual).toEqual(expected);
});
it('is selected by default', () => {
const { wrapper } = setup({ options: DEFAULT_SORTING });
const expected = true;
const actual = wrapper
.find('.list-sorting-dropdown .radio-label input')
.prop('checked');
expect(actual).toEqual(expected);
});
});
describe('when two options are passed', () => {
it('renders a DropDown component', () => {
const { wrapper } = setup({
options: { ...DEFAULT_SORTING, ...USAGE_SORTING },
});
const expected = 1;
const actual = wrapper.find(Dropdown).length;
expect(actual).toEqual(expected);
});
it('renders two items', () => {
const { wrapper } = setup({
options: { ...DEFAULT_SORTING, ...USAGE_SORTING },
});
const expected = 2;
const actual = wrapper.find('.list-sorting-dropdown .radio-label')
.length;
expect(actual).toEqual(expected);
});
it('selects the first one by default', () => {
const { wrapper } = setup({
options: { ...DEFAULT_SORTING, ...USAGE_SORTING },
});
const expected = true;
const actual = wrapper
.find('.list-sorting-dropdown .radio-label input')
.at(0)
.prop('checked');
expect(actual).toEqual(expected);
});
});
});
describe('lifetime', () => {
describe('when selecting an option', () => {
it('should make it the selected', () => {
const { wrapper } = setup({
options: { ...DEFAULT_SORTING, ...USAGE_SORTING },
});
const expected = true;
wrapper
.find('.list-sorting-dropdown .radio-label input')
.at(1)
.simulate('change', { target: { value: 'usage' } });
const actual = wrapper
.find('.list-sorting-dropdown .radio-label input')
.at(1)
.prop('checked');
expect(actual).toEqual(expected);
});
it('should call the onChange handler', () => {
const onChangeSpy = jest.fn();
const { wrapper } = setup({
onChange: onChangeSpy,
options: { ...DEFAULT_SORTING, ...USAGE_SORTING },
});
const expected = 1;
wrapper
.find('.list-sorting-dropdown .radio-label input')
.at(1)
.simulate('change', { target: { value: 'usage' } });
const actual = onChangeSpy.mock.calls.length;
expect(actual).toEqual(expected);
});
it('should call the onChange handler with the proper value', () => {
const onChangeSpy = jest.fn();
const { wrapper } = setup({
onChange: onChangeSpy,
options: { ...DEFAULT_SORTING, ...USAGE_SORTING },
});
const expected = ['usage'];
wrapper
.find('.list-sorting-dropdown .radio-label input')
.at(1)
.simulate('change', { target: { value: 'usage' } });
const actual = onChangeSpy.mock.calls[0];
expect(actual).toEqual(expected);
});
});
});
});
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
import * as React from 'react';
import { Dropdown } from 'react-bootstrap';
import { SortCriteria } from 'interfaces';
import { SORT_BY_DROPDOWN_TITLE, SORT_BY_MENU_TITLE_TEXT } from '../constants';
import './styles.scss';
type Criterias = { [key: string]: SortCriteria };
export interface ListSortingDropdownProps {
options: Criterias;
onChange?: (value) => void;
}
type OptionType = string;
const TableReportsDropdown: React.FC<ListSortingDropdownProps> = ({
options,
onChange,
}: ListSortingDropdownProps) => {
const criterias = Object.entries(options);
if (criterias.length < 1) {
return null;
}
const [selectedOption, setSelectedOption] = React.useState<OptionType>(
criterias[0][1].key
);
const [isOpen, setOpen] = React.useState(false);
const handleChange = (e) => {
const { value } = e.target;
setSelectedOption(value);
setOpen(false);
if (onChange) {
onChange(value);
}
};
return (
<Dropdown
className="list-sorting-dropdown"
id="list-sorting-dropdown"
pullRight
open={isOpen}
onToggle={() => {
setOpen(!isOpen);
}}
>
<Dropdown.Toggle className="btn btn-default list-sorting-dropdown-button">
{SORT_BY_DROPDOWN_TITLE}
</Dropdown.Toggle>
<Dropdown.Menu className="list-sorting-dropdown-menu">
<h5 className="list-sorting-dropdown-menu-title">
{SORT_BY_MENU_TITLE_TEXT}
</h5>
{criterias.map(([_, { key, name }]) => (
<li key={name}>
<label className="list-sorting-dropdown-menu-item radio-label">
<input
type="radio"
name="sort-option"
value={key}
checked={selectedOption === key}
onChange={handleChange}
/>
<span className="list-sorting-dropdown-menu-item-text">
{name}
</span>
</label>
</li>
))}
</Dropdown.Menu>
</Dropdown>
);
};
export default TableReportsDropdown;
// Copyright Contributors to the Amundsen project.
// SPDX-License-Identifier: Apache-2.0
@import 'typography';
.list-sorting-dropdown {
.list-sorting-dropdown-button {
border: 0;
}
.list-sorting-dropdown-menu {
padding: $spacer-2;
}
.list-sorting-dropdown-menu-title {
@extend %text-caption-w2;
padding-bottom: $spacer-1;
}
.list-sorting-dropdown-menu-item {
&:hover {
cursor: pointer;
}
}
.list-sorting-dropdown-menu-item-text {
@extend %text-body-w3;
}
}
...@@ -22,7 +22,7 @@ interface StateFromProps { ...@@ -22,7 +22,7 @@ interface StateFromProps {
errorText: string; errorText: string;
} }
export type TableDashboardResourceListProps = OwnProps & StateFromProps; export type TableDashboardResourceListProps = StateFromProps & OwnProps;
export class TableDashboardResourceList extends React.Component< export class TableDashboardResourceList extends React.Component<
TableDashboardResourceListProps TableDashboardResourceListProps
...@@ -54,6 +54,7 @@ export class TableDashboardResourceList extends React.Component< ...@@ -54,6 +54,7 @@ export class TableDashboardResourceList extends React.Component<
export const mapStateToProps = (state: GlobalState) => { export const mapStateToProps = (state: GlobalState) => {
const relatedDashboards = state.tableMetadata.dashboards; const relatedDashboards = state.tableMetadata.dashboards;
return { return {
dashboards: relatedDashboards ? relatedDashboards.dashboards : [], dashboards: relatedDashboards ? relatedDashboards.dashboards : [],
isLoading: relatedDashboards ? relatedDashboards.isLoading : true, isLoading: relatedDashboards ? relatedDashboards.isLoading : true,
......
...@@ -7,3 +7,5 @@ export const FREQ_USERS_TITLE = 'Frequent Users'; ...@@ -7,3 +7,5 @@ export const FREQ_USERS_TITLE = 'Frequent Users';
export const LAST_UPDATED_TITLE = 'Last Updated'; export const LAST_UPDATED_TITLE = 'Last Updated';
export const OWNERS_TITLE = 'Owners'; export const OWNERS_TITLE = 'Owners';
export const TAG_TITLE = 'Tags'; export const TAG_TITLE = 'Tags';
export const SORT_BY_DROPDOWN_TITLE = 'Sort by';
export const SORT_BY_MENU_TITLE_TEXT = 'Sort by';
...@@ -17,6 +17,7 @@ import { TableDetail, TableDetailProps, MatchProps } from '.'; ...@@ -17,6 +17,7 @@ import { TableDetail, TableDetailProps, MatchProps } from '.';
jest.mock('config/config-utils', () => ({ jest.mock('config/config-utils', () => ({
indexDashboardsEnabled: jest.fn(), indexDashboardsEnabled: jest.fn(),
getTableSortCriterias: jest.fn(),
})); }));
const setup = ( const setup = (
......
...@@ -17,6 +17,7 @@ import { ...@@ -17,6 +17,7 @@ import {
getDescriptionSourceDisplayName, getDescriptionSourceDisplayName,
getMaxLength, getMaxLength,
getSourceIconClass, getSourceIconClass,
getTableSortCriterias,
indexDashboardsEnabled, indexDashboardsEnabled,
issueTrackingEnabled, issueTrackingEnabled,
notificationsEnabled, notificationsEnabled,
...@@ -40,6 +41,7 @@ import { ...@@ -40,6 +41,7 @@ import {
ResourceType, ResourceType,
TableMetadata, TableMetadata,
RequestMetadataType, RequestMetadataType,
SortCriteria,
} from 'interfaces'; } from 'interfaces';
import DataPreviewButton from './DataPreviewButton'; import DataPreviewButton from './DataPreviewButton';
...@@ -57,6 +59,7 @@ import WriterLink from './WriterLink'; ...@@ -57,6 +59,7 @@ import WriterLink from './WriterLink';
import TableReportsDropdown from './ResourceReportsDropdown'; import TableReportsDropdown from './ResourceReportsDropdown';
import RequestDescriptionText from './RequestDescriptionText'; import RequestDescriptionText from './RequestDescriptionText';
import RequestMetadataForm from './RequestMetadataForm'; import RequestMetadataForm from './RequestMetadataForm';
import ListSortingDropdown from './ListSortingDropdown';
import * as Constants from './constants'; import * as Constants from './constants';
...@@ -65,8 +68,12 @@ import './styles.scss'; ...@@ -65,8 +68,12 @@ import './styles.scss';
const SERVER_ERROR_CODE = 500; const SERVER_ERROR_CODE = 500;
const DASHBOARDS_PER_PAGE = 10; const DASHBOARDS_PER_PAGE = 10;
const TABLE_SOURCE = 'table_page'; const TABLE_SOURCE = 'table_page';
const SORT_CRITERIAS = {
...getTableSortCriterias(),
};
const COLUMN_TAB_KEY = 'columns';
export interface StateFromProps { export interface PropsFromState {
isLoading: boolean; isLoading: boolean;
isLoadingDashboards: boolean; isLoadingDashboards: boolean;
numRelatedDashboards: number; numRelatedDashboards: number;
...@@ -92,7 +99,7 @@ export interface MatchProps { ...@@ -92,7 +99,7 @@ export interface MatchProps {
table: string; table: string;
} }
export type TableDetailProps = StateFromProps & export type TableDetailProps = PropsFromState &
DispatchFromProps & DispatchFromProps &
RouteComponentProps<MatchProps>; RouteComponentProps<MatchProps>;
...@@ -105,13 +112,24 @@ const ErrorMessage = () => { ...@@ -105,13 +112,24 @@ const ErrorMessage = () => {
); );
}; };
export interface StateProps {
sortedBy: SortCriteria;
currentTab: string;
}
export class TableDetail extends React.Component< export class TableDetail extends React.Component<
TableDetailProps & RouteComponentProps<any> TableDetailProps & RouteComponentProps<any>,
StateProps
> { > {
private key: string; private key: string;
private didComponentMount: boolean = false; private didComponentMount: boolean = false;
state = {
sortedBy: SORT_CRITERIAS.sort_order,
currentTab: COLUMN_TAB_KEY,
};
componentDidMount() { componentDidMount() {
const { index, source } = getLoggingParams(this.props.location.search); const { index, source } = getLoggingParams(this.props.location.search);
...@@ -125,6 +143,7 @@ export class TableDetail extends React.Component< ...@@ -125,6 +143,7 @@ export class TableDetail extends React.Component<
if (this.key !== newKey) { if (this.key !== newKey) {
const { index, source } = getLoggingParams(this.props.location.search); const { index, source } = getLoggingParams(this.props.location.search);
this.key = newKey; this.key = newKey;
this.props.getTableData(this.key, index, source); this.props.getTableData(this.key, index, source);
} }
...@@ -147,7 +166,9 @@ export class TableDetail extends React.Component< ...@@ -147,7 +166,9 @@ export class TableDetail extends React.Component<
return `${params.database}://${params.cluster}.${params.schema}/${params.table}`; return `${params.database}://${params.cluster}.${params.schema}/${params.table}`;
} }
renderProgrammaticDesc = (descriptions: ProgrammaticDescription[]) => { renderProgrammaticDesc = (
descriptions: ProgrammaticDescription[] | undefined
) => {
if (!descriptions) { if (!descriptions) {
return null; return null;
} }
...@@ -164,6 +185,20 @@ export class TableDetail extends React.Component< ...@@ -164,6 +185,20 @@ export class TableDetail extends React.Component<
)); ));
}; };
handleSortingChange = (sortValue) => {
this.toggleSort(SORT_CRITERIAS[sortValue]);
};
toggleSort = (sorting: SortCriteria) => {
const { sortedBy } = this.state;
if (sorting !== sortedBy) {
this.setState({
sortedBy: sorting,
});
}
};
renderTabs(editText, editUrl) { renderTabs(editText, editUrl) {
const tabInfo: TabInfo[] = []; const tabInfo: TabInfo[] = [];
const { const {
...@@ -172,6 +207,7 @@ export class TableDetail extends React.Component< ...@@ -172,6 +207,7 @@ export class TableDetail extends React.Component<
tableData, tableData,
openRequestDescriptionDialog, openRequestDescriptionDialog,
} = this.props; } = this.props;
const { sortedBy } = this.state;
// Default Column content // Default Column content
tabInfo.push({ tabInfo.push({
...@@ -182,6 +218,7 @@ export class TableDetail extends React.Component< ...@@ -182,6 +218,7 @@ export class TableDetail extends React.Component<
database={tableData.database} database={tableData.database}
editText={editText} editText={editText}
editUrl={editUrl} editUrl={editUrl}
sortBy={sortedBy}
/> />
), ),
key: 'columns', key: 'columns',
...@@ -209,11 +246,20 @@ export class TableDetail extends React.Component< ...@@ -209,11 +246,20 @@ export class TableDetail extends React.Component<
}); });
} }
return <TabsComponent tabs={tabInfo} defaultTab="columns" />; return (
<TabsComponent
tabs={tabInfo}
defaultTab="columns"
onSelect={(key) => {
this.setState({ currentTab: key });
}}
/>
);
} }
render() { render() {
const { isLoading, statusCode, tableData } = this.props; const { isLoading, statusCode, tableData } = this.props;
const { currentTab } = this.state;
let innerContent; let innerContent;
// We want to avoid rendering the previous table's metadata before new data is fetched in componentDidMount // We want to avoid rendering the previous table's metadata before new data is fetched in componentDidMount
...@@ -348,6 +394,12 @@ export class TableDetail extends React.Component< ...@@ -348,6 +394,12 @@ export class TableDetail extends React.Component<
)} )}
</aside> </aside>
<main className="right-panel"> <main className="right-panel">
{currentTab === COLUMN_TAB_KEY && (
<ListSortingDropdown
options={SORT_CRITERIAS}
onChange={this.handleSortingChange}
/>
)}
{this.renderTabs(editText, editUrl)} {this.renderTabs(editText, editUrl)}
</main> </main>
</div> </div>
...@@ -386,7 +438,7 @@ export const mapDispatchToProps = (dispatch: any) => { ...@@ -386,7 +438,7 @@ export const mapDispatchToProps = (dispatch: any) => {
); );
}; };
export default connect<StateFromProps, DispatchFromProps>( export default connect<PropsFromState, DispatchFromProps>(
mapStateToProps, mapStateToProps,
mapDispatchToProps mapDispatchToProps
)(TableDetail); )(TableDetail);
...@@ -42,11 +42,21 @@ ...@@ -42,11 +42,21 @@
} }
.nav.nav-tabs { .nav.nav-tabs {
margin-top: $spacer-1; margin-top: $spacer-2;
padding: 0 $spacer-2; padding: 0 $spacer-2;
} }
.tabs-component .nav.nav-tabs > li { .tabs-component .nav.nav-tabs > li {
margin: 0 $spacer-1; margin: 0 $spacer-1;
} }
.right-panel {
position: relative;
}
.list-sorting-dropdown {
right: $spacer-3;
top: $spacer-2;
position: absolute;
}
} }
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