feat: Adds the MetadataBar component (#21090)
* feat: Adds the MetadataBar component * Addresses comments
This commit is contained in:
parent
db7e2b2e37
commit
151795663b
|
|
@ -115,6 +115,7 @@ module.exports = {
|
||||||
'jsx-a11y/anchor-is-valid': 1,
|
'jsx-a11y/anchor-is-valid': 1,
|
||||||
'jsx-a11y/click-events-have-key-events': 0, // re-enable up for discussion
|
'jsx-a11y/click-events-have-key-events': 0, // re-enable up for discussion
|
||||||
'jsx-a11y/mouse-events-have-key-events': 0, // re-enable up for discussion
|
'jsx-a11y/mouse-events-have-key-events': 0, // re-enable up for discussion
|
||||||
|
'max-classes-per-file': 0,
|
||||||
'new-cap': 0,
|
'new-cap': 0,
|
||||||
'no-bitwise': 0,
|
'no-bitwise': 0,
|
||||||
'no-continue': 0,
|
'no-continue': 0,
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@ module.exports = {
|
||||||
builder: 'webpack5',
|
builder: 'webpack5',
|
||||||
},
|
},
|
||||||
stories: [
|
stories: [
|
||||||
'../src/@(components|common|filters|explore)/**/*.stories.@(t|j)sx',
|
'../src/@(components|common|filters|explore)/**/*.stories.@(tsx|jsx|mdx)',
|
||||||
],
|
],
|
||||||
addons: [
|
addons: [
|
||||||
'@storybook/addon-essentials',
|
'@storybook/addon-essentials',
|
||||||
|
|
@ -47,6 +47,6 @@ module.exports = {
|
||||||
plugins: [...config.plugins, ...customConfig.plugins],
|
plugins: [...config.plugins, ...customConfig.plugins],
|
||||||
}),
|
}),
|
||||||
typescript: {
|
typescript: {
|
||||||
reactDocgen: 'none',
|
reactDocgen: 'react-docgen-typescript',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -66,4 +66,5 @@ addParameters({
|
||||||
method: 'alphabetical',
|
method: 'alphabetical',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
controls: { expanded: true },
|
||||||
});
|
});
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -223,6 +223,7 @@
|
||||||
"@hot-loader/react-dom": "^16.13.0",
|
"@hot-loader/react-dom": "^16.13.0",
|
||||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||||
"@storybook/addon-actions": "^6.4.22",
|
"@storybook/addon-actions": "^6.4.22",
|
||||||
|
"@storybook/addon-docs": "^6.5.10",
|
||||||
"@storybook/addon-essentials": "^6.4.22",
|
"@storybook/addon-essentials": "^6.4.22",
|
||||||
"@storybook/addon-knobs": "^6.3.1",
|
"@storybook/addon-knobs": "^6.3.1",
|
||||||
"@storybook/addon-links": "^6.4.22",
|
"@storybook/addon-links": "^6.4.22",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,143 @@
|
||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { ensureIsArray, styled, t } from '@superset-ui/core';
|
||||||
|
import Icons from 'src/components/Icons';
|
||||||
|
import { ContentType, MetadataType } from './ContentType';
|
||||||
|
|
||||||
|
const Header = styled.div`
|
||||||
|
font-weight: ${({ theme }) => theme.typography.weights.bold};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Info = ({
|
||||||
|
text,
|
||||||
|
header,
|
||||||
|
}: {
|
||||||
|
text?: string | string[];
|
||||||
|
header?: string;
|
||||||
|
}) => {
|
||||||
|
const values = ensureIsArray(text);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{header && <Header>{header}</Header>}
|
||||||
|
{values.map(value => (
|
||||||
|
<div key={value}>{value}</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const config = (contentType: ContentType) => {
|
||||||
|
const { type } = contentType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tooltips are very similar. It's pretty much blocks
|
||||||
|
* of header/text pairs. That's why they are implemented here.
|
||||||
|
* If more complex tooltips emerge, then we should extract the different
|
||||||
|
* types of tooltips to their own components and reference them here.
|
||||||
|
*/
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case MetadataType.DASHBOARDS:
|
||||||
|
return {
|
||||||
|
icon: Icons.FundProjectionScreenOutlined,
|
||||||
|
title: contentType.title,
|
||||||
|
tooltip: contentType.description ? (
|
||||||
|
<div>
|
||||||
|
<Info header={contentType.title} text={contentType.description} />
|
||||||
|
</div>
|
||||||
|
) : undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
case MetadataType.DESCRIPTION:
|
||||||
|
return {
|
||||||
|
icon: Icons.BookOutlined,
|
||||||
|
title: contentType.value,
|
||||||
|
};
|
||||||
|
|
||||||
|
case MetadataType.LAST_MODIFIED:
|
||||||
|
return {
|
||||||
|
icon: Icons.EditOutlined,
|
||||||
|
title: moment.utc(contentType.value).fromNow(),
|
||||||
|
tooltip: (
|
||||||
|
<div>
|
||||||
|
<Info
|
||||||
|
header={t('Last modified')}
|
||||||
|
text={moment.utc(contentType.value).fromNow()}
|
||||||
|
/>
|
||||||
|
<Info header={t('Modified by')} text={contentType.modifiedBy} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
case MetadataType.OWNER:
|
||||||
|
return {
|
||||||
|
icon: Icons.UserOutlined,
|
||||||
|
title: contentType.createdBy,
|
||||||
|
tooltip: (
|
||||||
|
<div>
|
||||||
|
<Info header={t('Created by')} text={contentType.createdBy} />
|
||||||
|
<Info header={t('Owners')} text={contentType.owners} />
|
||||||
|
<Info
|
||||||
|
header={t('Created on')}
|
||||||
|
text={moment.utc(contentType.createdOn).fromNow()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
case MetadataType.ROWS:
|
||||||
|
return {
|
||||||
|
icon: Icons.InsertRowBelowOutlined,
|
||||||
|
title: contentType.title,
|
||||||
|
tooltip: contentType.title,
|
||||||
|
};
|
||||||
|
|
||||||
|
case MetadataType.SQL:
|
||||||
|
return {
|
||||||
|
icon: Icons.ConsoleSqlOutlined,
|
||||||
|
title: contentType.title,
|
||||||
|
tooltip: contentType.title,
|
||||||
|
};
|
||||||
|
|
||||||
|
case MetadataType.TABLE:
|
||||||
|
return {
|
||||||
|
icon: Icons.Table,
|
||||||
|
title: contentType.title,
|
||||||
|
tooltip: contentType.title,
|
||||||
|
};
|
||||||
|
|
||||||
|
case MetadataType.TAGS:
|
||||||
|
return {
|
||||||
|
icon: Icons.TagsOutlined,
|
||||||
|
title: contentType.values.join(', '),
|
||||||
|
tooltip: (
|
||||||
|
<div>
|
||||||
|
<Info header={t('Tags')} text={contentType.values} />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw Error(`Invalid type provided: ${type}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { config };
|
||||||
|
|
@ -0,0 +1,91 @@
|
||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export enum MetadataType {
|
||||||
|
DASHBOARDS = 'dashboards',
|
||||||
|
DESCRIPTION = 'description',
|
||||||
|
LAST_MODIFIED = 'lastModified',
|
||||||
|
OWNER = 'owner',
|
||||||
|
ROWS = 'rows',
|
||||||
|
SQL = 'sql',
|
||||||
|
TABLE = 'table',
|
||||||
|
TAGS = 'tags',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Dashboards = {
|
||||||
|
type: MetadataType.DASHBOARDS;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
onClick?: (type: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Description = {
|
||||||
|
type: MetadataType.DESCRIPTION;
|
||||||
|
value: string;
|
||||||
|
onClick?: (type: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LastModified = {
|
||||||
|
type: MetadataType.LAST_MODIFIED;
|
||||||
|
value: Date;
|
||||||
|
modifiedBy: string;
|
||||||
|
onClick?: (type: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Owner = {
|
||||||
|
type: MetadataType.OWNER;
|
||||||
|
createdBy: string;
|
||||||
|
owners: string[];
|
||||||
|
createdOn: Date;
|
||||||
|
onClick?: (type: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Rows = {
|
||||||
|
type: MetadataType.ROWS;
|
||||||
|
title: string;
|
||||||
|
onClick?: (type: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Sql = {
|
||||||
|
type: MetadataType.SQL;
|
||||||
|
title: string;
|
||||||
|
onClick?: (type: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Table = {
|
||||||
|
type: MetadataType.TABLE;
|
||||||
|
title: string;
|
||||||
|
onClick?: (type: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Tags = {
|
||||||
|
type: MetadataType.TAGS;
|
||||||
|
values: string[];
|
||||||
|
onClick?: (type: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ContentType =
|
||||||
|
| Dashboards
|
||||||
|
| Description
|
||||||
|
| LastModified
|
||||||
|
| Owner
|
||||||
|
| Rows
|
||||||
|
| Sql
|
||||||
|
| Table
|
||||||
|
| Tags;
|
||||||
|
|
@ -0,0 +1,109 @@
|
||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import { css } from '@superset-ui/core';
|
||||||
|
import { useResizeDetector } from 'react-resize-detector';
|
||||||
|
import MetadataBar, { MetadataBarProps } from './index';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'MetadataBar',
|
||||||
|
component: MetadataBar,
|
||||||
|
};
|
||||||
|
|
||||||
|
const A_WEEK_AGO = new Date(Date.now() - 7 * 24 * 3600 * 1000);
|
||||||
|
|
||||||
|
export const Component = ({
|
||||||
|
items,
|
||||||
|
onClick,
|
||||||
|
}: MetadataBarProps & {
|
||||||
|
onClick: (type: string) => void;
|
||||||
|
}) => {
|
||||||
|
const { width, height, ref } = useResizeDetector();
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
items[0].onClick = onClick;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
css={css`
|
||||||
|
margin-top: 70px;
|
||||||
|
margin-left: 80px;
|
||||||
|
overflow: auto;
|
||||||
|
min-width: ${168}px;
|
||||||
|
max-width: ${740}px;
|
||||||
|
resize: horizontal;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<MetadataBar items={items} />
|
||||||
|
<span
|
||||||
|
css={css`
|
||||||
|
position: absolute;
|
||||||
|
top: 150px;
|
||||||
|
left: 115px;
|
||||||
|
`}
|
||||||
|
>{`${width}x${height}`}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Component.story = {
|
||||||
|
parameters: {
|
||||||
|
knobs: {
|
||||||
|
disable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Component.args = {
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
type: 'sql',
|
||||||
|
title: 'Click to view query',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'owner',
|
||||||
|
createdBy: 'Jane Smith',
|
||||||
|
owners: ['John Doe', 'Mary Wilson'],
|
||||||
|
createdOn: A_WEEK_AGO,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'lastModified',
|
||||||
|
value: A_WEEK_AGO,
|
||||||
|
modifiedBy: 'Jane Smith',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'tags',
|
||||||
|
values: ['management', 'research', 'poc'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'dashboards',
|
||||||
|
title: 'Added to 452 dashboards',
|
||||||
|
description:
|
||||||
|
'To preview the list of dashboards go to "More" settings on the right.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
Component.argTypes = {
|
||||||
|
onClick: {
|
||||||
|
action: 'onClick',
|
||||||
|
table: {
|
||||||
|
disable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,257 @@
|
||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React from 'react';
|
||||||
|
import { render, screen, within } from 'spec/helpers/testing-library';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import * as resizeDetector from 'react-resize-detector';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { supersetTheme } from '@superset-ui/core';
|
||||||
|
import { hexToRgb } from 'src/utils/colorUtils';
|
||||||
|
import MetadataBar, { MIN_NUMBER_ITEMS, MAX_NUMBER_ITEMS } from '.';
|
||||||
|
import { ContentType, MetadataType } from './ContentType';
|
||||||
|
|
||||||
|
const DASHBOARD_TITLE = 'Added to 452 dashboards';
|
||||||
|
const DASHBOARD_DESCRIPTION =
|
||||||
|
'To preview the list of dashboards go to "More" settings on the right.';
|
||||||
|
const DESCRIPTION_VALUE = 'This is the description';
|
||||||
|
const ROWS_TITLE = '500 rows';
|
||||||
|
const SQL_TITLE = 'Click to view query';
|
||||||
|
const TABLE_TITLE = 'database.schema.table';
|
||||||
|
const CREATED_BY = 'Jane Smith';
|
||||||
|
const DATE = new Date(Date.parse('2022-01-01'));
|
||||||
|
const MODIFIED_BY = 'Jane Smith';
|
||||||
|
const OWNERS = ['John Doe', 'Mary Wilson'];
|
||||||
|
const TAGS = ['management', 'research', 'poc'];
|
||||||
|
|
||||||
|
const runWithBarCollapsed = async (func: Function) => {
|
||||||
|
const spy = jest.spyOn(resizeDetector, 'useResizeDetector');
|
||||||
|
spy.mockReturnValue({ width: 80, ref: { current: undefined } });
|
||||||
|
await func();
|
||||||
|
spy.mockRestore();
|
||||||
|
};
|
||||||
|
|
||||||
|
const ITEMS: ContentType[] = [
|
||||||
|
{
|
||||||
|
type: MetadataType.DASHBOARDS,
|
||||||
|
title: DASHBOARD_TITLE,
|
||||||
|
description: DASHBOARD_DESCRIPTION,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: MetadataType.DESCRIPTION,
|
||||||
|
value: DESCRIPTION_VALUE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: MetadataType.LAST_MODIFIED,
|
||||||
|
value: DATE,
|
||||||
|
modifiedBy: MODIFIED_BY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: MetadataType.OWNER,
|
||||||
|
createdBy: CREATED_BY,
|
||||||
|
owners: OWNERS,
|
||||||
|
createdOn: DATE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: MetadataType.ROWS,
|
||||||
|
title: ROWS_TITLE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: MetadataType.SQL,
|
||||||
|
title: SQL_TITLE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: MetadataType.TABLE,
|
||||||
|
title: TABLE_TITLE,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: MetadataType.TAGS,
|
||||||
|
values: TAGS,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test('renders an array of items', () => {
|
||||||
|
render(<MetadataBar items={ITEMS.slice(0, 2)} />);
|
||||||
|
expect(screen.getByText(DASHBOARD_TITLE)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(DESCRIPTION_VALUE)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('throws errors when out of min/max restrictions', () => {
|
||||||
|
const spy = jest.spyOn(console, 'error');
|
||||||
|
spy.mockImplementation(() => {});
|
||||||
|
expect(() =>
|
||||||
|
render(<MetadataBar items={ITEMS.slice(0, MIN_NUMBER_ITEMS - 1)} />),
|
||||||
|
).toThrow(
|
||||||
|
`The minimum number of items for the metadata bar is ${MIN_NUMBER_ITEMS}.`,
|
||||||
|
);
|
||||||
|
expect(() =>
|
||||||
|
render(<MetadataBar items={ITEMS.slice(0, MAX_NUMBER_ITEMS + 1)} />),
|
||||||
|
).toThrow(
|
||||||
|
`The maximum number of items for the metadata bar is ${MAX_NUMBER_ITEMS}.`,
|
||||||
|
);
|
||||||
|
spy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removes duplicated items when rendering', () => {
|
||||||
|
render(<MetadataBar items={[...ITEMS.slice(0, 2), ...ITEMS.slice(0, 2)]} />);
|
||||||
|
expect(screen.getAllByRole('img').length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('collapses the bar when min width is reached', async () => {
|
||||||
|
await runWithBarCollapsed(() => {
|
||||||
|
render(<MetadataBar items={ITEMS.slice(0, 2)} />);
|
||||||
|
expect(screen.queryByText(DASHBOARD_TITLE)).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText(DESCRIPTION_VALUE)).not.toBeInTheDocument();
|
||||||
|
expect(screen.getAllByRole('img').length).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('always renders a tooltip when the bar is collapsed', async () => {
|
||||||
|
await runWithBarCollapsed(async () => {
|
||||||
|
render(<MetadataBar items={ITEMS.slice(0, 2)} />);
|
||||||
|
userEvent.hover(screen.getAllByRole('img')[0]);
|
||||||
|
expect(await screen.findByText(DASHBOARD_DESCRIPTION)).toBeInTheDocument();
|
||||||
|
userEvent.hover(screen.getAllByRole('img')[1]);
|
||||||
|
expect(await screen.findByText(DESCRIPTION_VALUE)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders a tooltip when one is provided even if not collapsed', async () => {
|
||||||
|
render(<MetadataBar items={ITEMS.slice(0, 2)} />);
|
||||||
|
expect(screen.getByText(DASHBOARD_TITLE)).toBeInTheDocument();
|
||||||
|
userEvent.hover(screen.getAllByRole('img')[0]);
|
||||||
|
expect(await screen.findByText(DASHBOARD_DESCRIPTION)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders underlined text and emits event when clickable', () => {
|
||||||
|
const onClick = jest.fn();
|
||||||
|
const items = [{ ...ITEMS[0], onClick }, ITEMS[1]];
|
||||||
|
render(<MetadataBar items={items} />);
|
||||||
|
const element = screen.getByText(DASHBOARD_TITLE);
|
||||||
|
userEvent.click(element);
|
||||||
|
expect(onClick).toHaveBeenCalled();
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
|
expect(style.textDecoration).toBe('underline');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders clicable items with blue icons when the bar is collapsed', async () => {
|
||||||
|
await runWithBarCollapsed(async () => {
|
||||||
|
const onClick = jest.fn();
|
||||||
|
const items = [{ ...ITEMS[0], onClick }, ITEMS[1]];
|
||||||
|
render(<MetadataBar items={items} />);
|
||||||
|
const images = screen.getAllByRole('img');
|
||||||
|
const clickableColor = window.getComputedStyle(images[0]).color;
|
||||||
|
const nonClickableColor = window.getComputedStyle(images[1]).color;
|
||||||
|
expect(clickableColor).toBe(hexToRgb(supersetTheme.colors.primary.base));
|
||||||
|
expect(nonClickableColor).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('renders the items sorted', () => {
|
||||||
|
const { container } = render(<MetadataBar items={ITEMS.slice(0, 6)} />);
|
||||||
|
const nodes = container.firstChild?.childNodes as NodeListOf<HTMLElement>;
|
||||||
|
expect(within(nodes[0]).getByText(DASHBOARD_TITLE)).toBeInTheDocument();
|
||||||
|
expect(within(nodes[1]).getByText(ROWS_TITLE)).toBeInTheDocument();
|
||||||
|
expect(within(nodes[2]).getByText(SQL_TITLE)).toBeInTheDocument();
|
||||||
|
expect(within(nodes[3]).getByText(DESCRIPTION_VALUE)).toBeInTheDocument();
|
||||||
|
expect(within(nodes[4]).getByText(CREATED_BY)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('correctly renders the dashboards tooltip', async () => {
|
||||||
|
render(<MetadataBar items={ITEMS.slice(0, 2)} />);
|
||||||
|
userEvent.hover(screen.getByText(DASHBOARD_TITLE));
|
||||||
|
const tooltip = await screen.findByRole('tooltip');
|
||||||
|
expect(tooltip).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(DASHBOARD_TITLE)).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(DASHBOARD_DESCRIPTION)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('correctly renders the description tooltip', async () => {
|
||||||
|
await runWithBarCollapsed(async () => {
|
||||||
|
render(<MetadataBar items={ITEMS.slice(0, 2)} />);
|
||||||
|
userEvent.hover(screen.getAllByRole('img')[1]);
|
||||||
|
const tooltip = await screen.findByRole('tooltip');
|
||||||
|
expect(tooltip).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(DESCRIPTION_VALUE)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('correctly renders the last modified tooltip', async () => {
|
||||||
|
const dateText = moment.utc(DATE).fromNow();
|
||||||
|
render(<MetadataBar items={ITEMS.slice(0, 3)} />);
|
||||||
|
userEvent.hover(screen.getByText(dateText));
|
||||||
|
const tooltip = await screen.findByRole('tooltip');
|
||||||
|
expect(tooltip).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(dateText)).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(MODIFIED_BY)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('correctly renders the owner tooltip', async () => {
|
||||||
|
const dateText = moment.utc(DATE).fromNow();
|
||||||
|
render(<MetadataBar items={ITEMS.slice(0, 4)} />);
|
||||||
|
userEvent.hover(screen.getByText(CREATED_BY));
|
||||||
|
const tooltip = await screen.findByRole('tooltip');
|
||||||
|
expect(tooltip).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(CREATED_BY)).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(dateText)).toBeInTheDocument();
|
||||||
|
OWNERS.forEach(owner =>
|
||||||
|
expect(within(tooltip).getByText(owner)).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('correctly renders the rows tooltip', async () => {
|
||||||
|
await runWithBarCollapsed(async () => {
|
||||||
|
render(<MetadataBar items={ITEMS.slice(4, 8)} />);
|
||||||
|
userEvent.hover(screen.getAllByRole('img')[0]);
|
||||||
|
const tooltip = await screen.findByRole('tooltip');
|
||||||
|
expect(tooltip).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(ROWS_TITLE)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('correctly renders the sql tooltip', async () => {
|
||||||
|
await runWithBarCollapsed(async () => {
|
||||||
|
render(<MetadataBar items={ITEMS.slice(4, 8)} />);
|
||||||
|
userEvent.hover(screen.getAllByRole('img')[1]);
|
||||||
|
const tooltip = await screen.findByRole('tooltip');
|
||||||
|
expect(tooltip).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(SQL_TITLE)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('correctly renders the table tooltip', async () => {
|
||||||
|
await runWithBarCollapsed(async () => {
|
||||||
|
render(<MetadataBar items={ITEMS.slice(4, 8)} />);
|
||||||
|
userEvent.hover(screen.getAllByRole('img')[2]);
|
||||||
|
const tooltip = await screen.findByRole('tooltip');
|
||||||
|
expect(tooltip).toBeInTheDocument();
|
||||||
|
expect(within(tooltip).getByText(TABLE_TITLE)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('correctly renders the tags tooltip', async () => {
|
||||||
|
await runWithBarCollapsed(async () => {
|
||||||
|
render(<MetadataBar items={ITEMS.slice(4, 8)} />);
|
||||||
|
userEvent.hover(screen.getAllByRole('img')[3]);
|
||||||
|
const tooltip = await screen.findByRole('tooltip');
|
||||||
|
expect(tooltip).toBeInTheDocument();
|
||||||
|
TAGS.forEach(tag =>
|
||||||
|
expect(within(tooltip).getByText(tag)).toBeInTheDocument(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,137 @@
|
||||||
|
import { Meta, Source } from '@storybook/addon-docs';
|
||||||
|
|
||||||
|
<Meta title="MetadataBar/Overview" />
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
The metadata bar component is used to display additional information about an entity. Some of the common applications in Superset are:
|
||||||
|
|
||||||
|
- Display the chart's metadata in Explore to help the user understand what dashboards this chart is added to and get
|
||||||
|
to know the details of the chart
|
||||||
|
- Display the database's metadata in a drill to detail modal to help the user understand what data they are looking
|
||||||
|
at while accessing the feature in the dashboard
|
||||||
|
|
||||||
|
# Variations
|
||||||
|
|
||||||
|
The metadata bar is by default a static component (besides the links in text).
|
||||||
|
The variations in this component are related to content and entity type as all of the details are predefined
|
||||||
|
in the code and should be specific for each metadata object.
|
||||||
|
|
||||||
|
Content types are predefined and consistent across the whole app. This means that
|
||||||
|
they will be displayed and behave in a consistent manner, keeping the same ordering,
|
||||||
|
information formatting, and interactions. For example, the Owner content type will always
|
||||||
|
have the same icon and when hovered it will present who created the entity, its current owners, and when the entity was created.
|
||||||
|
|
||||||
|
To extend the list of content types, a developer needs to request the inclusion of the new type in the design system.
|
||||||
|
This process is important to make sure the new type is reviewed by the design team, improving Superset consistency.
|
||||||
|
|
||||||
|
To check each content type in detail and its interactions, check the [MetadataBar](/story/metadatabar--component) page.
|
||||||
|
Below you can find the configurations for each content type:
|
||||||
|
|
||||||
|
<Source
|
||||||
|
language="jsx"
|
||||||
|
format={true}
|
||||||
|
code={`
|
||||||
|
export enum MetadataType {
|
||||||
|
DASHBOARDS = 'dashboards',
|
||||||
|
DESCRIPTION = 'description',
|
||||||
|
LAST_MODIFIED = 'lastModified',
|
||||||
|
OWNER = 'owner',
|
||||||
|
ROWS = 'rows',
|
||||||
|
SQL = 'sql',
|
||||||
|
TABLE = 'table',
|
||||||
|
TAGS = 'tags',
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Source
|
||||||
|
language="jsx"
|
||||||
|
format={true}
|
||||||
|
code={`
|
||||||
|
export type Dashboards = {
|
||||||
|
type: MetadataType.DASHBOARDS;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
onClick?: (type: string) => void;
|
||||||
|
};`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Source
|
||||||
|
language="jsx"
|
||||||
|
format={true}
|
||||||
|
code={`
|
||||||
|
export type Description = {
|
||||||
|
type: MetadataType.DESCRIPTION;
|
||||||
|
value: string;
|
||||||
|
onClick?: (type: string) => void;
|
||||||
|
};`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Source
|
||||||
|
language="jsx"
|
||||||
|
format={true}
|
||||||
|
code={`
|
||||||
|
export type LastModified = {
|
||||||
|
type: MetadataType.LAST_MODIFIED;
|
||||||
|
value: Date;
|
||||||
|
modifiedBy: string;
|
||||||
|
onClick?: (type: string) => void;
|
||||||
|
};`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Source
|
||||||
|
language="jsx"
|
||||||
|
format={true}
|
||||||
|
code={`
|
||||||
|
export type Owner = {
|
||||||
|
type: MetadataType.OWNER;
|
||||||
|
createdBy: string;
|
||||||
|
owners: string[];
|
||||||
|
createdOn: Date;
|
||||||
|
onClick?: (type: string) => void;
|
||||||
|
};`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Source
|
||||||
|
language="jsx"
|
||||||
|
format={true}
|
||||||
|
code={`
|
||||||
|
export type Rows = {
|
||||||
|
type: MetadataType.ROWS;
|
||||||
|
title: string;
|
||||||
|
onClick?: (type: string) => void;
|
||||||
|
};`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Source
|
||||||
|
language="jsx"
|
||||||
|
format={true}
|
||||||
|
code={`
|
||||||
|
export type Sql = {
|
||||||
|
type: MetadataType.SQL;
|
||||||
|
title: string;
|
||||||
|
onClick?: (type: string) => void;
|
||||||
|
};`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Source
|
||||||
|
language="jsx"
|
||||||
|
format={true}
|
||||||
|
code={`
|
||||||
|
export type Table = {
|
||||||
|
type: MetadataType.TABLE;
|
||||||
|
title: string;
|
||||||
|
onClick?: (type: string) => void;
|
||||||
|
};`}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Source
|
||||||
|
language="jsx"
|
||||||
|
format={true}
|
||||||
|
code={`
|
||||||
|
export type Tags = {
|
||||||
|
type: MetadataType.TAGS;
|
||||||
|
values: string[];
|
||||||
|
onClick?: (type: string) => void;
|
||||||
|
};`}
|
||||||
|
/>
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
/**
|
||||||
|
* Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
* or more contributor license agreements. See the NOTICE file
|
||||||
|
* distributed with this work for additional information
|
||||||
|
* regarding copyright ownership. The ASF licenses this file
|
||||||
|
* to you under the Apache License, Version 2.0 (the
|
||||||
|
* "License"); you may not use this file except in compliance
|
||||||
|
* with the License. You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing,
|
||||||
|
* software distributed under the License is distributed on an
|
||||||
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
* KIND, either express or implied. See the License for the
|
||||||
|
* specific language governing permissions and limitations
|
||||||
|
* under the License.
|
||||||
|
*/
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import { useResizeDetector } from 'react-resize-detector';
|
||||||
|
import { uniqWith } from 'lodash';
|
||||||
|
import { styled } from '@superset-ui/core';
|
||||||
|
import { Tooltip } from 'src/components/Tooltip';
|
||||||
|
import { ContentType } from './ContentType';
|
||||||
|
import { config } from './ContentConfig';
|
||||||
|
|
||||||
|
export const MIN_NUMBER_ITEMS = 2;
|
||||||
|
export const MAX_NUMBER_ITEMS = 6;
|
||||||
|
|
||||||
|
const HORIZONTAL_PADDING = 12;
|
||||||
|
const VERTICAL_PADDING = 8;
|
||||||
|
const ICON_PADDING = 8;
|
||||||
|
const SPACE_BETWEEN_ITEMS = 16;
|
||||||
|
const ICON_WIDTH = 16;
|
||||||
|
const TEXT_MIN_WIDTH = 70;
|
||||||
|
const TEXT_MAX_WIDTH = 150;
|
||||||
|
const ORDER = {
|
||||||
|
dashboards: 0,
|
||||||
|
rows: 1,
|
||||||
|
sql: 2,
|
||||||
|
table: 3,
|
||||||
|
tags: 4,
|
||||||
|
description: 5,
|
||||||
|
owner: 6,
|
||||||
|
lastModified: 7,
|
||||||
|
};
|
||||||
|
|
||||||
|
const Bar = styled.div<{ count: number }>`
|
||||||
|
${({ theme, count }) => `
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: ${VERTICAL_PADDING}px ${HORIZONTAL_PADDING}px;
|
||||||
|
background-color: ${theme.colors.grayscale.light4};
|
||||||
|
color: ${theme.colors.grayscale.base};
|
||||||
|
font-size: ${theme.typography.sizes.s}px;
|
||||||
|
min-width: ${
|
||||||
|
HORIZONTAL_PADDING * 2 +
|
||||||
|
(ICON_WIDTH + SPACE_BETWEEN_ITEMS) * count -
|
||||||
|
SPACE_BETWEEN_ITEMS
|
||||||
|
}px;
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledItem = styled.div<{
|
||||||
|
collapsed: boolean;
|
||||||
|
last: boolean;
|
||||||
|
onClick?: () => void;
|
||||||
|
}>`
|
||||||
|
${({ theme, collapsed, last, onClick }) => `
|
||||||
|
max-width: ${
|
||||||
|
ICON_WIDTH +
|
||||||
|
ICON_PADDING +
|
||||||
|
TEXT_MAX_WIDTH +
|
||||||
|
(last ? 0 : SPACE_BETWEEN_ITEMS)
|
||||||
|
}px;
|
||||||
|
min-width: ${ICON_WIDTH + (last ? 0 : SPACE_BETWEEN_ITEMS)}px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ${collapsed ? 'unset' : 'ellipsis'};
|
||||||
|
white-space: nowrap;
|
||||||
|
padding-right: ${last ? 0 : SPACE_BETWEEN_ITEMS}px;
|
||||||
|
text-decoration: ${onClick ? 'underline' : 'none'};
|
||||||
|
cursor: ${onClick ? 'pointer' : 'default'};
|
||||||
|
& > span {
|
||||||
|
color: ${onClick && collapsed ? theme.colors.primary.base : 'undefined'};
|
||||||
|
padding-right: ${collapsed ? 0 : ICON_PADDING}px;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Make sure big tootips are truncated
|
||||||
|
const TootipContent = styled.div`
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 20;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Item = ({
|
||||||
|
barWidth,
|
||||||
|
contentType,
|
||||||
|
collapsed,
|
||||||
|
last = false,
|
||||||
|
}: {
|
||||||
|
barWidth: number | undefined;
|
||||||
|
contentType: ContentType;
|
||||||
|
collapsed: boolean;
|
||||||
|
last?: boolean;
|
||||||
|
}) => {
|
||||||
|
const { icon, title, tooltip = title } = config(contentType);
|
||||||
|
const [isTruncated, setIsTruncated] = useState(false);
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const Icon = icon;
|
||||||
|
const { type, onClick } = contentType;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsTruncated(
|
||||||
|
ref.current ? ref.current.scrollWidth > ref.current.clientWidth : false,
|
||||||
|
);
|
||||||
|
}, [barWidth, setIsTruncated, contentType]);
|
||||||
|
|
||||||
|
const content = (
|
||||||
|
<StyledItem
|
||||||
|
collapsed={collapsed}
|
||||||
|
last={last}
|
||||||
|
onClick={onClick ? () => onClick(type) : undefined}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<Icon iconSize="l" />
|
||||||
|
{!collapsed && title}
|
||||||
|
</StyledItem>
|
||||||
|
);
|
||||||
|
return isTruncated || collapsed || (tooltip && tooltip !== title) ? (
|
||||||
|
<Tooltip title={<TootipContent>{tooltip}</TootipContent>}>
|
||||||
|
{content}
|
||||||
|
</Tooltip>
|
||||||
|
) : (
|
||||||
|
content
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface MetadataBarProps {
|
||||||
|
/**
|
||||||
|
* Array of content type configurations. To see the available properties
|
||||||
|
* for each content type, check {@link ContentType}
|
||||||
|
*/
|
||||||
|
items: ContentType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The metadata bar component is used to display additional information about an entity.
|
||||||
|
* Content types are predefined and consistent across the whole app. This means that
|
||||||
|
* they will be displayed and behave in a consistent manner, keeping the same ordering,
|
||||||
|
* information formatting, and interactions.
|
||||||
|
* To extend the list of content types, a developer needs to request the inclusion of the new type in the design system.
|
||||||
|
* This process is important to make sure the new type is reviewed by the design team, improving Superset consistency.
|
||||||
|
*/
|
||||||
|
const MetadataBar = ({ items }: MetadataBarProps) => {
|
||||||
|
const { width, ref } = useResizeDetector();
|
||||||
|
const uniqueItems = uniqWith(items, (a, b) => a.type === b.type);
|
||||||
|
const sortedItems = uniqueItems.sort((a, b) => ORDER[a.type] - ORDER[b.type]);
|
||||||
|
const count = sortedItems.length;
|
||||||
|
if (count < MIN_NUMBER_ITEMS) {
|
||||||
|
throw Error('The minimum number of items for the metadata bar is 2.');
|
||||||
|
}
|
||||||
|
if (count > MAX_NUMBER_ITEMS) {
|
||||||
|
throw Error('The maximum number of items for the metadata bar is 6.');
|
||||||
|
}
|
||||||
|
// Calculates the breakpoint width to collapse the bar.
|
||||||
|
// The last item does not have a space, so we subtract SPACE_BETWEEN_ITEMS from the total.
|
||||||
|
const breakpoint =
|
||||||
|
(ICON_WIDTH + ICON_PADDING + TEXT_MIN_WIDTH + SPACE_BETWEEN_ITEMS) * count -
|
||||||
|
SPACE_BETWEEN_ITEMS;
|
||||||
|
const collapsed = Boolean(width && width < breakpoint);
|
||||||
|
return (
|
||||||
|
<Bar ref={ref} count={count}>
|
||||||
|
{sortedItems.map((item, index) => (
|
||||||
|
<Item
|
||||||
|
barWidth={width}
|
||||||
|
key={index}
|
||||||
|
contentType={item}
|
||||||
|
collapsed={collapsed}
|
||||||
|
last={index === count - 1}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Bar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MetadataBar;
|
||||||
|
|
@ -39,7 +39,7 @@ const TruncatedTextWithTooltip: React.FC = ({ children, ...props }) => {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsTruncated(
|
setIsTruncated(
|
||||||
ref.current ? ref.current.offsetWidth < ref.current.scrollWidth : false,
|
ref.current ? ref.current.scrollWidth > ref.current.clientWidth : false,
|
||||||
);
|
);
|
||||||
}, [children]);
|
}, [children]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||||
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');
|
||||||
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
|
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
|
||||||
|
const createMdxCompiler = require('@storybook/addon-docs/mdx-compiler-plugin');
|
||||||
const {
|
const {
|
||||||
WebpackManifestPlugin,
|
WebpackManifestPlugin,
|
||||||
getCompilerHooks,
|
getCompilerHooks,
|
||||||
|
|
@ -442,6 +443,24 @@ const config = {
|
||||||
test: /\.geojson$/,
|
test: /\.geojson$/,
|
||||||
type: 'asset/resource',
|
type: 'asset/resource',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
test: /\.(stories|story)\.mdx$/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'babel-loader',
|
||||||
|
// may or may not need this line depending on your app's setup
|
||||||
|
options: {
|
||||||
|
plugins: ['@babel/plugin-transform-react-jsx'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
loader: '@mdx-js/loader',
|
||||||
|
options: {
|
||||||
|
compilers: [createMdxCompiler({})],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
externals: {
|
externals: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue