diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index dc8ce1f3a..9ca3516f8 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -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
diff --git a/superset-frontend/src/components/MetadataBar/ContentConfig.tsx b/superset-frontend/src/components/MetadataBar/ContentConfig.tsx
index 55dbf8216..8e9958da1 100644
--- a/superset-frontend/src/components/MetadataBar/ContentConfig.tsx
+++ b/superset-frontend/src/components/MetadataBar/ContentConfig.tsx
@@ -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: (
-
+
),
@@ -95,10 +91,7 @@ const config = (contentType: ContentType) => {
-
+
),
};
diff --git a/superset-frontend/src/components/MetadataBar/ContentType.ts b/superset-frontend/src/components/MetadataBar/ContentType.ts
index 9a4e6082e..13c070739 100644
--- a/superset-frontend/src/components/MetadataBar/ContentType.ts
+++ b/superset-frontend/src/components/MetadataBar/ContentType.ts
@@ -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;
};
diff --git a/superset-frontend/src/components/MetadataBar/MetadataBar.stories.tsx b/superset-frontend/src/components/MetadataBar/MetadataBar.stories.tsx
index 6501a1b64..8501397b5 100644
--- a/superset-frontend/src/components/MetadataBar/MetadataBar.stories.tsx
+++ b/superset-frontend/src/components/MetadataBar/MetadataBar.stories.tsx
@@ -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,
diff --git a/superset-frontend/src/components/MetadataBar/MetadataBar.test.tsx b/superset-frontend/src/components/MetadataBar/MetadataBar.test.tsx
index d04570948..549b917ec 100644
--- a/superset-frontend/src/components/MetadataBar/MetadataBar.test.tsx
+++ b/superset-frontend/src/components/MetadataBar/MetadataBar.test.tsx
@@ -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();
- 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();
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(),
);
diff --git a/superset-frontend/src/components/MetadataBar/MetadataBar.tsx b/superset-frontend/src/components/MetadataBar/MetadataBar.tsx
index 5217cbcc9..57733b716 100644
--- a/superset-frontend/src/components/MetadataBar/MetadataBar.tsx
+++ b/superset-frontend/src/components/MetadataBar/MetadataBar.tsx
@@ -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}
>
-
- {!collapsed && title}
+
+ {!collapsed && (
+
+ {title}
+
+ )}
);
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();
+ 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 (
{sortedItems.map((item, index) => (
diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.test.tsx b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.test.tsx
index 70e67c263..45a342b5f 100644
--- a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.test.tsx
+++ b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.test.tsx
@@ -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();
});
diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx
index 6c2088a5a..ea3c6f573 100644
--- a/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx
+++ b/superset-frontend/src/dashboard/components/DrillDetailPane/DrillDetailPane.tsx
@@ -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({
diff --git a/superset-frontend/src/dashboard/components/DrillDetailPane/types.ts b/superset-frontend/src/dashboard/components/DrillDetailPane/types.ts
index 058377e0f..ea49c22ce 100644
--- a/superset-frontend/src/dashboard/components/DrillDetailPane/types.ts
+++ b/superset-frontend/src/dashboard/components/DrillDetailPane/types.ts
@@ -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: {
diff --git a/superset-frontend/src/explore/actions/hydrateExplore.ts b/superset-frontend/src/explore/actions/hydrateExplore.ts
index 929dd2a7f..dd97e7382 100644
--- a/superset-frontend/src/explore/actions/hydrateExplore.ts
+++ b/superset-frontend/src/explore/actions/hydrateExplore.ts
@@ -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
diff --git a/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx b/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx
index 6ac8f91be..2b2ef2755 100644
--- a/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx
+++ b/superset-frontend/src/explore/components/ExploreChartHeader/ExploreChartHeader.test.tsx
@@ -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(, { 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(, { useRedux: true });
+ await waitFor(() =>
+ expect(
+ screen.queryByText('Added to 1 dashboard(s)'),
+ ).not.toBeInTheDocument(),
+ );
+});
+
test('Save chart', async () => {
const props = createProps();
render(, { useRedux: true });
diff --git a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
index 9e1b27b24..51e87ee20 100644
--- a/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
+++ b/superset-frontend/src/explore/components/ExploreChartHeader/index.jsx
@@ -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 ;
+ }, [metadata, slice?.description]);
+
const oldSliceName = slice?.slice_name;
return (
<>
@@ -168,16 +211,19 @@ export const ExploreChartHeader = ({
showTooltip: true,
}}
titlePanelAdditionalItems={
- sliceFormData ? (
-
- ) : null
+
+ {sliceFormData ? (
+
+ ) : null}
+ {metadataBar}
+
}
rightPanelAdditionalItems={
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]() {
diff --git a/superset-frontend/src/explore/types.ts b/superset-frontend/src/explore/types.ts
index ec92a0c1e..1598b7e4e 100644
--- a/superset-frontend/src/explore/types.ts
+++ b/superset-frontend/src/explore/types.ts
@@ -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 {
diff --git a/superset/datasets/api.py b/superset/datasets/api.py
index 65b660dba..be5ab7c43 100644
--- a/superset/datasets/api.py
+++ b/superset/datasets/api.py
@@ -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",
]
diff --git a/superset/explore/commands/get.py b/superset/explore/commands/get.py
index 3a656ea2b..c10de79e2 100644
--- a/superset/explore/commands/get.py
+++ b/superset/explore/commands/get.py
@@ -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: