mirror of
https://github.com/apache/superset.git
synced 2024-09-06 22:07:34 -04:00
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;
|
'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;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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>
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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()
|
||||||
|
|
||||||
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
Loading…
Reference in New Issue
Block a user