mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
d2a13c4715
commit
3aa8eb0176
@ -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);
|
||||
|
||||
|
@ -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');
|
||||
},
|
||||
});
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
@ -4,5 +4,8 @@ export const VisualizationTab = pageFactory({
|
||||
url: '',
|
||||
selectors: {
|
||||
xAxisSection: 'X-Axis section',
|
||||
axesSection: 'Axes section',
|
||||
legendSection: 'Legend section',
|
||||
displaySection: 'Display section',
|
||||
},
|
||||
});
|
||||
|
@ -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',
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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};
|
||||
|
@ -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};
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 => {
|
||||
|
@ -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};
|
||||
}
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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),
|
||||
};
|
||||
};
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
@ -6,7 +6,7 @@ export interface PanelEditorTab {
|
||||
}
|
||||
|
||||
export enum PanelEditorTabId {
|
||||
Query = 'Query',
|
||||
Query = 'query',
|
||||
Transform = 'transform',
|
||||
Visualize = 'visualize',
|
||||
Alert = 'alert',
|
||||
|
@ -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),
|
||||
},
|
||||
|
@ -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 = {
|
||||
|
@ -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> {
|
||||
|
@ -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)}
|
||||
|
@ -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>
|
||||
|
@ -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);
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
@ -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);
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@ -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 }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
@ -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 }));
|
||||
};
|
||||
};
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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,
|
||||
};
|
||||
});
|
@ -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,
|
||||
};
|
||||
|
@ -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() {
|
||||
|
@ -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>,
|
||||
];
|
||||
|
@ -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 }) {
|
||||
|
@ -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;
|
||||
|
@ -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> 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>
|
||||
|
@ -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>
|
||||
|
51
public/app/plugins/panel/graph/tab_series_overrides.html
Normal file
51
public/app/plugins/panel/graph/tab_series_overrides.html
Normal 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> Add series override
|
||||
</button>
|
||||
</div>
|
1
public/app/plugins/panel/graph/tab_thresholds.html
Normal file
1
public/app/plugins/panel/graph/tab_thresholds.html
Normal file
@ -0,0 +1 @@
|
||||
<graph-threshold-form panel-ctrl="ctrl"></graph-threshold-form>
|
@ -1,2 +0,0 @@
|
||||
<graph-threshold-form panel-ctrl="ctrl"></graph-threshold-form>
|
||||
<graph-time-region-form panel-ctrl="ctrl"></graph-time-region-form>
|
1
public/app/plugins/panel/graph/tab_time_regions.html
Normal file
1
public/app/plugins/panel/graph/tab_time_regions.html
Normal file
@ -0,0 +1 @@
|
||||
<graph-time-region-form panel-ctrl="ctrl"></graph-time-region-form>
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user