NewPanelEdit: Improvements to angular panels and other fixes (#23678)

* Removed old editor components

* Angular panel improvements

* Progress

* Updated tests

* Simple persistence for angular panel option state

* Improving graph edit experiance

* Improving series overrides

* updated e2e test

* Regstry: refactoring
This commit is contained in:
Torkel Ödegaard 2020-04-20 08:47:25 +02:00 committed by GitHub
parent d2a13c4715
commit 3aa8eb0176
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
55 changed files with 508 additions and 1396 deletions

View File

@ -203,7 +203,7 @@ const assertAdding3dependantQueryVariablesScenario = (queryVariables: QueryVaria
e2e.pages.SaveDashboardModal.save().click();
e2e.flows.assertSuccessNotification();
e2e.pages.Components.BackButton.backArrow().click();
e2e.components.BackButton.backArrow().click();
assertVariableLabelsAndComponents(asserts);
@ -258,7 +258,7 @@ const assertDuplicateItem = (queryVariables: QueryVariableData[]) => {
e2e.pages.SaveDashboardModal.save().click();
e2e.flows.assertSuccessNotification();
e2e.pages.Components.BackButton.backArrow().click();
e2e.components.BackButton.backArrow().click();
e2e.pages.Dashboard.SubMenu.submenuItemLabels(newItem.label).should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(newItem.selectedOption)
@ -294,7 +294,7 @@ const assertDeleteItem = (queryVariables: QueryVariableData[]) => {
e2e.pages.SaveDashboardModal.save().click();
e2e.flows.assertSuccessNotification();
e2e.pages.Components.BackButton.backArrow().click();
e2e.components.BackButton.backArrow().click();
e2e.pages.Dashboard.SubMenu.submenuItemLabels(itemToDelete.label).should('not.exist');
@ -341,7 +341,7 @@ const assertUpdateItem = (data: QueryVariableData[]) => {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalHideSelect().select('');
e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInput().type(updatedItem.query);
e2e.pages.Components.BackButton.backArrow().click();
e2e.components.BackButton.backArrow().click();
e2e()
.window()
@ -397,7 +397,7 @@ const assertMoveDownItem = (data: QueryVariableData[]) => {
});
});
e2e.pages.Components.BackButton.backArrow().click();
e2e.components.BackButton.backArrow().click();
assertVariableLabelsAndComponents(queryVariables);
@ -542,7 +542,7 @@ const assertMoveUpItem = (data: QueryVariableData[]) => {
});
});
e2e.pages.Components.BackButton.backArrow().click();
e2e.components.BackButton.backArrow().click();
assertVariableLabelsAndComponents(queryVariables);

View File

@ -15,13 +15,17 @@ e2e.scenario({
e2e.pages.Dashboard.Toolbar.toolbarItems('Add panel').click();
e2e.pages.AddDashboard.ctaButtons('Add Query').click();
e2e.pages.Dashboard.Panels.DataSource.TestData.QueryTab.scenarioSelect().select('CSV Metric Values');
e2e.components.DataSource.TestData.QueryTab.scenarioSelect().select('CSV Metric Values');
e2e.pages.Dashboard.Panels.Visualization.Graph.VisualizationTab.xAxisSection()
.contains('Show')
.click();
// Make sure the graph renders via checking legend
e2e.components.Panels.Visualization.Graph.Legend.legendItemAlias('A-series').should('be.visible');
// e2e.pages.Dashboard.Panels.Panel.title('Panel Title').click();
// e2e.pages.Dashboard.Panels.Panel.headerItems('Inspect').click();
// Expand options section
e2e.components.Panels.Visualization.Graph.VisualizationTab.legendSection().click();
// Disable legend
e2e.components.Panels.Visualization.Graph.Legend.showLegendSwitch().click();
e2e.components.Panels.Visualization.Graph.Legend.legendItemAlias('A-series').should('not.exist');
},
});

View File

