mirror of https://github.com/apache/superset.git
feat: add extension point for workspace home page (#21033)
* updates to allow insertion of workspace home sidescroll/table UI * fix types * fix User type import * add welcome message to ui registry * add extra fields to individual chart/query GET results (for workspace home required info) * update list view card to support a subtitle * add id to individual chart fetch * update chart api test * another test fix * fix saved query test * update extension types + insert point * fix typing * fix type name
This commit is contained in:
parent
d817a1dc87
commit
83dd85166f
|
@ -45,7 +45,9 @@ export type Extensions = Partial<{
|
|||
'embedded.documentation.url': string;
|
||||
'dashboard.nav.right': React.ComponentType;
|
||||
'navbar.right': React.ComponentType;
|
||||
'welcome.message': React.ComponentType;
|
||||
'welcome.banner': React.ComponentType;
|
||||
'welcome.main.replacement': React.ComponentType;
|
||||
}>;
|
||||
|
||||
/**
|
||||
|
|
|
@ -71,7 +71,7 @@ const Cover = styled.div`
|
|||
const TitleContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-direction: row;
|
||||
flex-direction: column;
|
||||
|
||||
.card-actions {
|
||||
margin-left: auto;
|
||||
|
@ -82,6 +82,12 @@ const TitleContainer = styled.div`
|
|||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.titleRow {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
flex-direction: row;
|
||||
}
|
||||
`;
|
||||
|
||||
const TitleLink = styled.span`
|
||||
|
@ -141,6 +147,7 @@ const AnchorLink: React.FC<LinkProps> = ({ to, children }) => (
|
|||
|
||||
interface CardProps {
|
||||
title?: React.ReactNode;
|
||||
subtitle?: React.ReactNode;
|
||||
url?: string;
|
||||
linkComponent?: React.ComponentType<LinkProps>;
|
||||
imgURL?: string;
|
||||
|
@ -161,6 +168,7 @@ interface CardProps {
|
|||
|
||||
function ListViewCard({
|
||||
title,
|
||||
subtitle,
|
||||
url,
|
||||
linkComponent,
|
||||
titleRight,
|
||||
|
@ -245,24 +253,27 @@ function ListViewCard({
|
|||
<AntdCard.Meta
|
||||
title={
|
||||
<TitleContainer>
|
||||
<Tooltip title={title}>
|
||||
<TitleLink>
|
||||
<Link to={url!}>
|
||||
{certifiedBy && (
|
||||
<>
|
||||
<CertifiedBadge
|
||||
certifiedBy={certifiedBy}
|
||||
details={certificationDetails}
|
||||
/>{' '}
|
||||
</>
|
||||
)}
|
||||
{title}
|
||||
</Link>
|
||||
</TitleLink>
|
||||
</Tooltip>
|
||||
{titleRight && <TitleRight>{titleRight}</TitleRight>}
|
||||
<div className="card-actions" data-test="card-actions">
|
||||
{actions}
|
||||
{subtitle || null}
|
||||
<div className="titleRow">
|
||||
<Tooltip title={title}>
|
||||
<TitleLink>
|
||||
<Link to={url!}>
|
||||
{certifiedBy && (
|
||||
<>
|
||||
<CertifiedBadge
|
||||
certifiedBy={certifiedBy}
|
||||
details={certificationDetails}
|
||||
/>{' '}
|
||||
</>
|
||||
)}
|
||||
{title}
|
||||
</Link>
|
||||
</TitleLink>
|
||||
</Tooltip>
|
||||
{titleRight && <TitleRight>{titleRight}</TitleRight>}
|
||||
<div className="card-actions" data-test="card-actions">
|
||||
{actions}
|
||||
</div>
|
||||
</div>
|
||||
</TitleContainer>
|
||||
}
|
||||
|
|
|
@ -57,4 +57,5 @@ const StyledGroup = styled(AntdRadio.Group)`
|
|||
|
||||
export const Radio = Object.assign(StyledRadio, {
|
||||
Group: StyledGroup,
|
||||
Button: AntdRadio.Button,
|
||||
});
|
||||
|
|
|
@ -179,7 +179,11 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
|
|||
setItem(LocalStorageKeys.homepage_collapse_state, state);
|
||||
};
|
||||
|
||||
const WelcomeMessageExtension = extensionsRegistry.get('welcome.message');
|
||||
const WelcomeTopExtension = extensionsRegistry.get('welcome.banner');
|
||||
const WelcomeMainExtension = extensionsRegistry.get(
|
||||
'welcome.main.replacement',
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const activeTab = getItem(LocalStorageKeys.homepage_activity_filter, null);
|
||||
|
@ -282,71 +286,82 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
|
|||
!activityData?.Examples && !activityData?.Viewed;
|
||||
return (
|
||||
<WelcomeContainer>
|
||||
{WelcomeMessageExtension && <WelcomeMessageExtension />}
|
||||
{WelcomeTopExtension && <WelcomeTopExtension />}
|
||||
<WelcomeNav>
|
||||
<h1 className="welcome-header">Home</h1>
|
||||
{isFeatureEnabled(FeatureFlag.THUMBNAILS) ? (
|
||||
<div className="switch">
|
||||
<AntdSwitch checked={checked} onChange={handleToggle} />
|
||||
<span>Thumbnails</span>
|
||||
</div>
|
||||
) : null}
|
||||
</WelcomeNav>
|
||||
<Collapse activeKey={activeState} onChange={handleCollapse} ghost bigger>
|
||||
<Collapse.Panel header={t('Recents')} key="1">
|
||||
{activityData &&
|
||||
(activityData.Viewed ||
|
||||
activityData.Examples ||
|
||||
activityData.Created) &&
|
||||
activeChild !== 'Loading' ? (
|
||||
<ActivityTable
|
||||
user={{ userId: user.userId! }} // user is definitely not a guest user on this page
|
||||
activeChild={activeChild}
|
||||
setActiveChild={setActiveChild}
|
||||
activityData={activityData}
|
||||
loadedCount={loadedCount}
|
||||
/>
|
||||
) : (
|
||||
<LoadingCards />
|
||||
)}
|
||||
</Collapse.Panel>
|
||||
<Collapse.Panel header={t('Dashboards')} key="2">
|
||||
{!dashboardData || isRecentActivityLoading ? (
|
||||
<LoadingCards cover={checked} />
|
||||
) : (
|
||||
<DashboardTable
|
||||
user={user}
|
||||
mine={dashboardData}
|
||||
showThumbnails={checked}
|
||||
examples={activityData?.Examples}
|
||||
/>
|
||||
)}
|
||||
</Collapse.Panel>
|
||||
<Collapse.Panel header={t('Charts')} key="3">
|
||||
{!chartData || isRecentActivityLoading ? (
|
||||
<LoadingCards cover={checked} />
|
||||
) : (
|
||||
<ChartTable
|
||||
showThumbnails={checked}
|
||||
user={user}
|
||||
mine={chartData}
|
||||
examples={activityData?.Examples}
|
||||
/>
|
||||
)}
|
||||
</Collapse.Panel>
|
||||
<Collapse.Panel header={t('Saved queries')} key="4">
|
||||
{!queryData ? (
|
||||
<LoadingCards cover={checked} />
|
||||
) : (
|
||||
<SavedQueries
|
||||
showThumbnails={checked}
|
||||
user={user}
|
||||
mine={queryData}
|
||||
featureFlag={isFeatureEnabled(FeatureFlag.THUMBNAILS)}
|
||||
/>
|
||||
)}
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
{WelcomeMainExtension && <WelcomeMainExtension />}
|
||||
{(!WelcomeTopExtension || !WelcomeMainExtension) && (
|
||||
<>
|
||||
<WelcomeNav>
|
||||
<h1 className="welcome-header">Home</h1>
|
||||
{isFeatureEnabled(FeatureFlag.THUMBNAILS) ? (
|
||||
<div className="switch">
|
||||
<AntdSwitch checked={checked} onChange={handleToggle} />
|
||||
<span>Thumbnails</span>
|
||||
</div>
|
||||
) : null}
|
||||
</WelcomeNav>
|
||||
<Collapse
|
||||
activeKey={activeState}
|
||||
onChange={handleCollapse}
|
||||
ghost
|
||||
bigger
|
||||
>
|
||||
<Collapse.Panel header={t('Recents')} key="1">
|
||||
{activityData &&
|
||||
(activityData.Viewed ||
|
||||
activityData.Examples ||
|
||||
activityData.Created) &&
|
||||
activeChild !== 'Loading' ? (
|
||||
<ActivityTable
|
||||
user={{ userId: user.userId! }} // user is definitely not a guest user on this page
|
||||
activeChild={activeChild}
|
||||
setActiveChild={setActiveChild}
|
||||
activityData={activityData}
|
||||
loadedCount={loadedCount}
|
||||
/>
|
||||
) : (
|
||||
<LoadingCards />
|
||||
)}
|
||||
</Collapse.Panel>
|
||||
<Collapse.Panel header={t('Dashboards')} key="2">
|
||||
{!dashboardData || isRecentActivityLoading ? (
|
||||
<LoadingCards cover={checked} />
|
||||
) : (
|
||||
<DashboardTable
|
||||
user={user}
|
||||
mine={dashboardData}
|
||||
showThumbnails={checked}
|
||||
examples={activityData?.Examples}
|
||||
/>
|
||||
)}
|
||||
</Collapse.Panel>
|
||||
<Collapse.Panel header={t('Charts')} key="3">
|
||||
{!chartData || isRecentActivityLoading ? (
|
||||
<LoadingCards cover={checked} />
|
||||
) : (
|
||||
<ChartTable
|
||||
showThumbnails={checked}
|
||||
user={user}
|
||||
mine={chartData}
|
||||
examples={activityData?.Examples}
|
||||
/>
|
||||
)}
|
||||
</Collapse.Panel>
|
||||
<Collapse.Panel header={t('Saved queries')} key="4">
|
||||
{!queryData ? (
|
||||
<LoadingCards cover={checked} />
|
||||
) : (
|
||||
<SavedQueries
|
||||
showThumbnails={checked}
|
||||
user={user}
|
||||
mine={queryData}
|
||||
featureFlag={isFeatureEnabled(FeatureFlag.THUMBNAILS)}
|
||||
/>
|
||||
)}
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</>
|
||||
)}
|
||||
</WelcomeContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -114,16 +114,20 @@ class ChartRestApi(BaseSupersetModelRestApi):
|
|||
"cache_timeout",
|
||||
"certified_by",
|
||||
"certification_details",
|
||||
"changed_on_delta_humanized",
|
||||
"dashboards.dashboard_title",
|
||||
"dashboards.id",
|
||||
"dashboards.json_metadata",
|
||||
"description",
|
||||
"id",
|
||||
"owners.first_name",
|
||||
"owners.id",
|
||||
"owners.last_name",
|
||||
"owners.username",
|
||||
"params",
|
||||
"slice_name",
|
||||
"thumbnail_url",
|
||||
"url",
|
||||
"viz_type",
|
||||
"query_context",
|
||||
"is_managed_externally",
|
||||
|
|
|
@ -81,6 +81,7 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
|
|||
base_filters = [["id", SavedQueryFilter, lambda: []]]
|
||||
|
||||
show_columns = [
|
||||
"changed_on_delta_humanized",
|
||||
"created_by.first_name",
|
||||
"created_by.id",
|
||||
"created_by.last_name",
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
# isort:skip_file
|
||||
"""Unit tests for Superset"""
|
||||
import json
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from zipfile import is_zipfile, ZipFile
|
||||
|
||||
|
@ -762,7 +763,19 @@ class TestChartApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixin):
|
|||
"is_managed_externally": False,
|
||||
}
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
self.assertEqual(data["result"], expected_result)
|
||||
self.assertIn("changed_on_delta_humanized", data["result"])
|
||||
self.assertIn("id", data["result"])
|
||||
self.assertIn("thumbnail_url", data["result"])
|
||||
self.assertIn("url", data["result"])
|
||||
for key, value in data["result"].items():
|
||||
# We can't assert timestamp values or id/urls
|
||||
if key not in (
|
||||
"changed_on_delta_humanized",
|
||||
"id",
|
||||
"thumbnail_url",
|
||||
"url",
|
||||
):
|
||||
self.assertEqual(value, expected_result[key])
|
||||
db.session.delete(chart)
|
||||
db.session.commit()
|
||||
|
||||
|
|
|
@ -525,8 +525,10 @@ class TestSavedQueryApi(SupersetTestCase):
|
|||
"label": "label1",
|
||||
}
|
||||
data = json.loads(rv.data.decode("utf-8"))
|
||||
self.assertIn("changed_on_delta_humanized", data["result"])
|
||||
for key, value in data["result"].items():
|
||||
assert value == expected_result[key]
|
||||
if key not in ("changed_on_delta_humanized",):
|
||||
assert value == expected_result[key]
|
||||
|
||||
def test_get_saved_query_not_found(self):
|
||||
"""
|
||||
|
|
Loading…
Reference in New Issue