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:
Moriah Kreeger 2022-08-15 12:16:40 -07:00 committed by GitHub
parent d817a1dc87
commit 83dd85166f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 134 additions and 85 deletions

View File

@ -45,7 +45,9 @@ export type Extensions = Partial<{
'embedded.documentation.url': string; 'embedded.documentation.url': string;
'dashboard.nav.right': React.ComponentType; 'dashboard.nav.right': React.ComponentType;
'navbar.right': React.ComponentType; 'navbar.right': React.ComponentType;
'welcome.message': React.ComponentType;
'welcome.banner': React.ComponentType; 'welcome.banner': React.ComponentType;
'welcome.main.replacement': React.ComponentType;
}>; }>;
/** /**

View File

@ -71,7 +71,7 @@ const Cover = styled.div`
const TitleContainer = styled.div` const TitleContainer = styled.div`
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
flex-direction: row; flex-direction: column;
.card-actions { .card-actions {
margin-left: auto; margin-left: auto;
@ -82,6 +82,12 @@ const TitleContainer = styled.div`
align-items: center; align-items: center;
} }
} }
.titleRow {
display: flex;
justify-content: flex-start;
flex-direction: row;
}
`; `;
const TitleLink = styled.span` const TitleLink = styled.span`
@ -141,6 +147,7 @@ const AnchorLink: React.FC<LinkProps> = ({ to, children }) => (
interface CardProps { interface CardProps {
title?: React.ReactNode; title?: React.ReactNode;
subtitle?: React.ReactNode;
url?: string; url?: string;
linkComponent?: React.ComponentType<LinkProps>; linkComponent?: React.ComponentType<LinkProps>;
imgURL?: string; imgURL?: string;
@ -161,6 +168,7 @@ interface CardProps {
function ListViewCard({ function ListViewCard({
title, title,
subtitle,
url, url,
linkComponent, linkComponent,
titleRight, titleRight,
@ -245,24 +253,27 @@ function ListViewCard({
<AntdCard.Meta <AntdCard.Meta
title={ title={
<TitleContainer> <TitleContainer>
<Tooltip title={title}> {subtitle || null}
<TitleLink> <div className="titleRow">
<Link to={url!}> <Tooltip title={title}>
{certifiedBy && ( <TitleLink>
<> <Link to={url!}>
<CertifiedBadge {certifiedBy && (
certifiedBy={certifiedBy} <>
details={certificationDetails} <CertifiedBadge
/>{' '} certifiedBy={certifiedBy}
</> details={certificationDetails}
)} />{' '}
{title} </>
</Link> )}
</TitleLink> {title}
</Tooltip> </Link>
{titleRight && <TitleRight>{titleRight}</TitleRight>} </TitleLink>
<div className="card-actions" data-test="card-actions"> </Tooltip>
{actions} {titleRight && <TitleRight>{titleRight}</TitleRight>}
<div className="card-actions" data-test="card-actions">
{actions}
</div>
</div> </div>
</TitleContainer> </TitleContainer>
} }

View File

@ -57,4 +57,5 @@ const StyledGroup = styled(AntdRadio.Group)`
export const Radio = Object.assign(StyledRadio, { export const Radio = Object.assign(StyledRadio, {
Group: StyledGroup, Group: StyledGroup,
Button: AntdRadio.Button,
}); });

View File

@ -179,7 +179,11 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
setItem(LocalStorageKeys.homepage_collapse_state, state); setItem(LocalStorageKeys.homepage_collapse_state, state);
}; };
const WelcomeMessageExtension = extensionsRegistry.get('welcome.message');
const WelcomeTopExtension = extensionsRegistry.get('welcome.banner'); const WelcomeTopExtension = extensionsRegistry.get('welcome.banner');
const WelcomeMainExtension = extensionsRegistry.get(
'welcome.main.replacement',
);
useEffect(() => { useEffect(() => {
const activeTab = getItem(LocalStorageKeys.homepage_activity_filter, null); const activeTab = getItem(LocalStorageKeys.homepage_activity_filter, null);
@ -282,71 +286,82 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
!activityData?.Examples && !activityData?.Viewed; !activityData?.Examples && !activityData?.Viewed;
return ( return (
<WelcomeContainer> <WelcomeContainer>
{WelcomeMessageExtension && <WelcomeMessageExtension />}
{WelcomeTopExtension && <WelcomeTopExtension />} {WelcomeTopExtension && <WelcomeTopExtension />}
<WelcomeNav> {WelcomeMainExtension && <WelcomeMainExtension />}
<h1 className="welcome-header">Home</h1> {(!WelcomeTopExtension || !WelcomeMainExtension) && (
{isFeatureEnabled(FeatureFlag.THUMBNAILS) ? ( <>
<div className="switch"> <WelcomeNav>
<AntdSwitch checked={checked} onChange={handleToggle} /> <h1 className="welcome-header">Home</h1>
<span>Thumbnails</span> {isFeatureEnabled(FeatureFlag.THUMBNAILS) ? (
</div> <div className="switch">
) : null} <AntdSwitch checked={checked} onChange={handleToggle} />
</WelcomeNav> <span>Thumbnails</span>
<Collapse activeKey={activeState} onChange={handleCollapse} ghost bigger> </div>
<Collapse.Panel header={t('Recents')} key="1"> ) : null}
{activityData && </WelcomeNav>
(activityData.Viewed || <Collapse
activityData.Examples || activeKey={activeState}
activityData.Created) && onChange={handleCollapse}
activeChild !== 'Loading' ? ( ghost
<ActivityTable bigger
user={{ userId: user.userId! }} // user is definitely not a guest user on this page >
activeChild={activeChild} <Collapse.Panel header={t('Recents')} key="1">
setActiveChild={setActiveChild} {activityData &&
activityData={activityData} (activityData.Viewed ||
loadedCount={loadedCount} activityData.Examples ||
/> activityData.Created) &&
) : ( activeChild !== 'Loading' ? (
<LoadingCards /> <ActivityTable
)} user={{ userId: user.userId! }} // user is definitely not a guest user on this page
</Collapse.Panel> activeChild={activeChild}
<Collapse.Panel header={t('Dashboards')} key="2"> setActiveChild={setActiveChild}
{!dashboardData || isRecentActivityLoading ? ( activityData={activityData}
<LoadingCards cover={checked} /> loadedCount={loadedCount}
) : ( />
<DashboardTable ) : (
user={user} <LoadingCards />
mine={dashboardData} )}
showThumbnails={checked} </Collapse.Panel>
examples={activityData?.Examples} <Collapse.Panel header={t('Dashboards')} key="2">
/> {!dashboardData || isRecentActivityLoading ? (
)} <LoadingCards cover={checked} />
</Collapse.Panel> ) : (
<Collapse.Panel header={t('Charts')} key="3"> <DashboardTable
{!chartData || isRecentActivityLoading ? ( user={user}
<LoadingCards cover={checked} /> mine={dashboardData}
) : ( showThumbnails={checked}
<ChartTable examples={activityData?.Examples}
showThumbnails={checked} />
user={user} )}
mine={chartData} </Collapse.Panel>
examples={activityData?.Examples} <Collapse.Panel header={t('Charts')} key="3">
/> {!chartData || isRecentActivityLoading ? (
)} <LoadingCards cover={checked} />
</Collapse.Panel> ) : (
<Collapse.Panel header={t('Saved queries')} key="4"> <ChartTable
{!queryData ? ( showThumbnails={checked}
<LoadingCards cover={checked} /> user={user}
) : ( mine={chartData}
<SavedQueries examples={activityData?.Examples}
showThumbnails={checked} />
user={user} )}
mine={queryData} </Collapse.Panel>
featureFlag={isFeatureEnabled(FeatureFlag.THUMBNAILS)} <Collapse.Panel header={t('Saved queries')} key="4">
/> {!queryData ? (
)} <LoadingCards cover={checked} />
</Collapse.Panel> ) : (
</Collapse> <SavedQueries
showThumbnails={checked}
user={user}
mine={queryData}
featureFlag={isFeatureEnabled(FeatureFlag.THUMBNAILS)}
/>
)}
</Collapse.Panel>
</Collapse>
</>
)}
</WelcomeContainer> </WelcomeContainer>
); );
} }

View File

@ -114,16 +114,20 @@ class ChartRestApi(BaseSupersetModelRestApi):
"cache_timeout", "cache_timeout",
"certified_by", "certified_by",
"certification_details", "certification_details",
"changed_on_delta_humanized",
"dashboards.dashboard_title", "dashboards.dashboard_title",
"dashboards.id", "dashboards.id",
"dashboards.json_metadata", "dashboards.json_metadata",
"description", "description",
"id",
"owners.first_name", "owners.first_name",
"owners.id", "owners.id",
"owners.last_name", "owners.last_name",
"owners.username", "owners.username",
"params", "params",
"slice_name", "slice_name",
"thumbnail_url",
"url",
"viz_type", "viz_type",
"query_context", "query_context",
"is_managed_externally", "is_managed_externally",

View File

@ -81,6 +81,7 @@ class SavedQueryRestApi(BaseSupersetModelRestApi):
base_filters = [["id", SavedQueryFilter, lambda: []]] base_filters = [["id", SavedQueryFilter, lambda: []]]
show_columns = [ show_columns = [
"changed_on_delta_humanized",
"created_by.first_name", "created_by.first_name",
"created_by.id", "created_by.id",
"created_by.last_name", "created_by.last_name",

View File

@ -17,6 +17,7 @@
# isort:skip_file # isort:skip_file
"""Unit tests for Superset""" """Unit tests for Superset"""
import json import json
import logging
from io import BytesIO from io import BytesIO
from zipfile import is_zipfile, ZipFile from zipfile import is_zipfile, ZipFile
@ -762,7 +763,19 @@ class TestChartApi(SupersetTestCase, ApiOwnersTestCaseMixin, InsertChartMixin):
"is_managed_externally": False, "is_managed_externally": False,
} }
data = json.loads(rv.data.decode("utf-8")) 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.delete(chart)
db.session.commit() db.session.commit()

View File

@ -525,8 +525,10 @@ class TestSavedQueryApi(SupersetTestCase):
"label": "label1", "label": "label1",
} }
data = json.loads(rv.data.decode("utf-8")) data = json.loads(rv.data.decode("utf-8"))
self.assertIn("changed_on_delta_humanized", data["result"])
for key, value in data["result"].items(): 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): def test_get_saved_query_not_found(self):
""" """