Skip to content

Commit

Permalink
feat(table): create new Table component based on AWSUI Table component (
Browse files Browse the repository at this point in the history
#129)

feat: add helper functions for

- iconUtils.tsx - copied from synchro-charts with changes.
- iconUtils.spec.ts - copied from synchro-charts with changes:
-- add one test cases, that color are provided, compared with the original.
- spinner.tsx - copied from synchro-charts with adjustments for React
- spinner.spec.tsx - copied from synchro-charts with adjustments for React

feat: add the main table component

fix: remove generic types for TableProps

fix: add size to loading spinner

fix: remove unnecessary TS compiler options

update a few things based on comments from PR.

Fix: remove full file eslint disable. Switch to minimal eslint disable.
  • Loading branch information
square-li authored and diehbria committed Oct 17, 2022
1 parent c215558 commit 43deb53
Show file tree
Hide file tree
Showing 15 changed files with 563 additions and 6 deletions.
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export * from './common/types';
export * from './common/viewport';
export * from './common/time';
export * from './common/combineProviders';
export * from './common/number';

export * from './data-module/TimeSeriesDataModule';
export * from './mockWidgetProperties';
Expand Down
5 changes: 5 additions & 0 deletions packages/table/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import 'jest-extended';

export declare global {
// eslint-disable-next-line no-var,vars-on-top
var IS_REACT_ACT_ENVIRONMENT: boolean;
}
3 changes: 2 additions & 1 deletion packages/table/jest.config.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['jest-extended/all'],
coverageDirectory: 'coverage',
collectCoverageFrom: ['src/**/*.{ts,tsx}'],
testPathIgnorePatterns: ['/dist'],
coverageReporters: ['text-summary', 'cobertura', 'html', 'json', 'json-summary'],
Expand Down
1 change: 1 addition & 0 deletions packages/table/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"eslint-config-airbnb-typescript": "^12.3.1",
"jest": "^28.1.0",
"jest-cli": "^28.1.0",
"jest-environment-jsdom": "^28.1.1",
"jest-extended": "^2.0.0",
"rollup-plugin-import-css": "^3.0.3",
"sass": "^1.30.0",
Expand Down
2 changes: 1 addition & 1 deletion packages/table/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// export * from './table'; WIP
export * from './table';
export * from './utils';
18 changes: 18 additions & 0 deletions packages/table/src/table/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React, { FunctionComponent } from 'react';
import { PropertyFilter, Table as AWSUITable } from '@awsui/components-react';
import { useCollection } from '@awsui/collection-hooks';
import { TableProps } from '../utils';
import { defaultI18nStrings, getDefaultColumnDefinitions } from '../utils/tableHelpers';

export const Table: FunctionComponent<TableProps> = (props) => {
const { items, useCollectionOption = { sorting: {} }, columnDefinitions } = props;
const { collectionProps, propertyFilterProps } = useCollection(items, useCollectionOption);
return (
<AWSUITable
{...props}
{...collectionProps}
columnDefinitions={getDefaultColumnDefinitions(columnDefinitions)}
filter={<PropertyFilter {...propertyFilterProps} i18nStrings={defaultI18nStrings} />}
/>
);
};
78 changes: 78 additions & 0 deletions packages/table/src/utils/iconUtilis.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { STATUS_ICONS, StatusIcon } from '@synchro-charts/core';
import { getIcons } from './iconUtils';

describe('sets default pixel size', () => {
it.each(STATUS_ICONS)('renders %p at correct default size', (iconName) => {
const icon = getIcons(iconName)!;
expect(icon?.props.width).toBe('16px');
expect(icon?.props.height).toBe('16px');
});
});

describe('sets customized color', () => {
it.each(STATUS_ICONS)('renders %p with customized color or stroke', (iconName) => {
const icon = getIcons(iconName, '#fffff')!;
expect(icon?.props.fill || icon?.props.stroke).toBe('#fffff');
});
});

describe('sets icon size', () => {
it.each(STATUS_ICONS)('renders %p at the correct size', (iconName) => {
const size = 100;
const icon = getIcons(iconName, undefined, size)!;
expect(icon.props.width).toBe(`${size}px`);
expect(icon.props.height).toBe(`${size}px`);
});
});

it('returns normal icon from StatusIcon.ACKNOWLEDGED provided', () => {
const iconName = StatusIcon.ACKNOWLEDGED;
const icon = getIcons(iconName)!;
expect(icon.props.stroke).toEqual('#3184c2');
});

it('returns normal icon from StatusIcon.ACTIVE provided', () => {
const iconName = StatusIcon.ACTIVE;
const icon = getIcons(iconName)!;
expect(icon.props.fill).toEqual('#d13212');
});

it('returns normal icon from StatusIcon.ACKNOWLEDGED provided', () => {
const iconName = StatusIcon.ACKNOWLEDGED;
const icon = getIcons(iconName)!;
expect(icon.props.stroke).toEqual('#3184c2');
});

it('returns normal icon from StatusIcon.DISABLED provided', () => {
const iconName = StatusIcon.DISABLED;
const icon = getIcons(iconName)!;
expect(icon.props.stroke).toEqual('#687078');
});

it('returns normal icon from StatusIcon.LATCHED provided', () => {
const iconName = StatusIcon.LATCHED;
const icon = getIcons(iconName)!;
expect(icon.props.fill).toEqual('#f89256');
});

it('returns normal icon from StatusIcon.SNOOZED provided', () => {
const iconName = StatusIcon.SNOOZED;
const icon = getIcons(iconName)!;
expect(icon.props.stroke).toEqual('#879596');
});

it('returns normal icon from StatusIcon.SNOOZED provided with color', () => {
const iconName = StatusIcon.SNOOZED;
const icon = getIcons(iconName, 'white')!;
expect(icon.props.stroke).toEqual('white');
});

it('returns error icon from StatusIcon.ERROR', () => {
const iconName = StatusIcon.ERROR;
const icon = getIcons(iconName);
expect(icon).not.toBeNull();
});

it('returned undefined when invalid icon requested', () => {
expect(getIcons('fake-icon' as StatusIcon)).toBeUndefined();
});
113 changes: 113 additions & 0 deletions packages/table/src/utils/iconUtils.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, { ReactElement, SVGAttributes } from 'react';
import { StatusIcon } from '@synchro-charts/core';

const DEFAULT_SIZE_PX = 16;

export const icons = {
normal(color?: string, size: number = DEFAULT_SIZE_PX) {
return (
<svg width={`${size}px`} height={`${size}px`} viewBox="0 0 16 16" fill={color ? `${color}` : '#1d8102'}>
<path d="M8 0C3.6 0 0 3.6 0 8s3.6 8 8 8 8-3.6 8-8-3.6-8-8-8zm0 14c-3.3 0-6-2.7-6-6s2.7-6 6-6 6 2.7 6 6-2.7 6-6 6z" />
<path d="M7 8.6l-2-2L3.6 8 7 11.4l4.9-4.9-1.4-1.4z" />
</svg>
);
},
active(color?: string, size: number = DEFAULT_SIZE_PX) {
return (
<svg width={`${size}px`} height={`${size}px`} fill={color ? `${color}` : '#d13212'} viewBox="0 0 16 16">
<g fill="none" fillRule="evenodd">
<circle cx="8" cy="8" r="7" stroke={color ? `${color}` : '#d13212'} strokeWidth="2" />
<g transform="translate(7 4)">
<mask id="b" fill="#fff">
<path id="a" d="M2.00129021 6v2h-2V6h2zm0-6v5h-2V0h2z" />
</mask>
<g mask="url(#b)">
<path fill={color ? `${color}` : '#d13212'} d="M-7-5H9v16H-7z" />
</g>
</g>
</g>
</svg>
);
},
acknowledged(color?: string, size: number = DEFAULT_SIZE_PX) {
return (
<svg width={`${size}px`} height={`${size}px`} viewBox="0 0 16 16" stroke={color ? `${color}` : '#3184c2'}>
<path
fill="none"
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M2 12.286h5.143L8.857 14l1.714-1.714H14V2H2v10.286z"
/>
<path
fill="none"
strokeLinecap="round"
strokeWidth="2"
strokeMiterlimit="10"
d="M4.99 7H5v.01h-.01zM7.99 7H8v.01h-.01zM10.99 7H11v.01h-.01z"
/>
</svg>
);
},
disabled(color?: string, size: number = DEFAULT_SIZE_PX) {
return (
<svg width={`${size}px`} height={`${size}px`} viewBox="0 0 16 16" stroke={color ? `${color}` : '#687078'}>
<g fill="none" strokeWidth="2">
<circle cx="8" cy="8" r="7" strokeLinejoin="round" />
<path strokeLinecap="square" strokeMiterlimit="10" d="M11 8H5" />
</g>
</svg>
);
},
latched(color?: string, size: number = DEFAULT_SIZE_PX) {
return (
<svg width={`${size}px`} height={`${size}px`} viewBox="0 0 16 16" fill={color ? `${color}` : '#f89256'}>
<path d="M15.9 14.6l-7-14c-.3-.7-1.5-.7-1.8 0l-7 14c-.2.3-.1.7 0 1 .2.2.6.4.9.4h14c.3 0 .7-.2.9-.5.1-.3.1-.6 0-.9zM2.6 14L8 3.2 13.4 14H2.6z" />
<path d="M7 11v2h2v-2zM7 6h2v4H7z" />
</svg>
);
},
snoozed(color?: string, size: number = DEFAULT_SIZE_PX) {
return (
<svg width={`${size}px`} height={`${size}px`} viewBox="0 0 16 16" stroke={color ? `${color}` : '#879596'}>
<g fill="none" strokeWidth="2">
<circle cx="8" cy="8" r="7" strokeLinejoin="round" />
<path strokeLinecap="square" strokeMiterlimit="10" d="M8 5v4H5" />
</g>
</svg>
);
},
error(color?: string, size: number = DEFAULT_SIZE_PX) {
return (
<svg
width={`${size}px`}
height={`${size}px`}
viewBox="0 0 16 16"
fill={color ? `${color}` : '#FF0000'}
data-test-tag="error"
>
<path
className="st4"
d="M13.7 2.3C12.1.8 10.1 0 8 0S3.9.8 2.3 2.3C.8 3.9 0 5.9 0 8s.8 4.1 2.3 5.7C3.9 15.2 5.9 16 8 16s4.1-.8 5.7-2.3C15.2 12.1 16 10.1 16 8s-.8-4.1-2.3-5.7zm-1.5 9.9C11.1 13.4 9.6 14 8 14s-3.1-.6-4.2-1.8S2 9.6 2 8s.6-3.1 1.8-4.2S6.4 2 8 2s3.1.6 4.2 1.8S14 6.4 14 8s-.6 3.1-1.8 4.2z"
/>
<path
className="st4"
d="M10.1 4.5L8 6.6 5.9 4.5 4.5 5.9 6.6 8l-2.1 2.1 1.4 1.4L8 9.4l2.1 2.1 1.4-1.4L9.4 8l2.1-2.1z"
/>
</svg>
);
},
};

export const getIcons: (
name: StatusIcon,
color?: string,
size?: number
) => ReactElement<SVGAttributes<unknown>> | undefined = (name: StatusIcon, color?: string, size?: number) => {
if (icons[name]) {
return icons[name](color, size);
}
/* eslint-disable-next-line no-console */
console.warn(`Invalid status icon requested: ${name}`);
return undefined;
};
2 changes: 1 addition & 1 deletion packages/table/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { createTableItems } from './createTableItems';
export { CellItem, Item, ItemRef, TableItem } from './types';
export { CellItem, Item, ItemRef, TableItem, TableProps, ColumnDefinition, CellItemProps } from './types';
78 changes: 78 additions & 0 deletions packages/table/src/utils/spinner.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import React from 'react';
import { createRoot, Root } from 'react-dom/client';
import { act } from 'react-dom/test-utils';
import { LoadingSpinner } from './spinner';

globalThis.IS_REACT_ACT_ENVIRONMENT = true;
describe('size', () => {
let container: HTMLDivElement;
let root: Root;

beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});

afterEach(() => {
act(() => {
root.unmount();
container.remove();
});
});

it('does not specify height and width when no size provided', async () => {
act(() => {
root.render(<LoadingSpinner />);
});

const svgElement = container.getElementsByTagName('svg').item(0)!;

expect(svgElement.style.height).toBe('');
expect(svgElement.style.width).toBe('');
});

it('does specify height and width when size provided', async () => {
const SIZE = 34;
act(() => {
root.render(<LoadingSpinner size={SIZE} />);
});
const svgElement = container.getElementsByTagName('svg').item(0)!;
expect(svgElement.style.height).toBe(`${SIZE}px`);
expect(svgElement.style.width).toBe(`${SIZE}px`);
});
});

