mirror of https://github.com/apache/superset.git
feat: Adds the MetadataBar to the Explore header (#21560)
This commit is contained in:
parent
5ea9249059
commit
0dda5fe1cf
|
@ -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
|
||||
|
|
|
@ -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 '.';
|
||||
|
@ -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>
|
||||
),
|
||||
|
@ -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>
|
||||
),
|
||||
};
|
||||
|
|
|
@ -43,7 +43,7 @@ export type Description = {
|
|||
|
||||
export type LastModified = {
|
||||
type: MetadataType.LAST_MODIFIED;
|
||||
value: Date;
|
||||
value: string;
|
||||
modifiedBy: string;
|
||||
onClick?: (type: string) => void;
|
||||
};
|
||||
|
@ -52,7 +52,7 @@ export type Owner = {
|
|||
type: MetadataType.OWNER;
|
||||
createdBy: string;
|
||||
owners: string[];
|
||||
createdOn: Date;
|
||||
createdOn: string;
|
||||
onClick?: (type: string) => void;
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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, {
|
||||
|
@ -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();
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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(),
|
||||
);
|
||||
|
|
|
@ -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';
|
||||
|
@ -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'};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
|
@ -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) ? (
|
||||
|
@ -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;
|
||||
|
@ -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) => (
|
||||
|
|
|
@ -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];
|
||||
|
@ -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: {
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
@ -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({
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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();
|
||||
|
@ -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
|
||||
|
|
|
@ -36,7 +36,7 @@ window.featureFlags = {
|
|||
[FeatureFlag.EMBEDDABLE_CHARTS]: true,
|
||||
};
|
||||
|
||||
const createProps = () => ({
|
||||
const createProps = (additionalProps = {}) => ({
|
||||
chart: {
|
||||
id: 1,
|
||||
latestQueryFormData: {
|
||||
|
@ -63,7 +63,7 @@ const createProps = () => ({
|
|||
changed_on: '2021-03-19T16:30:56.750230',
|
||||
changed_on_humanized: '7 days ago',
|
||||
datasource: 'FCC 2018 Survey',
|
||||
description: null,
|
||||
description: 'Simple description',
|
||||
description_markeddown: '',
|
||||
edit_url: '/chart/edit/318',
|
||||
form_data: {
|
||||
|
@ -106,10 +106,19 @@ const createProps = () => ({
|
|||
user: {
|
||||
userId: 1,
|
||||
},
|
||||
metadata: {
|
||||
created_on_humanized: 'a week ago',
|
||||
changed_on_humanized: '2 days ago',
|
||||
owners: ['John Doe'],
|
||||
created_by: 'John Doe',
|
||||
changed_by: 'John Doe',
|
||||
dashboards: [{ id: 1, dashboard_title: 'Test' }],
|
||||
},
|
||||
onSaveChart: jest.fn(),
|
||||
canOverwrite: false,
|
||||
canDownload: false,
|
||||
isStarred: false,
|
||||
...additionalProps,
|
||||
});
|
||||
|
||||
fetchMock.post(
|
||||
|
@ -147,6 +156,27 @@ test('Cancelling changes to the properties should reset previous properties', as
|
|||
expect(await screen.findByDisplayValue(prevChartName)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders the metadata bar when saved', async () => {
|
||||
const props = createProps({ showTitlePanelItems: true });
|
||||
render(<ExploreHeader {...props} />, { useRedux: true });
|
||||
expect(
|
||||
await screen.findByText('Added to 1 dashboard(s)'),
|
||||
).toBeInTheDocument();
|
||||
expect(await screen.findByText('Simple description')).toBeInTheDocument();
|
||||
expect(await screen.findByText('John Doe')).toBeInTheDocument();
|
||||
expect(await screen.findByText('2 days ago')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('does not render the metadata bar when not saved', async () => {
|
||||
const props = createProps({ showTitlePanelItems: true, slice: null });
|
||||
render(<ExploreHeader {...props} />, { useRedux: true });
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.queryByText('Added to 1 dashboard(s)'),
|
||||
).not.toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
|
||||
test('Save chart', async () => {
|
||||
const props = createProps();
|
||||
render(<ExploreHeader {...props} />, { useRedux: true });
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
@ -24,6 +24,7 @@ import { Tooltip } from 'src/components/Tooltip';
|
|||
import {
|
||||
CategoricalColorNamespace,
|
||||
css,
|
||||
logging,
|
||||
SupersetClient,
|
||||
t,
|
||||
} from '@superset-ui/core';
|
||||
|
@ -35,6 +36,7 @@ import Icons from 'src/components/Icons';
|
|||
import PropertiesModal from 'src/explore/components/PropertiesModal';
|
||||
import { sliceUpdated } from 'src/explore/actions/exploreActions';
|
||||
import { PageHeaderWithActions } from 'src/components/PageHeaderWithActions';
|
||||
import MetadataBar, { MetadataType } from 'src/components/MetadataBar';
|
||||
import { useExploreAdditionalActionsMenu } from '../useExploreAdditionalActionsMenu';
|
||||
|
||||
const propTypes = {
|
||||
|
@ -60,6 +62,15 @@ const saveButtonStyles = theme => css`
|
|||
}
|
||||
`;
|
||||
|
||||
const additionalItemsStyles = theme => css`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: ${theme.gridUnit}px;
|
||||
& > span {
|
||||
margin-right: ${theme.gridUnit * 3}px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ExploreChartHeader = ({
|
||||
dashboardId,
|
||||
slice,
|
||||
|
@ -75,51 +86,51 @@ export const ExploreChartHeader = ({
|
|||
sliceName,
|
||||
onSaveChart,
|
||||
saveDisabled,
|
||||
metadata,
|
||||
}) => {
|
||||
const { latestQueryFormData, sliceFormData } = chart;
|
||||
const [isPropertiesModalOpen, setIsPropertiesModalOpen] = useState(false);
|
||||
|
||||
const fetchChartDashboardData = async () => {
|
||||
await SupersetClient.get({
|
||||
endpoint: `/api/v1/chart/${slice.slice_id}`,
|
||||
})
|
||||
.then(res => {
|
||||
const response = res?.json?.result;
|
||||
if (response && response.dashboards && response.dashboards.length) {
|
||||
const { dashboards } = response;
|
||||
const dashboard =
|
||||
dashboardId &&
|
||||
dashboards.length &&
|
||||
dashboards.find(d => d.id === dashboardId);
|
||||
const updateCategoricalNamespace = async () => {
|
||||
const { dashboards } = metadata || {};
|
||||
const dashboard =
|
||||
dashboardId && dashboards && dashboards.find(d => d.id === dashboardId);
|
||||
|
||||
if (dashboard && dashboard.json_metadata) {
|
||||
// setting the chart to use the dashboard custom label colors if any
|
||||
const metadata = JSON.parse(dashboard.json_metadata);
|
||||
const sharedLabelColors = metadata.shared_label_colors || {};
|
||||
const customLabelColors = metadata.label_colors || {};
|
||||
const mergedLabelColors = {
|
||||
...sharedLabelColors,
|
||||
...customLabelColors,
|
||||
};
|
||||
if (dashboard) {
|
||||
try {
|
||||
// Dashboards from metadata don't contain the json_metadata field
|
||||
// to avoid unnecessary payload. Here we query for the dashboard json_metadata.
|
||||
const response = await SupersetClient.get({
|
||||
endpoint: `/api/v1/dashboard/${dashboard.id}`,
|
||||
});
|
||||
const result = response?.json?.result;
|
||||
|
||||
const categoricalNamespace =
|
||||
CategoricalColorNamespace.getNamespace();
|
||||
// setting the chart to use the dashboard custom label colors if any
|
||||
const metadata = JSON.parse(result.json_metadata);
|
||||
const sharedLabelColors = metadata.shared_label_colors || {};
|
||||
const customLabelColors = metadata.label_colors || {};
|
||||
const mergedLabelColors = {
|
||||
...sharedLabelColors,
|
||||
...customLabelColors,
|
||||
};
|
||||
|
||||
Object.keys(mergedLabelColors).forEach(label => {
|
||||
categoricalNamespace.setColor(
|
||||
label,
|
||||
mergedLabelColors[label],
|
||||
metadata.color_scheme,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {});
|
||||
const categoricalNamespace = CategoricalColorNamespace.getNamespace();
|
||||
|
||||
Object.keys(mergedLabelColors).forEach(label => {
|
||||
categoricalNamespace.setColor(
|
||||
label,
|
||||
mergedLabelColors[label],
|
||||
metadata.color_scheme,
|
||||
);
|
||||
});
|
||||
} catch (error) {
|
||||
logging.info(t('Unable to retrieve dashboard colors'));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (dashboardId) fetchChartDashboardData();
|
||||
if (dashboardId) updateCategoricalNamespace();
|
||||
}, []);
|
||||
|
||||
const openPropertiesModal = () => {
|
||||
|
@ -140,6 +151,38 @@ export const ExploreChartHeader = ({
|
|||
ownState,
|
||||
);
|
||||
|
||||
const metadataBar = useMemo(() => {
|
||||
if (!metadata) {
|
||||
return null;
|
||||
}
|
||||
const items = [];
|
||||
items.push({
|
||||
type: MetadataType.DASHBOARDS,
|
||||
title:
|
||||
metadata.dashboards.length > 0
|
||||
? t('Added to %s dashboard(s)', metadata.dashboards.length)
|
||||
: t('Not added to any dashboard'),
|
||||
});
|
||||
items.push({
|
||||
type: MetadataType.LAST_MODIFIED,
|
||||
value: metadata.changed_on_humanized,
|
||||
modifiedBy: metadata.changed_by || t('Not available'),
|
||||
});
|
||||
items.push({
|
||||
type: MetadataType.OWNER,
|
||||
createdBy: metadata.created_by || t('Not available'),
|
||||
owners: metadata.owners.length > 0 ? metadata.owners : t('None'),
|
||||
createdOn: metadata.created_on_humanized,
|
||||
});
|
||||
if (slice?.description) {
|
||||
items.push({
|
||||
type: MetadataType.DESCRIPTION,
|
||||
value: slice?.description,
|
||||
});
|
||||
}
|
||||
return <MetadataBar items={items} />;
|
||||
}, [metadata, slice?.description]);
|
||||
|
||||
const oldSliceName = slice?.slice_name;
|
||||
return (
|
||||
<>
|
||||
|
@ -168,16 +211,19 @@ export const ExploreChartHeader = ({
|
|||
showTooltip: true,
|
||||
}}
|
||||
titlePanelAdditionalItems={
|
||||
sliceFormData ? (
|
||||
<AlteredSliceTag
|
||||
className="altered"
|
||||
origFormData={{
|
||||
...sliceFormData,
|
||||
chartTitle: oldSliceName,
|
||||
}}
|
||||
currentFormData={{ ...formData, chartTitle: sliceName }}
|
||||
/>
|
||||
) : null
|
||||
<div css={additionalItemsStyles}>
|
||||
{sliceFormData ? (
|
||||
<AlteredSliceTag
|
||||
className="altered"
|
||||
origFormData={{
|
||||
...sliceFormData,
|
||||
chartTitle: oldSliceName,
|
||||
}}
|
||||
currentFormData={{ ...formData, chartTitle: sliceName }}
|
||||
/>
|
||||
) : null}
|
||||
{metadataBar}
|
||||
</div>
|
||||
}
|
||||
rightPanelAdditionalItems={
|
||||
<Tooltip
|
||||
|
|
|
@ -44,6 +44,14 @@ const reduxState = {
|
|||
slice: {
|
||||
slice_id: 1,
|
||||
},
|
||||
metadata: {
|
||||
created_on_humanized: 'a week ago',
|
||||
changed_on_humanized: '2 days ago',
|
||||
owners: ['John Doe'],
|
||||
created_by: 'John Doe',
|
||||
changed_by: 'John Doe',
|
||||
dashboards: [{ id: 1, dashboard_title: 'Test' }],
|
||||
},
|
||||
},
|
||||
charts: {
|
||||
1: {
|
||||
|
|
|
@ -565,6 +565,7 @@ function ExploreViewContainer(props) {
|
|||
reports={props.reports}
|
||||
onSaveChart={toggleModal}
|
||||
saveDisabled={errorMessage || props.chart.chartStatus === 'loading'}
|
||||
metadata={props.metadata}
|
||||
/>
|
||||
<ExplorePanelContainer id="explore-container">
|
||||
<Global
|
||||
|
@ -706,7 +707,7 @@ ExploreViewContainer.propTypes = propTypes;
|
|||
function mapStateToProps(state) {
|
||||
const { explore, charts, common, impressionId, dataMask, reports, user } =
|
||||
state;
|
||||
const { controls, slice, datasource } = explore;
|
||||
const { controls, slice, datasource, metadata } = explore;
|
||||
const form_data = getFormDataFromControls(controls);
|
||||
const slice_id = form_data.slice_id ?? slice?.slice_id ?? 0; // 0 - unsaved chart
|
||||
form_data.extra_form_data = mergeExtraFormData(
|
||||
|
@ -752,6 +753,7 @@ function mapStateToProps(state) {
|
|||
user,
|
||||
exploreState: explore,
|
||||
reports,
|
||||
metadata,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -159,6 +159,7 @@ function PropertiesModal({
|
|||
...payload,
|
||||
...res.json.result,
|
||||
id: slice.slice_id,
|
||||
owners: selectedOwners,
|
||||
};
|
||||
onSave(updatedChart);
|
||||
addSuccessToast(t('Chart properties updated'));
|
||||
|
|
|
@ -245,9 +245,17 @@ export default function exploreReducer(state = {}, action) {
|
|||
slice: {
|
||||
...state.slice,
|
||||
...action.slice,
|
||||
owners: action.slice.owners ?? null,
|
||||
owners: action.slice.owners
|
||||
? action.slice.owners.map(owner => owner.value)
|
||||
: null,
|
||||
},
|
||||
sliceName: action.slice.slice_name ?? state.sliceName,
|
||||
metadata: {
|
||||
...state.metadata,
|
||||
owners: action.slice.owners
|
||||
? action.slice.owners.map(owner => owner.label)
|
||||
: null,
|
||||
},
|
||||
};
|
||||
},
|
||||
[actions.SET_FORCE_QUERY]() {
|
||||
|
|
|
@ -72,6 +72,13 @@ export interface ExplorePageInitialData {
|
|||
dataset: Dataset;
|
||||
form_data: QueryFormData;
|
||||
slice: Slice | null;
|
||||
metadata?: {
|
||||
created_on_humanized: string;
|
||||
changed_on_humanized: string;
|
||||
owners: string[];
|
||||
created_by?: string;
|
||||
changed_by?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface ExploreResponsePayload {
|
||||
|
|
|
@ -179,9 +179,11 @@ class DatasetRestApi(BaseSupersetModelRestApi):
|
|||
"extra",
|
||||
"kind",
|
||||
"created_on",
|
||||
"created_on_humanized",
|
||||
"created_by.first_name",
|
||||
"created_by.last_name",
|
||||
"changed_on",
|
||||
"changed_on_humanized",
|
||||
"changed_by.first_name",
|
||||
"changed_by.last_name",
|
||||
]
|
||||
|
|
|
@ -153,11 +153,29 @@ class GetExploreCommand(BaseCommand, ABC):
|
|||
except (SupersetException, SQLAlchemyError):
|
||||
dataset_data = dummy_dataset_data
|
||||
|
||||
metadata = None
|
||||
|
||||
if slc:
|
||||
metadata = {
|
||||
"created_on_humanized": slc.created_on_humanized,
|
||||
"changed_on_humanized": slc.changed_on_humanized,
|
||||
"owners": [owner.get_full_name() for owner in slc.owners],
|
||||
"dashboards": [
|
||||
{"id": dashboard.id, "dashboard_title": dashboard.dashboard_title}
|
||||
for dashboard in slc.dashboards
|
||||
],
|
||||
}
|
||||
if slc.created_by:
|
||||
metadata["created_by"] = slc.created_by.get_full_name()
|
||||
if slc.changed_by:
|
||||
metadata["changed_by"] = slc.changed_by.get_full_name()
|
||||
|
||||
return {
|
||||
"dataset": sanitize_datasource_data(dataset_data),
|
||||
"form_data": form_data,
|
||||
"slice": slc.data if slc else None,
|
||||
"message": message,
|
||||
"metadata": metadata,
|
||||
}
|
||||
|
||||
def validate(self) -> None:
|
||||
|
|
Loading…
Reference in New Issue