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: