mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 12:23:10 +08:00
NewPanelEditor: Save dashboard from edit mode now works, and other fixes (#23668)
This commit is contained in:
@ -67,13 +67,14 @@ const getPropertiesForVariant = (theme: GrafanaTheme, variant: ButtonVariant) =>
|
|||||||
export interface StyleProps {
|
export interface StyleProps {
|
||||||
theme: GrafanaTheme;
|
theme: GrafanaTheme;
|
||||||
size: ComponentSize;
|
size: ComponentSize;
|
||||||
icon?: IconName;
|
|
||||||
variant: ButtonVariant;
|
variant: ButtonVariant;
|
||||||
textAndIcon?: boolean;
|
hasIcon: boolean;
|
||||||
|
hasText: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getButtonStyles = stylesFactory(({ theme, size, variant, icon }: StyleProps) => {
|
export const getButtonStyles = stylesFactory((props: StyleProps) => {
|
||||||
const { padding, fontSize, height } = getPropertiesForButtonSize(theme, size, icon);
|
const { theme, variant } = props;
|
||||||
|
const { padding, fontSize, height } = getPropertiesForButtonSize(props);
|
||||||
const { background, borderColor, variantStyles } = getPropertiesForVariant(theme, variant);
|
const { background, borderColor, variantStyles } = getPropertiesForVariant(theme, variant);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -105,9 +106,6 @@ export const getButtonStyles = stylesFactory(({ theme, size, variant, icon }: St
|
|||||||
${variantStyles}
|
${variantStyles}
|
||||||
`
|
`
|
||||||
),
|
),
|
||||||
buttonWithIcon: css`
|
|
||||||
padding-left: ${theme.spacing.sm};
|
|
||||||
`,
|
|
||||||
// used for buttons with icon only
|
// used for buttons with icon only
|
||||||
iconButton: css`
|
iconButton: css`
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
@ -139,7 +137,8 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
theme,
|
theme,
|
||||||
size: otherProps.size || 'md',
|
size: otherProps.size || 'md',
|
||||||
variant: variant || 'primary',
|
variant: variant || 'primary',
|
||||||
icon,
|
hasText: children !== undefined,
|
||||||
|
hasIcon: icon !== undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -162,7 +161,8 @@ export const LinkButton = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>(
|
|||||||
theme,
|
theme,
|
||||||
size: otherProps.size || 'md',
|
size: otherProps.size || 'md',
|
||||||
variant: variant || 'primary',
|
variant: variant || 'primary',
|
||||||
icon,
|
hasText: children !== undefined,
|
||||||
|
hasIcon: icon !== undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -17,10 +17,16 @@ export interface RadioButtonProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme, size: RadioButtonSize, fullWidth?: boolean) => {
|
const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme, size: RadioButtonSize, fullWidth?: boolean) => {
|
||||||
const { fontSize, height } = getPropertiesForButtonSize(theme, size);
|
const { fontSize, height } = getPropertiesForButtonSize({
|
||||||
|
theme,
|
||||||
|
size,
|
||||||
|
hasIcon: false,
|
||||||
|
hasText: true,
|
||||||
|
variant: 'secondary',
|
||||||
|
});
|
||||||
|
|
||||||
const horizontalPadding = theme.spacing[size] ?? theme.spacing.md;
|
const horizontalPadding = theme.spacing[size] ?? theme.spacing.md;
|
||||||
const c = theme.palette;
|
const c = theme.palette;
|
||||||
|
|
||||||
const textColor = theme.colors.textSemiWeak;
|
const textColor = theme.colors.textSemiWeak;
|
||||||
const textColorHover = theme.colors.text;
|
const textColorHover = theme.colors.text;
|
||||||
const textColorActive = theme.isLight ? c.blue77 : c.blue95;
|
const textColorActive = theme.isLight ? c.blue77 : c.blue95;
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import { ComponentSize } from '../../types/size';
|
import { StyleProps } from '../Button';
|
||||||
import { IconName } from '../../types';
|
|
||||||
|
|
||||||
export const getFocusCss = (theme: GrafanaTheme) => `
|
export const getFocusCss = (theme: GrafanaTheme) => `
|
||||||
outline: 2px dotted transparent;
|
outline: 2px dotted transparent;
|
||||||
@ -91,8 +90,9 @@ export const inputSizesPixels = (size: string) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPropertiesForButtonSize = (theme: GrafanaTheme, size: ComponentSize, icon?: IconName) => {
|
export const getPropertiesForButtonSize = (props: StyleProps) => {
|
||||||
const { spacing, typography, height } = theme;
|
const { hasText, hasIcon, size } = props;
|
||||||
|
const { spacing, typography, height } = props.theme;
|
||||||
|
|
||||||
switch (size) {
|
switch (size) {
|
||||||
case 'sm':
|
case 'sm':
|
||||||
@ -104,14 +104,14 @@ export const getPropertiesForButtonSize = (theme: GrafanaTheme, size: ComponentS
|
|||||||
|
|
||||||
case 'lg':
|
case 'lg':
|
||||||
return {
|
return {
|
||||||
padding: `0 ${spacing.lg} 0 ${icon ? spacing.md : spacing.lg}`,
|
padding: `0 ${hasText ? spacing.lg : spacing.md} 0 ${hasIcon ? spacing.md : spacing.lg}`,
|
||||||
fontSize: typography.size.lg,
|
fontSize: typography.size.lg,
|
||||||
height: height.lg,
|
height: height.lg,
|
||||||
};
|
};
|
||||||
case 'md':
|
case 'md':
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
padding: `0 ${spacing.md} 0 ${icon ? spacing.sm : spacing.md}`,
|
padding: `0 ${hasText ? spacing.md : spacing.sm} 0 ${hasIcon ? spacing.sm : spacing.md}`,
|
||||||
fontSize: typography.size.md,
|
fontSize: typography.size.md,
|
||||||
height: height.md,
|
height: height.md,
|
||||||
};
|
};
|
||||||
|
@ -19,6 +19,8 @@ export const getFormStyles = stylesFactory(
|
|||||||
theme,
|
theme,
|
||||||
variant: options.variant,
|
variant: options.variant,
|
||||||
size: options.size,
|
size: options.size,
|
||||||
|
hasIcon: false,
|
||||||
|
hasText: true,
|
||||||
}),
|
}),
|
||||||
input: getInputStyles({ theme, invalid: options.invalid }),
|
input: getInputStyles({ theme, invalid: options.invalid }),
|
||||||
switch: getSwitchStyles(theme),
|
switch: getSwitchStyles(theme),
|
||||||
|
@ -32,6 +32,7 @@ export const getModalStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
`,
|
`,
|
||||||
modalHeader: css`
|
modalHeader: css`
|
||||||
|
label: modalHeader;
|
||||||
background: ${theme.colors.bg2};
|
background: ${theme.colors.bg2};
|
||||||
border-bottom: 1px solid ${theme.colors.pageHeaderBorder};
|
border-bottom: 1px solid ${theme.colors.pageHeaderBorder};
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -42,6 +43,7 @@ export const getModalStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
margin: 0 ${theme.spacing.md};
|
margin: 0 ${theme.spacing.md};
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
line-height: 42px;
|
||||||
`,
|
`,
|
||||||
modalHeaderIcon: css`
|
modalHeaderIcon: css`
|
||||||
margin-right: ${theme.spacing.md};
|
margin-right: ${theme.spacing.md};
|
||||||
|
@ -48,7 +48,7 @@ export class DashboardSettings extends PureComponent<Props> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dashboard-settings">
|
<div className="dashboard-settings">
|
||||||
<div className="navbar navbar--shadow">
|
<div className="navbar navbar--edit">
|
||||||
<div className="navbar-edit">
|
<div className="navbar-edit">
|
||||||
<BackButton surface="body" onClick={this.onClose} />
|
<BackButton surface="body" onClick={this.onClose} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,7 +17,7 @@ import { Unsubscribable } from 'rxjs';
|
|||||||
import { DisplayMode, displayModes, PanelEditorTab } from './types';
|
import { DisplayMode, displayModes, PanelEditorTab } from './types';
|
||||||
import { PanelEditorTabs } from './PanelEditorTabs';
|
import { PanelEditorTabs } from './PanelEditorTabs';
|
||||||
import { DashNavTimeControls } from '../DashNav/DashNavTimeControls';
|
import { DashNavTimeControls } from '../DashNav/DashNavTimeControls';
|
||||||
import { LocationState } from 'app/types';
|
import { LocationState, CoreEvents } from 'app/types';
|
||||||
import { calculatePanelSize } from './utils';
|
import { calculatePanelSize } from './utils';
|
||||||
import { initPanelEditor, panelEditorCleanUp, updatePanelEditorUIState } from './state/actions';
|
import { initPanelEditor, panelEditorCleanUp, updatePanelEditorUIState } from './state/actions';
|
||||||
import { PanelEditorUIState, setDiscardChanges } from './state/reducers';
|
import { PanelEditorUIState, setDiscardChanges } from './state/reducers';
|
||||||
@ -29,6 +29,8 @@ import { VariableModel } from 'app/features/templating/types';
|
|||||||
import { getVariables } from 'app/features/variables/state/selectors';
|
import { getVariables } from 'app/features/variables/state/selectors';
|
||||||
import { SubMenuItems } from 'app/features/dashboard/components/SubMenu/SubMenuItems';
|
import { SubMenuItems } from 'app/features/dashboard/components/SubMenu/SubMenuItems';
|
||||||
import { BackButton } from 'app/core/components/BackButton/BackButton';
|
import { BackButton } from 'app/core/components/BackButton/BackButton';
|
||||||
|
import { appEvents } from 'app/core/core';
|
||||||
|
import { SaveDashboardModalProxy } from '../SaveDashboard/SaveDashboardModalProxy';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
dashboard: DashboardModel;
|
dashboard: DashboardModel;
|
||||||
@ -82,6 +84,17 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onOpenDashboardSettings = () => {
|
||||||
|
this.props.updateLocation({ query: { editview: 'settings' }, partial: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
onSaveDashboard = () => {
|
||||||
|
appEvents.emit(CoreEvents.showModalReact, {
|
||||||
|
component: SaveDashboardModalProxy,
|
||||||
|
props: { dashboard: this.props.dashboard },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
onChangeTab = (tab: PanelEditorTab) => {
|
onChangeTab = (tab: PanelEditorTab) => {
|
||||||
this.props.updateLocation({ query: { tab: tab.id }, partial: true });
|
this.props.updateLocation({ query: { tab: tab.id }, partial: true });
|
||||||
};
|
};
|
||||||
@ -107,8 +120,14 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
|||||||
this.forceUpdate();
|
this.forceUpdate();
|
||||||
};
|
};
|
||||||
|
|
||||||
onDragFinished = (pane: Pane, size: number) => {
|
onDragFinished = (pane: Pane, size?: number) => {
|
||||||
document.body.style.cursor = 'auto';
|
document.body.style.cursor = 'auto';
|
||||||
|
|
||||||
|
// When the drag handle is just clicked size is undefined
|
||||||
|
if (!size) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const targetPane = pane === Pane.Top ? 'topPaneSize' : 'rightPaneSize';
|
const targetPane = pane === Pane.Top ? 'topPaneSize' : 'rightPaneSize';
|
||||||
const { updatePanelEditorUIState } = this.props;
|
const { updatePanelEditorUIState } = this.props;
|
||||||
updatePanelEditorUIState({
|
updatePanelEditorUIState({
|
||||||
@ -228,12 +247,27 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
|
|||||||
</div>
|
</div>
|
||||||
<div className={styles.toolbarLeft}>
|
<div className={styles.toolbarLeft}>
|
||||||
<div className={styles.toolbarItem}>
|
<div className={styles.toolbarItem}>
|
||||||
<Button onClick={this.onDiscard} variant="secondary">
|
<Button
|
||||||
Discard changes
|
icon="cog"
|
||||||
|
onClick={this.onOpenDashboardSettings}
|
||||||
|
variant="secondary"
|
||||||
|
title="Open dashboad settings"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.toolbarItem}>
|
||||||
|
<Button onClick={this.onDiscard} variant="secondary" title="Undo all changes">
|
||||||
|
Discard
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.toolbarItem}>
|
<div className={styles.toolbarItem}>
|
||||||
<Button onClick={this.onPanelExit}>Apply</Button>
|
<Button onClick={this.onSaveDashboard} variant="secondary" title="Apply changes and save dashboard">
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className={styles.toolbarItem}>
|
||||||
|
<Button onClick={this.onPanelExit} title="Apply changes and go back to dashboard">
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -335,7 +369,7 @@ enum Pane {
|
|||||||
/*
|
/*
|
||||||
* Styles
|
* Styles
|
||||||
*/
|
*/
|
||||||
const getStyles = stylesFactory((theme: GrafanaTheme, props: Props) => {
|
export const getStyles = stylesFactory((theme: GrafanaTheme, props: Props) => {
|
||||||
const { uiState } = props;
|
const { uiState } = props;
|
||||||
const handleColor = theme.palette.blue95;
|
const handleColor = theme.palette.blue95;
|
||||||
const paneSpaceing = theme.spacing.md;
|
const paneSpaceing = theme.spacing.md;
|
||||||
@ -361,7 +395,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme, props: Props) => {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: ${theme.zIndex.modal};
|
z-index: ${theme.zIndex.sidemenu};
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
@ -73,6 +73,7 @@ const pluginsSlice = createSlice({
|
|||||||
state.querySubscription = action.payload.querySubscription;
|
state.querySubscription = action.payload.querySubscription;
|
||||||
state.initDone = true;
|
state.initDone = true;
|
||||||
state.isOpen = true;
|
state.isOpen = true;
|
||||||
|
state.shouldDiscardChanges = false;
|
||||||
},
|
},
|
||||||
setEditorPanelData: (state, action: PayloadAction<PanelData>) => {
|
setEditorPanelData: (state, action: PayloadAction<PanelData>) => {
|
||||||
state.getData = () => action.payload;
|
state.getData = () => action.payload;
|
||||||
|
@ -80,6 +80,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
|||||||
background: ${theme.isLight ? theme.palette.gray7 : theme.palette.black};
|
background: ${theme.isLight ? theme.palette.gray7 : theme.palette.black};
|
||||||
padding: ${theme.spacing.sm} 0 ${theme.spacing.sm} ${theme.spacing.md};
|
padding: ${theme.spacing.sm} 0 ${theme.spacing.sm} ${theme.spacing.md};
|
||||||
height: 400px;
|
height: 400px;
|
||||||
|
width: 100%;
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -71,6 +71,20 @@ describe('DashboardModel', () => {
|
|||||||
|
|
||||||
expect(panels.length).toBe(1);
|
expect(panels.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should save model in edit mode', () => {
|
||||||
|
const model = new DashboardModel({});
|
||||||
|
model.addPanel({ type: 'graph' });
|
||||||
|
|
||||||
|
const panel = model.initEditPanel(model.panels[0]);
|
||||||
|
panel.title = 'updated';
|
||||||
|
|
||||||
|
const saveModel = model.getSaveModelClone();
|
||||||
|
const savedPanel = saveModel.panels[0];
|
||||||
|
|
||||||
|
expect(savedPanel.title).toBe('updated');
|
||||||
|
expect(savedPanel.id).toBe(model.panels[0].id);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('row and panel manipulation', () => {
|
describe('row and panel manipulation', () => {
|
||||||
|
@ -180,10 +180,20 @@ export class DashboardModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get panel save models
|
// get panel save models
|
||||||
copy.panels = _.chain(this.panels)
|
copy.panels = this.panels
|
||||||
.filter((panel: PanelModel) => panel.type !== 'add-panel')
|
.filter((panel: PanelModel) => panel.type !== 'add-panel')
|
||||||
.map((panel: PanelModel) => panel.getSaveModel())
|
.map((panel: PanelModel) => {
|
||||||
.value();
|
// If we save while editing we should include the panel in edit mode instead of the
|
||||||
|
// unmodified source panel
|
||||||
|
if (this.panelInEdit && this.panelInEdit.editSourceId === panel.id) {
|
||||||
|
const saveModel = this.panelInEdit.getSaveModel();
|
||||||
|
// while editing a panel we modify its id, need to restore it here
|
||||||
|
saveModel.id = this.panelInEdit.editSourceId;
|
||||||
|
return saveModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return panel.getSaveModel();
|
||||||
|
});
|
||||||
|
|
||||||
// sort by keys
|
// sort by keys
|
||||||
copy = sortByKeys(copy);
|
copy = sortByKeys(copy);
|
||||||
|
@ -15,8 +15,9 @@
|
|||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--shadow {
|
&--edit {
|
||||||
box-shadow: $side-menu-shadow;
|
background: $panel-bg;
|
||||||
|
border-bottom: $panel-border;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -43,11 +44,11 @@
|
|||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
display: block;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: $font-size-lg;
|
font-size: $font-size-lg;
|
||||||
min-height: $navbarHeight;
|
min-height: $navbarHeight;
|
||||||
line-height: $navbarHeight;
|
|
||||||
|
|
||||||
.gicon {
|
.gicon {
|
||||||
top: -2px;
|
top: -2px;
|
||||||
@ -204,5 +205,5 @@ i.navbar-page-btn__search {
|
|||||||
display: flex;
|
display: flex;
|
||||||
height: $navbarHeight;
|
height: $navbarHeight;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-right: 13px;
|
padding-right: 16px;
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,10 @@
|
|||||||
font-size: 75%;
|
font-size: 75%;
|
||||||
padding-left: 8px;
|
padding-left: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gf-form {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.variable-value-link {
|
.variable-value-link {
|
||||||
|
Reference in New Issue
Block a user