Skip to content

Commit

Permalink
feat: Adds the MetadataBar to the Explore header (apache#21560)
Browse files Browse the repository at this point in the history
  • Loading branch information
michael-s-molina authored Sep 29, 2022
1 parent 5ea9249 commit 0dda5fe
Show file tree
Hide file tree
Showing 19 changed files with 261 additions and 107 deletions.
4 changes: 3 additions & 1 deletion .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@
.github/workflows/docker-ephemeral-env.yml @robdiciuccio @craig-rueda @rusackas @eschutho @dpgaspar @nytai @mistercrunch
.github/workflows/ephemeral*.yml @robdiciuccio @craig-rueda @rusackas @eschutho @dpgaspar @nytai @mistercrunch

# Notify some committers of changes in the Select component
# Notify some committers of changes in the components

/superset-frontend/src/components/Select/ @michael-s-molina @geido @ktmud
/superset-frontend/src/components/MetadataBar/ @michael-s-molina

# Notify Helm Chart maintainers about changes in it

/helm/superset/ @craig-rueda @dpgaspar @villebro

# Notify E2E test maintainers of changes

/superset-frontend/cypress-base/ @jinghua-qa @geido
13 changes: 3 additions & 10 deletions superset-frontend/src/components/MetadataBar/ContentConfig.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
* 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 '.';
Expand Down Expand Up @@ -75,13 +74,10 @@ const config = (contentType: ContentType) => {
case MetadataType.LAST_MODIFIED:
return {
icon: Icons.EditOutlined,
title: moment.utc(contentType.value).fromNow(),
title: contentType.value,
tooltip: (
<div>
<Info
header={t('Last modified')}
text={moment.utc(contentType.value).fromNow()}
/>
<Info header={t('Last modified')} text={contentType.value} />
<Info header={t('Modified by')} text={contentType.modifiedBy} />
</div>
),
Expand All @@ -95,10 +91,7 @@ const config = (contentType: ContentType) => {
<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()}
/>
<Info header={t('Created on')} text={contentType.createdOn} />
</div>
),
};
Expand Down
4 changes: 2 additions & 2 deletions superset-frontend/src/components/MetadataBar/ContentType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export type Description = {

export type LastModified = {
type: MetadataType.LAST_MODIFIED;
value: Date;
value: string;
modifiedBy: string;
onClick?: (type: string) => void;
};
Expand All @@ -52,7 +52,7 @@ export type Owner = {
type: MetadataType.OWNER;
createdBy: string;
owners: string[];
createdOn: Date;
createdOn: string;
onClick?: (type: string) => void;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export default {
component: MetadataBar,
};

const A_WEEK_AGO = new Date(Date.now() - 7 * 24 * 3600 * 1000);
const A_WEEK_AGO = 'a week ago';

export const Component = ({
items,
Expand Down
29 changes: 18 additions & 11 deletions superset-frontend/src/components/MetadataBar/MetadataBar.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ 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, {
Expand All @@ -38,14 +37,22 @@ 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 A_WEEK_AGO = 'a week ago';
const TWO_DAYS_AGO = '2 days ago';

const runWithBarCollapsed = async (func: Function) => {
const spy = jest.spyOn(resizeDetector, 'useResizeDetector');
spy.mockReturnValue({ width: 80, ref: { current: undefined } });
let width: number;
spy.mockImplementation(props => {
if (props?.onResize && !width) {
width = 80;
props.onResize(width);
}
return { ref: { current: undefined } };
});
await func();
spy.mockRestore();
};
Expand All @@ -62,14 +69,14 @@ const ITEMS: ContentType[] = [
},
{
type: MetadataType.LAST_MODIFIED,
value: DATE,
value: TWO_DAYS_AGO,
modifiedBy: MODIFIED_BY,
},
{
type: MetadataType.OWNER,
createdBy: CREATED_BY,
owners: OWNERS,
createdOn: DATE,
createdOn: A_WEEK_AGO,
},
{
type: MetadataType.ROWS,
Expand Down Expand Up @@ -162,7 +169,9 @@ test('renders clicable items with blue icons when the bar is collapsed', async (
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();
expect(nonClickableColor).toBe(
hexToRgb(supersetTheme.colors.grayscale.base),
);
});
});

Expand Down Expand Up @@ -196,23 +205,21 @@ test('correctly renders the description tooltip', async () => {
});

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));
userEvent.hover(screen.getByText(TWO_DAYS_AGO));
const tooltip = await screen.findByRole('tooltip');
expect(tooltip).toBeInTheDocument();
expect(within(tooltip).getByText(dateText)).toBeInTheDocument();
expect(within(tooltip).getByText(TWO_DAYS_AGO)).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();
expect(within(tooltip).getByText(A_WEEK_AGO)).toBeInTheDocument();
OWNERS.forEach(owner =>
expect(within(tooltip).getByText(owner)).toBeInTheDocument(),
);
Expand Down
66 changes: 48 additions & 18 deletions superset-frontend/src/components/MetadataBar/MetadataBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useResizeDetector } from 'react-resize-detector';
import { uniqWith } from 'lodash';
import { styled } from '@superset-ui/core';
Expand Down Expand Up @@ -67,23 +67,38 @@ const StyledItem = styled.div<{
onClick?: () => void;
}>`
${({ theme, collapsed, last, onClick }) => `
display: flex;
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;
min-width: ${
collapsed
? ICON_WIDTH + (last ? 0 : SPACE_BETWEEN_ITEMS)
: ICON_WIDTH +
ICON_PADDING +
TEXT_MIN_WIDTH +
(last ? 0 : SPACE_BETWEEN_ITEMS)
}px;
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'};
& .metadata-icon {
color: ${
onClick && collapsed
? theme.colors.primary.base
: theme.colors.grayscale.base
};
padding-right: ${collapsed ? 0 : ICON_PADDING}px;
}
& .metadata-text {
min-width: ${TEXT_MIN_WIDTH}px;
overflow: hidden;
text-overflow: ${collapsed ? 'unset' : 'ellipsis'};
white-space: nowrap;
text-decoration: ${onClick ? 'underline' : 'none'};
}
`}
`;

Expand Down Expand Up @@ -124,10 +139,13 @@ const Item = ({
collapsed={collapsed}
last={last}
onClick={onClick ? () => onClick(type) : undefined}
ref={ref}
>
<Icon iconSize="l" />
{!collapsed && title}
<Icon iconSize="l" className="metadata-icon" />
{!collapsed && (
<span ref={ref} className="metadata-text">
{title}
</span>
)}
</StyledItem>
);
return isTruncated || collapsed || (tooltip && tooltip !== title) ? (
Expand Down Expand Up @@ -156,7 +174,8 @@ export interface MetadataBarProps {
* 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 [width, setWidth] = useState<number>();
const [collapsed, setCollapsed] = useState(false);
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;
Expand All @@ -166,12 +185,23 @@ const MetadataBar = ({ items }: MetadataBarProps) => {
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);

const onResize = useCallback(
width => {
// 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;
setWidth(width);
setCollapsed(Boolean(width && width < breakpoint));
},
[count],
);

const { ref } = useResizeDetector({ onResize });

return (
<Bar ref={ref} count={count}>
{sortedItems.map((item, index) => (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import { getMockStoreWithNativeFilters } from 'spec/fixtures/mockStore';
import chartQueries, { sliceId } from 'spec/fixtures/mockChartQueries';
import { QueryFormData, SupersetClient } from '@superset-ui/core';
import fetchMock from 'fetch-mock';
import moment from 'moment';
import DrillDetailPane from './DrillDetailPane';

const chart = chartQueries[sliceId];
Expand All @@ -48,8 +47,8 @@ const SAMPLES_ENDPOINT =
const DATASET_ENDPOINT = 'glob:*/api/v1/dataset/*';

const MOCKED_DATASET = {
changed_on: new Date(Date.parse('2022-01-01')),
created_on: new Date(Date.parse('2022-01-01')),
changed_on_humanized: '2 days ago',
created_on_humanized: 'a week ago',
description: 'Simple description',
table_name: 'test_table',
changed_by: {
Expand Down Expand Up @@ -170,7 +169,7 @@ test('should render the metadata bar', async () => {
),
).toBeInTheDocument();
expect(
await screen.findByText(moment.utc(MOCKED_DATASET.changed_on).fromNow()),
await screen.findByText(MOCKED_DATASET.changed_on_humanized),
).toBeInTheDocument();
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,8 @@ export default function DrillDetailPane({
const items: ContentType[] = [];
if (result) {
const {
changed_on,
created_on,
changed_on_humanized,
created_on_humanized,
description,
table_name,
changed_by,
Expand All @@ -275,14 +275,14 @@ export default function DrillDetailPane({
});
items.push({
type: MetadataType.LAST_MODIFIED,
value: changed_on,
value: changed_on_humanized,
modifiedBy,
});
items.push({
type: MetadataType.OWNER,
createdBy,
owners: formattedOwners,
createdOn: created_on,
createdOn: created_on_humanized,
});
if (description) {
items.push({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ export type Dataset = {
first_name: string;
last_name: string;
};
changed_on: Date;
created_on: Date;
changed_on_humanized: string;
created_on_humanized: string;
description: string;
table_name: string;
owners: {
Expand Down
3 changes: 2 additions & 1 deletion superset-frontend/src/explore/actions/hydrateExplore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ enum ColorSchemeType {

export const HYDRATE_EXPLORE = 'HYDRATE_EXPLORE';
export const hydrateExplore =
({ form_data, slice, dataset }: ExplorePageInitialData) =>
({ form_data, slice, dataset, metadata }: ExplorePageInitialData) =>
(dispatch: Dispatch, getState: () => ExplorePageState) => {
const { user, datasources, charts, sliceEntities, common, explore } =
getState();
Expand Down Expand Up @@ -123,6 +123,7 @@ export const hydrateExplore =
controlsTransferred: explore.controlsTransferred,
standalone: getUrlParam(URL_PARAMS.standalone),
force: getUrlParam(URL_PARAMS.force),
metadata,
};

// apply initial mapStateToProps for all controls, must execute AFTER
Expand Down
Loading

0 comments on commit 0dda5fe

Please sign in to comment.