feat(react): eliminate noises in react build, test and cleanup, get rid of warnings (#2618)

This commit is contained in:
Brian 2021-05-28 02:23:09 +08:00 committed by GitHub
parent b38ea63d53
commit 409246a99d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1109 additions and 28957 deletions

View File

@ -1 +1,4 @@
**/*.graphql
dist/
build/
node_modules/

View File

@ -8,12 +8,10 @@ module.exports = {
'airbnb-typescript',
'airbnb/hooks',
'prettier',
'prettier/react',
'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
'plugin:prettier/recommended',
],
parserOptions: {
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features
sourceType: 'module', // Allows for the use of imports
ecmaFeatures: {
jsx: true, // Allows for the parsing of JSX
@ -22,8 +20,8 @@ module.exports = {
},
rules: {
eqeqeq: ['error', 'always'],
'react/destructuring-assignment': 'warn',
'no-console': 'warn',
'react/destructuring-assignment': 'off',
'no-console': 'off',
'no-debugger': 'warn',
'require-await': 'warn',
'import/prefer-default-export': 'off', // TODO: remove this lint rule
@ -41,9 +39,9 @@ module.exports = {
},
],
'@typescript-eslint/no-empty-interface': 'off',
'react/destructuring-assignment': 'warn',
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": 'off',
"import/no-extraneous-dependencies": 'off'
},
settings: {
react: {

View File

@ -26,4 +26,5 @@ yarn-debug.log*
yarn-error.log*
# gql codegen
*.generated.ts
*.generated.ts
/.vscode

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,7 @@
"@analytics/segment": "^1.1.0",
"@ant-design/colors": "^5.0.0",
"@ant-design/icons": "^4.3.0",
"@apollo/client": "^3.3.6",
"@apollo/client": "^3.3.19",
"@craco/craco": "^6.1.1",
"@data-ui/xy-chart": "^0.0.84",
"@react-hook/window-size": "^3.0.7",
@ -37,7 +37,7 @@
"@vx/shape": "^0.0.199",
"@vx/zoom": "^0.0.199",
"analytics": "^0.7.5",
"antd": "^4.15.2",
"antd": "^4.16.0",
"apollo-link": "^1.2.14",
"apollo-link-error": "^1.1.13",
"apollo-link-http": "^1.5.17",
@ -57,6 +57,7 @@
"react-router": "^5.2.0",
"react-router-dom": "^5.1.6",
"react-scripts": "4.0.3",
"sinon": "^11.1.1",
"styled-components": "^5.2.1",
"typescript": "^4.1.3",
"uuid": "^8.3.2",
@ -72,7 +73,8 @@
"storybook": "start-storybook -p 6006 -s public",
"build-storybook": "build-storybook -s public",
"generate": "graphql-codegen --config codegen.yml",
"lint": "eslint . --ext .ts,.tsx --quiet"
"lint": "eslint . --ext .ts,.tsx --quiet",
"lint-fix": "eslint '*/**/*.{ts,tsx}' --quiet --fix"
},
"eslintConfig": {
"extends": [
@ -107,15 +109,15 @@
"@types/graphql": "^14.5.0",
"@types/query-string": "^6.3.0",
"@types/styled-components": "^5.1.7",
"@typescript-eslint/eslint-plugin": "^4.11.0",
"@typescript-eslint/parser": "^4.11.0",
"babel-loader": "8.1.0",
"eslint": "^7.11.0",
"eslint-config-airbnb-typescript": "^12.0.0",
"eslint-config-prettier": "^7.1.0",
"eslint-plugin-prettier": "^3.3.0",
"eslint-plugin-react": "^7.21.5",
"prettier": "^2.2.1"
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"babel-loader": "8.2.2",
"eslint": "^7.27.0",
"eslint-config-airbnb-typescript": "^12.3.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-react": "^7.23.2",
"prettier": "^2.3.0"
},
"proxy": "http://localhost:9002",
"resolutions": {

View File

@ -1,7 +1,11 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { render } from '@testing-library/react';
import App from './App';
test('renders the app', () => {
// eslint-disable-next-line jest/expect-expect
test('renders the app', async () => {
const promise = Promise.resolve();
render(<App />);
await act(() => promise);
});

View File

@ -1,4 +1,5 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { render, waitFor, fireEvent } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
@ -8,6 +9,7 @@ import { mocks } from '../../../../../Mocks';
describe('DataJobProfile', () => {
it('renders', async () => {
const promise = Promise.resolve();
const { getByText, queryAllByText } = render(
<MockedProvider mocks={mocks} addTypename={false}>
<TestPageContainer initialEntries={['/pipelines/urn:li:dataFlow:1']}>
@ -18,9 +20,11 @@ describe('DataJobProfile', () => {
await waitFor(() => expect(queryAllByText('DataFlowInfoName').length).toBeGreaterThanOrEqual(1));
expect(getByText('DataFlowInfo1 Description')).toBeInTheDocument();
await act(() => promise);
});
it('topological sort', async () => {
const promise = Promise.resolve();
const { getByTestId, getByText, queryAllByText, getAllByTestId } = render(
<MockedProvider
mocks={mocks}
@ -38,15 +42,17 @@ describe('DataJobProfile', () => {
await waitFor(() => expect(queryAllByText('DataFlowInfoName').length).toBeGreaterThanOrEqual(1));
const rawButton = getByText('Task');
fireEvent.click(rawButton);
act(() => {
fireEvent.click(rawButton);
});
await waitFor(() => expect(getByTestId('dataflow-jobs-list')).toBeInTheDocument());
await waitFor(() => expect(queryAllByText('DataJobInfoName3').length).toBeGreaterThanOrEqual(1));
await new Promise((r) => setTimeout(r, 1000));
const jobsList = getAllByTestId('datajob-item-preview');
expect(jobsList.length).toBe(3);
expect(jobsList[0].innerHTML).toMatch(/DataJobInfoName3/);
expect(jobsList[1].innerHTML).toMatch(/DataJobInfoName/);
expect(jobsList[2].innerHTML).toMatch(/DataJobInfoName2/);
await act(() => promise);
});
});

View File

@ -23,54 +23,52 @@ const TypeIconContainer = styled.div`
width: 40px;
`;
const TypeSubtitle = styled(Typography.Text)<{ hasIcon: boolean }>`
const TypeSubtitle = styled(Typography.Text)<{ hasicon?: string }>`
font-size: 8px;
text-align: center;
${(props) => (props.hasIcon ? '' : 'margin-top: 4px;')}
${(props) => (props.hasicon ? '' : 'margin-top: 4px;')}
`;
const IconSpan = styled.span`
font-size: 18px;
`;
const DATA_TYPE_ICON_MAP: Record<
SchemaFieldDataType,
{ icon: FC<{ style: any }> | null; size: number; text: string }
> = {
[SchemaFieldDataType.Boolean]: {
icon: FieldBinaryOutlined,
size: 18,
text: 'Boolean',
},
[SchemaFieldDataType.Fixed]: { icon: FieldBinaryOutlined, size: 18, text: 'Fixed' },
[SchemaFieldDataType.String]: {
icon: () => (
<IconSpan role="img" aria-label="calendar" className="anticon anticon-calendar">
<VscSymbolString />
</IconSpan>
),
size: 20,
text: 'String',
},
[SchemaFieldDataType.Bytes]: {
icon: () => (
<IconSpan role="img" aria-label="calendar" className="anticon anticon-calendar">
<VscFileBinary />
</IconSpan>
),
size: 18,
text: 'Bytes',
},
[SchemaFieldDataType.Number]: { icon: NumberOutlined, size: 14, text: 'Number' },
[SchemaFieldDataType.Date]: { icon: CalendarOutlined, size: 18, text: 'Date' },
[SchemaFieldDataType.Time]: { icon: FieldTimeOutlined, size: 18, text: 'Time' },
[SchemaFieldDataType.Enum]: { icon: UnorderedListOutlined, size: 18, text: 'Enum' },
[SchemaFieldDataType.Null]: { icon: QuestionCircleOutlined, size: 16, text: '' },
[SchemaFieldDataType.Map]: { icon: null, size: 0, text: 'Map' },
[SchemaFieldDataType.Array]: { icon: UnorderedListOutlined, size: 14, text: 'Array' },
[SchemaFieldDataType.Union]: { icon: UnderlineOutlined, size: 14, text: 'Union' },
[SchemaFieldDataType.Struct]: { icon: null, size: 0, text: 'Struct' },
};
const DATA_TYPE_ICON_MAP: Record<SchemaFieldDataType, { icon: FC<{ style: any }> | null; size: number; text: string }> =
{
[SchemaFieldDataType.Boolean]: {
icon: FieldBinaryOutlined,
size: 18,
text: 'Boolean',
},
[SchemaFieldDataType.Fixed]: { icon: FieldBinaryOutlined, size: 18, text: 'Fixed' },
[SchemaFieldDataType.String]: {
icon: () => (
<IconSpan role="img" aria-label="calendar" className="anticon anticon-calendar">
<VscSymbolString />
</IconSpan>
),
size: 20,
text: 'String',
},
[SchemaFieldDataType.Bytes]: {
icon: () => (
<IconSpan role="img" aria-label="calendar" className="anticon anticon-calendar">
<VscFileBinary />
</IconSpan>
),
size: 18,
text: 'Bytes',
},
[SchemaFieldDataType.Number]: { icon: NumberOutlined, size: 14, text: 'Number' },
[SchemaFieldDataType.Date]: { icon: CalendarOutlined, size: 18, text: 'Date' },
[SchemaFieldDataType.Time]: { icon: FieldTimeOutlined, size: 18, text: 'Time' },
[SchemaFieldDataType.Enum]: { icon: UnorderedListOutlined, size: 18, text: 'Enum' },
[SchemaFieldDataType.Null]: { icon: QuestionCircleOutlined, size: 16, text: '' },
[SchemaFieldDataType.Map]: { icon: null, size: 0, text: 'Map' },
[SchemaFieldDataType.Array]: { icon: UnorderedListOutlined, size: 14, text: 'Array' },
[SchemaFieldDataType.Union]: { icon: UnderlineOutlined, size: 14, text: 'Union' },
[SchemaFieldDataType.Struct]: { icon: null, size: 0, text: 'Struct' },
};
type Props = {
type: SchemaFieldDataType;
@ -93,7 +91,7 @@ export default function TypeIcon({ type, nativeDataType }: Props) {
<NativeDataTypeTooltip>
<TypeIconContainer data-testid={`icon-${type}`}>
{Icon && <Icon style={{ fontSize: size }} />}
<TypeSubtitle type="secondary" hasIcon={!!Icon}>
<TypeSubtitle type="secondary" hasicon={Icon ? 'yes' : undefined}>
{text}
</TypeSubtitle>
</TypeIconContainer>

View File

@ -86,8 +86,7 @@ export const sampleSchema: Schema = {
],
platformSchema: {
__typename: 'TableSchema',
schema:
'{ "type": "record", "name": "SampleHdfsSchema", "namespace": "com.linkedin.dataset", "doc": "Sample HDFS dataset", "fields": [ { "name": "field_foo", "type": [ "string" ] }, { "name": "field_bar", "type": [ "boolean" ] } ] }',
schema: '{ "type": "record", "name": "SampleHdfsSchema", "namespace": "com.linkedin.dataset", "doc": "Sample HDFS dataset", "fields": [ { "name": "field_foo", "type": [ "string" ] }, { "name": "field_bar", "type": [ "boolean" ] } ] }',
},
};

View File

@ -1,4 +1,5 @@
import React from 'react';
import { act } from 'react-dom/test-utils';
import { fireEvent, render, waitFor } from '@testing-library/react';
import { MockedProvider } from '@apollo/client/testing';
import { Route } from 'react-router';
@ -9,17 +10,8 @@ import { mocks } from '../../../Mocks';
import { PageRoutes } from '../../../conf/Global';
describe('SearchPage', () => {
it('renders', () => {
render(
<MockedProvider mocks={mocks} addTypename={false}>
<TestPageContainer initialEntries={['/search/dataset?filter_platform=hive,kafka&page=1&query=sample']}>
<Route path={PageRoutes.SEARCH_RESULTS} render={() => <SearchPage />} />
</TestPageContainer>
</MockedProvider>,
);
});
it('renders loading', async () => {
const promise = Promise.resolve();
const { getByText } = render(
<MockedProvider mocks={mocks} addTypename={false}>
<TestPageContainer initialEntries={['/search/dataset?filter_platform=hive,kafka&page=1&query=sample']}>
@ -28,9 +20,11 @@ describe('SearchPage', () => {
</MockedProvider>,
);
await waitFor(() => expect(getByText('Loading...')).toBeInTheDocument());
await act(() => promise);
});
it('renders the selected filters as checked', async () => {
const promise = Promise.resolve();
const { getByTestId, queryByTestId } = render(
<MockedProvider mocks={mocks} addTypename={false}>
<TestPageContainer initialEntries={['/search/dataset?filter_platform=kafka&page=1&query=test']}>
@ -41,7 +35,9 @@ describe('SearchPage', () => {
await waitFor(() => expect(getByTestId('filters-button')).toBeInTheDocument());
const filtersButton = getByTestId('filters-button');
fireEvent.click(filtersButton);
act(() => {
fireEvent.click(filtersButton);
});
await waitFor(() => expect(queryByTestId('facet-platform-kafka')).toBeInTheDocument());
@ -53,9 +49,11 @@ describe('SearchPage', () => {
const prodOriginBox = getByTestId('facet-origin-PROD');
expect(prodOriginBox).toHaveProperty('checked', false);
await act(() => promise);
});
it('renders multiple checked filters at once', async () => {
const promise = Promise.resolve();
const { getByTestId, queryByTestId } = render(
<MockedProvider mocks={mocks} addTypename={false}>
<TestPageContainer initialEntries={['/search/dataset?filter_platform=kafka,hdfs&page=1&query=test']}>
@ -66,7 +64,9 @@ describe('SearchPage', () => {
await waitFor(() => expect(getByTestId('filters-button')).toBeInTheDocument());
const filtersButton = getByTestId('filters-button');
fireEvent.click(filtersButton);
act(() => {
fireEvent.click(filtersButton);
});
await waitFor(() => expect(queryByTestId('facet-platform-kafka')).toBeInTheDocument());
@ -78,9 +78,11 @@ describe('SearchPage', () => {
const prodOriginBox = getByTestId('facet-origin-PROD');
expect(prodOriginBox).toHaveProperty('checked', false);
await act(() => promise);
});
it('clicking a filter selects a new filter', async () => {
const promise = Promise.resolve();
const { getByTestId, queryByTestId } = render(
<MockedProvider mocks={mocks} addTypename={false}>
<TestPageContainer initialEntries={['/search/dataset?filter_platform=kafka&page=1&query=test']}>
@ -91,7 +93,9 @@ describe('SearchPage', () => {
await waitFor(() => expect(getByTestId('filters-button')).toBeInTheDocument());
const filtersButton = getByTestId('filters-button');
fireEvent.click(filtersButton);
act(() => {
fireEvent.click(filtersButton);
});
await waitFor(() => expect(queryByTestId('facet-platform-kafka')).toBeInTheDocument());
@ -100,8 +104,9 @@ describe('SearchPage', () => {
const hdfsPlatformBox = getByTestId('facet-platform-hdfs');
expect(hdfsPlatformBox).toHaveProperty('checked', false);
fireEvent.click(hdfsPlatformBox);
act(() => {
fireEvent.click(hdfsPlatformBox);
});
await waitFor(() => expect(queryByTestId('facet-platform-kafka')).toBeInTheDocument());
@ -110,5 +115,6 @@ describe('SearchPage', () => {
const hdfsPlatformBox2 = getByTestId('facet-platform-hdfs');
expect(hdfsPlatformBox2).toHaveProperty('checked', true);
await act(() => promise);
});
});

View File

@ -15,8 +15,9 @@ type Props = {
export default function AvatarsGroup({ owners, entityRegistry, maxCount = 6, size = 'default' }: Props) {
return (
<Avatar.Group maxCount={maxCount} size={size}>
{(owners || [])?.map((owner) => (
<div data-testid={`avatar-tag-${owner.owner.urn}`} key={owner.owner.urn}>
{(owners || [])?.map((owner, key) => (
// eslint-disable-next-line react/no-array-index-key
<div data-testid={`avatar-tag-${owner.owner.urn}`} key={`${owner.owner.urn}-${key}`}>
{owner.owner.__typename === 'CorpUser' ? (
<CustomAvatar
name={owner.owner.info?.fullName || owner.owner.info?.firstName || owner.owner.info?.email}

View File

@ -1,4 +1,4 @@
import { Avatar, AvatarProps, Tooltip } from 'antd';
import { Avatar, Tooltip } from 'antd';
import { TooltipPlacement } from 'antd/lib/tooltip';
import React from 'react';
import { Link } from 'react-router-dom';
@ -6,12 +6,10 @@ import styled from 'styled-components';
import defaultAvatar from '../../../images/default_avatar.png';
const AvatarStyled = styled(
({ size: _, isGroup: __, ...props }: AvatarProps & { size?: number; isGroup?: boolean }) => <Avatar {...props} />,
)`
const AvatarStyled = styled(Avatar)<{ size?: number; isgroup?: string }>`
color: #fff;
background-color: ${(props) =>
props.isGroup ? '#ccc' : '#ccc'}; // TODO: make it different style for corpGroup vs corpUser
props.isgroup ? '#ccc' : '#ccc'}; // TODO: make it different style for corpGroup vs corpUser
text-align: center;
font-size: ${(props) => (props.size ? `${Math.max(props.size / 2.0, 14)}px` : '14px')} !important;
&& > span {
@ -41,20 +39,20 @@ export default function CustomAvatar({
isGroup = false,
}: Props) {
const avatarWithInitial = name ? (
<AvatarStyled style={style} size={size} isGroup={isGroup}>
<AvatarStyled style={style} size={size} isgroup={isGroup ? 'true' : undefined}>
{name.charAt(0).toUpperCase()}
</AvatarStyled>
) : (
<AvatarStyled src={defaultAvatar} style={style} size={size} isGroup={isGroup} />
<AvatarStyled src={defaultAvatar} style={style} size={size} isgroup={isGroup ? 'true' : undefined} />
);
const avatarWithDefault = useDefaultAvatar ? (
<AvatarStyled src={defaultAvatar} style={style} size={size} isGroup={isGroup} />
<AvatarStyled src={defaultAvatar} style={style} size={size} isgroup={isGroup ? 'true' : undefined} />
) : (
avatarWithInitial
);
const avatar =
photoUrl && photoUrl !== '' ? (
<AvatarStyled src={photoUrl} style={style} size={size} isGroup={isGroup} />
<AvatarStyled src={photoUrl} style={style} size={size} isgroup={isGroup ? 'true' : undefined} />
) : (
avatarWithDefault
);

View File

@ -3,6 +3,7 @@
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
import sinon from 'sinon';
// Mock window.matchMedia interface.
// See https://jestjs.io/docs/en/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom
@ -16,3 +17,8 @@ global.matchMedia =
removeListener: jest.fn(),
};
});
const { location } = window;
delete window.location;
window.location = { ...location, replace: () => {} };
sinon.stub(window.location, 'replace');

File diff suppressed because it is too large Load Diff