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,6 +253,8 @@ function ListViewCard({
|
|||||||
<AntdCard.Meta
|
<AntdCard.Meta
|
||||||
title={
|
title={
|
||||||
<TitleContainer>
|
<TitleContainer>
|
||||||
|
{subtitle || null}
|
||||||
|
<div className="titleRow">
|
||||||
<Tooltip title={title}>
|
<Tooltip title={title}>
|
||||||
<TitleLink>
|
<TitleLink>
|
||||||
<Link to={url!}>
|
<Link to={url!}>
|
||||||
@ -264,6 +274,7 @@ function ListViewCard({
|
|||||||
<div className="card-actions" data-test="card-actions">
|
<div className="card-actions" data-test="card-actions">
|
||||||
{actions}
|
{actions}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</TitleContainer>
|
</TitleContainer>
|
||||||
}
|
}
|
||||||
description={description}
|
description={description}
|
||||||
|
@ -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,7 +286,11 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
|
|||||||
!activityData?.Examples && !activityData?.Viewed;
|
!activityData?.Examples && !activityData?.Viewed;
|
||||||
return (
|
return (
|
||||||
<WelcomeContainer>
|
<WelcomeContainer>
|
||||||
|
{WelcomeMessageExtension && <WelcomeMessageExtension />}
|
||||||
{WelcomeTopExtension && <WelcomeTopExtension />}
|
{WelcomeTopExtension && <WelcomeTopExtension />}
|
||||||
|
{WelcomeMainExtension && <WelcomeMainExtension />}
|
||||||
|
{(!WelcomeTopExtension || !WelcomeMainExtension) && (
|
||||||
|
<>
|
||||||
<WelcomeNav>
|
<WelcomeNav>
|
||||||
<h1 className="welcome-header">Home</h1>
|
<h1 className="welcome-header">Home</h1>
|
||||||
{isFeatureEnabled(FeatureFlag.THUMBNAILS) ? (
|
{isFeatureEnabled(FeatureFlag.THUMBNAILS) ? (
|
||||||
@ -292,7 +300,12 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</WelcomeNav>
|
</WelcomeNav>
|
||||||
<Collapse activeKey={activeState} onChange={handleCollapse} ghost bigger>
|
<Collapse
|
||||||
|
activeKey={activeState}
|
||||||
|
onChange={handleCollapse}
|
||||||
|
ghost
|
||||||
|
bigger
|
||||||
|
>
|
||||||
<Collapse.Panel header={t('Recents')} key="1">
|
<Collapse.Panel header={t('Recents')} key="1">
|
||||||
{activityData &&
|
{activityData &&
|
||||||
(activityData.Viewed ||
|
(activityData.Viewed ||
|
||||||
@ -347,6 +360,8 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
|
|||||||
)}
|
)}
|
||||||
</Collapse.Panel>
|
</Collapse.Panel>
|
||||||
</Collapse>
|
</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,7 +525,9 @@ 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():
|
||||||
|
if key not in ("changed_on_delta_humanized",):
|
||||||
assert value == expected_result[key]
|
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