From f93f896b5719b86a0bf3d2dfe435c2322deb8bae Mon Sep 17 00:00:00 2001 From: Tamika Tannis Date: Mon, 31 Aug 2020 10:12:51 -0700 Subject: [PATCH] feat: Improve Nested Column Types UI (#627) * feat: Create ColumnType component (#604) * Create ColumnType component Signed-off-by: Tamika Tannis * Update Modal UI Signed-off-by: Tamika Tannis * Code cleanup; Add a test file * Prevent ColumnListItem expand/collapse from being triggered * Lint fix Signed-off-by: Tamika Tannis * feat: Parse column types (#611) * WIP: Create a parser & render parsed text Signed-off-by: Tamika Tannis * Cleanup logic Signed-off-by: Tamika Tannis * More cleanup Signed-off-by: Tamika Tannis * Lint fix Signed-off-by: Tamika Tannis * Code cleanup Signed-off-by: Tamika Tannis * Code cleanup Signed-off-by: Tamika Tannis * Code cleanup Signed-off-by: Tamika Tannis * Use more appropriate elements; Fix typo Signed-off-by: Tamika Tannis * Parser tests Signed-off-by: Tamika Tannis * Fix button; Fix test; Remove obsolete style Signed-off-by: Tamika Tannis * Fix duplicate test name Signed-off-by: Tamika Tannis * style: Improve UI styles and interactions (#617) * Vertically center modal Signed-off-by: Tamika Tannis * Match design font specifications Signed-off-by: Tamika Tannis * Miscellaneous cleanup Signed-off-by: Tamika Tannis * Use variables Signed-off-by: Tamika Tannis * test: Improves unit tests for ColumnType + QA fixes (#625) * Parser tests Signed-off-by: Tamika Tannis * Updates from design qa Signed-off-by: Tamika Tannis * Improve ColumnType tests Signed-off-by: Tamika Tannis * log support Signed-off-by: Tamika Tannis * Code cleanup Signed-off-by: Tamika Tannis * Fix some lint warning Signed-off-by: Tamika Tannis * Betterer update Signed-off-by: Tamika Tannis --- .../static/.betterer.results | 38 ++-- .../static/css/_fonts-default.scss | 8 + .../static/css/_popovers.scss | 5 - .../static/css/_variables-default.scss | 3 + .../static/fonts/SpaceMono-Regular.ttf | Bin 0 -> 90972 bytes .../TableDetail/ColumnList/index.tsx | 3 + .../ColumnListItem/ColumnType/index.spec.tsx | 86 +++++++++ .../ColumnListItem/ColumnType/index.tsx | 149 +++++++++++++++ .../ColumnListItem/ColumnType/parser.spec.ts | 180 ++++++++++++++++++ .../ColumnListItem/ColumnType/parser.ts | 156 +++++++++++++++ .../ColumnListItem/ColumnType/styles.scss | 85 +++++++++ .../TableDetail/ColumnListItem/index.spec.tsx | 7 +- .../TableDetail/ColumnListItem/index.tsx | 82 ++------ .../TableDetail/SourceLink/index.spec.tsx | 2 +- .../js/components/TableDetail/index.tsx | 10 +- 15 files changed, 720 insertions(+), 94 deletions(-) create mode 100755 frontend/amundsen_application/static/fonts/SpaceMono-Regular.ttf create mode 100644 frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/ColumnType/index.spec.tsx create mode 100644 frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/ColumnType/index.tsx create mode 100644 frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/ColumnType/parser.spec.ts create mode 100644 frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/ColumnType/parser.ts create mode 100644 frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/ColumnType/styles.scss diff --git a/frontend/amundsen_application/static/.betterer.results b/frontend/amundsen_application/static/.betterer.results index 323eeb7fee..134a8f4ed2 100644 --- a/frontend/amundsen_application/static/.betterer.results +++ b/frontend/amundsen_application/static/.betterer.results @@ -73,11 +73,11 @@ exports[`strict null compilation`] = { "js/components/SearchPage/index.tsx:1421221531": [ [175, 11, 12, "Property \'filterSections\' is missing in type \'{}\' but required in type \'Readonly>\'.", "250899467"] ], - "js/components/TableDetail/ColumnList/index.tsx:2024292996": [ - [20, 6, 7, "Object is possibly \'undefined\'.", "3718923584"], - [25, 21, 7, "Object is possibly \'undefined\'.", "3718923584"], - [30, 6, 8, "Type \'string | undefined\' is not assignable to type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "1427606500"], - [31, 6, 7, "Type \'string | undefined\' is not assignable to type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "3817619378"] + "js/components/TableDetail/ColumnList/index.tsx:355148301": [ + [22, 6, 7, "Object is possibly \'undefined\'.", "3718923584"], + [27, 21, 7, "Object is possibly \'undefined\'.", "3718923584"], + [33, 6, 8, "Type \'string | undefined\' is not assignable to type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "1427606500"], + [34, 6, 7, "Type \'string | undefined\' is not assignable to type \'string\'.\\n Type \'undefined\' is not assignable to type \'string\'.", "3817619378"] ], "js/components/TableDetail/ColumnStats/index.spec.tsx:1228258528": [ [90, 39, 4, "Argument of type \'null\' is not assignable to parameter of type \'number\'.", "2087897566"] @@ -86,7 +86,7 @@ exports[`strict null compilation`] = { [141, 17, 16, "Object is possibly \'undefined\'.", "3451845569"], [257, 30, 34, "Argument of type \'number | null\' is not assignable to parameter of type \'number\'.\\n Type \'null\' is not assignable to type \'number\'.", "3967943985"] ], - "js/components/TableDetail/SourceLink/index.spec.tsx:3683846951": [ + "js/components/TableDetail/SourceLink/index.spec.tsx:2646548231": [ [39, 21, 100, "Object is possibly \'null\'.", "1316242242"] ], "js/components/TableDetail/TableDashboardResourceList/index.tsx:3147978263": [ @@ -107,19 +107,19 @@ exports[`strict null compilation`] = { "js/components/TableDetail/index.spec.tsx:2169788066": [ [32, 4, 8, "Argument of type \'Partial> | undefined\' is not assignable to parameter of type \'Partial>\'.\\n Type \'undefined\' is not assignable to type \'Partial>\'.", "2700611480"] ], - "js/components/TableDetail/index.tsx:1105490806": [ + "js/components/TableDetail/index.tsx:3223260709": [ [153, 10, 13, "Type \'null\' is not assignable to type \'((newValue: string, onSuccess?: (() => any) | undefined, onFailure?: (() => any) | undefined) => void) | undefined\'.", "67794331"], - [164, 6, 7, "Type \'Element\' is not assignable to type \'never\'.", "3716929964"], - [171, 6, 3, "Type \'string\' is not assignable to type \'never\'.", "193424690"], - [172, 6, 5, "Type \'string\' is not assignable to type \'never\'.", "183222373"], - [182, 8, 7, "Type \'Element\' is not assignable to type \'never\'.", "3716929964"], - [183, 11, 26, "Type \'{ itemsPerPage: number; source: string; }\' is missing the following properties from type \'Readonly & OwnProps>\': isLoading, dashboards, errorText", "2224258167"], - [188, 8, 3, "Type \'string\' is not assignable to type \'never\'.", "193424690"], - [189, 8, 5, "Type \'string | Element\' is not assignable to type \'never\'.\\n Type \'string\' is not assignable to type \'never\'.", "183222373"], - [263, 16, 7, "Type \'string | null\' is not assignable to type \'string | undefined\'.\\n Type \'null\' is not assignable to type \'string | undefined\'.", "3817619378"], - [305, 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"], - [319, 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"], - [324, 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"] + [165, 6, 7, "Type \'Element\' is not assignable to type \'never\'.", "3716929964"], + [173, 6, 3, "Type \'string\' is not assignable to type \'never\'.", "193424690"], + [174, 6, 5, "Type \'string\' is not assignable to type \'never\'.", "183222373"], + [184, 8, 7, "Type \'Element\' is not assignable to type \'never\'.", "3716929964"], + [185, 11, 26, "Type \'{ itemsPerPage: number; source: string; }\' is missing the following properties from type \'Readonly & OwnProps>\': isLoading, dashboards, errorText", "2224258167"], + [190, 8, 3, "Type \'string\' is not assignable to type \'never\'.", "193424690"], + [191, 8, 5, "Type \'string | Element\' is not assignable to type \'never\'.\\n Type \'string\' is not assignable to type \'never\'.", "183222373"], + [265, 16, 7, "Type \'string | null\' is not assignable to type \'string | undefined\'.\\n Type \'null\' is not assignable to type \'string | undefined\'.", "3817619378"], + [307, 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"], + [321, 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"], + [326, 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/components/common/Announcements/AnnouncementsList/index.spec.tsx:1395073325": [ [94, 23, 124, "Object is possibly \'null\'.", "4248337497"] @@ -163,7 +163,7 @@ exports[`strict null compilation`] = { "js/components/common/EntityCard/EntityCardSection/index.tsx:1592385405": [ [40, 4, 23, "Object is possibly \'null\'.", "1725552512"] ], - "js/components/common/Flag/index.tsx:128873066": [ + "js/components/common/Flag/index.tsx:2997458704": [ [44, 27, 8, "Argument of type \'string | null\' is not assignable to parameter of type \'string\'.\\n Type \'null\' is not assignable to type \'string\'.", "4036080041"] ], "js/components/common/OwnerEditor/index.spec.tsx:3936675418": [ diff --git a/frontend/amundsen_application/static/css/_fonts-default.scss b/frontend/amundsen_application/static/css/_fonts-default.scss index 8ff0c9a7f7..4be35fcbb0 100644 --- a/frontend/amundsen_application/static/css/_fonts-default.scss +++ b/frontend/amundsen_application/static/css/_fonts-default.scss @@ -3,6 +3,14 @@ @import 'variables'; +// Space Mono +@font-face { + font-family: 'Space Mono'; + font-style: normal; + font-weight: $font-weight-header-regular; + src: url('/static/fonts/SpaceMono-Regular.ttf') format('truetype'); +} + // Roboto @font-face { font-family: 'Roboto'; diff --git a/frontend/amundsen_application/static/css/_popovers.scss b/frontend/amundsen_application/static/css/_popovers.scss index 8d2aeaf054..f6fa3d6c26 100644 --- a/frontend/amundsen_application/static/css/_popovers.scss +++ b/frontend/amundsen_application/static/css/_popovers.scss @@ -72,8 +72,3 @@ margin: auto; width: 24px; } - -.column-type-popover { - max-width: 552px; // arbitrary 2x default - word-break: break-word; -} diff --git a/frontend/amundsen_application/static/css/_variables-default.scss b/frontend/amundsen_application/static/css/_variables-default.scss index a2602e94fc..e572bd83e9 100644 --- a/frontend/amundsen_application/static/css/_variables-default.scss +++ b/frontend/amundsen_application/static/css/_variables-default.scss @@ -45,6 +45,7 @@ $font-weight-header-regular: 500 !default; $font-weight-header-bold: 700 !default; $font-family-monospace: 'Menlo-Bold', menlo, monospace !default; +$font-family-monospace-code: 'Space Mono', menlo, monospace !default; $font-family-serif: georgia, 'Times New Roman', times, serif !default; $font-size-small: 12px !default; @@ -162,6 +163,8 @@ $w2-headline-line-height: 32px; $w3-headline-font-size: 22px; $w3-headline-line-height: 26px; +$code-font-size: 12px; + $title-font-weight: $font-weight-body-bold; $subtitle-font-weight: $font-weight-body-semi-bold; $body-font-weight: $font-weight-body-regular; diff --git a/frontend/amundsen_application/static/fonts/SpaceMono-Regular.ttf b/frontend/amundsen_application/static/fonts/SpaceMono-Regular.ttf new file mode 100755 index 0000000000000000000000000000000000000000..3374aca03058e66d3a2cae074f0bcbd73466aad5 GIT binary patch literal 90972 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 = ({ columns, + database, editText, editUrl, }: ColumnListProps) => { @@ -27,6 +29,7 @@ const ColumnList: React.FC = ({ null); + +const setup = (propOverrides?: Partial) => { + const props = { + columnName: 'test', + database: 'presto', + type: + 'row(test_id varchar,test2 row(test2_id varchar,started_at timestamp,ended_at timestamp))', + ...propOverrides, + }; + const wrapper = mount(); + return { wrapper, props }; +}; +const { wrapper, props } = setup(); + +describe('ColumnType', () => { + describe('lifecycle', () => { + describe('when clicking on column-type-btn', () => { + it('should call showModal on the instance', () => { + const clickSpy = jest.spyOn(wrapper.instance(), 'showModal'); + wrapper.instance().forceUpdate(); + wrapper.find('.column-type-btn').simulate('click'); + + expect(clickSpy).toHaveBeenCalled(); + }); + + it('should log the interaction', () => { + logClickSpy.mockClear(); + wrapper.find('.column-type-btn').simulate('click'); + + expect(logClickSpy).toHaveBeenCalled(); + }); + }); + }); + + describe('render', () => { + it('renders the column type string for simple types', () => { + const { wrapper, props } = setup({ type: 'varchar(32)' }); + expect(wrapper.find('.column-type').text()).toBe(props.type); + }); + + describe('for nested types', () => { + it('renders the truncated column type string', () => { + const actual = wrapper.find('.column-type-btn').text(); + const expected = 'row(...)'; + + expect(actual).toBe(expected); + }); + + describe('renders a modal', () => { + it('exists', () => { + const actual = wrapper.find(Modal).exists(); + const expected = true; + + expect(actual).toBe(expected); + }); + + it('renders props.type in modal body', () => { + const actual = wrapper.find('.sub-title').text(); + const expected = props.columnName; + + expect(actual).toBe(expected); + }); + + it('renders props.type in modal body', () => { + const actual = wrapper.find('.modal-body').text(); + const expected = props.type; + + expect(actual).toBe(expected); + }); + }); + }); + }); +}); diff --git a/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/ColumnType/index.tsx b/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/ColumnType/index.tsx new file mode 100644 index 0000000000..16cdd71e96 --- /dev/null +++ b/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/ColumnType/index.tsx @@ -0,0 +1,149 @@ +// Copyright Contributors to the Amundsen project. +// SPDX-License-Identifier: Apache-2.0 + +import * as React from 'react'; +import { Modal, OverlayTrigger, Popover } from 'react-bootstrap'; + +import { logClick } from 'ducks/utilMethods'; + +import './styles.scss'; + +import { + getTruncatedText, + parseNestedType, + NestedType, + ParsedType, +} from './parser'; + +const CTA_TEXT = 'Click to see nested fields'; +const MODAL_TITLE = 'Nested Type'; +const TEXT_INDENT = 8; + +export interface ColumnTypeProps { + columnName: string; + database: string; + type: string; +} + +export interface ColumnTypeState { + showModal: boolean; +} + +export class ColumnType extends React.Component< + ColumnTypeProps, + ColumnTypeState +> { + nestedType: NestedType | null; + + constructor(props) { + super(props); + + this.state = { + showModal: false, + }; + const { database, type } = this.props; + this.nestedType = parseNestedType(type, database); + } + + hideModal = (e) => { + this.stopPropagation(e); + this.setState({ showModal: false }); + }; + + showModal = (e) => { + logClick(e); + this.stopPropagation(e); + this.setState({ showModal: true }); + }; + + stopPropagation = (e) => { + if (e) { + e.stopPropagation(); + } + }; + + createLineItem = (text: string, textIndent: number) => { + return ( +

+ {text} +
+ ); + }; + + renderParsedChildren = (children: ParsedType[], level: number) => { + const textIndent = level * TEXT_INDENT; + return children.map((item) => { + if (typeof item === 'string') { + return this.createLineItem(item, textIndent); + } + return this.renderNestedType(item, level); + }); + }; + + renderNestedType = (nestedType: NestedType, level: number = 0) => { + const { head, tail, children } = nestedType; + const textIndent = level * TEXT_INDENT; + return ( +
+ {this.createLineItem(head, textIndent)} + {this.renderParsedChildren(children, level + 1)} + {this.createLineItem(tail, textIndent)} +
+ ); + }; + + render = () => { + const { columnName, type } = this.props; + + if (this.nestedType === null) { + return

{type}

; + } + + const popoverHover = ( + + {CTA_TEXT} + + ); + return ( +
+ + + + + + +
{MODAL_TITLE}
+
{columnName}
+
+
+ +
+ {this.renderNestedType(this.nestedType)} +
+
+
+
+ ); + }; +} + +export default ColumnType; diff --git a/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/ColumnType/parser.spec.ts b/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/ColumnType/parser.spec.ts new file mode 100644 index 0000000000..cd614b17f9 --- /dev/null +++ b/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/ColumnType/parser.spec.ts @@ -0,0 +1,180 @@ +import * as Parser from './parser'; + +describe('getTruncatedText', () => { + it('returns correct text', () => { + const nestedType: Parser.NestedType = { + head: 'hello<', + children: ['how are you'], + tail: '>', + }; + const expected = 'hello<...>'; + + expect(Parser.getTruncatedText(nestedType)).toEqual(expected); + }); + + it('returns correct text with delimeters removed', () => { + const nestedType: Parser.NestedType = { + head: 'hello<', + children: ['how are you'], + tail: '>,', + }; + const expected = 'hello<...>'; + + expect(Parser.getTruncatedText(nestedType)).toEqual(expected); + }); +}); + +describe('isNestedType', () => { + it('returns true for supported complex types', () => { + expect(Parser.isNestedType('struct', 'hive')).toEqual(true); + }); + + it('returns false for unsupported complex types', () => { + expect(Parser.isNestedType('xyz', 'hive')).toEqual(false); + }); + + it('returns false for unsupported databases', () => { + expect(Parser.isNestedType('struct', 'xyz')).toEqual(false); + }); + + it('returns falsde for non-complex types', () => { + expect(Parser.isNestedType('string', 'hive')).toEqual(false); + }); +}); + +describe('parseNestedType', () => { + it('returns null if not a complex type', () => { + expect(Parser.parseNestedType('test', 'hive')).toEqual(null); + }); + + describe('hive support', () => { + it('returns expected NestedType for nested structs', () => { + const columnType = + 'array>,id:string>>'; + const expected: Parser.NestedType = { + head: 'array<', + children: [ + { + head: 'struct<', + children: [ + 'amount:bigint,', + { + head: 'column:struct<', + children: [ + 'column_id:string,', + 'name:string,', + { + head: 'template:struct<', + children: ['code:string,', 'currency:string'], + tail: '>', + }, + ], + tail: '>,', + }, + 'id:string', + ], + tail: '>', + }, + ], + tail: '>', + }; + + expect(Parser.parseNestedType(columnType, 'hive')).toEqual(expected); + }); + }); + + describe('presto support', () => { + it('returns expected NestedType for row', () => { + const columnType = + 'row("c0_test" timestamp(3),"c1" row("c2" timestamp(3),"c3_test" varchar,"c4" double,"c5" double,"c6" row("c7" varchar,"c8" varchar),"c9" row("c10" varchar,"c11" varchar,"c12" row("c13_id" varchar,"c14" varchar)))'; + const expected: Parser.NestedType = { + head: 'row(', + children: [ + 'c0_test timestamp(3),', + { + head: 'c1 row(', + children: [ + 'c2 timestamp(3),', + 'c3_test varchar,', + 'c4 double,', + 'c5 double,', + { + head: 'c6 row(', + children: ['c7 varchar,', 'c8 varchar'], + tail: '),', + }, + { + head: 'c9 row(', + children: [ + 'c10 varchar,', + 'c11 varchar,', + { + head: 'c12 row(', + children: ['c13_id varchar,', 'c14 varchar'], + tail: ')', + }, + ], + tail: ')', + }, + ], + tail: ')', + }, + ], + tail: ')', + }; + + expect(Parser.parseNestedType(columnType, 'presto')).toEqual(expected); + }); + + it('returns expected NestedType for array', () => { + const columnType = + 'array(row("total" bigint,"currency" varchar,"status" varchar,"payments" array(row("method" varchar,"payment" varchar,"amount" bigint,"authed" bigint,"id" varchar)),"id" varchar,"line_items" array(row("type" varchar,"amount" bigint,"id" varchar))))'; + const expected: Parser.NestedType = { + head: 'array(', + children: [ + { + head: 'row(', + children: [ + 'total bigint,', + 'currency varchar,', + 'status varchar,', + { + head: 'payments array(', + children: [ + { + head: 'row(', + children: [ + 'method varchar,', + 'payment varchar,', + 'amount bigint,', + 'authed bigint,', + 'id varchar', + ], + tail: ')', + }, + ], + tail: '),', + }, + 'id varchar,', + { + head: 'line_items array(', + children: [ + { + head: 'row(', + children: ['type varchar,', 'amount bigint,', 'id varchar'], + tail: ')', + }, + ], + tail: ')', + }, + ], + tail: ')', + }, + ], + tail: ')', + }; + + expect(Parser.parseNestedType(columnType, 'presto')).toEqual(expected); + }); + }); +}); diff --git a/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/ColumnType/parser.ts b/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/ColumnType/parser.ts new file mode 100644 index 0000000000..81966a72de --- /dev/null +++ b/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/ColumnType/parser.ts @@ -0,0 +1,156 @@ +// Copyright Contributors to the Amundsen project. +// SPDX-License-Identifier: Apache-2.0 + +export type ParsedType = string | NestedType; + +export interface NestedType { + head: string; + tail: string; + children: ParsedType[]; +} +enum DatabaseId { + Hive = 'hive', + Presto = 'presto', +} +const SUPPORTED_TYPES = { + // https://cwiki.apache.org/confluence/display/Hive/LanguageManual+Types#LanguageManualTypes-ComplexTypes + [DatabaseId.Hive]: ['array', 'map', 'struct', 'uniontype'], + // https://prestosql.io/docs/current/language/types.html#structural + [DatabaseId.Presto]: ['array', 'map', 'row'], +}; +const OPEN_DELIMETERS = { + '(': ')', + '<': '>', + '[': ']', +}; +const CLOSE_DELIMETERS = { + ')': '(', + '>': '<', + ']': '[', +}; +const SEPARATOR_DELIMETER = ','; + +/* + * Iterates through the columnType string and recursively creates a NestedType + */ +function parseNestedTypeHelper( + columnType: string, + startIndex: number = 0, + currentIndex: number = 0 +): { nextStartIndex: number; results: ParsedType[] } { + const children: ParsedType[] = []; + + while (currentIndex < columnType.length) { + const currentChar = columnType.charAt(currentIndex); + + if (currentChar === SEPARATOR_DELIMETER) { + /* Case 1: End of non-nested item */ + children.push(columnType.substring(startIndex, currentIndex + 1).trim()); + startIndex = currentIndex + 1; + currentIndex = startIndex; + } else if (currentChar in CLOSE_DELIMETERS) { + /* Case 2: End of a nested item */ + if (startIndex !== currentIndex) { + children.push(columnType.substring(startIndex, currentIndex).trim()); + } + return { + nextStartIndex: currentIndex + 1, + results: children, + }; + } else if (currentChar in OPEN_DELIMETERS) { + /* Case 3: Beginning of a nested item */ + if ( + columnType.substring(startIndex, currentIndex).endsWith('timestamp') + ) { + /* + Case 3.1: A non-supported item like timestamp() in Presto + Advance until we reach the closing character for this item. + On the next iteration Case 1 will apply. + */ + while ( + columnType.charAt(currentIndex) !== OPEN_DELIMETERS[currentChar] + ) { + currentIndex++; + } + currentIndex++; + } else { + /* Case 3.2: A supported nested item */ + const parsedResults = parseNestedTypeHelper( + columnType, + currentIndex + 1, + currentIndex + 1 + ); + let isLast: boolean = true; + let { nextStartIndex } = parsedResults; + + if (columnType.charAt(nextStartIndex) === SEPARATOR_DELIMETER) { + isLast = false; + nextStartIndex++; + } + + children.push({ + head: columnType.substring(startIndex, currentIndex + 1), + tail: `${OPEN_DELIMETERS[currentChar]}${ + isLast ? '' : SEPARATOR_DELIMETER + }`, + children: parsedResults.results, + }); + + startIndex = nextStartIndex; + currentIndex = startIndex; + } + } else { + currentIndex++; + } + } + + return { + nextStartIndex: currentIndex + 1, + results: children, + }; +} + +/* + * Returns whether or not a columnType string represents a complex type for the given database + */ +export function isNestedType(columnType: string, databaseId: string): boolean { + const supportedTypes = SUPPORTED_TYPES[databaseId]; + let isNested = false; + if (supportedTypes) { + supportedTypes.forEach((supportedType) => { + if ( + columnType.startsWith(supportedType) && + columnType !== supportedType + ) { + isNested = true; + } + }); + } + return isNested; +} + +/** + * Returns a NestedType object for supported complex types, else returns null + */ +export function parseNestedType( + columnType: string, + databaseId: string +): NestedType | null { + // Presto includes un-needed "" characters + if (databaseId === DatabaseId.Presto) { + columnType = columnType.replace(/"/g, ''); + } + + if (isNestedType(columnType, databaseId)) { + return parseNestedTypeHelper(columnType).results[0] as NestedType; + } + return null; +} + +/* + * Returns the truncated string representation for a NestedType + */ +export function getTruncatedText(nestedType: NestedType): string { + const { head, tail } = nestedType; + return `${head}...${tail.replace(SEPARATOR_DELIMETER, '')}`; +} diff --git a/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/ColumnType/styles.scss b/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/ColumnType/styles.scss new file mode 100644 index 0000000000..c6b7f4d3d3 --- /dev/null +++ b/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/ColumnType/styles.scss @@ -0,0 +1,85 @@ +// Copyright Contributors to the Amundsen project. +// SPDX-License-Identifier: Apache-2.0 + +@import 'variables'; +@import 'typography'; + +/* Fixed heights via designs */ +$modal-content-height: 484px; +$modal-dialog-height: 484px; +$modal-header-height: 94px; + +/* Fixed witdh via designs */ +$modal-dialog-width: 418px; + +.column-type-btn { + border: none; + background: none; + color: $link-color; + padding-left: 0; + + &:hover, + &:focus { + color: $link-hover-color; + cursor: pointer; + } +} + +.column-type-modal { + .modal-body { + border-bottom: 1px solid $stroke-light; + border-top: 1px solid $stroke-light; + height: calc(#{$modal-dialog-height - $modal-header-height - $spacer-3}); + font-family: $font-family-monospace-code; + font-size: $code-font-size; + /* Override react-bootstrap styles to match design */ + margin: 0 $spacer-3 !important; + padding: $spacer-1 0 !important; + text-align: initial !important; + } + + .modal-content { + height: $modal-content-height; + } + + .modal-dialog { + height: $modal-dialog-height; + width: $modal-dialog-width; + overflow-y: hidden; + } + + .modal-header { + border-bottom: none; + height: $modal-header-height; + /* Override react-bootstrap styles to match design */ + padding: $spacer-3 !important; + + .main-title { + @extend %text-title-w1; + } + .sub-title { + @extend %text-subtitle-w3; + color: $text-secondary; + } + } +} + +/* + These three styles vertically center the modal: https://codepen.io/dimbslmh/full/mKfCc + Bootstrap4 will have a dedicated class to handle this. +*/ +.modal { + text-align: center; + padding: 0 !important; +} +.modal:before { + content: ''; + display: inline-block; + height: 100%; + vertical-align: middle; + margin-right: -4px; +} +.modal-dialog { + display: inline-block; + vertical-align: middle; +} diff --git a/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/index.spec.tsx b/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/index.spec.tsx index dd15f69150..5da3511280 100644 --- a/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/index.spec.tsx +++ b/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/index.spec.tsx @@ -16,6 +16,8 @@ import AppConfig from 'config/config'; import * as UtilMethods from 'ducks/utilMethods'; import { RequestMetadataType } from 'interfaces/Notifications'; +import ColumnType from './ColumnType'; + const logClickSpy = jest.spyOn(UtilMethods, 'logClick'); logClickSpy.mockImplementation(() => null); @@ -36,6 +38,7 @@ describe('ColumnListItem', () => { }, ], }, + database: 'hive', index: 0, openRequestDescriptionDialog: jest.fn(), editText: 'Click to edit discription in source', @@ -107,9 +110,9 @@ describe('ColumnListItem', () => { expect(columnDesc.text()).toBe(props.data.description); }); - it('renders the correct resource type', () => { + it('renders the ColumnType', () => { const resourceType = wrapper.find('.resource-type'); - expect(resourceType.text()).toBe(props.data.col_type.toLowerCase()); + expect(resourceType.find(ColumnType).exists()).toBe(true); }); it('renders the dropdown when notifications is enabled', () => { diff --git a/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/index.tsx b/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/index.tsx index e538c20204..7d58dd890b 100644 --- a/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/index.tsx +++ b/frontend/amundsen_application/static/js/components/TableDetail/ColumnListItem/index.tsx @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import * as React from 'react'; -import { Dropdown, MenuItem, OverlayTrigger, Popover } from 'react-bootstrap'; +import { Dropdown, MenuItem } from 'react-bootstrap'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; @@ -16,6 +16,7 @@ import { RequestMetadataType, TableColumn } from 'interfaces'; import './styles.scss'; import EditableSection from 'components/common/EditableSection'; +import ColumnType from './ColumnType'; const MORE_BUTTON_TEXT = 'More options'; const EDITABLE_SECTION_TITLE = 'Description'; @@ -29,6 +30,7 @@ interface DispatchFromProps { interface OwnProps { data: TableColumn; + database: string; index: number; editText: string; editUrl: string; @@ -52,15 +54,15 @@ export class ColumnListItem extends React.Component< } toggleExpand = (e) => { - const metadata = this.props.data; + const { data } = this.props; if (!this.state.isExpanded) { logClick(e, { - target_id: `column::${metadata.name}`, + target_id: `column::${data.name}`, target_type: 'column stats', - label: `${metadata.name} ${metadata.col_type}`, + label: `${data.name} ${data.col_type}`, }); } - if (this.shouldRenderDescription() || metadata.stats.length !== 0) { + if (this.shouldRenderDescription() || data.stats.length !== 0) { this.setState((prevState) => ({ isExpanded: !prevState.isExpanded, })); @@ -89,58 +91,8 @@ export class ColumnListItem extends React.Component< return true; }; - renderColumnType = (columnIndex: number, type: string) => { - const truncatedTypes: string[] = ['array', 'struct', 'map', 'row']; - let shouldTrucate = false; - - const fullText = type.toLowerCase(); - let text = fullText; - - truncatedTypes.forEach((truncatedType) => { - if (type.startsWith(truncatedType) && type !== truncatedType) { - shouldTrucate = true; - const lastChar = type.charAt(type.length - 1); - if (lastChar === '>') { - text = `${truncatedType}<...>`; - } else if (lastChar === ')') { - text = `${truncatedType}(...)`; - } else { - text = `${truncatedType}...`; - } - } - }); - - if (shouldTrucate) { - const popoverHover = ( - - {fullText} - - ); - return ( - - - {text} - - - ); - } - return
{text}
; - }; - render() { - const metadata = this.props.data; + const { data, database } = this.props; return (
  • @@ -150,15 +102,19 @@ export class ColumnListItem extends React.Component< !this.state.isExpanded ? 'my-auto' : '' }`} > -
    {metadata.name}
    +
    {data.name}
    {!this.state.isExpanded && (
    - {metadata.description} + {data.description}
    )}
    - {this.renderColumnType(this.props.index, metadata.col_type)} +
    {/* Placeholder */}
    @@ -191,20 +147,20 @@ export class ColumnListItem extends React.Component< {this.shouldRenderDescription() && ( )}
    - + )} diff --git a/frontend/amundsen_application/static/js/components/TableDetail/SourceLink/index.spec.tsx b/frontend/amundsen_application/static/js/components/TableDetail/SourceLink/index.spec.tsx index 7aa9a97a24..9aefc68382 100644 --- a/frontend/amundsen_application/static/js/components/TableDetail/SourceLink/index.spec.tsx +++ b/frontend/amundsen_application/static/js/components/TableDetail/SourceLink/index.spec.tsx @@ -1,10 +1,10 @@ import * as React from 'react'; import { mount } from 'enzyme'; -import SourceLink, { SourceLinkProps } from '.'; import AvatarLabel from 'components/common/AvatarLabel'; import AppConfig from 'config/config'; import { ResourceType } from 'interfaces/Resources'; +import SourceLink, { SourceLinkProps } from '.'; const setup = (propOverrides?: Partial) => { const props = { diff --git a/frontend/amundsen_application/static/js/components/TableDetail/index.tsx b/frontend/amundsen_application/static/js/components/TableDetail/index.tsx index e99a5af4a4..1908623828 100644 --- a/frontend/amundsen_application/static/js/components/TableDetail/index.tsx +++ b/frontend/amundsen_application/static/js/components/TableDetail/index.tsx @@ -159,18 +159,20 @@ export class TableDetail extends React.Component< renderTabs(editText, editUrl) { const tabInfo = []; + const { isLoadingDashboards, numRelatedDashboards, tableData } = this.props; // Default Column content tabInfo.push({ content: ( ), key: 'columns', - title: `Columns (${this.props.tableData.columns.length})`, + title: `Columns (${tableData.columns.length})`, }); if (indexDashboardsEnabled()) { @@ -187,9 +189,9 @@ export class TableDetail extends React.Component< /> ), key: 'dashboards', - title: this.props.isLoadingDashboards + title: isLoadingDashboards ? loadingTitle - : `Dashboards (${this.props.numRelatedDashboards})`, + : `Dashboards (${numRelatedDashboards})`, }); }