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;
'dashboard.nav.right': React.ComponentType;
'navbar.right': React.ComponentType;
'welcome.message': 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`
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>
}

View File

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

View File

@ -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>
);
}

View File

@ -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",

View File

@ -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",

View File

@ -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()

View File

@ -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):
"""