describe('dark mode', () => {
let container: HTMLDivElement;
let root: Root;

beforeEach(() => {
container = document.createElement('div');
document.body.appendChild(container);
root = createRoot(container);
});

afterEach(() => {
act(() => {
root.unmount();
container.remove();
});
});

it('does not have the dark class present when dark is false', async () => {
act(() => {
root.render(<LoadingSpinner />);
});
const svgElement = container.getElementsByTagName('svg').item(0)!;
expect(svgElement.classList).not.toContain('dark');
});

it('does not have the dark class present when dark is false', async () => {
act(() => {
root.render(<LoadingSpinner dark />);
});
const svgElement = container.getElementsByTagName('svg').item(0)!;
expect(svgElement.classList).toContain('dark');
});
});
61 changes: 61 additions & 0 deletions packages/table/src/utils/spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copied from Synchro-charts "ScLoadingSpinner"
import React from 'react';

export const LoadingSpinner: React.FunctionComponent<{ size?: number; dark?: boolean } | Record<string, never>> = (
props
) => {
const { size, dark = false } = props;

return (
<svg
className={dark ? 'dark' : undefined}
style={size != null ? { width: `${size}px`, height: `${size}px` } : {}}
data-testid="loading"
viewBox="0 0 200 200"
>
<defs>
<clipPath id="a">
<path d="M200 100a100 100 0 11-2.19-20.79l-9.78 2.08A90 90 0 10190 100z" />
</clipPath>
<filter id="b" x="0" y="0">
<feGaussianBlur in="SourceGraphic" stdDeviation="3" />
</filter>
<path id="c" d="M250 100a150 150 0 01-3.28 31.19L100 100z" />
</defs>
<g clipPath="url(#a)">
<g filter="url(#b)" transform="rotate(-6 100 100)">
<use xlinkHref="#c" fillOpacity="0" />
<use xlinkHref="#c" fillOpacity=".03" transform="rotate(12 100 100)" />
<use xlinkHref="#c" fillOpacity=".07" transform="rotate(24 100 100)" />
<use xlinkHref="#c" fillOpacity=".1" transform="rotate(36 100 100)" />
<use xlinkHref="#c" fillOpacity=".14" transform="rotate(48 100 100)" />
<use xlinkHref="#c" fillOpacity=".17" transform="rotate(60 100 100)" />
<use xlinkHref="#c" fillOpacity=".2" transform="rotate(72 100 100)" />
<use xlinkHref="#c" fillOpacity=".24" transform="rotate(84 100 100)" />
<use xlinkHref="#c" fillOpacity=".28" transform="rotate(96 100 100)" />
<use xlinkHref="#c" fillOpacity=".31" transform="rotate(108 100 100)" />
<use xlinkHref="#c" fillOpacity=".34" transform="rotate(120 100 100)" />
<use xlinkHref="#c" fillOpacity=".38" transform="rotate(132 100 100)" />
<use xlinkHref="#c" fillOpacity=".41" transform="rotate(144 100 100)" />
<use xlinkHref="#c" fillOpacity=".45" transform="rotate(156 100 100)" />
<use xlinkHref="#c" fillOpacity=".48" transform="rotate(168 100 100)" />
<use xlinkHref="#c" fillOpacity=".52" transform="rotate(180 100 100)" />
<use xlinkHref="#c" fillOpacity=".55" transform="rotate(192 100 100)" />
<use xlinkHref="#c" fillOpacity=".59" transform="rotate(204 100 100)" />
<use xlinkHref="#c" fillOpacity=".62" transform="rotate(216 100 100)" />
<use xlinkHref="#c" fillOpacity=".66" transform="rotate(228 100 100)" />
<use xlinkHref="#c" fillOpacity=".69" transform="rotate(240 100 100)" />
<use xlinkHref="#c" fillOpacity=".7" transform="rotate(252 100 100)" />
<use xlinkHref="#c" fillOpacity=".72" transform="rotate(264 100 100)" />
<use xlinkHref="#c" fillOpacity=".76" transform="rotate(276 100 100)" />
<use xlinkHref="#c" fillOpacity=".79" transform="rotate(288 100 100)" />
<use xlinkHref="#c" fillOpacity=".83" transform="rotate(300 100 100)" />
<use xlinkHref="#c" fillOpacity=".86" transform="rotate(312 100 100)" />
<use xlinkHref="#c" fillOpacity=".93" transform="rotate(324 100 100)" />
<use xlinkHref="#c" fillOpacity=".97" transform="rotate(336 100 100)" />
<use xlinkHref="#c" transform="rotate(348 100 100)" />
</g>
</g>
</svg>
);
};
Loading

0 comments on commit 43deb53

Please sign in to comment.