@ -46,20 +46,26 @@ export class Registry<T extends RegistryItem> {
getIfExists(id: string | undefined): T | undefined {
if (!this.initialized) {
if (this.init) {
for (const ext of this.init()) {
this.register(ext);
}
}
this.sort();
this.initialized = true;
this.initialize();
}
if (id) {
return this.byId.get(id);
}
return undefined;
}
private initialize() {
if (this.init) {
for (const ext of this.init()) {
this.register(ext);
}
}
this.sort();
this.initialized = true;
}
get(id: string): T {
const v = this.getIfExists(id);
if (!v) {
@ -70,7 +76,7 @@ export class Registry<T extends RegistryItem> {
selectOptions(current?: string[], filter?: (ext: T) => boolean): RegistrySelectInfo {
if (!this.initialized) {
this.getIfExists('xxx'); // will trigger init
this.initialize();
}
const select = {
@ -111,6 +117,10 @@ export class Registry<T extends RegistryItem> {
* Return a list of values by ID, or all values if not specified
*/
list(ids?: any[]): T[] {
if (!this.initialized) {
this.initialize();
}
if (ids) {
const found: T[] = [];
for (const id of ids) {
@ -121,16 +131,23 @@ export class Registry<T extends RegistryItem> {
}
return found;
}
return this.ordered;
}
isEmpty(): boolean {
if (!this.initialized) {
this.getIfExists('xxx'); // will trigger init
this.initialize();
}
return this.ordered; // copy of everythign just in case
return this.ordered.length === 0;
}
register(ext: T) {
if (this.byId.has(ext.id)) {
throw new Error('Duplicate Key:' + ext.id);
}
this.byId.set(ext.id, ext);
this.ordered.push(ext);

View File

@ -4,7 +4,7 @@
// toBe, toEqual and so forth. That's why this file is not type checked and will be so until we
// can solve the above mentioned issue with Cypress/Jest.
import { e2eScenario, ScenarioArguments } from './support/scenario';
import { Pages } from './pages';
import { Pages, Components } from './pages';
import { Flows } from './flows';
import { getScenarioContext, setScenarioContext } from './support/scenarioContext';
@ -21,6 +21,7 @@ const e2eObject = {
imgSrcToBlob: (url: string) => Cypress.Blob.imgSrcToBlob(url),
scenario: (args: ScenarioArguments) => e2eScenario(args),
pages: Pages,
components: Components,
flows: Flows,
getScenarioContext,
setScenarioContext,

View File

@ -1,5 +1,13 @@
import { VisualizationTab } from './visualizationTab';
import { pageFactory } from '../../support';
export const Graph = {
VisualizationTab,
Legend: pageFactory({
url: '',
selectors: {
legendItemAlias: (name: string) => `gpl alias ${name}`,
showLegendSwitch: 'gpl show legend',
},
}),
};

View File

@ -4,5 +4,8 @@ export const VisualizationTab = pageFactory({
url: '',
selectors: {
xAxisSection: 'X-Axis section',
axesSection: 'Axes section',
legendSection: 'Legend section',
displaySection: 'Display section',
},
});

View File

@ -39,26 +39,27 @@ export const Pages = {
},
},
},
Panels: {
Panel,
EditPanel,
DataSource: {
TestData,
},
Visualization: {
Graph,
},
},
},
Dashboards,
SaveDashboardAsModal,
SaveDashboardModal,
SharePanelModal,
Components: {
BackButton: pageFactory({
selectors: {
backArrow: 'Go Back button',
},
}),
},
};
export const Components = {
DataSource: {
TestData,
},
Panels: {
Panel,
EditPanel,
Visualization: {
Graph,
},
},
BackButton: pageFactory({
selectors: {
backArrow: 'Go Back button',
},
}),
};

View File

@ -1,67 +1,34 @@
.panel-options-group {
margin-bottom: 10px;
border: $panel-options-group-border;
border-radius: $border-radius;
background: $page-bg;
border-bottom: $panel-border;
}
.panel-options-group__header {
padding: 4px 8px;
background: $panel-options-group-header-bg;
padding: 8px 16px 8px 8px;
position: relative;
border-radius: $border-radius $border-radius 0 0;
display: flex;
align-items: center;
.btn {
position: absolute;
right: 0;
top: 0;
}
}
.panel-options-group__add-btn {
background: none;
border: none;
display: flex;
align-items: center;
padding: 0;
cursor: pointer;
font-weight: 500;
color: $text-color-semi-weak;
&:hover {
.panel-options-group__add-circle {
background-color: $btn-primary-bg;
color: $white;
color: $text-color;
.panel-options-group__icon {
color: $text-color;
}
}
}
.panel-options-group__add-circle {
@include gradientBar($btn-success-bg, $btn-success-bg-hl, #fff);
border-radius: 50px;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
margin-right: 6px;
i {
position: relative;
top: 1px;
}
.panel-options-group__icon {
color: $text-color-weak;
margin-right: 8px;
}
.panel-options-group__title {
font-size: 16px;
position: relative;
top: 1px;
}
.panel-options-group__body {
padding: 20px;
&--queries {
min-height: 200px;
}
padding: 8px 16px 16px 32px;
}

View File

@ -87,8 +87,9 @@ $body-bg: ${theme.colors.bodyBg};
$page-bg: ${theme.colors.bodyBg};
$dashboard-bg: ${theme.colors.dashboardBg};
$text-color: ${theme.colors.text};
$text-color-strong: ${theme.colors.textStrong};
$text-color: ${theme.colors.text};
$text-color-semi-weak: ${theme.colors.textSemiWeak};
$text-color-weak: ${theme.colors.textWeak};
$text-color-faint: ${theme.colors.textFaint};
$text-color-emphasis: ${theme.colors.textStrong};

View File

@ -83,6 +83,7 @@ $dashboard-bg: ${theme.colors.dashboardBg};
$text-color: ${theme.colors.text};
$text-color-strong: ${theme.colors.textStrong};
$text-color-semi-weak: ${theme.colors.textSemiWeak};
$text-color-weak: ${theme.colors.textWeak};
$text-color-faint: ${theme.colors.textFaint};
$text-color-emphasis: ${theme.colors.textStrong};

View File

@ -14,7 +14,7 @@ export const BackButton: React.FC<Props> = ({ surface, onClick }) => {
tooltipPlacement="bottom"
size="xxl"
surface={surface}
aria-label={e2e.pages.Components.BackButton.selectors.backArrow}
aria-label={e2e.components.BackButton.selectors.backArrow}
onClick={onClick}
/>
);

View File

@ -16,9 +16,9 @@ import { DashboardModel } from '../dashboard/state/DashboardModel';
import { PanelModel } from '../dashboard/state/PanelModel';
import { TestRuleResult } from './TestRuleResult';
import { AppNotificationSeverity, StoreState } from 'app/types';
import { PanelEditorTabIds, getPanelEditorTab } from '../dashboard/panel_editor/state/reducers';
import { changePanelEditorTab } from '../dashboard/panel_editor/state/actions';
import { CoreEvents } from 'app/types';
import { updateLocation } from 'app/core/actions';
import { PanelEditorTabId } from '../dashboard/components/PanelEditor/types';
interface OwnProps {
dashboard: DashboardModel;
@ -30,7 +30,7 @@ interface ConnectedProps {
}
interface DispatchProps {
changePanelEditorTab: typeof changePanelEditorTab;
updateLocation: typeof updateLocation;
}
export type Props = OwnProps & ConnectedProps & DispatchProps;
@ -161,8 +161,8 @@ class UnConnectedAlertTab extends PureComponent<Props, State> {
};
switchToQueryTab = () => {
const { changePanelEditorTab } = this.props;
changePanelEditorTab(getPanelEditorTab(PanelEditorTabIds.Queries));
const { updateLocation } = this.props;
updateLocation({ query: { tab: PanelEditorTabId.Query }, partial: true });
};
renderValidationMessage = () => {
@ -228,6 +228,6 @@ const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (
};
};
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { changePanelEditorTab };
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { updateLocation };
export const AlertTab = connect(mapStateToProps, mapDispatchToProps)(UnConnectedAlertTab);

View File

@ -11,6 +11,7 @@ import { PanelPlugin, PanelPluginMeta } from '@grafana/data';
import { PanelCtrl } from 'app/plugins/sdk';
import { changePanelPlugin } from '../../state/actions';
import { StoreState } from 'app/types';
import { getSectionOpenState, saveSectionOpenState } from './state/utils';
interface OwnProps {
panel: PanelModel;
@ -84,16 +85,18 @@ export class AngularPanelOptionsUnconnected extends PureComponent<Props> {
let template = '';
for (let i = 0; i < panelCtrl.editorTabs.length; i++) {
template +=
`
<div class="panel-options-group" ng-cloak>` +
(i > 0
? `<div class="panel-options-group__header">
<span class="panel-options-group__title">{{ctrl.editorTabs[${i}].title}}
</span>
</div>`
: '') +
`<div class="panel-options-group__body">
const tab = panelCtrl.editorTabs[i];
tab.isOpen = getSectionOpenState(tab.title, i === 0);
template += `
<div class="panel-options-group" ng-cloak>
<div class="panel-options-group__header" ng-click="toggleOptionGroup(${i})" aria-label="${tab.title} section">
<div class="panel-options-group__icon">
<icon name="ctrl.editorTabs[${i}].isOpen ? 'angle-down' : 'angle-right'"></icon>
</div>
<div class="panel-options-group__title">${tab.title}</div>
</div>
<div class="panel-options-group__body" ng-if="ctrl.editorTabs[${i}].isOpen">
<panel-editor-tab editor-tab="ctrl.editorTabs[${i}]" ctrl="ctrl"></panel-editor-tab>
</div>
</div>
@ -101,7 +104,14 @@ export class AngularPanelOptionsUnconnected extends PureComponent<Props> {
}
const loader = getAngularLoader();
const scopeProps = { ctrl: panelCtrl };
const scopeProps = {
ctrl: panelCtrl,
toggleOptionGroup: (index: number) => {
const tab = panelCtrl.editorTabs[index];
tab.isOpen = !tab.isOpen;
saveSectionOpenState(tab.title, tab.isOpen as boolean);
},
};
this.angularOptions = loader.load(this.element, scopeProps, template);
}

View File

@ -66,7 +66,7 @@ export const DynamicConfigValueEditor: React.FC<DynamicConfigValueEditorProps> =
} else {
editor = (
<div>
<Field label={renderLabel()} description={item.description}>
<Field label={renderLabel()()} description={item.description}>
<item.override
value={property.value}
onChange={value => {

View File

@ -64,6 +64,8 @@ const getStyles = stylesFactory((theme: GrafanaTheme, isExpanded: boolean, isNes
font-weight: ${theme.typography.weight.semibold};
&:hover {
color: ${theme.colors.text};
.editor-options-group-toggle {
color: ${theme.colors.text};
}

View File

@ -86,7 +86,7 @@ export const OptionsPaneContent: React.FC<Props> = ({
<TabsBar className={styles.tabsBar}>
<TabsBarContent
width={width}
showFields={!plugin.meta.skipDataQuery}
plugin={plugin}
isSearching={isSearching}
styles={styles}
activeTab={activeTab}
@ -124,7 +124,7 @@ export const OptionsPaneContent: React.FC<Props> = ({
export const TabsBarContent: React.FC<{
width: number;
showFields: boolean;
plugin: PanelPlugin;
isSearching: boolean;
activeTab: string;
styles: OptionsPaneStyles;
@ -132,7 +132,7 @@ export const TabsBarContent: React.FC<{
setSearchMode: (mode: boolean) => void;
setActiveTab: (tab: string) => void;
panel: PanelModel;
}> = ({ width, showFields, isSearching, activeTab, onClose, setSearchMode, setActiveTab, styles, panel }) => {
}> = ({ width, plugin, isSearching, activeTab, onClose, setSearchMode, setActiveTab, styles, panel }) => {
const overridesCount =
panel.getFieldConfig().overrides.length === 0 ? undefined : panel.getFieldConfig().overrides.length;
@ -174,7 +174,9 @@ export const TabsBarContent: React.FC<{
// Show the appropriate tabs
let tabs = tabSelections;
let active = tabs.find(v => v.value === activeTab);
if (!showFields) {
// If no field configs hide Fields & Override tab
if (plugin.fieldConfigRegistry.isEmpty()) {
active = tabSelections[0];
tabs = [active];
}

View File

@ -336,17 +336,17 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
}
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, props) => {
const panel = state.panelEditorNew.getPanel();
const panel = state.panelEditor.getPanel();
const { plugin } = getPanelStateById(state.dashboard, panel.id);
return {
location: state.location,
plugin: plugin,
panel: state.panelEditorNew.getPanel(),
data: state.panelEditorNew.getData(),
initDone: state.panelEditorNew.initDone,
panel: state.panelEditor.getPanel(),
data: state.panelEditor.getData(),
initDone: state.panelEditor.initDone,
tabs: getPanelEditorTabs(state.location, plugin),
uiState: state.panelEditorNew.ui,
uiState: state.panelEditor.ui,
variables: getVariables(state),
};
};

View File

@ -6,7 +6,7 @@ import { OptionsGroup } from './OptionsGroup';
import { getPanelLinksVariableSuggestions } from '../../../panel/panellinks/link_srv';
import { getVariables } from '../../../variables/state/selectors';
import { PanelOptionsEditor } from './PanelOptionsEditor';
import { AngularPanelOptions } from '../../panel_editor/AngularPanelOptions';
import { AngularPanelOptions } from './AngularPanelOptions';
interface Props {
panel: PanelModel;
@ -29,7 +29,7 @@ export const PanelOptionsTab: FC<Props> = ({
}) => {
const elements: JSX.Element[] = [];
const linkVariablesSuggestions = useMemo(() => getPanelLinksVariableSuggestions(), []);
const panelLinksCount = panel && panel.links ? panel.links.length : undefined;
const panelLinksCount = panel && panel.links ? panel.links.length : 0;
const variableOptions = getVariableOptions();
const directionOptions = [
@ -92,9 +92,7 @@ export const PanelOptionsTab: FC<Props> = ({
elements.push(
<OptionsGroup
renderTitle={isExpanded => (
<>
Panel links {!isExpanded && panelLinksCount && panelLinksCount !== 0 && <Counter value={panelLinksCount} />}
</>
<>Panel links {!isExpanded && panelLinksCount > 0 && <Counter value={panelLinksCount} />}</>
)}
key="panel links"
defaultToClosed={true}

View File

@ -1,5 +1,5 @@
import { thunkTester } from '../../../../../../test/core/thunk/thunkTester';
import { closeCompleted, initialState, PanelEditorStateNew } from './reducers';
import { closeCompleted, initialState, PanelEditorState } from './reducers';
import { initPanelEditor, panelEditorCleanUp } from './actions';
import { cleanUpEditPanel, panelModelAndPluginReady } from '../../../state/reducers';
import { DashboardModel, PanelModel } from '../../../state';
@ -36,7 +36,7 @@ describe('panelEditor actions', () => {
const panel = sourcePanel.getEditClone();
panel.updateOptions({ prop: true });
const state: PanelEditorStateNew = {
const state: PanelEditorState = {
...initialState(),
getPanel: () => panel,
getSourcePanel: () => sourcePanel,
@ -44,7 +44,7 @@ describe('panelEditor actions', () => {
};
const dispatchedActions = await thunkTester({
panelEditorNew: state,
panelEditor: state,
dashboard: {
getModel: () => dashboard,
},
@ -70,7 +70,7 @@ describe('panelEditor actions', () => {
panel.plugin = getPanelPlugin({ id: 'table' });
panel.updateOptions({ prop: true });
const state: PanelEditorStateNew = {
const state: PanelEditorState = {
...initialState(),
getPanel: () => panel,
getSourcePanel: () => sourcePanel,
@ -78,7 +78,7 @@ describe('panelEditor actions', () => {
};
const dispatchedActions = await thunkTester({
panelEditorNew: state,
panelEditor: state,
dashboard: {
getModel: () => dashboard,
},
@ -103,7 +103,7 @@ describe('panelEditor actions', () => {
const panel = sourcePanel.getEditClone();
panel.updateOptions({ prop: true });
const state: PanelEditorStateNew = {
const state: PanelEditorState = {
...initialState(),
shouldDiscardChanges: true,
getPanel: () => panel,
@ -112,7 +112,7 @@ describe('panelEditor actions', () => {
};
const dispatchedActions = await thunkTester({
panelEditorNew: state,
panelEditor: state,
dashboard: {
getModel: () => dashboard,
},

View File

@ -34,7 +34,7 @@ export function initPanelEditor(sourcePanel: PanelModel, dashboard: DashboardMod
export function panelEditorCleanUp(): ThunkResult<void> {
return (dispatch, getStore) => {
const dashboard = getStore().dashboard.getModel();
const { getPanel, getSourcePanel, querySubscription, shouldDiscardChanges } = getStore().panelEditorNew;
const { getPanel, getSourcePanel, querySubscription, shouldDiscardChanges } = getStore().panelEditor;
if (!shouldDiscardChanges) {
const panel = getPanel();
@ -76,7 +76,7 @@ export function panelEditorCleanUp(): ThunkResult<void> {
export function updatePanelEditorUIState(uiState: Partial<PanelEditorUIState>): ThunkResult<void> {
return (dispatch, getStore) => {
const nextState = { ...getStore().panelEditorNew.ui, ...uiState };
const nextState = { ...getStore().panelEditor.ui, ...uiState };
dispatch(setPanelEditorUIState(nextState));
store.setObject(PANEL_EDITOR_UI_STATE_STORAGE_KEY, nextState);
};

View File

@ -25,7 +25,7 @@ export interface PanelEditorUIState {
mode: DisplayMode;
}
export interface PanelEditorStateNew {
export interface PanelEditorState {
/* These are functions as they are mutaded later on and redux toolkit will Object.freeze state so
* we need to store these using functions instead */
getSourcePanel: () => PanelModel;
@ -38,7 +38,7 @@ export interface PanelEditorStateNew {
ui: PanelEditorUIState;
}
export const initialState = (): PanelEditorStateNew => {
export const initialState = (): PanelEditorState => {
return {
getPanel: () => new PanelModel({}),
getSourcePanel: () => new PanelModel({}),
@ -64,7 +64,7 @@ interface InitEditorPayload {
}
const pluginsSlice = createSlice({
name: 'panelEditorNew',
name: 'panelEditor',
initialState: initialState(),
reducers: {
updateEditorInitState: (state, action: PayloadAction<InitEditorPayload>) => {
@ -99,4 +99,4 @@ export const {
setPanelEditorUIState,
} = pluginsSlice.actions;
export const panelEditorReducerNew = pluginsSlice.reducer;
export const panelEditorReducer = pluginsSlice.reducer;

View File

@ -0,0 +1,9 @@
import store from 'app/core/store';
export function saveSectionOpenState(id: string, isOpen: boolean) {
store.set(`panel-edit-section-${id}`, isOpen ? 'true' : 'false');
}
export function getSectionOpenState(id: string, defaultValue: boolean) {
return store.getBool(`panel-edit-section-${id}`, defaultValue);
}

View File

@ -6,7 +6,7 @@ export interface PanelEditorTab {
}
export enum PanelEditorTabId {
Query = 'Query',
Query = 'query',
Transform = 'transform',
Visualize = 'visualize',
Alert = 'alert',

View File

@ -245,7 +245,7 @@ describe('DashboardPage', () => {
editPanel: '1',
},
},
panelEditorNew: {},
panelEditor: {},
dashboard: {
getModel: () => ({} as DashboardModel),
},
@ -262,7 +262,7 @@ describe('DashboardPage', () => {
viewPanel: '2',
},
},
panelEditorNew: {},
panelEditor: {},
dashboard: {
getModel: () => ({} as DashboardModel),
},

View File

@ -56,7 +56,7 @@ export interface Props {
notifyApp: typeof notifyApp;
updateLocation: typeof updateLocation;
inspectTab?: InspectTab;
isNewEditorOpen?: boolean;
isPanelEditorOpen?: boolean;
}
export interface State {
@ -260,7 +260,7 @@ export class DashboardPage extends PureComponent<Props, State> {
isInitSlow,
initError,
inspectTab,
isNewEditorOpen,
isPanelEditorOpen,
updateLocation,
} = this.props;
@ -303,8 +303,8 @@ export class DashboardPage extends PureComponent<Props, State> {
dashboard={dashboard}
viewPanel={viewPanel}
editPanel={editPanel}
isNewEditorOpen={isNewEditorOpen}
scrollTop={approximateScrollTop}
isPanelEditorOpen={isPanelEditorOpen}
/>
</div>
</CustomScrollbar>
@ -333,7 +333,7 @@ export const mapStateToProps = (state: StoreState) => ({
initError: state.dashboard.initError,
dashboard: state.dashboard.getModel() as DashboardModel,
inspectTab: state.location.query.inspectTab,
isNewEditorOpen: state.panelEditorNew.isOpen,
isPanelEditorOpen: state.panelEditor.isOpen,
});
const mapDispatchToProps = {

View File

@ -97,7 +97,7 @@ export interface Props {
editPanel: PanelModel | null;
viewPanel: PanelModel | null;
scrollTop: number;
isNewEditorOpen?: boolean;
isPanelEditorOpen?: boolean;
}
export class DashboardGrid extends PureComponent<Props> {

View File

@ -159,7 +159,7 @@ export class PanelHeader extends Component<Props, State> {
className="panel-title-container"
onClick={this.onMenuToggle}
onMouseDown={this.onMouseDown}
aria-label={e2e.pages.Dashboard.Panels.Panel.selectors.title(title)}
aria-label={e2e.components.Panels.Panel.selectors.title(title)}
>
<div className="panel-title">
{Object.values(notices).map(this.renderNotice)}

View File

@ -30,10 +30,7 @@ export const PanelHeaderMenuItem: FC<Props & PanelMenuItem> = props => {
<li className={isSubMenu ? 'dropdown-submenu' : undefined}>
<a onClick={props.onClick} href={props.href}>
{props.iconClassName && <Icon name={props.iconClassName as IconName} className={menuIconClassName} />}
<span
className="dropdown-item-text"
aria-label={e2e.pages.Dashboard.Panels.Panel.selectors.headerItems(props.text)}
>
<span className="dropdown-item-text" aria-label={e2e.components.Panels.Panel.selectors.headerItems(props.text)}>
{props.text}
{isSubMenu && <Icon name="angle-right" className={shortcutIconClassName} />}
</span>

View File

@ -1,120 +0,0 @@
// Libraries
import React, { PureComponent } from 'react';
import { connect, MapStateToProps, MapDispatchToProps } from 'react-redux';
// Utils & Services
import { AngularComponent, getAngularLoader } from '@grafana/runtime';
// Types
import { PanelModel, DashboardModel } from '../state';
import { PanelPlugin, PanelPluginMeta } from '@grafana/data';
import { PanelCtrl } from 'app/plugins/sdk';
import { changePanelPlugin } from '../state/actions';
import { StoreState } from 'app/types';
interface OwnProps {
panel: PanelModel;
dashboard: DashboardModel;
plugin: PanelPlugin;
}
interface ConnectedProps {
angularPanelComponent: AngularComponent;
}
interface DispatchProps {
changePanelPlugin: typeof changePanelPlugin;
}
type Props = OwnProps & ConnectedProps & DispatchProps;
export class AngularPanelOptionsUnconnected extends PureComponent<Props> {
element?: HTMLElement;
angularOptions: AngularComponent;
constructor(props: Props) {
super(props);
}
componentDidMount() {
this.loadAngularOptions();
}
componentDidUpdate(prevProps: Props) {
if (this.props.plugin !== prevProps.plugin) {
this.cleanUpAngularOptions();
}
this.loadAngularOptions();
}
componentWillUnmount() {
this.cleanUpAngularOptions();
}
cleanUpAngularOptions() {
if (this.angularOptions) {
this.angularOptions.destroy();
this.angularOptions = null;
}
}
loadAngularOptions() {
const { panel, angularPanelComponent, changePanelPlugin } = this.props;
if (!this.element || !angularPanelComponent || this.angularOptions) {
return;
}
const scope = angularPanelComponent.getScope();
// When full page reloading in edit mode the angular panel has on fully compiled & instantiated yet
if (!scope.$$childHead) {
setTimeout(() => {
this.forceUpdate();
});
return;
}
const panelCtrl: PanelCtrl = scope.$$childHead.ctrl;
panelCtrl.initEditMode();
panelCtrl.onPluginTypeChange = (plugin: PanelPluginMeta) => {
changePanelPlugin(panel, plugin.id);
};
let template = '';
for (let i = 0; i < panelCtrl.editorTabs.length; i++) {
template +=
`
<div class="panel-options-group" ng-cloak>` +
(i > 0
? `<div class="panel-options-group__header">
<span class="panel-options-group__title">{{ctrl.editorTabs[${i}].title}}
</span>
</div>`
: '') +
`<div class="panel-options-group__body">
<panel-editor-tab editor-tab="ctrl.editorTabs[${i}]" ctrl="ctrl"></panel-editor-tab>
</div>
</div>
`;
}
const loader = getAngularLoader();
const scopeProps = { ctrl: panelCtrl };
this.angularOptions = loader.load(this.element, scopeProps, template);
}
render() {
return <div ref={elem => (this.element = elem)} />;
}
}
const mapStateToProps: MapStateToProps<ConnectedProps, OwnProps, StoreState> = (state, props) => {
return {
angularPanelComponent: state.dashboard.panels[props.panel.id].angularComponent,
};
};
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = { changePanelPlugin };
export const AngularPanelOptions = connect(mapStateToProps, mapDispatchToProps)(AngularPanelOptionsUnconnected);

View File

@ -1,77 +0,0 @@
// Libraries
import React, { PureComponent } from 'react';
// Components
import { getAngularLoader, AngularComponent } from '@grafana/runtime';
import { EditorTabBody } from './EditorTabBody';
import './../../panel/GeneralTabCtrl';
// Types
import { PanelModel } from '../state/PanelModel';
import { DataLink } from '@grafana/data';
import { PanelOptionsGroup, DataLinksEditor } from '@grafana/ui';
import { getPanelLinksVariableSuggestions } from 'app/features/panel/panellinks/link_srv';
interface Props {
panel: PanelModel;
}
export class GeneralTab extends PureComponent<Props> {
element: any;
component: AngularComponent;
constructor(props: Props) {
super(props);
}
componentDidMount() {
if (!this.element) {
return;
}
const { panel } = this.props;
const loader = getAngularLoader();
const template = '<panel-general-tab />';
const scopeProps = {
ctrl: {
panel: panel,
},
};
this.component = loader.load(this.element, scopeProps, template);
}
componentWillUnmount() {
if (this.component) {
this.component.destroy();
}
}
onDataLinksChanged = (links: DataLink[], callback?: () => void) => {
this.props.panel.links = links;
this.props.panel.render();
this.forceUpdate(callback);
};
render() {
const { panel } = this.props;
const suggestions = getPanelLinksVariableSuggestions();
return (
<EditorTabBody heading="General" toolbarItems={[]}>
<>
<div ref={element => (this.element = element)} />
<PanelOptionsGroup title="Panel links">
<DataLinksEditor
value={panel.links}
onChange={this.onDataLinksChanged}
suggestions={suggestions}
maxLinks={10}
/>
</PanelOptionsGroup>
</>
</EditorTabBody>
);
}
}

View File

@ -1,135 +0,0 @@
import React, { PureComponent } from 'react';
import classNames from 'classnames';
import { connect } from 'react-redux';
import { Tooltip } from '@grafana/ui';
import { PanelPlugin, PanelPluginMeta } from '@grafana/data';
import { config } from '@grafana/runtime';
import { e2e } from '@grafana/e2e';
import { QueriesTab } from './QueriesTab';
import VisualizationTab from './VisualizationTab';
import { GeneralTab } from './GeneralTab';
import { AlertTab } from '../../alerting/AlertTab';
import { PanelModel } from '../state/PanelModel';
import { DashboardModel } from '../state/DashboardModel';
import { StoreState } from '../../../types';
import { panelEditorCleanUp, PanelEditorTab, PanelEditorTabIds } from './state/reducers';
import { changePanelEditorTab, refreshPanelEditor } from './state/actions';
import { changePanelPlugin } from '../state/actions';
import { getActiveTabAndTabs } from './state/selectors';
interface PanelEditorProps {
panel: PanelModel;
dashboard: DashboardModel;
plugin: PanelPlugin;
activeTab: PanelEditorTabIds;
tabs: PanelEditorTab[];
refreshPanelEditor: typeof refreshPanelEditor;
panelEditorCleanUp: typeof panelEditorCleanUp;
changePanelEditorTab: typeof changePanelEditorTab;
changePanelPlugin: typeof changePanelPlugin;
}
class UnConnectedPanelEditor extends PureComponent<PanelEditorProps> {
constructor(props: PanelEditorProps) {
super(props);
}
componentDidMount(): void {
this.refreshFromState();
}
componentWillUnmount(): void {
const { panelEditorCleanUp } = this.props;
panelEditorCleanUp();
}
refreshFromState = (meta?: PanelPluginMeta) => {
const { refreshPanelEditor, plugin } = this.props;
meta = meta || plugin.meta;
refreshPanelEditor({
hasQueriesTab: !meta.skipDataQuery,
usesGraphPlugin: meta.id === 'graph',
alertingEnabled: config.alertingEnabled,
});
};
onChangeTab = (tab: PanelEditorTab) => {
const { changePanelEditorTab } = this.props;
// Angular Query Components can potentially refresh the PanelModel
// onBlur so this makes sure we change tab after that
setTimeout(() => changePanelEditorTab(tab), 10);
};
onPluginTypeChange = (newType: PanelPluginMeta) => {
this.props.changePanelPlugin(this.props.panel, newType.id);
this.refreshFromState(newType);
};
renderCurrentTab(activeTab: string) {
const { panel, dashboard, plugin } = this.props;
switch (activeTab) {
case 'advanced':
return <GeneralTab panel={panel} />;
case 'queries':
return <QueriesTab panel={panel} dashboard={dashboard} />;
case 'alert':
return <AlertTab dashboard={dashboard} panel={panel} />;
case 'visualization':
return (
<VisualizationTab
panel={panel}
dashboard={dashboard}
plugin={plugin}
onPluginTypeChange={this.onPluginTypeChange}
/>
);
default:
return null;
}
}
render() {
const { activeTab, tabs } = this.props;
return (
<div className="panel-editor-container__editor">
<div className="panel-editor-tabs">
{tabs.map(tab => {
return <TabItem tab={tab} activeTab={activeTab} onClick={this.onChangeTab} key={tab.id} />;
})}
</div>
<div className="panel-editor__right">{this.renderCurrentTab(activeTab)}</div>
</div>
);
}
}
const mapStateToProps = (state: StoreState) => getActiveTabAndTabs(state.location, state.panelEditor);
const mapDispatchToProps = { refreshPanelEditor, panelEditorCleanUp, changePanelEditorTab, changePanelPlugin };
export const PanelEditor = connect(mapStateToProps, mapDispatchToProps)(UnConnectedPanelEditor);
interface TabItemParams {
tab: PanelEditorTab;
activeTab: string;
onClick: (tab: PanelEditorTab) => void;
}
function TabItem({ tab, activeTab, onClick }: TabItemParams) {
const tabClasses = classNames({
'panel-editor-tabs__link': true,
active: activeTab === tab.id,
});
return (
<div className="panel-editor-tabs__item" onClick={() => onClick(tab)}>
<a className={tabClasses} aria-label={e2e.pages.Dashboard.Panels.EditPanel.selectors.tabItems(tab.text)}>
<Tooltip content={`${tab.text}`} placement="auto">
<i className={`gicon gicon-${tab.id}${activeTab === tab.id ? '-active' : ''}`} />
</Tooltip>
</a>
</div>
);
}

View File

@ -1,233 +0,0 @@
// Libraries
import React, { PureComponent } from 'react';
// Utils & Services
import { connect } from 'react-redux';
import { StoreState } from 'app/types';
import { updateLocation } from 'app/core/actions';
// Components
import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
import { VizTypePicker } from './VizTypePicker';
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
import { FadeIn } from 'app/core/components/Animations/FadeIn';
import { AngularPanelOptions } from './AngularPanelOptions';
// Types
import { PanelModel, DashboardModel } from '../state';
import { VizPickerSearch } from './VizPickerSearch';
import PluginStateinfo from 'app/features/plugins/PluginStateInfo';
import { Unsubscribable } from 'rxjs';
import { Icon } from '@grafana/ui';
import {
PanelPlugin,
PanelPluginMeta,
PanelData,
LoadingState,
DefaultTimeRange,
FieldConfigSource,
} from '@grafana/data';
interface Props {
panel: PanelModel;
dashboard: DashboardModel;
plugin: PanelPlugin;
onPluginTypeChange: (newType: PanelPluginMeta) => void;
updateLocation: typeof updateLocation;
urlOpenVizPicker: boolean;
}
interface State {
isVizPickerOpen: boolean;
searchQuery: string;
scrollTop: number;
hasBeenFocused: boolean;
data: PanelData;
}
export class VisualizationTab extends PureComponent<Props, State> {
element: HTMLElement;
querySubscription: Unsubscribable;
constructor(props: Props) {
super(props);
this.state = {
isVizPickerOpen: this.props.urlOpenVizPicker,
hasBeenFocused: false,
searchQuery: '',
scrollTop: 0,
data: {
state: LoadingState.NotStarted,
series: [],
timeRange: DefaultTimeRange,
},
};
}
getReactPanelOptions = () => {
const { panel } = this.props;
return panel.getOptions();
};
getReactPanelFieldConfig = () => {
const { panel } = this.props;
return panel.getFieldConfig();
};
renderPanelOptions() {
const { plugin, dashboard, panel } = this.props;
if (plugin.angularPanelCtrl) {
return <AngularPanelOptions plugin={plugin} dashboard={dashboard} panel={panel} />;
}
if (plugin.editor) {
return (
<plugin.editor
data={this.state.data}
options={this.getReactPanelOptions()}
onOptionsChange={this.onPanelOptionsChanged}
// TODO[FieldConfig]: Remove when we switch old editor to new
fieldConfig={this.getReactPanelFieldConfig()}
// TODO[FieldConfig]: Remove when we switch old editor to new
onFieldConfigChange={this.onPanelFieldConfigChange}
/>
);
}
return <p>Visualization has no options</p>;
}
componentDidMount() {
const { panel } = this.props;
const queryRunner = panel.getQueryRunner();
this.querySubscription = queryRunner.getData().subscribe({
next: (data: PanelData) => this.setState({ data }),
});
}
componentWillUnmount() {
if (this.querySubscription) {
this.querySubscription.unsubscribe();
}
}
clearQuery = () => {
this.setState({ searchQuery: '' });
};
onPanelOptionsChanged = (options: any, callback?: () => void) => {
this.props.panel.updateOptions(options);
this.forceUpdate(callback);
};
// TODO[FieldConfig]: Remove when we switch old editor to new
onPanelFieldConfigChange = (config: FieldConfigSource, callback?: () => void) => {
this.props.panel.updateFieldConfig(config);
this.forceUpdate(callback);
};
onOpenVizPicker = () => {
this.setState({ isVizPickerOpen: true, scrollTop: 0 });
};
onCloseVizPicker = () => {
if (this.props.urlOpenVizPicker) {
this.props.updateLocation({ query: { openVizPicker: null }, partial: true });
}
this.setState({ isVizPickerOpen: false, hasBeenFocused: false });
};
onSearchQueryChange = (value: string) => {
this.setState({
searchQuery: value,
});
};
renderToolbar = (): JSX.Element => {
const { plugin } = this.props;
const { isVizPickerOpen, searchQuery } = this.state;
const { meta } = plugin;
if (isVizPickerOpen) {
return (
<VizPickerSearch
plugin={meta}
searchQuery={searchQuery}
onChange={this.onSearchQueryChange}
onClose={this.onCloseVizPicker}
/>
);
} else {
return (
<>
<div className="toolbar__main" onClick={this.onOpenVizPicker}>
<img className="toolbar__main-image" src={meta.info.logos.small} />
<div className="toolbar__main-name">{meta.name}</div>
<Icon name="angle-down" style={{ marginLeft: '4px', marginBottom: 0 }} />
</div>
<PluginStateinfo state={meta.state} />
</>
);
}
};
onPluginTypeChange = (plugin: PanelPluginMeta) => {
if (plugin.id === this.props.plugin.meta.id) {
this.setState({ isVizPickerOpen: false });
} else {
this.props.onPluginTypeChange(plugin);
}
};
renderHelp = () => <PluginHelp plugin={this.props.plugin.meta} type="help" />;
setScrollTop = (event: React.MouseEvent<HTMLElement>) => {
const target = event.target as HTMLElement;
this.setState({ scrollTop: target.scrollTop });
};
render() {
const { plugin } = this.props;
const { isVizPickerOpen, searchQuery, scrollTop } = this.state;
const { meta } = plugin;
const pluginHelp: EditorToolbarView = {
heading: 'Help',
icon: 'question-circle',
render: this.renderHelp,
};
return (
<EditorTabBody
heading="Visualization"
renderToolbar={this.renderToolbar}
toolbarItems={[pluginHelp]}
scrollTop={scrollTop}
setScrollTop={this.setScrollTop}
>
<>
<FadeIn in={isVizPickerOpen} duration={200} unmountOnExit={true} onExited={this.clearQuery}>
<VizTypePicker
current={meta}
onTypeChange={this.onPluginTypeChange}
searchQuery={searchQuery}
onClose={this.onCloseVizPicker}
/>
</FadeIn>
{this.renderPanelOptions()}
</>
</EditorTabBody>
);
}
}
const mapStateToProps = (state: StoreState) => ({
urlOpenVizPicker: !!state.location.query.openVizPicker,
});
const mapDispatchToProps = {
updateLocation,
};
export default connect(mapStateToProps, mapDispatchToProps)(VisualizationTab);

View File

@ -1,26 +0,0 @@
import React, { PureComponent } from 'react';
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
import { PanelPluginMeta } from '@grafana/data';
import { Icon } from '@grafana/ui';
interface Props {
plugin: PanelPluginMeta;
searchQuery: string;
onChange: (query: string) => void;
onClose: () => void;
}
export class VizPickerSearch extends PureComponent<Props> {
render() {
const { searchQuery, onChange, onClose } = this.props;
return (
<>
<FilterInput placeholder="" onChange={onChange} value={searchQuery} />
<button className="btn btn-link toolbar__close" onClick={onClose}>
<Icon name="angle-up" />
</button>
</>
);
}
}

View File

@ -1,127 +0,0 @@
import { thunkTester } from '../../../../../test/core/thunk/thunkTester';
import { getPanelEditorTab, initialState, panelEditorInitCompleted, PanelEditorTabIds } from './reducers';
import { changePanelEditorTab, refreshPanelEditor } from './actions';
import { updateLocation } from '../../../../core/actions';
describe('refreshPanelEditor', () => {
describe('when called and there is no activeTab in state', () => {
it('then the dispatched action should default the activeTab to PanelEditorTabIds.Queries', async () => {
const activeTab = PanelEditorTabIds.Queries;
const tabs = [
getPanelEditorTab(PanelEditorTabIds.Queries),
getPanelEditorTab(PanelEditorTabIds.Visualization),
getPanelEditorTab(PanelEditorTabIds.Advanced),
getPanelEditorTab(PanelEditorTabIds.Alert),
];
const dispatchedActions = await thunkTester({ panelEditor: { ...initialState, activeTab: null } })
.givenThunk(refreshPanelEditor)
.whenThunkIsDispatched({ hasQueriesTab: true, alertingEnabled: true, usesGraphPlugin: true });
expect(dispatchedActions.length).toBe(1);
expect(dispatchedActions[0]).toEqual(panelEditorInitCompleted({ activeTab, tabs }));
});
});
describe('when called and there is already an activeTab in state', () => {
it('then the dispatched action should include activeTab from state', async () => {
const activeTab = PanelEditorTabIds.Visualization;
const tabs = [
getPanelEditorTab(PanelEditorTabIds.Queries),
getPanelEditorTab(PanelEditorTabIds.Visualization),
getPanelEditorTab(PanelEditorTabIds.Advanced),
getPanelEditorTab(PanelEditorTabIds.Alert),
];
const dispatchedActions = await thunkTester({ panelEditor: { ...initialState, activeTab } })
.givenThunk(refreshPanelEditor)
.whenThunkIsDispatched({ hasQueriesTab: true, alertingEnabled: true, usesGraphPlugin: true });
expect(dispatchedActions.length).toBe(1);
expect(dispatchedActions[0]).toEqual(panelEditorInitCompleted({ activeTab, tabs }));
});
});
describe('when called and plugin has no queries tab', () => {
it('then the dispatched action should not include Queries tab and default the activeTab to PanelEditorTabIds.Visualization', async () => {
const activeTab = PanelEditorTabIds.Visualization;
const tabs = [
getPanelEditorTab(PanelEditorTabIds.Visualization),
getPanelEditorTab(PanelEditorTabIds.Advanced),
getPanelEditorTab(PanelEditorTabIds.Alert),
];
const dispatchedActions = await thunkTester({ panelEditor: { ...initialState } })
.givenThunk(refreshPanelEditor)
.whenThunkIsDispatched({ hasQueriesTab: false, alertingEnabled: true, usesGraphPlugin: true });
expect(dispatchedActions.length).toBe(1);
expect(dispatchedActions[0]).toEqual(panelEditorInitCompleted({ activeTab, tabs }));
});
});
describe('when called and alerting is enabled and the visualization is the graph plugin', () => {
it('then the dispatched action should include the alert tab', async () => {
const activeTab = PanelEditorTabIds.Queries;
const tabs = [
getPanelEditorTab(PanelEditorTabIds.Queries),
getPanelEditorTab(PanelEditorTabIds.Visualization),
getPanelEditorTab(PanelEditorTabIds.Advanced),
getPanelEditorTab(PanelEditorTabIds.Alert),
];
const dispatchedActions = await thunkTester({ panelEditor: { ...initialState } })
.givenThunk(refreshPanelEditor)
.whenThunkIsDispatched({ hasQueriesTab: true, alertingEnabled: true, usesGraphPlugin: true });
expect(dispatchedActions.length).toBe(1);
expect(dispatchedActions[0]).toEqual(panelEditorInitCompleted({ activeTab, tabs }));
});
});
describe('when called and alerting is not enabled', () => {
it('then the dispatched action should not include the alert tab', async () => {
const activeTab = PanelEditorTabIds.Queries;
const tabs = [
getPanelEditorTab(PanelEditorTabIds.Queries),
getPanelEditorTab(PanelEditorTabIds.Visualization),
getPanelEditorTab(PanelEditorTabIds.Advanced),
];
const dispatchedActions = await thunkTester({ panelEditor: { ...initialState } })
.givenThunk(refreshPanelEditor)
.whenThunkIsDispatched({ hasQueriesTab: true, alertingEnabled: false, usesGraphPlugin: true });
expect(dispatchedActions.length).toBe(1);
expect(dispatchedActions[0]).toEqual(panelEditorInitCompleted({ activeTab, tabs }));
});
});
describe('when called and the visualization is not the graph plugin', () => {
it('then the dispatched action should not include the alert tab', async () => {
const activeTab = PanelEditorTabIds.Queries;
const tabs = [
getPanelEditorTab(PanelEditorTabIds.Queries),
getPanelEditorTab(PanelEditorTabIds.Visualization),
getPanelEditorTab(PanelEditorTabIds.Advanced),
];
const dispatchedActions = await thunkTester({ panelEditor: { ...initialState } })
.givenThunk(refreshPanelEditor)
.whenThunkIsDispatched({ hasQueriesTab: true, alertingEnabled: true, usesGraphPlugin: false });
expect(dispatchedActions.length).toBe(1);
expect(dispatchedActions[0]).toEqual(panelEditorInitCompleted({ activeTab, tabs }));
});
});
});
describe('changePanelEditorTab', () => {
describe('when called', () => {
it('then it should dispatch correct actions', async () => {
const activeTab = getPanelEditorTab(PanelEditorTabIds.Visualization);
const dispatchedActions = await thunkTester({})
.givenThunk(changePanelEditorTab)
.whenThunkIsDispatched(activeTab);
expect(dispatchedActions.length).toBe(1);
expect(dispatchedActions).toEqual([
updateLocation({ query: { tab: activeTab.id, openVizPicker: null }, partial: true }),
]);
});
});
});

View File

@ -1,42 +0,0 @@
import { getPanelEditorTab, panelEditorInitCompleted, PanelEditorTab, PanelEditorTabIds } from './reducers';
import { ThunkResult } from '../../../../types';
import { updateLocation } from '../../../../core/actions';
export const refreshPanelEditor = (props: {
hasQueriesTab?: boolean;
usesGraphPlugin?: boolean;
alertingEnabled?: boolean;
}): ThunkResult<void> => {
return async (dispatch, getState) => {
let activeTab = getState().panelEditor.activeTab || PanelEditorTabIds.Queries;
const { hasQueriesTab, usesGraphPlugin, alertingEnabled } = props;
const tabs: PanelEditorTab[] = [
getPanelEditorTab(PanelEditorTabIds.Queries),
getPanelEditorTab(PanelEditorTabIds.Visualization),
getPanelEditorTab(PanelEditorTabIds.Advanced),
];
// handle panels that do not have queries tab
if (!hasQueriesTab) {
// remove queries tab
tabs.shift();
// switch tab
if (activeTab === PanelEditorTabIds.Queries) {
activeTab = PanelEditorTabIds.Visualization;
}
}
if (alertingEnabled && usesGraphPlugin) {
tabs.push(getPanelEditorTab(PanelEditorTabIds.Alert));
}
dispatch(panelEditorInitCompleted({ activeTab, tabs }));
};
};
export const changePanelEditorTab = (activeTab: PanelEditorTab): ThunkResult<void> => {
return async dispatch => {
dispatch(updateLocation({ query: { tab: activeTab.id, openVizPicker: null }, partial: true }));
};
};

View File

@ -1,43 +0,0 @@
import { reducerTester } from '../../../../../test/core/redux/reducerTester';
import {
getPanelEditorTab,
initialState,
panelEditorCleanUp,
panelEditorInitCompleted,
panelEditorReducer,
PanelEditorState,
PanelEditorTab,
PanelEditorTabIds,
} from './reducers';
describe('panelEditorReducer', () => {
describe('when panelEditorInitCompleted is dispatched', () => {
it('then state should be correct', () => {
const activeTab = PanelEditorTabIds.Alert;
const tabs: PanelEditorTab[] = [
getPanelEditorTab(PanelEditorTabIds.Queries),
getPanelEditorTab(PanelEditorTabIds.Visualization),
getPanelEditorTab(PanelEditorTabIds.Advanced),
];
reducerTester<PanelEditorState>()
.givenReducer(panelEditorReducer, initialState)
.whenActionIsDispatched(panelEditorInitCompleted({ activeTab, tabs }))
.thenStateShouldEqual({ activeTab, tabs });
});
});
describe('when panelEditorCleanUp is dispatched', () => {
it('then state should be intialState', () => {
const activeTab = PanelEditorTabIds.Alert;
const tabs: PanelEditorTab[] = [
getPanelEditorTab(PanelEditorTabIds.Queries),
getPanelEditorTab(PanelEditorTabIds.Visualization),
getPanelEditorTab(PanelEditorTabIds.Advanced),
];
reducerTester<PanelEditorState>()
.givenReducer(panelEditorReducer, { activeTab, tabs })
.whenActionIsDispatched(panelEditorCleanUp())
.thenStateShouldEqual(initialState);
});
});
});

View File

@ -1,62 +0,0 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export interface PanelEditorInitCompleted {
activeTab: PanelEditorTabIds;
tabs: PanelEditorTab[];
}
export interface PanelEditorTab {
id: string;
text: string;
}
export enum PanelEditorTabIds {
Queries = 'queries',
Visualization = 'visualization',
Advanced = 'advanced',
Alert = 'alert',
}
export const panelEditorTabTexts = {
[PanelEditorTabIds.Queries]: 'Queries',
[PanelEditorTabIds.Visualization]: 'Visualization',
[PanelEditorTabIds.Advanced]: 'General',
[PanelEditorTabIds.Alert]: 'Alert',
};
export const getPanelEditorTab = (tabId: PanelEditorTabIds): PanelEditorTab => {
return {
id: tabId,
text: panelEditorTabTexts[tabId],
};
};
export interface PanelEditorState {
activeTab: PanelEditorTabIds;
tabs: PanelEditorTab[];
}
export const initialState: PanelEditorState = {
activeTab: null,
tabs: [],
};
const panelEditorSlice = createSlice({
name: 'panelEditor',
initialState,
reducers: {
panelEditorInitCompleted: (state, action: PayloadAction<PanelEditorInitCompleted>): PanelEditorState => {
const { activeTab, tabs } = action.payload;
return {
...state,
activeTab,
tabs,
};
},
panelEditorCleanUp: (state, action: PayloadAction<undefined>): PanelEditorState => initialState,
},
});
export const { panelEditorCleanUp, panelEditorInitCompleted } = panelEditorSlice.actions;
export const panelEditorReducer = panelEditorSlice.reducer;

View File

@ -1,88 +0,0 @@
import { getActiveTabAndTabs } from './selectors';
import { LocationState } from '../../../../types';
import { getPanelEditorTab, PanelEditorState, PanelEditorTab, PanelEditorTabIds } from './reducers';
describe('getActiveTabAndTabs', () => {
describe('when called and location state contains tab', () => {
it('then it should return location state', () => {
const activeTabId = 1337;
const location: LocationState = {
path: 'a path',
lastUpdated: 1,
replace: false,
routeParams: {},
query: {
tab: activeTabId,
},
url: 'an url',
};
const panelEditor: PanelEditorState = {
activeTab: PanelEditorTabIds.Queries,
tabs: [],
};
const result = getActiveTabAndTabs(location, panelEditor);
expect(result).toEqual({
activeTab: activeTabId,
tabs: [],
});
});
});
describe('when called without location state and PanelEditor state contains tabs', () => {
it('then it should return the id for the first tab in PanelEditor state', () => {
const activeTabId = PanelEditorTabIds.Visualization;
const tabs = [getPanelEditorTab(PanelEditorTabIds.Visualization), getPanelEditorTab(PanelEditorTabIds.Advanced)];
const location: LocationState = {
path: 'a path',
lastUpdated: 1,
replace: false,
routeParams: {},
query: {
tab: undefined,
},
url: 'an url',
};
const panelEditor: PanelEditorState = {
activeTab: PanelEditorTabIds.Advanced,
tabs,
};
const result = getActiveTabAndTabs(location, panelEditor);
expect(result).toEqual({
activeTab: activeTabId,
tabs,
});
});
});
describe('when called without location state and PanelEditor state does not contain tabs', () => {
it('then it should return PanelEditorTabIds.Queries', () => {
const activeTabId = PanelEditorTabIds.Queries;
const tabs: PanelEditorTab[] = [];
const location: LocationState = {
path: 'a path',
lastUpdated: 1,
replace: false,
routeParams: {},
query: {
tab: undefined,
},
url: 'an url',
};
const panelEditor: PanelEditorState = {
activeTab: PanelEditorTabIds.Advanced,
tabs,
};
const result = getActiveTabAndTabs(location, panelEditor);
expect(result).toEqual({
activeTab: activeTabId,
tabs,
});
});
});
});

View File

@ -1,11 +0,0 @@
import memoizeOne from 'memoize-one';
import { LocationState } from '../../../../types';
import { PanelEditorState, PanelEditorTabIds } from './reducers';
export const getActiveTabAndTabs = memoizeOne((location: LocationState, panelEditor: PanelEditorState) => {
const panelEditorTab = panelEditor.tabs.length > 0 ? panelEditor.tabs[0].id : PanelEditorTabIds.Queries;
return {
activeTab: location.query.tab || panelEditorTab,
tabs: panelEditor.tabs,
};
});

View File

@ -10,8 +10,7 @@ import {
import { AngularComponent } from '@grafana/runtime';
import { EDIT_PANEL_ID } from 'app/core/constants';
import { processAclItems } from 'app/core/utils/acl';
import { panelEditorReducer } from '../panel_editor/state/reducers';
import { panelEditorReducerNew } from '../components/PanelEditor/state/reducers';
import { panelEditorReducer } from '../components/PanelEditor/state/reducers';
import { DashboardModel } from './DashboardModel';
import { PanelModel } from './PanelModel';
import { PanelPlugin } from '@grafana/data';
@ -131,5 +130,4 @@ export const dashboardReducer = dashbardSlice.reducer;
export default {
dashboard: dashboardReducer,
panelEditor: panelEditorReducer,
panelEditorNew: panelEditorReducerNew,
};

View File

@ -34,7 +34,7 @@ export class TestDataQueryCtrl extends QueryCtrl {
digest: (promise: Promise<any>) => Promise<any>;
showLabels = false;
selectors: typeof e2e.pages.Dashboard.Panels.DataSource.TestData.QueryTab.selectors;
selectors: typeof e2e.components.DataSource.TestData.QueryTab.selectors;
/** @ngInject */
constructor($scope: IScope, $injector: any) {
@ -45,7 +45,7 @@ export class TestDataQueryCtrl extends QueryCtrl {
this.newPointTime = dateTime();
this.selectedPoint = { text: 'Select point', value: null };
this.showLabels = showLabelsFor.includes(this.target.scenarioId);
this.selectors = e2e.pages.Dashboard.Panels.DataSource.TestData.QueryTab.selectors;
this.selectors = e2e.components.DataSource.TestData.QueryTab.selectors;
}
getPoints() {

View File

@ -2,6 +2,7 @@ import React, { PureComponent } from 'react';
import classNames from 'classnames';
import { TimeSeries } from 'app/core/core';
import { SeriesColorPicker, Icon } from '@grafana/ui';
import { e2e } from '@grafana/e2e';
export const LEGEND_STATS = ['min', 'max', 'avg', 'current', 'total'];
@ -137,7 +138,13 @@ class LegendSeriesLabel extends PureComponent<LegendSeriesLabelProps & LegendSer
onColorChange={onColorChange}
onToggleAxis={onToggleAxis}
/>,
<a className="graph-legend-alias pointer" title={label} key="label" onClick={e => this.props.onLabelClick(e)}>
<a
className="graph-legend-alias pointer"
title={label}
key="label"
onClick={e => this.props.onLabelClick(e)}
aria-label={e2e.components.Panels.Visualization.Graph.Legend.selectors.legendItemAlias(label)}
>
{label}
</a>,
];

View File

@ -8,7 +8,7 @@ export class AxesEditorCtrl {
xAxisModes: any;
xAxisStatOptions: any;
xNameSegment: any;
selectors: typeof e2e.pages.Dashboard.Panels.Visualization.Graph.VisualizationTab.selectors;
selectors: typeof e2e.components.Panels.Visualization.Graph.VisualizationTab.selectors;
/** @ngInject */
constructor(private $scope: any) {
@ -45,7 +45,7 @@ export class AxesEditorCtrl {
this.panel.xaxis.name = 'specify field';
}
}
this.selectors = e2e.pages.Dashboard.Panels.Visualization.Graph.VisualizationTab.selectors;
this.selectors = e2e.components.Panels.Visualization.Graph.VisualizationTab.selectors;
}
setUnitFormat(axis: { format: any }) {

View File

@ -168,10 +168,12 @@ class GraphCtrl extends MetricsPanelCtrl {
}
onInitEditMode() {
this.addEditorTab('Display options', 'public/app/plugins/panel/graph/tab_display.html');
this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html');
this.addEditorTab('Series overides', 'public/app/plugins/panel/graph/tab_series_overrides.html');
this.addEditorTab('Axes', axesEditorComponent);
this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html');
this.addEditorTab('Thresholds & Time Regions', 'public/app/plugins/panel/graph/tab_thresholds_time_regions.html');
this.addEditorTab('Thresholds', 'public/app/plugins/panel/graph/tab_thresholds.html');
this.addEditorTab('Time regions', 'public/app/plugins/panel/graph/time_regions.html');
this.addEditorTab('Data links', 'public/app/plugins/panel/graph/tab_drilldown_links.html');
this.subTabIndex = 0;
this.hiddenSeriesTainted = false;

View File

@ -1,205 +1,151 @@
<div class="editor-row">
<div class="section gf-form-group">
<h5 class="section-heading">Draw Modes</h5>
<gf-form-switch
class="gf-form"
label="Bars"
label-class="width-5"
checked="ctrl.panel.bars"
on-change="ctrl.render()"
></gf-form-switch>
<gf-form-switch
class="gf-form"
label="Lines"
label-class="width-5"
checked="ctrl.panel.lines"
on-change="ctrl.render()"
></gf-form-switch>
<gf-form-switch
class="gf-form"
label="Points"
label-class="width-5"
checked="ctrl.panel.points"
on-change="ctrl.render()"
></gf-form-switch>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Mode Options</h5>
<div class="gf-form">
<label class="gf-form-label width-8">Fill</label>
<div class="gf-form-select-wrapper max-width-5">
<select
class="gf-form-input"
ng-model="ctrl.panel.fill"
ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]"
ng-change="ctrl.render()"
ng-disabled="!ctrl.panel.lines"
></select>
</div>
</div>
<div class="gf-form" ng-if="ctrl.panel.lines && ctrl.panel.fill">
<label class="gf-form-label width-8">Fill Gradient</label>
<div class="gf-form-select-wrapper max-width-5">
<select
class="gf-form-input"
ng-model="ctrl.panel.fillGradient"
ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]"
ng-change="ctrl.render()"
></select>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label width-8">Line Width</label>
<div class="gf-form-select-wrapper max-width-5">
<select
class="gf-form-input"
ng-model="ctrl.panel.linewidth"
ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]"
ng-change="ctrl.render()"
ng-disabled="!ctrl.panel.lines"
></select>
</div>
</div>
<gf-form-switch
ng-disabled="!ctrl.panel.lines"
class="gf-form"
label="Staircase"
label-class="width-8"
checked="ctrl.panel.steppedLine"
on-change="ctrl.render()"
>
</gf-form-switch>
<div class="gf-form" ng-if="ctrl.panel.points">
<label class="gf-form-label width-8">Point Radius</label>
<div class="gf-form-select-wrapper max-width-5">
<select
class="gf-form-input"
ng-model="ctrl.panel.pointradius"
ng-options="f for f in [0.5,1,2,3,4,5,6,7,8,9,10]"
ng-change="ctrl.render()"
></select>
</div>
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Hover tooltip</h5>
<div class="gf-form">
<label class="gf-form-label width-9">Mode</label>
<div class="gf-form-select-wrapper max-width-8">
<select
class="gf-form-input"
ng-model="ctrl.panel.tooltip.shared"
ng-options="f.value as f.text for f in [{text: 'All series', value: true}, {text: 'Single', value: false}]"
ng-change="ctrl.render()"
></select>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label width-9">Sort order</label>
<div class="gf-form-select-wrapper max-width-8">
<select
class="gf-form-input"
ng-model="ctrl.panel.tooltip.sort"
ng-options="f.value as f.text for f in [{text: 'None', value: 0}, {text: 'Increasing', value: 1}, {text: 'Decreasing', value: 2}]"
ng-change="ctrl.render()"
></select>
</div>
</div>
<div class="gf-form" ng-show="ctrl.panel.stack">
<label class="gf-form-label width-9">Stacked value</label>
<div class="gf-form-select-wrapper max-width-8">
<select
class="gf-form-input"
ng-model="ctrl.panel.tooltip.value_type"
ng-options="f for f in ['cumulative','individual']"
ng-change="ctrl.render()"
></select>
</div>
<div class="gf-form-group">
<gf-form-switch
class="gf-form"
label="Bars"
label-class="width-8"
checked="ctrl.panel.bars"
on-change="ctrl.render()"
></gf-form-switch>
<gf-form-switch
class="gf-form"
label="Lines"
label-class="width-8"
checked="ctrl.panel.lines"
on-change="ctrl.render()"
></gf-form-switch>
<div class="gf-form" ng-if="ctrl.panel.lines">
<label class="gf-form-label width-8">Line width</label>
<div class="gf-form-select-wrapper max-width-5">
<select
class="gf-form-input"
ng-model="ctrl.panel.linewidth"
ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]"
ng-change="ctrl.render()"
></select>
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Stacking & Null value</h5>
<gf-form-switch
class="gf-form"
label="Stack"
label-class="width-7"
checked="ctrl.panel.stack"
on-change="ctrl.render()"
>
</gf-form-switch>
<gf-form-switch
class="gf-form"
ng-show="ctrl.panel.stack"
label="Percent"
label-class="width-7"
checked="ctrl.panel.percentage"
on-change="ctrl.render()"
>
</gf-form-switch>
<div class="gf-form">
<label class="gf-form-label width-7">Null value</label>
<div class="gf-form-select-wrapper">
<select
class="gf-form-input max-width-9"
ng-model="ctrl.panel.nullPointMode"
ng-options="f for f in ['connected', 'null', 'null as zero']"
ng-change="ctrl.render()"
></select>
</div>
<gf-form-switch
ng-disabled="!ctrl.panel.lines"
class="gf-form"
label="Staircase"
label-class="width-8"
checked="ctrl.panel.steppedLine"
on-change="ctrl.render()"
></gf-form-switch>
<div class="gf-form" ng-if="ctrl.panel.lines">
<label class="gf-form-label width-8">Area fill</label>
<div class="gf-form-select-wrapper max-width-5">
<select
class="gf-form-input"
ng-model="ctrl.panel.fill"
ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]"
ng-change="ctrl.render()"
></select>
</div>
</div>
<div class="gf-form" ng-if="ctrl.panel.lines && ctrl.panel.fill">
<label class="gf-form-label width-8">Fill gradient</label>
<div class="gf-form-select-wrapper max-width-5">
<select
class="gf-form-input"
ng-model="ctrl.panel.fillGradient"
ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]"
ng-change="ctrl.render()"
></select>
</div>
</div>
<gf-form-switch
class="gf-form"
label="Points"
label-class="width-8"
checked="ctrl.panel.points"
on-change="ctrl.render()"
></gf-form-switch>
<div class="gf-form" ng-if="ctrl.panel.points">
<label class="gf-form-label width-8">Point Radius</label>
<div class="gf-form-select-wrapper max-width-5">
<select
class="gf-form-input"
ng-model="ctrl.panel.pointradius"
ng-options="f for f in [0.5,1,2,3,4,5,6,7,8,9,10]"
ng-change="ctrl.render()"
></select>
</div>
</div>
</div>
<div>
<div class="gf-form-inline" ng-repeat="override in ctrl.panel.seriesOverrides" ng-controller="SeriesOverridesCtrl">
<div class="gf-form">
<label class="gf-form-label">alias or regex</label>
</div>
<div class="gf-form width-15">
<input
type="text"
ng-model="override.alias"
bs-typeahead="getSeriesNames"
ng-blur="ctrl.render()"
data-min-length="0"
data-items="100"
class="gf-form-input width-15"
/>
</div>
<div class="gf-form" ng-repeat="option in currentOverrides">
<label class="gf-form-label">
<icon name="'times'" size="'sm'" ng-click="removeOverride(option)" style="margin-right: 4px;"></icon>
<span ng-show="option.propertyName === 'color'">
Color: <icon name="'circle'" type="'mono'" ng-style="{color:option.value}"></icon>
</span>
<span ng-show="option.propertyName !== 'color'"> {{ option.name }}: {{ option.value }} </span>
</label>
</div>
<div class="gf-form">
<span
class="dropdown"
dropdown-typeahead2="overrideMenu"
dropdown-typeahead-on-select="setOverride($item, $subItem)"
button-template-class="gf-form-label"
>
</span>
</div>
<div class="gf-form gf-form--grow">
<div class="gf-form-label gf-form-label--grow"></div>
</div>
<div class="gf-form">
<label class="gf-form-label">
<icon name="'trash-alt'" ng-click="ctrl.removeSeriesOverride(override)"></icon>
</label>
<div class="gf-form-group">
<h5 class="section-heading">Hover tooltip</h5>
<div class="gf-form">
<label class="gf-form-label width-9">Mode</label>
<div class="gf-form-select-wrapper max-width-8">
<select
class="gf-form-input"
ng-model="ctrl.panel.tooltip.shared"
ng-options="f.value as f.text for f in [{text: 'All series', value: true}, {text: 'Single', value: false}]"
ng-change="ctrl.render()"
></select>
</div>
</div>
<div class="gf-form-button-row">
<button class="btn btn-inverse" ng-click="ctrl.addSeriesOverride()">
<icon name="'plus'"></icon>&nbsp;Add series override<tip>Regex match example: /server[0-3]/i </tip>
</button>
<div class="gf-form">
<label class="gf-form-label width-9">Sort order</label>
<div class="gf-form-select-wrapper max-width-8">
<select
class="gf-form-input"
ng-model="ctrl.panel.tooltip.sort"
ng-options="f.value as f.text for f in [{text: 'None', value: 0}, {text: 'Increasing', value: 1}, {text: 'Decreasing', value: 2}]"
ng-change="ctrl.render()"
></select>
</div>
</div>
<div class="gf-form" ng-show="ctrl.panel.stack">
<label class="gf-form-label width-9">Stacked value</label>
<div class="gf-form-select-wrapper max-width-8">
<select
class="gf-form-input"
ng-model="ctrl.panel.tooltip.value_type"
ng-options="f for f in ['cumulative','individual']"
ng-change="ctrl.render()"
></select>
</div>
</div>
</div>
<div class="gf-form-group">
<h5 class="section-heading">Stacking & Null value</h5>
<gf-form-switch
class="gf-form"
label="Stack"
label-class="width-7"
checked="ctrl.panel.stack"
on-change="ctrl.render()"
>
</gf-form-switch>
<gf-form-switch
class="gf-form"
ng-show="ctrl.panel.stack"
label="Percent"
label-class="width-7"
checked="ctrl.panel.percentage"
on-change="ctrl.render()"
>
</gf-form-switch>
<div class="gf-form">
<label class="gf-form-label width-7">Null value</label>
<div class="gf-form-select-wrapper">
<select
class="gf-form-input max-width-9"
ng-model="ctrl.panel.nullPointMode"
ng-options="f for f in ['connected', 'null', 'null as zero']"
ng-change="ctrl.render()"
></select>
</div>
</div>
</div>

View File

@ -1,73 +1,134 @@
<div class="editor-row">
<div class="section gf-form-group">
<h5 class="section-heading">Options</h5>
<gf-form-switch class="gf-form"
label="Show" label-class="width-7"
checked="ctrl.panel.legend.show" on-change="ctrl.render()">
</gf-form-switch>
<gf-form-switch class="gf-form"
label="As Table" label-class="width-7"
checked="ctrl.panel.legend.alignAsTable" on-change="ctrl.render()">
</gf-form-switch>
<gf-form-switch class="gf-form"
label="To the right" label-class="width-7"
checked="ctrl.panel.legend.rightSide" on-change="ctrl.render()">
</gf-form-switch>
<div ng-if="ctrl.panel.legend.rightSide" class="gf-form">
<label class="gf-form-label width-7">Width</label>
<input type="number" class="gf-form-input max-width-5" placeholder="250" bs-tooltip="'Set a min-width for the legend side table/block'" data-placement="right" ng-model="ctrl.panel.legend.sideWidth" ng-change="ctrl.render()" ng-model-onblur>
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Options</h5>
<gf-form-switch
class="gf-form"
label="Show"
label-class="width-7"
checked="ctrl.panel.legend.show"
on-change="ctrl.render()"
aria-label="gpl show legend"
>
</gf-form-switch>
<gf-form-switch
class="gf-form"
label="As Table"
label-class="width-7"
checked="ctrl.panel.legend.alignAsTable"
on-change="ctrl.render()"
>
</gf-form-switch>
<gf-form-switch
class="gf-form"
label="To the right"
label-class="width-7"
checked="ctrl.panel.legend.rightSide"
on-change="ctrl.render()"
>
</gf-form-switch>
<div ng-if="ctrl.panel.legend.rightSide" class="gf-form">
<label class="gf-form-label width-7">Width</label>
<input
type="number"
class="gf-form-input max-width-5"
placeholder="250"
bs-tooltip="'Set a min-width for the legend side table/block'"
data-placement="right"
ng-model="ctrl.panel.legend.sideWidth"
ng-change="ctrl.render()"
ng-model-onblur
/>
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Values</h5>
<div class="section gf-form-group">
<h5 class="section-heading">Values</h5>
<div class="gf-form-inline">
<gf-form-switch class="gf-form"
label="Min" label-class="width-4"
checked="ctrl.panel.legend.min" on-change="ctrl.legendValuesOptionChanged()">
</gf-form-switch>
<div class="gf-form-inline">
<gf-form-switch
class="gf-form"
label="Min"
label-class="width-4"
checked="ctrl.panel.legend.min"
on-change="ctrl.legendValuesOptionChanged()"
>
</gf-form-switch>
<gf-form-switch class="gf-form max-width-12"
label="Max" label-class="width-6" switch-class="max-width-5"
checked="ctrl.panel.legend.max" on-change="ctrl.legendValuesOptionChanged()">
</gf-form-switch>
</div>
<gf-form-switch
class="gf-form max-width-12"
label="Max"
label-class="width-6"
switch-class="max-width-5"
checked="ctrl.panel.legend.max"
on-change="ctrl.legendValuesOptionChanged()"
>
</gf-form-switch>
</div>
<div class="gf-form-inline">
<gf-form-switch class="gf-form"
label="Avg" label-class="width-4"
checked="ctrl.panel.legend.avg" on-change="ctrl.legendValuesOptionChanged()">
</gf-form-switch>
<div class="gf-form-inline">
<gf-form-switch
class="gf-form"
label="Avg"
label-class="width-4"
checked="ctrl.panel.legend.avg"
on-change="ctrl.legendValuesOptionChanged()"
>
</gf-form-switch>
<gf-form-switch class="gf-form max-width-12"
label="Current" label-class="width-6" switch-class="max-width-5"
checked="ctrl.panel.legend.current" on-change="ctrl.legendValuesOptionChanged()">
</gf-form-switch>
</div>
<gf-form-switch
class="gf-form max-width-12"
label="Current"
label-class="width-6"
switch-class="max-width-5"
checked="ctrl.panel.legend.current"
on-change="ctrl.legendValuesOptionChanged()"
>
</gf-form-switch>
</div>
<div class="gf-form-inline">
<gf-form-switch class="gf-form"
label="Total" label-class="width-4"
checked="ctrl.panel.legend.total" on-change="ctrl.legendValuesOptionChanged()">
</gf-form-switch>
<div class="gf-form-inline">
<gf-form-switch
class="gf-form"
label="Total"
label-class="width-4"
checked="ctrl.panel.legend.total"
on-change="ctrl.legendValuesOptionChanged()"
>
</gf-form-switch>
<div class="gf-form">
<label class="gf-form-label width-6">Decimals</label>
<input type="number" class="gf-form-input width-5" placeholder="auto" bs-tooltip="'Override automatic decimal precision for legend and tooltips'" data-placement="right" ng-model="ctrl.panel.decimals" ng-change="ctrl.render()" ng-model-onblur>
</div>
</div>
</div>
<div class="gf-form">
<label class="gf-form-label width-6">Decimals</label>
<input
type="number"
class="gf-form-input width-5"
placeholder="auto"
bs-tooltip="'Override automatic decimal precision for legend and tooltips'"
data-placement="right"
ng-model="ctrl.panel.decimals"
ng-change="ctrl.render()"
ng-model-onblur
/>
</div>
</div>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Hide series</h5>
<gf-form-switch class="gf-form"
label="With only nulls" label-class="width-10"
checked="ctrl.panel.legend.hideEmpty" on-change="ctrl.render()">
</gf-form-switch>
<gf-form-switch class="gf-form"
label="With only zeros" label-class="width-10"
checked="ctrl.panel.legend.hideZero" on-change="ctrl.render()">
</gf-form-switch>
</div>
<div class="section gf-form-group">
<h5 class="section-heading">Hide series</h5>
<gf-form-switch
class="gf-form"
label="With only nulls"
label-class="width-10"
checked="ctrl.panel.legend.hideEmpty"
on-change="ctrl.render()"
>
</gf-form-switch>
<gf-form-switch
class="gf-form"
label="With only zeros"
label-class="width-10"
checked="ctrl.panel.legend.hideZero"
on-change="ctrl.render()"
>
</gf-form-switch>
</div>
</div>

View File

@ -0,0 +1,51 @@
<div
class="graph-series-override"
ng-repeat="override in ctrl.panel.seriesOverrides"
ng-controller="SeriesOverridesCtrl"
>
<div class="gf-form">
<label class="gf-form-label">Alias or regex</label>
<input
type="text"
ng-model="override.alias"
bs-typeahead="getSeriesNames"
ng-blur="ctrl.render()"
data-min-length="0"
data-items="100"
class="gf-form-input width-15"
placeholder="For regex use /pattern/"
/>
<label class="gf-form-label pointer" ng-click="ctrl.removeSeriesOverride(override)">
<icon name="'trash-alt'"></icon>
</label>
</div>
<div class="graph-series-override__properties">
<div class="gf-form" ng-repeat="option in currentOverrides">
<label class="gf-form-label gf-form-label--grow">
<span ng-show="option.propertyName === 'color'">
Color: <icon name="'circle'" type="'mono'" ng-style="{color:option.value}"></icon>
</span>
<span ng-show="option.propertyName !== 'color'"> {{ option.name }}: {{ option.value }} </span>
<icon
name="'times'"
size="'sm'"
ng-click="removeOverride(option)"
style="margin-right: 4px;cursor: pointer;"
></icon>
</label>
</div>
<div class="gf-form">
<span
class="dropdown"
dropdown-typeahead2="overrideMenu"
dropdown-typeahead-on-select="setOverride($item, $subItem)"
button-template-class="gf-form-label"
></span>
</div>
</div>
</div>
<div class="gf-form-button-row">
<button class="btn btn-inverse" ng-click="ctrl.addSeriesOverride()">
<icon name="'plus'"></icon>&nbsp;Add series override
</button>
</div>

View File

@ -0,0 +1 @@
<graph-threshold-form panel-ctrl="ctrl"></graph-threshold-form>

View File

@ -1,2 +0,0 @@
<graph-threshold-form panel-ctrl="ctrl"></graph-threshold-form>
<graph-time-region-form panel-ctrl="ctrl"></graph-time-region-form>

View File

@ -0,0 +1 @@
<graph-time-region-form panel-ctrl="ctrl"></graph-time-region-form>

View File

@ -75,7 +75,7 @@
<div class="gf-form">
<label class="gf-form-label">
<a class="pointer" ng-click="ctrl.removeTimeRegion($index)">
<icon name="'trash-alt"></icon>
<icon name="'trash-alt'"></icon>
</a>
</label>
</div>

View File

@ -15,8 +15,7 @@ import { AppNotificationsState } from './appNotifications';
import { PluginsState } from './plugins';
import { ApplicationState } from './application';
import { LdapState } from './ldap';
import { PanelEditorState } from '../features/dashboard/panel_editor/state/reducers';
import { PanelEditorStateNew } from '../features/dashboard/components/PanelEditor/state/reducers';
import { PanelEditorState } from '../features/dashboard/components/PanelEditor/state/reducers';
import { ApiKeysState } from './apiKeys';
import { TemplatingState } from '../features/variables/state/reducers';
import { ImportDashboardState } from '../features/manage-dashboards/state/reducers';
@ -30,7 +29,6 @@ export interface StoreState {
folder: FolderState;
dashboard: DashboardState;
panelEditor: PanelEditorState;
panelEditorNew: PanelEditorStateNew;
dataSources: DataSourcesState;
dataSourceSettings: DataSourceSettingsState;
explore: ExploreState;

View File

@ -90,8 +90,9 @@ $body-bg: #141619;
$page-bg: #141619;
$dashboard-bg: #0b0c0e;
$text-color: #c7d0d9;
$text-color-strong: #f7f8fa;
$text-color: #c7d0d9;
$text-color-semi-weak: #9fa7b3;
$text-color-weak: #7b8087;
$text-color-faint: #464c54;
$text-color-emphasis: #f7f8fa;

View File

@ -86,6 +86,7 @@ $dashboard-bg: #f7f8fa;
$text-color: #464c54;
$text-color-strong: #202226;
$text-color-semi-weak: #464c54;
$text-color-weak: #7b8087;
$text-color-faint: #9fa7b3;
$text-color-emphasis: #202226;

View File

@ -220,18 +220,8 @@
color: $purple;
}
.graph-series-override {
input {
float: left;
margin-right: 10px;
}
.graph-series-override-option {
float: left;
padding: 2px 6px;
}
.graph-series-override-selector {
float: left;
}
.graph-series-override__properties {
margin-left: $space-md;
}
.graph-tooltip {