Compare commits

...

7 Commits

Author SHA1 Message Date
Diego Pucci
abfad960e8 fix(Dashboard): Title to rely only on loading prop 2024-10-15 19:28:30 +03:00
Diego Pucci
a0a21315ea fix(Dashboard): Header title to accept empty string 2024-10-15 18:48:15 +03:00
Diego Pucci
3bdacded28 chore(Dashboard): Unblock header actions while loading 2024-10-15 18:02:19 +03:00
Diego Pucci
670ef0ea4f chore(Dashboard): Load title 2024-10-15 17:33:19 +03:00
Diego Pucci
3cb3c8b235 chore(Fave): Set Fave to default false 2024-10-15 17:09:19 +03:00
Diego Pucci
0f4d054532 Merge branch 'master' of https://github.com/apache/superset into geido/feat/progressive-dashboard-header 2024-10-15 17:02:18 +03:00
Diego Pucci
3593c6d9b0 chore(MetadataBar): Add loading state 2024-10-01 14:40:10 +03:00
8 changed files with 147 additions and 83 deletions

View File

@@ -30,13 +30,15 @@ import {
import { css, SupersetTheme, t } from '@superset-ui/core';
import { Tooltip } from 'src/components/Tooltip';
import { useResizeDetector } from 'react-resize-detector';
import { Skeleton } from 'src/components';
export type DynamicEditableTitleProps = {
title: string;
placeholder: string;
title?: string;
placeholder?: string;
onSave: (title: string) => void;
canEdit: boolean;
label: string | undefined;
label?: string | undefined;
loading?: boolean;
};
const titleStyles = (theme: SupersetTheme) => css`
@@ -84,6 +86,7 @@ export const DynamicEditableTitle = memo(
onSave,
canEdit,
label,
loading,
}: DynamicEditableTitleProps) => {
const [isEditing, setIsEditing] = useState(false);
const [currentTitle, setCurrentTitle] = useState(title || '');
@@ -96,7 +99,7 @@ export const DynamicEditableTitle = memo(
});
useEffect(() => {
setCurrentTitle(title);
setCurrentTitle(title ?? '');
}, [title]);
useEffect(() => {
@@ -173,6 +176,17 @@ export const DynamicEditableTitle = memo(
[canEdit],
);
if (loading) {
return (
<Skeleton.Button
active
css={css`
min-width: 300px;
`}
/>
);
}
return (
<div css={titleStyles} ref={containerRef}>
<Tooltip

View File

@@ -24,7 +24,7 @@ import { Tooltip } from 'src/components/Tooltip';
import Icons from 'src/components/Icons';
export interface FaveStarProps {
itemId: number;
itemId?: number;
isStarred?: boolean;
showTooltip?: boolean;
saveFaveStar(id: number, isStarred: boolean): any;
@@ -41,19 +41,23 @@ const StyledLink = styled.a`
const FaveStar = ({
itemId,
isStarred,
isStarred = false,
showTooltip,
saveFaveStar,
fetchFaveStar,
}: FaveStarProps) => {
useEffect(() => {
fetchFaveStar?.(itemId);
if (itemId) {
fetchFaveStar?.(itemId);
}
}, [fetchFaveStar, itemId]);
const onClick = useCallback(
(e: MouseEvent) => {
e.preventDefault();
saveFaveStar(itemId, !!isStarred);
if (itemId) {
e.preventDefault();
saveFaveStar(itemId, !!isStarred);
}
},
[isStarred, itemId, saveFaveStar],
);

View File

@@ -23,6 +23,7 @@ import { styled } from '@superset-ui/core';
import { Tooltip, TooltipPlacement } from 'src/components/Tooltip';
import { ContentType } from './ContentType';
import { config } from './ContentConfig';
import Loading from '../Loading';
export const MIN_NUMBER_ITEMS = 2;
export const MAX_NUMBER_ITEMS = 6;
@@ -60,6 +61,10 @@ const Bar = styled.div<{ count: number }>`
}px;
border-radius: ${theme.borderRadius}px;
line-height: 1;
& .loading {
margin: 0 auto;
}
`}
`;
@@ -181,6 +186,10 @@ export interface MetadataBarProps {
* Defaults to "top".
*/
tooltipPlacement?: TooltipPlacement;
/**
* Loading state. If true, the bar will display loading spinners.
*/
loading?: boolean;
}
/**
@@ -191,16 +200,21 @@ export interface MetadataBarProps {
* To extend the list of content types, a developer needs to request the inclusion of the new type in the design system.
* This process is important to make sure the new type is reviewed by the design team, improving Superset consistency.
*/
const MetadataBar = ({ items, tooltipPlacement = 'top' }: MetadataBarProps) => {
const MetadataBar = ({
items,
tooltipPlacement = 'top',
loading = false,
}: MetadataBarProps) => {
const [width, setWidth] = useState<number>();
const [collapsed, setCollapsed] = useState(false);
const uniqueItems = uniqWith(items, (a, b) => a.type === b.type);
const sortedItems = uniqueItems.sort((a, b) => ORDER[a.type] - ORDER[b.type]);
const count = sortedItems.length;
if (count < MIN_NUMBER_ITEMS) {
if (!loading && count < MIN_NUMBER_ITEMS) {
throw Error('The minimum number of items for the metadata bar is 2.');
}
if (count > MAX_NUMBER_ITEMS) {
if (!loading && count > MAX_NUMBER_ITEMS) {
throw Error('The maximum number of items for the metadata bar is 6.');
}
@@ -222,16 +236,20 @@ const MetadataBar = ({ items, tooltipPlacement = 'top' }: MetadataBarProps) => {
return (
<Bar ref={ref} count={count} data-test="metadata-bar">
{sortedItems.map((item, index) => (
<Item
barWidth={width}
key={index}
contentType={item}
collapsed={collapsed}
last={index === count - 1}
tooltipPlacement={tooltipPlacement}
/>
))}
{!loading ? (
sortedItems.map((item, index) => (
<Item
barWidth={width}
key={index}
contentType={item}
collapsed={collapsed}
last={index === count - 1}
tooltipPlacement={tooltipPlacement}
/>
))
) : (
<Loading position="inline" />
)}
</Bar>
);
};

View File

@@ -40,7 +40,7 @@ import { MenuKeys } from 'src/dashboard/types';
const propTypes = {
addSuccessToast: PropTypes.func.isRequired,
addDangerToast: PropTypes.func.isRequired,
dashboardInfo: PropTypes.object.isRequired,
dashboardInfo: PropTypes.object,
dashboardId: PropTypes.number,
dashboardTitle: PropTypes.string,
dataMask: PropTypes.object.isRequired,
@@ -196,7 +196,7 @@ export class HeaderActionsDropdown extends PureComponent {
});
const refreshIntervalOptions =
dashboardInfo.common?.conf?.DASHBOARD_AUTO_REFRESH_INTERVALS;
dashboardInfo?.common?.conf?.DASHBOARD_AUTO_REFRESH_INTERVALS;
const dashboardComponentId = [...(directPathToChild || [])].pop();
@@ -216,6 +216,7 @@ export class HeaderActionsDropdown extends PureComponent {
<Menu.Item
key={MenuKeys.ToggleFullscreen}
onClick={this.handleMenuClick}
disabled={isLoading}
>
{getUrlParam(URL_PARAMS.standalone)
? t('Exit fullscreen')
@@ -226,23 +227,25 @@ export class HeaderActionsDropdown extends PureComponent {
<Menu.Item
key={MenuKeys.EditProperties}
onClick={this.handleMenuClick}
disabled={isLoading}
>
{t('Edit properties')}
</Menu.Item>
)}
{editMode && (
<Menu.Item key={MenuKeys.EditCss}>
<Menu.Item key={MenuKeys.EditCss} disabled={isLoading}>
<CssEditor
triggerNode={<span>{t('Edit CSS')}</span>}
initialCss={this.state.css}
onChange={this.changeCss}
addDangerToast={addDangerToast}
disabled={isLoading}
/>
</Menu.Item>
)}
<Menu.Divider />
{userCanSave && (
<Menu.Item key={MenuKeys.SaveModal}>
<Menu.Item key={MenuKeys.SaveModal} disabled={isLoading}>
<SaveModal
addSuccessToast={this.props.addSuccessToast}
addDangerToast={this.props.addDangerToast}
@@ -303,6 +306,7 @@ export class HeaderActionsDropdown extends PureComponent {
<Menu.Item
key={MenuKeys.ManageEmbedded}
onClick={this.handleMenuClick}
disabled={isLoading}
>
{t('Embed dashboard')}
</Menu.Item>
@@ -311,9 +315,12 @@ export class HeaderActionsDropdown extends PureComponent {
{!editMode ? (
this.state.showReportSubMenu ? (
<>
<Menu.SubMenu title={t('Manage email report')}>
<Menu.SubMenu
title={t('Manage email report')}
disabled={isLoading}
>
<HeaderReportDropdown
dashboardId={dashboardInfo.id}
dashboardId={dashboardInfo?.id}
setShowReportSubMenu={this.setShowReportSubMenu}
showReportSubMenu={this.state.showReportSubMenu}
setIsDropdownVisible={setIsDropdownVisible}
@@ -326,17 +333,18 @@ export class HeaderActionsDropdown extends PureComponent {
) : (
<Menu>
<HeaderReportDropdown
dashboardId={dashboardInfo.id}
dashboardId={dashboardInfo?.id}
setShowReportSubMenu={this.setShowReportSubMenu}
setIsDropdownVisible={setIsDropdownVisible}
isDropdownVisible={isDropdownVisible}
useTextMenu
disabled={isLoading}
/>
</Menu>
)
) : null}
{editMode && !isEmpty(dashboardInfo?.metadata?.filter_scopes) && (
<Menu.Item key={MenuKeys.SetFilterMapping}>
<Menu.Item key={MenuKeys.SetFilterMapping} disabled={isLoading}>
<FilterScopeModal
className="m-r-5"
triggerNode={t('Set filter mapping')}
@@ -344,7 +352,7 @@ export class HeaderActionsDropdown extends PureComponent {
</Menu.Item>
)}
<Menu.Item key={MenuKeys.AutorefreshModal}>
<Menu.Item key={MenuKeys.AutorefreshModal} disabled={isLoading}>
<RefreshIntervalModal
addSuccessToast={this.props.addSuccessToast}
refreshFrequency={refreshFrequency}

View File

@@ -65,10 +65,11 @@ const propTypes = {
addDangerToast: PropTypes.func.isRequired,
addWarningToast: PropTypes.func.isRequired,
user: PropTypes.object, // UserWithPermissionsAndRoles,
dashboardInfo: PropTypes.object.isRequired,
dashboardInfo: PropTypes.object,
dashboardTitle: PropTypes.string,
dataMask: PropTypes.object.isRequired,
charts: PropTypes.objectOf(chartPropShape).isRequired,
chartsLoading: PropTypes.bool.isRequired,
layout: PropTypes.object.isRequired,
expandedSlices: PropTypes.object,
customCss: PropTypes.string,
@@ -78,7 +79,6 @@ const propTypes = {
setUnsavedChanges: PropTypes.func.isRequired,
isStarred: PropTypes.bool.isRequired,
isPublished: PropTypes.bool.isRequired,
isLoading: PropTypes.bool.isRequired,
onSave: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
fetchFaveStar: PropTypes.func.isRequired,
@@ -270,7 +270,7 @@ class Header extends PureComponent {
}
forceRefresh() {
if (!this.props.isLoading) {
if (!this.props.chartsLoading) {
const chartList = Object.keys(this.props.charts);
this.props.logEvent(LOG_ACTIONS_FORCE_REFRESH_DASHBOARD, {
force: true,
@@ -288,10 +288,10 @@ class Header extends PureComponent {
}
startPeriodicRender(interval) {
let intervalMessage;
const { dashboardInfo } = this.props;
if (interval) {
const { dashboardInfo } = this.props;
let intervalMessage;
if (interval && dashboardInfo) {
const periodicRefreshOptions =
dashboardInfo.common?.conf?.DASHBOARD_AUTO_REFRESH_INTERVALS;
const predefinedValue = periodicRefreshOptions.find(
@@ -340,11 +340,13 @@ class Header extends PureComponent {
);
};
this.refreshTimer = setPeriodicRunner({
interval,
periodicRender,
refreshTimer: this.refreshTimer,
});
if (dashboardInfo) {
this.refreshTimer = setPeriodicRunner({
interval,
periodicRender,
refreshTimer: this.refreshTimer,
});
}
}
toggleEditMode() {
@@ -388,7 +390,7 @@ class Header extends PureComponent {
roles: dashboardInfo.roles,
slug,
metadata: {
...dashboardInfo?.metadata,
...dashboardInfo.metadata,
color_namespace: currentColorNamespace,
color_scheme: currentColorScheme,
positions,
@@ -434,28 +436,32 @@ class Header extends PureComponent {
getMetadataItems = () => {
const { dashboardInfo } = this.props;
return [
{
type: MetadataType.LastModified,
value: dashboardInfo.changed_on_delta_humanized,
value: dashboardInfo?.changed_on_delta_humanized,
modifiedBy:
getOwnerName(dashboardInfo.changed_by) || t('Not available'),
getOwnerName(dashboardInfo?.changed_by) || t('Not available'),
},
{
type: MetadataType.Owner,
createdBy: getOwnerName(dashboardInfo.created_by) || t('Not available'),
createdBy:
getOwnerName(dashboardInfo?.created_by) || t('Not available'),
owners:
dashboardInfo.owners.length > 0
? dashboardInfo.owners.map(getOwnerName)
dashboardInfo?.owners.length > 0
? dashboardInfo?.owners.map(getOwnerName)
: t('None'),
createdOn: dashboardInfo.created_on_delta_humanized,
createdOn: dashboardInfo?.created_on_delta_humanized,
},
];
};
render() {
const {
dashboardInfo,
dashboardTitle,
chartsLoading,
layout,
expandedSlices,
customCss,
@@ -474,27 +480,24 @@ class Header extends PureComponent {
editMode,
isPublished,
user,
dashboardInfo,
hasUnsavedChanges,
isLoading,
refreshFrequency,
shouldPersistRefreshFrequency,
setRefreshFrequency,
lastModifiedTime,
logEvent,
} = this.props;
const userCanEdit =
dashboardInfo.dash_edit_perm && !dashboardInfo.is_managed_externally;
const userCanShare = dashboardInfo.dash_share_perm;
const userCanSaveAs = dashboardInfo.dash_save_perm;
dashboardInfo?.dash_edit_perm && !dashboardInfo?.is_managed_externally;
const userCanShare = dashboardInfo?.dash_share_perm;
const userCanSaveAs = dashboardInfo?.dash_save_perm;
const userCanCurate =
isFeatureEnabled(FeatureFlag.EmbeddedSuperset) &&
findPermission('can_set_embedded', 'Dashboard', user.roles);
const refreshLimit =
dashboardInfo.common?.conf?.SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT;
dashboardInfo?.common?.conf?.SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT;
const refreshWarning =
dashboardInfo.common?.conf
dashboardInfo?.common?.conf
?.SUPERSET_DASHBOARD_PERIODICAL_REFRESH_WARNING_MESSAGE;
const handleOnPropertiesChange = updates => {
@@ -519,44 +522,47 @@ class Header extends PureComponent {
<div
css={headerContainerStyle}
data-test="dashboard-header-container"
data-test-id={dashboardInfo.id}
data-test-id={dashboardInfo?.id}
className="dashboard-header-container"
>
<PageHeaderWithActions
editableTitleProps={{
title: dashboardTitle,
canEdit: userCanEdit && editMode,
canEdit: !!(userCanEdit && editMode),
onSave: this.handleChangeText,
placeholder: t('Add the name of the dashboard'),
label: t('Dashboard title'),
showTooltip: false,
loading: !dashboardTitle,
}}
certificatiedBadgeProps={{
certifiedBy: dashboardInfo.certified_by,
details: dashboardInfo.certification_details,
certifiedBy: dashboardInfo?.certified_by,
details: dashboardInfo?.certification_details,
}}
faveStarProps={{
itemId: dashboardInfo.id,
itemId: dashboardInfo?.id,
fetchFaveStar: this.props.fetchFaveStar,
saveFaveStar: this.props.saveFaveStar,
isStarred: this.props.isStarred,
showTooltip: true,
showTooltip: !!dashboardInfo?.id,
}}
titlePanelAdditionalItems={[
!editMode && (
<PublishedStatus
dashboardId={dashboardInfo.id}
dashboardId={dashboardInfo?.id}
isPublished={isPublished}
savePublished={this.props.savePublished}
canEdit={userCanEdit}
canSave={userCanSaveAs}
visible={!editMode}
loading={!dashboardInfo?.id}
/>
),
!editMode && (
<MetadataBar
items={this.getMetadataItems()}
tooltipPlacement="bottom"
loading={!dashboardInfo?.id}
/>
),
]}
@@ -576,7 +582,7 @@ class Header extends PureComponent {
>
<StyledUndoRedoButton
buttonStyle="link"
disabled={undoLength < 1}
disabled={undoLength < 1 || !dashboardInfo}
onClick={undoLength && onUndo}
>
<Icons.Undo
@@ -596,7 +602,7 @@ class Header extends PureComponent {
>
<StyledUndoRedoButton
buttonStyle="link"
disabled={redoLength < 1}
disabled={redoLength < 1 || !dashboardInfo}
onClick={redoLength && onRedo}
>
<Icons.Redo
@@ -618,17 +624,20 @@ class Header extends PureComponent {
buttonStyle="default"
data-test="discard-changes-button"
aria-label={t('Discard')}
disabled={!hasUnsavedChanges}
loading={!dashboardInfo}
>
{t('Discard')}
</Button>
<Button
aria-label={t('Save')}
css={saveBtnStyle}
buttonSize="small"
disabled={!hasUnsavedChanges}
buttonStyle="primary"
onClick={this.overwriteDashboard}
data-test="header-save-button"
aria-label={t('Save')}
disabled={!hasUnsavedChanges}
loading={!dashboardInfo}
>
{t('Save')}
</Button>
@@ -670,7 +679,7 @@ class Header extends PureComponent {
<ConnectedHeaderActionsDropdown
addSuccessToast={this.props.addSuccessToast}
addDangerToast={this.props.addDangerToast}
dashboardId={dashboardInfo.id}
dashboardId={dashboardInfo?.id}
dashboardTitle={dashboardTitle}
dashboardInfo={dashboardInfo}
dataMask={dataMask}
@@ -693,7 +702,7 @@ class Header extends PureComponent {
userCanShare={userCanShare}
userCanSave={userCanSaveAs}
userCanCurate={userCanCurate}
isLoading={isLoading}
isLoading={chartsLoading || !dashboardInfo?.id}
showPropertiesModal={this.showPropertiesModal}
manageEmbedded={this.showEmbedModal}
refreshLimit={refreshLimit}
@@ -704,12 +713,12 @@ class Header extends PureComponent {
logEvent={logEvent}
/>
}
showFaveStar={user?.userId && dashboardInfo?.id}
showFaveStar={user?.userId}
showTitlePanelItems
/>
{this.state.showingPropertiesModal && (
<PropertiesModal
dashboardId={dashboardInfo.id}
dashboardId={dashboardInfo?.id}
dashboardInfo={dashboardInfo}
dashboardTitle={dashboardTitle}
show={this.state.showingPropertiesModal}
@@ -726,7 +735,7 @@ class Header extends PureComponent {
<DashboardEmbedModal
show={this.state.showingEmbedModal}
onHide={this.hideEmbedModal}
dashboardId={dashboardInfo.id}
dashboardId={dashboardInfo?.id}
/>
)}
<Global

View File

@@ -28,6 +28,7 @@ const propTypes = {
savePublished: PropTypes.func.isRequired,
canEdit: PropTypes.bool,
canSave: PropTypes.bool,
loading: PropTypes.bool,
};
const draftButtonTooltip = t(
@@ -54,6 +55,9 @@ export default class PublishedStatus extends Component {
}
render() {
if (this.props.loading) {
return <Label>{t('...')}</Label>;
}
// Show everybody the draft badge
if (!this.props.isPublished) {
// if they can edit the dash, make the badge a button

View File

@@ -50,16 +50,16 @@ const InnerStyledDiv = styled.div`
type RefreshIntervalModalProps = {
addSuccessToast: (msg: string) => void;
triggerNode: JSX.Element;
refreshFrequency: number;
refreshFrequency?: number;
onChange: (refreshLimit: number, editMode: boolean) => void;
editMode: boolean;
refreshLimit?: number;
refreshWarning: string | null;
refreshIntervalOptions: [number, string][];
refreshIntervalOptions?: [number, string][];
};
type RefreshIntervalModalState = {
refreshFrequency: number;
refreshFrequency?: number;
custom_hour: number;
custom_min: number;
custom_sec: number;
@@ -93,9 +93,11 @@ class RefreshIntervalModal extends PureComponent<
}
onSave() {
this.props.onChange(this.state.refreshFrequency, this.props.editMode);
this.modalRef?.current?.close();
this.props.addSuccessToast(t('Refresh interval saved'));
if (this.state.refreshFrequency !== undefined) {
this.props.onChange(this.state.refreshFrequency, this.props.editMode);
this.modalRef?.current?.close();
this.props.addSuccessToast(t('Refresh interval saved'));
}
}
onCancel() {
@@ -108,7 +110,7 @@ class RefreshIntervalModal extends PureComponent<
handleFrequencyChange(value: number) {
const { refreshIntervalOptions } = this.props;
this.setState({
refreshFrequency: value || refreshIntervalOptions[0][0],
refreshFrequency: value || refreshIntervalOptions?.[0]?.[0],
});
this.setState({
@@ -135,7 +137,7 @@ class RefreshIntervalModal extends PureComponent<
refresh_options.push({ value: -1, label: t('Custom interval') });
refresh_options.push(
...refreshIntervalOptions.map(option => ({
...refreshIntervalOptions?.map(option => ({
value: option[0],
label: t(option[1]),
})),
@@ -221,7 +223,11 @@ class RefreshIntervalModal extends PureComponent<
</FormLabel>
<Select
ariaLabel={t('Refresh interval')}
options={this.createIntervalOptions(refreshIntervalOptions)}
options={
refreshIntervalOptions
? this.createIntervalOptions(refreshIntervalOptions)
: []
}
value={refreshFrequency}
onChange={this.handleFrequencyChange}
sortComparator={propertyComparator('value')}
@@ -307,6 +313,7 @@ class RefreshIntervalModal extends PureComponent<
<Button
buttonStyle="primary"
buttonSize="small"
disabled={!refreshIntervalOptions}
onClick={() =>
this.refresh_custom_val(
custom_block,

View File

@@ -87,7 +87,7 @@ function mapStateToProps({
user,
isStarred: !!dashboardState.isStarred,
isPublished: !!dashboardState.isPublished,
isLoading: isDashboardLoading(charts),
chartsLoading: isDashboardLoading(charts),
hasUnsavedChanges: !!dashboardState.hasUnsavedChanges,
maxUndoHistoryExceeded: !!dashboardState.maxUndoHistoryExceeded,
lastModifiedTime: Math.max(