Merge branch 'master' into react-query-editor

This commit is contained in:
Torkel Ödegaard
2019-01-17 14:57:49 +01:00
87 changed files with 1294 additions and 911 deletions

View File

@@ -38,7 +38,7 @@ Name | Description
### IAM Roles
Currently all access to CloudWatch is done server side by the Grafana backend using the official AWS SDK. If you grafana
Currently all access to CloudWatch is done server side by the Grafana backend using the official AWS SDK. If your Grafana
server is running on AWS you can use IAM Roles and authentication will be handled automatically.
Checkout AWS docs on [IAM Roles](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html)

View File

@@ -11,6 +11,7 @@ interface Props {
hideTracksWhenNotNeeded?: boolean;
scrollTop?: number;
setScrollTop: (value: React.MouseEvent<HTMLElement>) => void;
autoHeightMin?: number | string;
}
/**
@@ -26,6 +27,7 @@ export class CustomScrollbar extends PureComponent<Props> {
hideTracksWhenNotNeeded: false,
scrollTop: 0,
setScrollTop: () => {},
autoHeightMin: '0'
};
private ref: React.RefObject<Scrollbars>;
@@ -65,7 +67,6 @@ export class CustomScrollbar extends PureComponent<Props> {
autoHeight={true}
// These autoHeightMin & autoHeightMax options affect firefox and chrome differently.
// Before these where set to inhert but that caused problems with cut of legends in firefox
autoHeightMin={'0'}
autoHeightMax={autoMaxHeight}
renderTrackHorizontal={props => <div {...props} className="track-horizontal" />}
renderTrackVertical={props => <div {...props} className="track-vertical" />}

View File

@@ -7,7 +7,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
Object {
"height": "auto",
"maxHeight": "100%",
"minHeight": "0",
"minHeight": 0,
"overflow": "hidden",
"position": "relative",
"width": "100%",
@@ -24,7 +24,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
"marginBottom": 0,
"marginRight": 0,
"maxHeight": "calc(100% + 0px)",
"minHeight": "calc(0 + 0px)",
"minHeight": 0,
"overflow": "scroll",
"position": "relative",
"right": undefined,

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { shallow } from 'enzyme';
import { FormField, Props } from './FormField';
const setup = (propOverrides?: object) => {
const props: Props = {
label: 'Test',
labelWidth: 11,
value: 10,
onChange: jest.fn(),
};
Object.assign(props, propOverrides);
return shallow(<FormField {...props} />);
};
describe('Render', () => {
it('should render component', () => {
const wrapper = setup();
expect(wrapper).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,25 @@
import React, { InputHTMLAttributes, FunctionComponent } from 'react';
import { FormLabel } from '..';
export interface Props extends InputHTMLAttributes<HTMLInputElement> {
label: string;
labelWidth?: number;
inputWidth?: number;
}
const defaultProps = {
labelWidth: 6,
inputWidth: 12,
};
const FormField: FunctionComponent<Props> = ({ label, labelWidth, inputWidth, ...inputProps }) => {
return (
<div className="form-field">
<FormLabel width={labelWidth}>{label}</FormLabel>
<input type="text" className={`gf-form-input width-${inputWidth}`} {...inputProps} />
</div>
);
};
FormField.defaultProps = defaultProps;
export { FormField };

View File

@@ -0,0 +1,12 @@
.form-field {
margin-bottom: $gf-form-margin;
display: flex;
flex-direction: row;
align-items: center;
text-align: left;
position: relative;
&--grow {
flex-grow: 1;
}
}

View File

@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<div
className="form-field"
>
<Component
width={11}
>
Test
</Component>
<input
className="gf-form-input width-12"
onChange={[MockFunction]}
type="text"
value={10}
/>
</div>
`;

View File

@@ -0,0 +1,42 @@
import React, { FunctionComponent, ReactNode } from 'react';
import classNames from 'classnames';
import { Tooltip } from '..';
interface Props {
children: ReactNode;
className?: string;
htmlFor?: string;
isFocused?: boolean;
isInvalid?: boolean;
tooltip?: string;
width?: number;
}
export const FormLabel: FunctionComponent<Props> = ({
children,
isFocused,
isInvalid,
className,
htmlFor,
tooltip,
width,
...rest
}) => {
const classes = classNames(`gf-form-label width-${width ? width : '10'}`, className, {
'gf-form-label--is-focused': isFocused,
'gf-form-label--is-invalid': isInvalid,
});
return (
<label className={classes} {...rest} htmlFor={htmlFor}>
{children}
{tooltip && (
<Tooltip placement="auto" content={tooltip}>
<div className="gf-form-help-icon--right-normal">
<i className="gicon gicon-question gicon--has-hover" />
</div>
</Tooltip>
)}
</label>
);
};

View File

@@ -1,23 +0,0 @@
import React, { SFC, ReactNode } from 'react';
import classNames from 'classnames';
interface Props {
children: ReactNode;
htmlFor?: string;
className?: string;
isFocused?: boolean;
isInvalid?: boolean;
}
export const GfFormLabel: SFC<Props> = ({ children, isFocused, isInvalid, className, htmlFor, ...rest }) => {
const classes = classNames('gf-form-label', className, {
'gf-form-label--is-focused': isFocused,
'gf-form-label--is-invalid': isInvalid,
});
return (
<label className={classes} {...rest} htmlFor={htmlFor}>
{children}
</label>
);
};

View File

@@ -6,7 +6,7 @@
}
.panel-options-group__header {
padding: 4px 20px;
padding: 4px 8px;
font-size: 1.1rem;
background: $panel-options-group-header-bg;
position: relative;

View File

@@ -16,7 +16,7 @@ import SelectOptionGroup from './SelectOptionGroup';
import IndicatorsContainer from './IndicatorsContainer';
import NoOptionsMessage from './NoOptionsMessage';
import resetSelectStyles from './resetSelectStyles';
import { CustomScrollbar } from '@grafana/ui';
import { CustomScrollbar } from '..';
export interface SelectOptionItem {
label?: string;

View File

@@ -102,6 +102,7 @@ $select-input-bg-disabled: $input-bg-disabled;
.gf-form-select-box__value-container {
display: table-cell;
padding: 6px 10px;
vertical-align: middle;
> div {
display: inline-block;
}

View File

@@ -8,6 +8,12 @@
height: 70px;
}
.thresholds-row:first-child > .thresholds-row-color-indicator {
border-top-left-radius: $border-radius;
border-top-right-radius: $border-radius;
overflow: hidden;
}
.thresholds-row:last-child > .thresholds-row-color-indicator {
border-bottom-left-radius: $border-radius;
border-bottom-right-radius: $border-radius;
@@ -33,7 +39,7 @@
}
.thresholds-row-color-indicator {
width: 20px;
width: 10px;
}
.thresholds-row-input {
@@ -45,18 +51,6 @@
display: flex;
justify-content: center;
flex-direction: row;
height: 42px;
}
.thresholds-row-input-inner > div {
border-left: 1px solid $input-label-border-color;
border-top: 1px solid $input-label-border-color;
border-bottom: 1px solid $input-label-border-color;
}
.thresholds-row-input-inner > *:nth-child(2) {
border-top-left-radius: $border-radius;
border-bottom-left-radius: $border-radius;
}
.thresholds-row-input-inner > *:last-child {
@@ -74,9 +68,11 @@
}
.thresholds-row-input-inner-value > input {
height: 100%;
padding: 8px 10px;
height: $gf-form-input-height;
padding: $input-padding-y $input-padding-x;
width: 150px;
border-top: 1px solid $input-label-border-color;
border-bottom: 1px solid $input-label-border-color;
}
.thresholds-row-input-inner-color {
@@ -85,6 +81,7 @@
align-items: center;
justify-content: center;
background-color: $input-bg;
border: 1px solid $input-label-border-color;
}
.thresholds-row-input-inner-color-colorpicker {
@@ -99,8 +96,10 @@
display: flex;
align-items: center;
justify-content: center;
height: 42px;
height: $gf-form-input-height;
padding: $input-padding-y $input-padding-x;
width: 42px;
background-color: $input-label-border-color;
background-color: $input-label-bg;
border: 1px solid $input-label-border-color;
cursor: pointer;
}

View File

@@ -1,22 +1,22 @@
import React, { PureComponent } from 'react';
import { MappingType, RangeMap, Select, ValueMap } from '@grafana/ui';
import React, { ChangeEvent, PureComponent } from 'react';
import { Label } from 'app/core/components/Label/Label';
import { MappingType, ValueMapping } from '../../types';
import { FormField, FormLabel, Select } from '..';
interface Props {
mapping: ValueMap | RangeMap;
updateMapping: (mapping) => void;
removeMapping: () => void;
export interface Props {
valueMapping: ValueMapping;
updateValueMapping: (valueMapping: ValueMapping) => void;
removeValueMapping: () => void;
}
interface State {
from: string;
from?: string;
id: number;
operator: string;
text: string;
to: string;
to?: string;
type: MappingType;
value: string;
value?: string;
}
const mappingOptions = [
@@ -25,36 +25,34 @@ const mappingOptions = [
];
export default class MappingRow extends PureComponent<Props, State> {
constructor(props) {
constructor(props: Props) {
super(props);
this.state = {
...props.mapping,
};
this.state = { ...props.valueMapping };
}
onMappingValueChange = event => {
onMappingValueChange = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({ value: event.target.value });
};
onMappingFromChange = event => {
onMappingFromChange = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({ from: event.target.value });
};
onMappingToChange = event => {
onMappingToChange = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({ to: event.target.value });
};
onMappingTextChange = event => {
onMappingTextChange = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({ text: event.target.value });
};
onMappingTypeChange = mappingType => {
onMappingTypeChange = (mappingType: MappingType) => {
this.setState({ type: mappingType });
};
updateMapping = () => {
this.props.updateMapping({ ...this.state });
this.props.updateValueMapping({ ...this.state } as ValueMapping);
};
renderRow() {
@@ -63,30 +61,28 @@ export default class MappingRow extends PureComponent<Props, State> {
if (type === MappingType.RangeToText) {
return (
<>
<div className="gf-form">
<Label width={4}>From</Label>
<FormField
label="From"
labelWidth={4}
inputWidth={8}
onBlur={this.updateMapping}
onChange={this.onMappingFromChange}
value={from}
/>
<FormField
label="To"
labelWidth={4}
inputWidth={8}
onBlur={this.updateMapping}
onChange={this.onMappingToChange}
value={to}
/>
<div className="gf-form gf-form--grow">
<FormLabel width={4}>Text</FormLabel>
<input
className="gf-form-input width-8"
value={from}
className="gf-form-input"
onBlur={this.updateMapping}
onChange={this.onMappingFromChange}
/>
</div>
<div className="gf-form">
<Label width={4}>To</Label>
<input
className="gf-form-input width-8"
value={to}
onBlur={this.updateMapping}
onChange={this.onMappingToChange}
/>
</div>
<div className="gf-form">
<Label width={4}>Text</Label>
<input
className="gf-form-input width-10"
value={text}
onBlur={this.updateMapping}
onChange={this.onMappingTextChange}
/>
</div>
@@ -96,17 +92,16 @@ export default class MappingRow extends PureComponent<Props, State> {
return (
<>
<div className="gf-form">
<Label width={4}>Value</Label>
<input
className="gf-form-input width-8"
onBlur={this.updateMapping}
onChange={this.onMappingValueChange}
value={value}
/>
</div>
<FormField
label="Value"
labelWidth={4}
onBlur={this.updateMapping}
onChange={this.onMappingValueChange}
value={value}
inputWidth={8}
/>
<div className="gf-form gf-form--grow">
<Label width={4}>Text</Label>
<FormLabel width={4}>Text</FormLabel>
<input
className="gf-form-input"
onBlur={this.updateMapping}
@@ -124,7 +119,7 @@ export default class MappingRow extends PureComponent<Props, State> {
return (
<div className="gf-form-inline">
<div className="gf-form">
<Label width={5}>Type</Label>
<FormLabel width={5}>Type</FormLabel>
<Select
placeholder="Choose type"
isSearchable={false}
@@ -136,7 +131,7 @@ export default class MappingRow extends PureComponent<Props, State> {
</div>
{this.renderRow()}
<div className="gf-form">
<button onClick={this.props.removeMapping} className="gf-form-label gf-form-label--btn">
<button onClick={this.props.removeValueMapping} className="gf-form-label gf-form-label--btn">
<i className="fa fa-times" />
</button>
</div>

View File

@@ -1,27 +1,23 @@
import React from 'react';
import { shallow } from 'enzyme';
import { GaugeOptions, MappingType, PanelOptionsProps } from '@grafana/ui';
import { defaultProps } from 'app/plugins/panel/gauge/GaugePanelOptions';
import ValueMappings from './ValueMappings';
import { ValueMappingsEditor, Props } from './ValueMappingsEditor';
import { MappingType } from '../../types/panel';
const setup = (propOverrides?: object) => {
const props: PanelOptionsProps<GaugeOptions> = {
const props: Props = {
onChange: jest.fn(),
options: {
...defaultProps.options,
mappings: [
{ id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
{ id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
],
},
valueMappings: [
{ id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
{ id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
],
};
Object.assign(props, propOverrides);
const wrapper = shallow(<ValueMappings {...props} />);
const wrapper = shallow(<ValueMappingsEditor {...props} />);
const instance = wrapper.instance() as ValueMappings;
const instance = wrapper.instance() as ValueMappingsEditor;
return {
instance,
@@ -40,18 +36,20 @@ describe('Render', () => {
describe('On remove mapping', () => {
it('Should remove mapping with id 0', () => {
const { instance } = setup();
instance.onRemoveMapping(1);
expect(instance.state.mappings).toEqual([
expect(instance.state.valueMappings).toEqual([
{ id: 2, operator: '', type: MappingType.RangeToText, from: '21', to: '30', text: 'Meh' },
]);
});
it('should remove mapping with id 1', () => {
const { instance } = setup();
instance.onRemoveMapping(2);
expect(instance.state.mappings).toEqual([
expect(instance.state.valueMappings).toEqual([
{ id: 1, operator: '', type: MappingType.ValueToText, value: '20', text: 'Ok' },
]);
});
@@ -67,7 +65,7 @@ describe('Next id to add', () => {
});
it('should default to 1', () => {
const { instance } = setup({ options: { ...defaultProps.options } });
const { instance } = setup({ valueMappings: [] });
expect(instance.state.nextIdToAdd).toEqual(1);
});

View File

@@ -1,33 +1,39 @@
import React, { PureComponent } from 'react';
import { GaugeOptions, PanelOptionsProps, MappingType, RangeMap, ValueMap, PanelOptionsGroup } from '@grafana/ui';
import MappingRow from './MappingRow';
import { MappingType, ValueMapping } from '../../types/panel';
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
export interface Props {
valueMappings: ValueMapping[];
onChange: (valueMappings: ValueMapping[]) => void;
}
interface State {
mappings: Array<ValueMap | RangeMap>;
valueMappings: ValueMapping[];
nextIdToAdd: number;
}
export default class ValueMappings extends PureComponent<PanelOptionsProps<GaugeOptions>, State> {
constructor(props) {
export class ValueMappingsEditor extends PureComponent<Props, State> {
constructor(props: Props) {
super(props);
const mappings = props.options.mappings;
const mappings = props.valueMappings;
this.state = {
mappings: mappings || [],
nextIdToAdd: mappings.length > 0 ? this.getMaxIdFromMappings(mappings) : 1,
valueMappings: mappings,
nextIdToAdd: mappings.length > 0 ? this.getMaxIdFromValueMappings(mappings) : 1,
};
}
getMaxIdFromMappings(mappings) {
getMaxIdFromValueMappings(mappings: ValueMapping[]) {
return Math.max.apply(null, mappings.map(mapping => mapping.id).map(m => m)) + 1;
}
addMapping = () =>
this.setState(prevState => ({
mappings: [
...prevState.mappings,
valueMappings: [
...prevState.valueMappings,
{
id: prevState.nextIdToAdd,
operator: '',
@@ -41,23 +47,23 @@ export default class ValueMappings extends PureComponent<PanelOptionsProps<Gauge
nextIdToAdd: prevState.nextIdToAdd + 1,
}));
onRemoveMapping = id => {
onRemoveMapping = (id: number) => {
this.setState(
prevState => ({
mappings: prevState.mappings.filter(m => {
valueMappings: prevState.valueMappings.filter(m => {
return m.id !== id;
}),
}),
() => {
this.props.onChange({ ...this.props.options, mappings: this.state.mappings });
this.props.onChange(this.state.valueMappings);
}
);
};
updateGauge = mapping => {
updateGauge = (mapping: ValueMapping) => {
this.setState(
prevState => ({
mappings: prevState.mappings.map(m => {
valueMappings: prevState.valueMappings.map(m => {
if (m.id === mapping.id) {
return { ...mapping };
}
@@ -66,24 +72,24 @@ export default class ValueMappings extends PureComponent<PanelOptionsProps<Gauge
}),
}),
() => {
this.props.onChange({ ...this.props.options, mappings: this.state.mappings });
this.props.onChange(this.state.valueMappings);
}
);
};
render() {
const { mappings } = this.state;
const { valueMappings } = this.state;
return (
<PanelOptionsGroup title="Value Mappings">
<div>
{mappings.length > 0 &&
mappings.map((mapping, index) => (
{valueMappings.length > 0 &&
valueMappings.map((valueMapping, index) => (
<MappingRow
key={`${mapping.text}-${index}`}
mapping={mapping}
updateMapping={this.updateGauge}
removeMapping={() => this.onRemoveMapping(mapping.id)}
key={`${valueMapping.text}-${index}`}
valueMapping={valueMapping}
updateValueMapping={this.updateGauge}
removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
/>
))}
</div>

View File

@@ -7,7 +7,9 @@ exports[`Render should render component 1`] = `
<div>
<MappingRow
key="Ok-0"
mapping={
removeValueMapping={[Function]}
updateValueMapping={[Function]}
valueMapping={
Object {
"id": 1,
"operator": "",
@@ -16,12 +18,12 @@ exports[`Render should render component 1`] = `
"value": "20",
}
}
removeMapping={[Function]}
updateMapping={[Function]}
/>
<MappingRow
key="Meh-1"
mapping={
removeValueMapping={[Function]}
updateValueMapping={[Function]}
valueMapping={
Object {
"from": "21",
"id": 2,
@@ -31,8 +33,6 @@ exports[`Render should render component 1`] = `
"type": 2,
}
}
removeMapping={[Function]}
updateMapping={[Function]}
/>
</div>
<div

View File

@@ -6,3 +6,5 @@
@import 'PanelOptionsGroup/PanelOptionsGroup';
@import 'PanelOptionsGrid/PanelOptionsGrid';
@import 'ColorPicker/ColorPicker';
@import 'ValueMappingsEditor/ValueMappingsEditor';
@import "FormField/FormField";

View File

@@ -9,12 +9,16 @@ export { IndicatorsContainer } from './Select/IndicatorsContainer';
export { NoOptionsMessage } from './Select/NoOptionsMessage';
export { default as resetSelectStyles } from './Select/resetSelectStyles';
// Forms
export { FormLabel } from './FormLabel/FormLabel';
export { FormField } from './FormField/FormField';
export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
export { ColorPicker } from './ColorPicker/ColorPicker';
export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover';
export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker';
export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
export { GfFormLabel } from './GfFormLabel/GfFormLabel';
export { Graph } from './Graph/Graph';
export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid';
export { ValueMappingsEditor } from './ValueMappingsEditor/ValueMappingsEditor';

View File

@@ -1,16 +0,0 @@
import { RangeMap, Threshold, ValueMap } from './panel';
export interface GaugeOptions {
baseColor: string;
decimals: number;
mappings: Array<RangeMap | ValueMap>;
maxValue: number;
minValue: number;
prefix: string;
showThresholdLabels: boolean;
showThresholdMarkers: boolean;
stat: string;
suffix: string;
thresholds: Threshold[];
unit: string;
}

View File

@@ -1,4 +1,3 @@
export * from './series';
export * from './time';
export * from './panel';
export * from './gauge';

View File

@@ -56,6 +56,8 @@ interface BaseMap {
type: MappingType;
}
export type ValueMapping = ValueMap | RangeMap;
export interface ValueMap extends BaseMap {
value: string;
}

View File

@@ -1,4 +1,4 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
import Transition from 'react-transition-group/Transition';
interface Props {
@@ -8,7 +8,7 @@ interface Props {
unmountOnExit?: boolean;
}
export const FadeIn: SFC<Props> = props => {
export const FadeIn: FC<Props> = props => {
const defaultStyle = {
transition: `opacity ${props.duration}ms linear`,
opacity: 0,

View File

@@ -0,0 +1,50 @@
import React, { FC } from 'react';
import { Tooltip } from '@grafana/ui';
interface Props {
appName: string;
buildVersion: string;
buildCommit: string;
newGrafanaVersionExists: boolean;
newGrafanaVersion: string;
}
export const Footer: FC<Props> = React.memo(({appName, buildVersion, buildCommit, newGrafanaVersionExists, newGrafanaVersion}) => {
return (
<footer className="footer">
<div className="text-center">
<ul>
<li>
<a href="http://docs.grafana.org" target="_blank">
<i className="fa fa-file-code-o" /> Docs
</a>
</li>
<li>
<a href="https://grafana.com/services/support" target="_blank">
<i className="fa fa-support" /> Support Plans
</a>
</li>
<li>
<a href="https://community.grafana.com/" target="_blank">
<i className="fa fa-comments-o" /> Community
</a>
</li>
<li>
<a href="https://grafana.com" target="_blank">{appName}</a> <span>v{buildVersion} (commit: {buildCommit})</span>
</li>
{newGrafanaVersionExists && (
<li>
<Tooltip placement="auto" content={newGrafanaVersion}>
<a href="https://grafana.com/get" target="_blank">
New version available!
</a>
</Tooltip>
</li>
)}
</ul>
</div>
</footer>
);
});
export default Footer;

View File

@@ -1,25 +0,0 @@
import React, { SFC, ReactNode } from 'react';
import { Tooltip } from '@grafana/ui';
interface Props {
tooltip?: string;
for?: string;
children: ReactNode;
width?: number;
className?: string;
}
export const Label: SFC<Props> = props => {
return (
<span className={`gf-form-label width-${props.width ? props.width : '10'}`}>
<span>{props.children}</span>
{props.tooltip && (
<Tooltip placement="auto" content={props.tooltip}>
<div className="gf-form-help-icon--right-normal">
<i className="gicon gicon-question gicon--has-hover" />
</div>
</Tooltip>
)}
</span>
);
};

View File

@@ -1,4 +1,4 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
export type LayoutMode = LayoutModes.Grid | LayoutModes.List;
@@ -12,7 +12,7 @@ interface Props {
onLayoutModeChanged: (mode: LayoutMode) => {};
}
const LayoutSelector: SFC<Props> = props => {
const LayoutSelector: FC<Props> = props => {
const { mode, onLayoutModeChanged } = props;
return (
<div className="layout-selector">

View File

@@ -0,0 +1,75 @@
// Libraries
import React, { Component } from 'react';
import config from 'app/core/config';
import { NavModel } from 'app/types';
import { getTitleFromNavModel } from 'app/core/selectors/navModel';
// Components
import PageHeader from '../PageHeader/PageHeader';
import Footer from '../Footer/Footer';
import PageContents from './PageContents';
import { CustomScrollbar } from '@grafana/ui';
interface Props {
title?: string;
children: JSX.Element[] | JSX.Element;
navModel: NavModel;
}
class Page extends Component<Props> {
private bodyClass = 'is-react';
private body = document.body;
static Header = PageHeader;
static Contents = PageContents;
componentDidMount() {
this.body.classList.add(this.bodyClass);
this.updateTitle();
}
componentDidUpdate(prevProps: Props) {
if (prevProps.title !== this.props.title) {
this.updateTitle();
}
}
componentWillUnmount() {
this.body.classList.remove(this.bodyClass);
}
updateTitle = () => {
const title = this.getPageTitle;
document.title = title ? title + ' - Grafana' : 'Grafana';
}
get getPageTitle () {
const { navModel } = this.props;
if (navModel) {
return getTitleFromNavModel(navModel) || undefined;
}
return undefined;
}
render() {
const { navModel } = this.props;
const { buildInfo } = config;
return (
<div className="page-scrollbar-wrapper">
<CustomScrollbar autoHeightMin={'100%'}>
<div className="page-scrollbar-content">
<PageHeader model={navModel} />
{this.props.children}
<Footer
appName="Grafana"
buildCommit={buildInfo.commit}
buildVersion={buildInfo.version}
newGrafanaVersion={buildInfo.latestVersion}
newGrafanaVersionExists={buildInfo.hasUpdate} />
</div>
</CustomScrollbar>
</div>
);
}
}
export default Page;

View File

@@ -0,0 +1,26 @@
// Libraries
import React, { Component } from 'react';
// Components
import PageLoader from '../PageLoader/PageLoader';
interface Props {
isLoading?: boolean;
children: JSX.Element[] | JSX.Element;
}
class PageContents extends Component<Props> {
render() {
const { isLoading } = this.props;
return (
<div className="page-container page-body">
{isLoading && <PageLoader />}
{this.props.children}
</div>
);
}
}
export default PageContents;

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { FormEvent } from 'react';
import { NavModel, NavModelItem } from 'app/types';
import classNames from 'classnames';
import appEvents from 'app/core/app_events';
@@ -12,8 +12,8 @@ const SelectNav = ({ main, customCss }: { main: NavModelItem; customCss: string
return navItem.active === true;
});
const gotoUrl = evt => {
const element = evt.target;
const gotoUrl = (evt: FormEvent) => {
const element = evt.target as HTMLSelectElement;
const url = element.options[element.selectedIndex].value;
appEvents.emit('location-change', { href: url });
};

View File

@@ -1,10 +1,10 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
interface Props {
pageName: string;
pageName?: string;
}
const PageLoader: SFC<Props> = ({ pageName }) => {
const PageLoader: FC<Props> = ({ pageName }) => {
const loadingText = `Loading ${pageName}...`;
return (
<div className="page-loader-wrapper">

View File

@@ -1,7 +1,6 @@
import React, { PureComponent } from 'react';
import { Label } from 'app/core/components/Label/Label';
import { Select } from '@grafana/ui';
import { FormLabel, Select } from '@grafana/ui';
import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
import { DashboardSearchHit } from 'app/types';
@@ -100,12 +99,12 @@ export class SharedPreferences extends PureComponent<Props, State> {
/>
</div>
<div className="gf-form">
<Label
<FormLabel
width={11}
tooltip="Not finding dashboard you want? Star it first, then it should appear in this select box."
>
Home Dashboard
</Label>
</FormLabel>
<Select
value={dashboards.find(dashboard => dashboard.id === homeDashboardId)}
getOptionValue={i => i.id}

View File

@@ -1,4 +1,4 @@
import React, { SFC, ReactNode, PureComponent } from 'react';
import React, { FC, ReactNode, PureComponent } from 'react';
import { Tooltip } from '@grafana/ui';
interface ToggleButtonGroupProps {
@@ -29,7 +29,7 @@ interface ToggleButtonProps {
tooltip?: string;
}
export const ToggleButton: SFC<ToggleButtonProps> = ({
export const ToggleButton: FC<ToggleButtonProps> = ({
children,
selected,
className = '',

View File

@@ -1,10 +1,10 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
export interface Props {
child: any;
}
const DropDownChild: SFC<Props> = props => {
const DropDownChild: FC<Props> = props => {
const { child } = props;
const listItemClassName = child.divider ? 'divider' : '';

View File

@@ -1,11 +1,11 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
import DropDownChild from './DropDownChild';
interface Props {
link: any;
}
const SideMenuDropDown: SFC<Props> = props => {
const SideMenuDropDown: FC<Props> = props => {
const { link } = props;
return (
<ul className="dropdown-menu dropdown-menu--sidemenu" role="menu">

View File

@@ -1,6 +1,6 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
const SignIn: SFC<any> = () => {
const SignIn: FC<any> = () => {
const loginUrl = `login?redirect=${encodeURIComponent(window.location.pathname)}`;
return (
<div className="sidemenu-item">

View File

@@ -1,9 +1,9 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
import _ from 'lodash';
import TopSectionItem from './TopSectionItem';
import config from '../../config';
const TopSection: SFC<any> = () => {
const TopSection: FC<any> = () => {
const navTree = _.cloneDeep(config.bootData.navTree);
const mainLinks = _.filter(navTree, item => !item.hideFromMenu);

View File

@@ -1,11 +1,11 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
import SideMenuDropDown from './SideMenuDropDown';
export interface Props {
link: any;
}
const TopSectionItem: SFC<Props> = props => {
const TopSectionItem: FC<Props> = props => {
const { link } = props;
return (
<div className="sidemenu-item dropdown">

View File

@@ -6,6 +6,8 @@ export interface BuildInfo {
commit: string;
isEnterprise: boolean;
env: string;
latestVersion: string;
hasUpdate: boolean;
}
export class Settings {

View File

@@ -41,3 +41,7 @@ export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel)
return getNotFoundModel();
}
export const getTitleFromNavModel = (navModel: NavModel) => {
return `${navModel.main.text}${navModel.node.text ? ': ' + navModel.node.text : '' }`;
};

View File

@@ -6,7 +6,14 @@ import { getMultipleMockKeys, getMockKey } from './__mocks__/apiKeysMock';
const setup = (propOverrides?: object) => {
const props: Props = {
navModel: {} as NavModel,
navModel: {
main: {
text: 'Configuration'
},
node: {
text: 'Api Keys'
}
} as NavModel,
apiKeys: [] as ApiKey[],
searchQuery: '',
hasFetched: false,

View File

@@ -6,8 +6,7 @@ import { NavModel, ApiKey, NewApiKey, OrgRole } from 'app/types';
import { getNavModel } from 'app/core/selectors/navModel';
import { getApiKeys, getApiKeysCount } from './state/selectors';
import { loadApiKeys, deleteApiKey, setSearchQuery, addApiKey } from './state/actions';
import PageHeader from 'app/core/components/PageHeader/PageHeader';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
import Page from 'app/core/components/Page/Page';
import SlideDown from 'app/core/components/Animations/SlideDown';
import ApiKeysAddedModal from './ApiKeysAddedModal';
import config from 'app/core/config';
@@ -240,18 +239,17 @@ export class ApiKeysPage extends PureComponent<Props, any> {
const { hasFetched, navModel, apiKeysCount } = this.props;
return (
<div>
<PageHeader model={navModel} />
{hasFetched ? (
apiKeysCount > 0 ? (
this.renderApiKeyList()
) : (
this.renderEmptyList()
)
) : (
<PageLoader pageName="Api keys" />
)}
</div>
<Page navModel={navModel}>
<Page.Contents isLoading={!hasFetched}>
{hasFetched && (
apiKeysCount > 0 ? (
this.renderApiKeyList()
) : (
this.renderEmptyList()
)
)}
</Page.Contents>
</Page>
);
}
}

View File

@@ -1,132 +1,152 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render API keys table if there are any keys 1`] = `
<div>
<PageHeader
model={Object {}}
<Page
navModel={
Object {
"main": Object {
"text": "Configuration",
},
"node": Object {
"text": "Api Keys",
},
}
}
>
<PageContents
isLoading={true}
/>
<PageLoader
pageName="Api keys"
/>
</div>
</Page>
`;
exports[`Render should render CTA if there are no API keys 1`] = `
<div>
<PageHeader
model={Object {}}
/>
<div
className="page-container page-body"
<Page
navModel={
Object {
"main": Object {
"text": "Configuration",
},
"node": Object {
"text": "Api Keys",
},
}
}
>
<PageContents
isLoading={false}
>
<EmptyListCTA
model={
Object {
"buttonIcon": "fa fa-plus",
"buttonLink": "#",
"buttonTitle": " New API Key",
"onClick": [Function],
"proTip": "Remember you can provide view-only API access to other applications.",
"proTipLink": "",
"proTipLinkTitle": "",
"proTipTarget": "_blank",
"title": "You haven't added any API Keys yet.",
}
}
/>
<Component
in={false}
<div
className="page-container page-body"
>
<div
className="cta-form"
<EmptyListCTA
model={
Object {
"buttonIcon": "fa fa-plus",
"buttonLink": "#",
"buttonTitle": " New API Key",
"onClick": [Function],
"proTip": "Remember you can provide view-only API access to other applications.",
"proTipLink": "",
"proTipLinkTitle": "",
"proTipTarget": "_blank",
"title": "You haven't added any API Keys yet.",
}
}
/>
<Component
in={false}
>
<button
className="cta-form__close btn btn-transparent"
onClick={[Function]}
<div
className="cta-form"
>
<i
className="fa fa-close"
/>
</button>
<h5>
Add API Key
</h5>
<form
className="gf-form-group"
onSubmit={[Function]}
>
<div
className="gf-form-inline"
<button
className="cta-form__close btn btn-transparent"
onClick={[Function]}
>
<i
className="fa fa-close"
/>
</button>
<h5>
Add API Key
</h5>
<form
className="gf-form-group"
onSubmit={[Function]}
>
<div
className="gf-form max-width-21"
className="gf-form-inline"
>
<span
className="gf-form-label"
<div
className="gf-form max-width-21"
>
Key name
</span>
<input
className="gf-form-input"
onChange={[Function]}
placeholder="Name"
type="text"
value=""
/>
</div>
<div
className="gf-form"
>
<span
className="gf-form-label"
>
Role
</span>
<span
className="gf-form-select-wrapper"
>
<select
className="gf-form-input gf-size-auto"
onChange={[Function]}
value="Viewer"
<span
className="gf-form-label"
>
<option
key="Viewer"
label="Viewer"
Key name
</span>
<input
className="gf-form-input"
onChange={[Function]}
placeholder="Name"
type="text"
value=""
/>
</div>
<div
className="gf-form"
>
<span
className="gf-form-label"
>
Role
</span>
<span
className="gf-form-select-wrapper"
>
<select
className="gf-form-input gf-size-auto"
onChange={[Function]}
value="Viewer"
>
Viewer
</option>
<option
key="Editor"
label="Editor"
value="Editor"
>
Editor
</option>
<option
key="Admin"
label="Admin"
value="Admin"
>
Admin
</option>
</select>
</span>
</div>
<div
className="gf-form"
>
<button
className="btn gf-form-btn btn-success"
<option
key="Viewer"
label="Viewer"
value="Viewer"
>
Viewer
</option>
<option
key="Editor"
label="Editor"
value="Editor"
>
Editor
</option>
<option
key="Admin"
label="Admin"
value="Admin"
>
Admin
</option>
</select>
</span>
</div>
<div
className="gf-form"
>
Add
</button>
<button
className="btn gf-form-btn btn-success"
>
Add
</button>
</div>
</div>
</div>
</form>
</div>
</Component>
</div>
</div>
</form>
</div>
</Component>
</div>
</PageContents>
</Page>
`;

View File

@@ -1,11 +1,11 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
import { PanelMenuItem } from '@grafana/ui';
interface Props {
children: any;
}
export const PanelHeaderMenuItem: SFC<Props & PanelMenuItem> = props => {
export const PanelHeaderMenuItem: FC<Props & PanelMenuItem> = props => {
const isSubMenu = props.type === 'submenu';
const isDivider = props.type === 'divider';
return isDivider ? (

View File

@@ -1,4 +1,4 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
import { Tooltip } from '@grafana/ui';
interface Props {
@@ -10,7 +10,7 @@ interface Props {
tooltipInfo?: any;
}
export const DataSourceOptions: SFC<Props> = ({ label, placeholder, name, value, onChange, tooltipInfo }) => {
export const DataSourceOptions: FC<Props> = ({ label, placeholder, name, value, onChange, tooltipInfo }) => {
const dsOption = (
<div className="gf-form gf-form--flex-end">
<label className="gf-form-label">{label}</label>

View File

@@ -10,7 +10,7 @@ import { Input } from 'app/core/components/Form';
import { EventsWithValidation } from 'app/core/components/Form/Input';
import { InputStatus } from 'app/core/components/Form/Input';
import DataSourceOption from './DataSourceOption';
import { GfFormLabel } from '@grafana/ui';
import { FormLabel } from '@grafana/ui';
// Types
import { PanelModel } from '../panel_model';
@@ -164,7 +164,7 @@ export class QueryOptions extends PureComponent<Props, State> {
{this.renderOptions()}
<div className="gf-form">
<GfFormLabel>Relative time</GfFormLabel>
<FormLabel>Relative time</FormLabel>
<Input
type="text"
className="width-6"

View File

@@ -1,4 +1,4 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
import { PluginDashboard } from '../../types';
export interface Props {
@@ -7,7 +7,7 @@ export interface Props {
onRemove: (dashboard) => void;
}
const DashboardsTable: SFC<Props> = ({ dashboards, onImport, onRemove }) => {
const DashboardsTable: FC<Props> = ({ dashboards, onImport, onRemove }) => {
function buttonText(dashboard: PluginDashboard) {
return dashboard.revision !== dashboard.importedRevision ? 'Update' : 'Re-import';
}

View File

@@ -10,7 +10,14 @@ const setup = (propOverrides?: object) => {
dataSources: [] as DataSource[],
layoutMode: LayoutModes.Grid,
loadDataSources: jest.fn(),
navModel: {} as NavModel,
navModel: {
main: {
text: 'Configuration'
},
node: {
text: 'Data Sources'
}
} as NavModel,
dataSourcesCount: 0,
searchQuery: '',
setDataSourcesSearchQuery: jest.fn(),

View File

@@ -1,15 +1,15 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { hot } from 'react-hot-loader';
import PageHeader from '../../core/components/PageHeader/PageHeader';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
import OrgActionBar from '../../core/components/OrgActionBar/OrgActionBar';
import EmptyListCTA from '../../core/components/EmptyListCTA/EmptyListCTA';
import Page from 'app/core/components/Page/Page';
import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import DataSourcesList from './DataSourcesList';
import { DataSource, NavModel } from 'app/types';
import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector';
import { DataSource, NavModel, StoreState } from 'app/types';
import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector';
import { loadDataSources, setDataSourcesLayoutMode, setDataSourcesSearchQuery } from './state/actions';
import { getNavModel } from '../../core/selectors/navModel';
import { getNavModel } from 'app/core/selectors/navModel';
import {
getDataSources,
getDataSourcesCount,
@@ -67,30 +67,30 @@ export class DataSourcesListPage extends PureComponent<Props> {
};
return (
<div>
<PageHeader model={navModel} />
<div className="page-container page-body">
{!hasFetched && <PageLoader pageName="Data sources" />}
{hasFetched && dataSourcesCount === 0 && <EmptyListCTA model={emptyListModel} />}
{hasFetched &&
dataSourcesCount > 0 && [
<OrgActionBar
layoutMode={layoutMode}
searchQuery={searchQuery}
onSetLayoutMode={mode => setDataSourcesLayoutMode(mode)}
setSearchQuery={query => setDataSourcesSearchQuery(query)}
linkButton={linkButton}
key="action-bar"
/>,
<DataSourcesList dataSources={dataSources} layoutMode={layoutMode} key="list" />,
]}
</div>
</div>
<Page navModel={navModel}>
<Page.Contents isLoading={!hasFetched}>
<>
{hasFetched && dataSourcesCount === 0 && <EmptyListCTA model={emptyListModel} />}
{hasFetched &&
dataSourcesCount > 0 && [
<OrgActionBar
layoutMode={layoutMode}
searchQuery={searchQuery}
onSetLayoutMode={mode => setDataSourcesLayoutMode(mode)}
setSearchQuery={query => setDataSourcesSearchQuery(query)}
linkButton={linkButton}
key="action-bar"
/>,
<DataSourcesList dataSources={dataSources} layoutMode={layoutMode} key="list" />,
]}
</>
</Page.Contents>
</Page>
);
}
}
function mapStateToProps(state) {
function mapStateToProps(state: StoreState) {
return {
navModel: getNavModel(state.navIndex, 'datasources'),
dataSources: getDataSources(state.dataSources),

View File

@@ -1,12 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render action bar and datasources 1`] = `
<div>
<PageHeader
model={Object {}}
/>
<div
className="page-container page-body"
<Page
navModel={
Object {
"main": Object {
"text": "Configuration",
},
"node": Object {
"text": "Data Sources",
},
}
}
>
<PageContents
isLoading={false}
>
<OrgActionBar
key="action-bar"
@@ -143,21 +151,25 @@ exports[`Render should render action bar and datasources 1`] = `
key="list"
layoutMode="grid"
/>
</div>
</div>
</PageContents>
</Page>
`;
exports[`Render should render component 1`] = `
<div>
<PageHeader
model={Object {}}
<Page
navModel={
Object {
"main": Object {
"text": "Configuration",
},
"node": Object {
"text": "Data Sources",
},
}
}
>
<PageContents
isLoading={true}
/>
<div
className="page-container page-body"
>
<PageLoader
pageName="Data sources"
/>
</div>
</div>
</Page>
`;

View File

@@ -1,5 +1,5 @@
import React, { SFC } from 'react';
import { Label } from 'app/core/components/Label/Label';
import React, { FC } from 'react';
import { FormLabel } from '@grafana/ui';
import { Switch } from '../../../core/components/Switch/Switch';
export interface Props {
@@ -9,19 +9,19 @@ export interface Props {
onDefaultChange: (value: boolean) => void;
}
const BasicSettings: SFC<Props> = ({ dataSourceName, isDefault, onDefaultChange, onNameChange }) => {
const BasicSettings: FC<Props> = ({ dataSourceName, isDefault, onDefaultChange, onNameChange }) => {
return (
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form max-width-30" style={{ marginRight: '3px' }}>
<Label
<FormLabel
tooltip={
'The name is used when you select the data source in panels. The Default data source is ' +
'preselected in new panels.'
}
>
Name
</Label>
</FormLabel>
<input
className="gf-form-input max-width-23"
type="text"

View File

@@ -1,4 +1,4 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
export interface Props {
isReadOnly: boolean;
@@ -6,7 +6,7 @@ export interface Props {
onSubmit: (event) => void;
}
const ButtonRow: SFC<Props> = ({ isReadOnly, onDelete, onSubmit }) => {
const ButtonRow: FC<Props> = ({ isReadOnly, onDelete, onSubmit }) => {
return (
<div className="gf-form-button-row">
<button type="submit" className="btn btn-success" disabled={isReadOnly} onClick={event => onSubmit(event)}>

View File

@@ -1,10 +1,10 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
interface Props {
message: any;
}
export const Alert: SFC<Props> = props => {
export const Alert: FC<Props> = props => {
const { message } = props;
return (
<div className="gf-form-group section">

View File

@@ -161,11 +161,17 @@ export function initializeExplore(
},
});
if (exploreDatasources.length > 1) {
if (exploreDatasources.length >= 1) {
let instance;
if (datasource) {
instance = await getDatasourceSrv().get(datasource);
} else {
try {
instance = await getDatasourceSrv().get(datasource);
} catch (error) {
console.error(error);
}
}
// Checking on instance here because requested datasource could be deleted already
if (!instance) {
instance = await getDatasourceSrv().get();
}
dispatch(loadDatasource(exploreId, instance));

View File

@@ -6,7 +6,14 @@ import { NavModel, Organization } from '../../types';
const setup = (propOverrides?: object) => {
const props: Props = {
organization: {} as Organization,
navModel: {} as NavModel,
navModel: {
main: {
text: 'Configuration'
},
node: {
text: 'Org details'
}
} as NavModel,
loadOrganization: jest.fn(),
setOrganizationName: jest.fn(),
updateOrganization: jest.fn(),

View File

@@ -1,13 +1,12 @@
import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import PageHeader from '../../core/components/PageHeader/PageHeader';
import PageLoader from '../../core/components/PageLoader/PageLoader';
import Page from 'app/core/components/Page/Page';
import OrgProfile from './OrgProfile';
import SharedPreferences from 'app/core/components/SharedPreferences/SharedPreferences';
import { loadOrganization, setOrganizationName, updateOrganization } from './state/actions';
import { NavModel, Organization, StoreState } from 'app/types';
import { getNavModel } from '../../core/selectors/navModel';
import { getNavModel } from 'app/core/selectors/navModel';
export interface Props {
navModel: NavModel;
@@ -35,22 +34,22 @@ export class OrgDetailsPage extends PureComponent<Props> {
const isLoading = Object.keys(organization).length === 0;
return (
<div>
<PageHeader model={navModel} />
<div className="page-container page-body">
{isLoading && <PageLoader pageName="Organization" />}
{!isLoading && (
<div>
<OrgProfile
onOrgNameChange={name => this.onOrgNameChange(name)}
onSubmit={this.onUpdateOrganization}
orgName={organization.name}
/>
<SharedPreferences resourceUri="org" />
<Page navModel={navModel}>
<Page.Contents isLoading={isLoading}>
<div className="page-container page-body">
{!isLoading && (
<div>
<OrgProfile
onOrgNameChange={name => this.onOrgNameChange(name)}
onSubmit={this.onUpdateOrganization}
orgName={organization.name}
/>
<SharedPreferences resourceUri="org" />
</div>
)}
</div>
)}
</div>
</div>
</Page.Contents>
</Page>
);
}
}

View File

@@ -1,4 +1,4 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
export interface Props {
orgName: string;
@@ -6,7 +6,7 @@ export interface Props {
onOrgNameChange: (orgName: string) => void;
}
const OrgProfile: SFC<Props> = ({ onSubmit, onOrgNameChange, orgName }) => {
const OrgProfile: FC<Props> = ({ onSubmit, onOrgNameChange, orgName }) => {
return (
<div>
<h3 className="page-sub-heading">Organization profile</h3>

View File

@@ -1,38 +1,58 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<div>
<PageHeader
model={Object {}}
/>
<div
className="page-container page-body"
<Page
navModel={
Object {
"main": Object {
"text": "Configuration",
},
"node": Object {
"text": "Org details",
},
}
}
>
<PageContents
isLoading={true}
>
<PageLoader
pageName="Organization"
<div
className="page-container page-body"
/>
</div>
</div>
</PageContents>
</Page>
`;
exports[`Render should render organization and preferences 1`] = `
<div>
<PageHeader
model={Object {}}
/>
<div
className="page-container page-body"
<Page
navModel={
Object {
"main": Object {
"text": "Configuration",
},
"node": Object {
"text": "Org details",
},
}
}
>
<PageContents
isLoading={false}
>
<div>
<OrgProfile
onOrgNameChange={[Function]}
onSubmit={[Function]}
orgName="Cool org"
/>
<SharedPreferences
resourceUri="org"
/>
<div
className="page-container page-body"
>
<div>
<OrgProfile
onOrgNameChange={[Function]}
onSubmit={[Function]}
orgName="Cool org"
/>
<SharedPreferences
resourceUri="org"
/>
</div>
</div>
</div>
</div>
</PageContents>
</Page>
`;

View File

@@ -1,4 +1,4 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
import classNames from 'classnames';
import PluginListItem from './PluginListItem';
import { Plugin } from 'app/types';
@@ -9,7 +9,7 @@ interface Props {
layoutMode: LayoutMode;
}
const PluginList: SFC<Props> = props => {
const PluginList: FC<Props> = props => {
const { plugins, layoutMode } = props;
const listStyle = classNames({

View File

@@ -1,11 +1,11 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
import { Plugin } from 'app/types';
interface Props {
plugin: Plugin;
}
const PluginListItem: SFC<Props> = props => {
const PluginListItem: FC<Props> = props => {
const { plugin } = props;
return (

View File

@@ -6,7 +6,14 @@ import { LayoutModes } from '../../core/components/LayoutSelector/LayoutSelector
const setup = (propOverrides?: object) => {
const props: Props = {
navModel: {} as NavModel,
navModel: {
main: {
text: 'Configuration'
},
node: {
text: 'Plugins'
}
} as NavModel,
plugins: [] as Plugin[],
searchQuery: '',
setPluginsSearchQuery: jest.fn(),

View File

@@ -1,15 +1,14 @@
import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import PageHeader from 'app/core/components/PageHeader/PageHeader';
import Page from 'app/core/components/Page/Page';
import OrgActionBar from 'app/core/components/OrgActionBar/OrgActionBar';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
import PluginList from './PluginList';
import { NavModel, Plugin } from 'app/types';
import { loadPlugins, setPluginsLayoutMode, setPluginsSearchQuery } from './state/actions';
import { getNavModel } from '../../core/selectors/navModel';
import { getNavModel } from 'app/core/selectors/navModel';
import { getLayoutMode, getPlugins, getPluginsSearchQuery } from './state/selectors';
import { LayoutMode } from '../../core/components/LayoutSelector/LayoutSelector';
import { LayoutMode } from 'app/core/components/LayoutSelector/LayoutSelector';
export interface Props {
navModel: NavModel;
@@ -48,23 +47,22 @@ export class PluginListPage extends PureComponent<Props> {
};
return (
<div>
<PageHeader model={navModel} />
<div className="page-container page-body">
<OrgActionBar
searchQuery={searchQuery}
layoutMode={layoutMode}
onSetLayoutMode={mode => setPluginsLayoutMode(mode)}
setSearchQuery={query => setPluginsSearchQuery(query)}
linkButton={linkButton}
/>
{hasFetched ? (
plugins && <PluginList plugins={plugins} layoutMode={layoutMode} />
) : (
<PageLoader pageName="Plugins" />
)}
</div>
</div>
<Page navModel={navModel}>
<Page.Contents isLoading={!hasFetched}>
<>
<OrgActionBar
searchQuery={searchQuery}
layoutMode={layoutMode}
onSetLayoutMode={mode => setPluginsLayoutMode(mode)}
setSearchQuery={query => setPluginsSearchQuery(query)}
linkButton={linkButton}
/>
{hasFetched && plugins && (
plugins && <PluginList plugins={plugins} layoutMode={layoutMode} />
)}
</>
</Page.Contents>
</Page>
);
}
}

View File

@@ -1,12 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<div>
<PageHeader
model={Object {}}
/>
<div
className="page-container page-body"
<Page
navModel={
Object {
"main": Object {
"text": "Configuration",
},
"node": Object {
"text": "Plugins",
},
}
}
>
<PageContents
isLoading={true}
>
<OrgActionBar
layoutMode="grid"
@@ -20,20 +28,25 @@ exports[`Render should render component 1`] = `
searchQuery=""
setSearchQuery={[Function]}
/>
<PageLoader
pageName="Plugins"
/>
</div>
</div>
</PageContents>
</Page>
`;
exports[`Render should render list 1`] = `
<div>
<PageHeader
model={Object {}}
/>
<div
className="page-container page-body"
<Page
navModel={
Object {
"main": Object {
"text": "Configuration",
},
"node": Object {
"text": "Plugins",
},
}
}
>
<PageContents
isLoading={false}
>
<OrgActionBar
layoutMode="grid"
@@ -51,6 +64,6 @@ exports[`Render should render list 1`] = `
layoutMode="grid"
plugins={Array []}
/>
</div>
</div>
</PageContents>
</Page>
`;

View File

@@ -6,7 +6,14 @@ import { getMockTeam, getMultipleMockTeams } from './__mocks__/teamMocks';
const setup = (propOverrides?: object) => {
const props: Props = {
navModel: {} as NavModel,
navModel: {
main: {
text: 'Configuration'
},
node: {
text: 'Team List'
}
} as NavModel,
teams: [] as Team[],
loadTeams: jest.fn(),
deleteTeam: jest.fn(),

View File

@@ -1,11 +1,10 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { hot } from 'react-hot-loader';
import PageHeader from 'app/core/components/PageHeader/PageHeader';
import Page from 'app/core/components/Page/Page';
import { DeleteButton } from '@grafana/ui';
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
import { NavModel, Team } from '../../types';
import { NavModel, Team } from 'app/types';
import { loadTeams, deleteTeam, setSearchQuery } from './state/actions';
import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors';
import { getNavModel } from 'app/core/selectors/navModel';
@@ -141,10 +140,11 @@ export class TeamList extends PureComponent<Props, any> {
const { hasFetched, navModel } = this.props;
return (
<div>
<PageHeader model={navModel} />
{hasFetched ? this.renderList() : <PageLoader pageName="Teams" />}
</div>
<Page navModel={navModel}>
<Page.Contents isLoading={!hasFetched}>
{hasFetched && this.renderList()}
</Page.Contents>
</Page>
);
}
}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import { FormLabel } from '@grafana/ui';
import { Label } from 'app/core/components/Label/Label';
import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences';
import { updateTeam } from './state/actions';
import { getRouteParamsId } from 'app/core/selectors/location';
@@ -51,7 +51,7 @@ export class TeamSettings extends React.Component<Props, State> {
<h3 className="page-sub-heading">Team Settings</h3>
<form name="teamDetailsForm" className="gf-form-group" onSubmit={this.onUpdate}>
<div className="gf-form max-width-30">
<Label>Name</Label>
<FormLabel>Name</FormLabel>
<input
type="text"
required
@@ -62,9 +62,9 @@ export class TeamSettings extends React.Component<Props, State> {
</div>
<div className="gf-form max-width-30">
<Label tooltip="This is optional and is primarily used to set the team profile avatar (via gravatar service)">
<FormLabel tooltip="This is optional and is primarily used to set the team profile avatar (via gravatar service)">
Email
</Label>
</FormLabel>
<input
type="email"
className="gf-form-input max-width-22"

View File

@@ -1,336 +1,356 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render component 1`] = `
<div>
<PageHeader
model={Object {}}
<Page
navModel={
Object {
"main": Object {
"text": "Configuration",
},
"node": Object {
"text": "Team List",
},
}
}
>
<PageContents
isLoading={true}
/>
<PageLoader
pageName="Teams"
/>
</div>
</Page>
`;
exports[`Render should render teams table 1`] = `
<div>
<PageHeader
model={Object {}}
/>
<div
className="page-container page-body"
<Page
navModel={
Object {
"main": Object {
"text": "Configuration",
},
"node": Object {
"text": "Team List",
},
}
}
>
<PageContents
isLoading={false}
>
<div
className="page-action-bar"
className="page-container page-body"
>
<div
className="gf-form gf-form--grow"
className="page-action-bar"
>
<label
className="gf-form--has-input-icon gf-form--grow"
<div
className="gf-form gf-form--grow"
>
<input
className="gf-form-input"
onChange={[Function]}
placeholder="Search teams"
type="text"
value=""
/>
<i
className="gf-form-input-icon fa fa-search"
/>
</label>
<label
className="gf-form--has-input-icon gf-form--grow"
>
<input
className="gf-form-input"
onChange={[Function]}
placeholder="Search teams"
type="text"
value=""
/>
<i
className="gf-form-input-icon fa fa-search"
/>
</label>
</div>
<div
className="page-action-bar__spacer"
/>
<a
className="btn btn-success"
href="org/teams/new"
>
New team
</a>
</div>
<div
className="page-action-bar__spacer"
/>
<a
className="btn btn-success"
href="org/teams/new"
className="admin-list-table"
>
New team
</a>
</div>
<div
className="admin-list-table"
>
<table
className="filter-table filter-table--hover form-inline"
>
<thead>
<tr>
<th />
<th>
Name
</th>
<th>
Email
</th>
<th>
Members
</th>
<th
style={
Object {
"width": "1%",
<table
className="filter-table filter-table--hover form-inline"
>
<thead>
<tr>
<th />
<th>
Name
</th>
<th>
Email
</th>
<th>
Members
</th>
<th
style={
Object {
"width": "1%",
}
}
}
/>
</tr>
</thead>
<tbody>
<tr
key="1"
>
<td
className="width-4 text-center link-td"
>
<a
href="org/teams/edit/1"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/1"
>
test-1
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/1"
>
test-1@test.com
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/1"
>
1
</a>
</td>
<td
className="text-right"
>
<DeleteButton
onConfirm={[Function]}
/>
</td>
</tr>
<tr
key="2"
>
<td
className="width-4 text-center link-td"
</tr>
</thead>
<tbody>
<tr
key="1"
>
<a
href="org/teams/edit/2"
<td
className="width-4 text-center link-td"
>
<img
className="filter-table__avatar"
src="some/url/"
<a
href="org/teams/edit/1"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/1"
>
test-1
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/1"
>
test-1@test.com
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/1"
>
1
</a>
</td>
<td
className="text-right"
>
<DeleteButton
onConfirm={[Function]}
/>
</a>
</td>
<td
className="link-td"
</td>
</tr>
<tr
key="2"
>
<a
href="org/teams/edit/2"
<td
className="width-4 text-center link-td"
>
test-2
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/2"
<a
href="org/teams/edit/2"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</a>
</td>
<td
className="link-td"
>
test-2@test.com
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/2"
<a
href="org/teams/edit/2"
>
test-2
</a>
</td>
<td
className="link-td"
>
2
</a>
</td>
<td
className="text-right"
>
<DeleteButton
onConfirm={[Function]}
/>
</td>
</tr>
<tr
key="3"
>
<td
className="width-4 text-center link-td"
>
<a
href="org/teams/edit/3"
<a
href="org/teams/edit/2"
>
test-2@test.com
</a>
</td>
<td
className="link-td"
>
<img
className="filter-table__avatar"
src="some/url/"
<a
href="org/teams/edit/2"
>
2
</a>
</td>
<td
className="text-right"
>
<DeleteButton
onConfirm={[Function]}
/>
</a>
</td>
<td
className="link-td"
</td>
</tr>
<tr
key="3"
>
<a
href="org/teams/edit/3"
<td
className="width-4 text-center link-td"
>
test-3
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/3"
<a
href="org/teams/edit/3"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</a>
</td>
<td
className="link-td"
>
test-3@test.com
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/3"
<a
href="org/teams/edit/3"
>
test-3
</a>
</td>
<td
className="link-td"
>
3
</a>
</td>
<td
className="text-right"
>
<DeleteButton
onConfirm={[Function]}
/>
</td>
</tr>
<tr
key="4"
>
<td
className="width-4 text-center link-td"
>
<a
href="org/teams/edit/4"
<a
href="org/teams/edit/3"
>
test-3@test.com
</a>
</td>
<td
className="link-td"
>
<img
className="filter-table__avatar"
src="some/url/"
<a
href="org/teams/edit/3"
>
3
</a>
</td>
<td
className="text-right"
>
<DeleteButton
onConfirm={[Function]}
/>
</a>
</td>
<td
className="link-td"
</td>
</tr>
<tr
key="4"
>
<a
href="org/teams/edit/4"
<td
className="width-4 text-center link-td"
>
test-4
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/4"
<a
href="org/teams/edit/4"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</a>
</td>
<td
className="link-td"
>
test-4@test.com
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/4"
<a
href="org/teams/edit/4"
>
test-4
</a>
</td>
<td
className="link-td"
>
4
</a>
</td>
<td
className="text-right"
>
<DeleteButton
onConfirm={[Function]}
/>
</td>
</tr>
<tr
key="5"
>
<td
className="width-4 text-center link-td"
>
<a
href="org/teams/edit/5"
<a
href="org/teams/edit/4"
>
test-4@test.com
</a>
</td>
<td
className="link-td"
>
<img
className="filter-table__avatar"
src="some/url/"
<a
href="org/teams/edit/4"
>
4
</a>
</td>
<td
className="text-right"
>
<DeleteButton
onConfirm={[Function]}
/>
</a>
</td>
<td
className="link-td"
</td>
</tr>
<tr
key="5"
>
<a
href="org/teams/edit/5"
<td
className="width-4 text-center link-td"
>
test-5
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/5"
<a
href="org/teams/edit/5"
>
<img
className="filter-table__avatar"
src="some/url/"
/>
</a>
</td>
<td
className="link-td"
>
test-5@test.com
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/5"
<a
href="org/teams/edit/5"
>
test-5
</a>
</td>
<td
className="link-td"
>
5
</a>
</td>
<td
className="text-right"
>
<DeleteButton
onConfirm={[Function]}
/>
</td>
</tr>
</tbody>
</table>
<a
href="org/teams/edit/5"
>
test-5@test.com
</a>
</td>
<td
className="link-td"
>
<a
href="org/teams/edit/5"
>
5
</a>
</td>
<td
className="text-right"
>
<DeleteButton
onConfirm={[Function]}
/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</PageContents>
</Page>
`;

View File

@@ -11,7 +11,14 @@ jest.mock('../../core/app_events', () => ({
const setup = (propOverrides?: object) => {
const props: Props = {
navModel: {} as NavModel,
navModel: {
main: {
text: 'Configuration'
},
node: {
text: 'Users'
}
} as NavModel,
users: [] as OrgUser[],
invitees: [] as Invitee[],
searchQuery: '',

View File

@@ -2,15 +2,14 @@ import React, { PureComponent } from 'react';
import { hot } from 'react-hot-loader';
import { connect } from 'react-redux';
import Remarkable from 'remarkable';
import PageHeader from 'app/core/components/PageHeader/PageHeader';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
import Page from 'app/core/components/Page/Page';
import UsersActionBar from './UsersActionBar';
import UsersTable from './UsersTable';
import InviteesTable from './InviteesTable';
import { Invitee, NavModel, OrgUser } from 'app/types';
import appEvents from 'app/core/app_events';
import { loadUsers, loadInvitees, setUsersSearchQuery, updateUser, removeUser } from './state/actions';
import { getNavModel } from '../../core/selectors/navModel';
import { getNavModel } from 'app/core/selectors/navModel';
import { getInvitees, getUsers, getUsersSearchQuery } from './state/selectors';
export interface Props {
@@ -105,16 +104,17 @@ export class UsersListPage extends PureComponent<Props, State> {
const externalUserMngInfoHtml = this.externalUserMngInfoHtml;
return (
<div>
<PageHeader model={navModel} />
<div className="page-container page-body">
<Page navModel={navModel}>
<Page.Contents isLoading={!hasFetched}>
<>
<UsersActionBar onShowInvites={this.onShowInvites} showInvites={this.state.showInvites} />
{externalUserMngInfoHtml && (
<div className="grafana-info-box" dangerouslySetInnerHTML={{ __html: externalUserMngInfoHtml }} />
)}
{hasFetched ? this.renderTable() : <PageLoader pageName="Users" />}
</div>
</div>
{hasFetched && this.renderTable()}
</>
</Page.Contents>
</Page>
);
}
}

View File

@@ -1,4 +1,4 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
import { OrgUser } from 'app/types';
export interface Props {
@@ -7,7 +7,7 @@ export interface Props {
onRemoveUser: (user: OrgUser) => void;
}
const UsersTable: SFC<Props> = props => {
const UsersTable: FC<Props> = props => {
const { users, onRoleChange, onRemoveUser } = props;
return (

View File

@@ -1,12 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render List page 1`] = `
<div>
<PageHeader
model={Object {}}
/>
<div
className="page-container page-body"
<Page
navModel={
Object {
"main": Object {
"text": "Configuration",
},
"node": Object {
"text": "Users",
},
}
}
>
<PageContents
isLoading={false}
>
<Connect(UsersActionBar)
onShowInvites={[Function]}
@@ -17,25 +25,30 @@ exports[`Render should render List page 1`] = `
onRoleChange={[Function]}
users={Array []}
/>
</div>
</div>
</PageContents>
</Page>
`;
exports[`Render should render component 1`] = `
<div>
<PageHeader
model={Object {}}
/>
<div
className="page-container page-body"
<Page
navModel={
Object {
"main": Object {
"text": "Configuration",
},
"node": Object {
"text": "Users",
},
}
}
>
<PageContents
isLoading={true}
>
<Connect(UsersActionBar)
onShowInvites={[Function]}
showInvites={false}
/>
<PageLoader
pageName="Users"
/>
</div>
</div>
</PageContents>
</Page>
`;

View File

@@ -1,4 +1,4 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
@@ -14,7 +14,7 @@ export interface Props {
usedAlignmentPeriod: string;
}
export const AlignmentPeriods: SFC<Props> = ({
export const AlignmentPeriods: FC<Props> = ({
alignmentPeriod,
templateSrv,
onChange,

View File

@@ -1,4 +1,4 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
import _ from 'lodash';
import { MetricSelect } from 'app/core/components/Select/MetricSelect';
@@ -12,7 +12,7 @@ export interface Props {
perSeriesAligner: string;
}
export const Alignments: SFC<Props> = ({ perSeriesAligner, templateSrv, onChange, alignOptions }) => {
export const Alignments: FC<Props> = ({ perSeriesAligner, templateSrv, onChange, alignOptions }) => {
return (
<>
<div className="gf-form-group">

View File

@@ -1,6 +1,6 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
export const AnnotationsHelp: SFC = () => {
export const AnnotationsHelp: FC = () => {
return (
<div className="gf-form grafana-info-box" style={{ padding: 0 }}>
<pre className="gf-form-pre alert alert-info" style={{ marginRight: 0 }}>

View File

@@ -1,4 +1,4 @@
import React, { SFC } from 'react';
import React, { FC } from 'react';
interface Props {
onValueChange: (e) => void;
@@ -7,7 +7,7 @@ interface Props {
label: string;
}
const SimpleSelect: SFC<Props> = props => {
const SimpleSelect: FC<Props> = props => {
const { label, onValueChange, value, options } = props;
return (
<div className="gf-form max-width-21">

View File

@@ -1,8 +1,8 @@
import React, { PureComponent } from 'react';
import { GaugeOptions, PanelOptionsProps, PanelOptionsGroup } from '@grafana/ui';
import { FormField, PanelOptionsProps, PanelOptionsGroup } from '@grafana/ui';
import { Switch } from 'app/core/components/Switch/Switch';
import { Label } from '../../../core/components/Label/Label';
import { GaugeOptions } from './types';
export default class GaugeOptionsEditor extends PureComponent<PanelOptionsProps<GaugeOptions>> {
onToggleThresholdLabels = () =>
@@ -21,14 +21,8 @@ export default class GaugeOptionsEditor extends PureComponent<PanelOptionsProps<
return (
<PanelOptionsGroup title="Gauge">
<div className="gf-form">
<Label width={8}>Min value</Label>
<input type="text" className="gf-form-input width-12" onChange={this.onMinValueChange} value={minValue} />
</div>
<div className="gf-form">
<Label width={8}>Max value</Label>
<input type="text" className="gf-form-input width-12" onChange={this.onMaxValueChange} value={maxValue} />
</div>
<FormField label="Min value" labelWidth={8} onChange={this.onMinValueChange} value={minValue} />
<FormField label="Max value" labelWidth={8} onChange={this.onMaxValueChange} value={maxValue} />
<Switch
label="Show labels"
labelClass="width-8"

View File

@@ -1,8 +1,9 @@
import React, { PureComponent } from 'react';
import { GaugeOptions, PanelProps, NullValueMode } from '@grafana/ui';
import { PanelProps, NullValueMode } from '@grafana/ui';
import { getTimeSeriesVMs } from 'app/viz/state/timeSeries';
import Gauge from 'app/viz/Gauge';
import { GaugeOptions } from './types';
interface Props extends PanelProps<GaugeOptions> {}

View File

@@ -1,16 +1,17 @@
import React, { PureComponent } from 'react';
import {
BasicGaugeColor,
GaugeOptions,
PanelOptionsProps,
ThresholdsEditor,
Threshold,
PanelOptionsGrid,
ValueMappingsEditor,
ValueMapping,
} from '@grafana/ui';
import ValueOptions from 'app/plugins/panel/gauge/ValueOptions';
import ValueMappings from 'app/plugins/panel/gauge/ValueMappings';
import GaugeOptionsEditor from './GaugeOptionsEditor';
import { GaugeOptions } from './types';
export const defaultProps = {
options: {
@@ -24,7 +25,7 @@ export const defaultProps = {
decimals: 0,
stat: 'avg',
unit: 'none',
mappings: [],
valueMappings: [],
thresholds: [],
},
};
@@ -32,7 +33,17 @@ export const defaultProps = {
export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<GaugeOptions>> {
static defaultProps = defaultProps;
onThresholdsChanged = (thresholds: Threshold[]) => this.props.onChange({ ...this.props.options, thresholds });
onThresholdsChanged = (thresholds: Threshold[]) =>
this.props.onChange({
...this.props.options,
thresholds,
});
onValueMappingsChanged = (valueMappings: ValueMapping[]) =>
this.props.onChange({
...this.props.options,
valueMappings,
});
render() {
const { onChange, options } = this.props;
@@ -44,7 +55,7 @@ export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<G
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
</PanelOptionsGrid>
<ValueMappings onChange={onChange} options={options} />
<ValueMappingsEditor onChange={this.onValueMappingsChanged} valueMappings={options.valueMappings} />
</>
);
}

View File

@@ -1,9 +1,7 @@
import React, { PureComponent } from 'react';
import { GaugeOptions, PanelOptionsProps, PanelOptionsGroup } from '@grafana/ui';
import { Label } from 'app/core/components/Label/Label';
import { Select} from '@grafana/ui';
import { FormField, FormLabel, PanelOptionsProps, PanelOptionsGroup, Select } from '@grafana/ui';
import UnitPicker from 'app/core/components/Select/UnitPicker';
import { GaugeOptions } from './types';
const statOptions = [
{ value: 'min', label: 'Min' },
@@ -42,7 +40,7 @@ export default class ValueOptions extends PureComponent<PanelOptionsProps<GaugeO
return (
<PanelOptionsGroup title="Value">
<div className="gf-form">
<Label width={labelWidth}>Stat</Label>
<FormLabel width={labelWidth}>Stat</FormLabel>
<Select
width={12}
options={statOptions}
@@ -51,27 +49,19 @@ export default class ValueOptions extends PureComponent<PanelOptionsProps<GaugeO
/>
</div>
<div className="gf-form">
<Label width={labelWidth}>Unit</Label>
<FormLabel width={labelWidth}>Unit</FormLabel>
<UnitPicker defaultValue={unit} onChange={this.onUnitChange} />
</div>
<div className="gf-form">
<Label width={labelWidth}>Decimals</Label>
<input
className="gf-form-input width-12"
type="number"
placeholder="auto"
value={decimals || ''}
onChange={this.onDecimalChange}
/>
</div>
<div className="gf-form">
<Label width={labelWidth}>Prefix</Label>
<input className="gf-form-input width-12" type="text" value={prefix || ''} onChange={this.onPrefixChange} />
</div>
<div className="gf-form">
<Label width={labelWidth}>Suffix</Label>
<input className="gf-form-input width-12" type="text" value={suffix || ''} onChange={this.onSuffixChange} />
</div>
<FormField
label="Decimals"
labelWidth={labelWidth}
placeholder="auto"
onChange={this.onDecimalChange}
value={decimals || ''}
type="number"
/>
<FormField label="Prefix" labelWidth={labelWidth} onChange={this.onPrefixChange} value={prefix || ''} />
<FormField label="Suffix" labelWidth={labelWidth} onChange={this.onSuffixChange} value={suffix || ''} />
</PanelOptionsGroup>
);
}

View File

@@ -1,2 +1,16 @@
import { Threshold, ValueMapping } from '@grafana/ui';
export interface GaugeOptions {
baseColor: string;
decimals: number;
valueMappings: ValueMapping[];
maxValue: number;
minValue: number;
prefix: string;
showThresholdLabels: boolean;
showThresholdMarkers: boolean;
stat: string;
suffix: string;
thresholds: Threshold[];
unit: string;
}

View File

@@ -12,7 +12,7 @@ const setup = (propOverrides?: object) => {
const props: Props = {
baseColor: BasicGaugeColor.Green,
maxValue: 100,
mappings: [],
valueMappings: [],
minValue: 0,
prefix: '',
showThresholdMarkers: true,

View File

@@ -1,6 +1,6 @@
import React, { PureComponent } from 'react';
import $ from 'jquery';
import { BasicGaugeColor, Threshold, TimeSeriesVMs, RangeMap, ValueMap, MappingType } from '@grafana/ui';
import { BasicGaugeColor, Threshold, TimeSeriesVMs, MappingType, ValueMapping } from '@grafana/ui';
import config from '../core/config';
import kbn from '../core/utils/kbn';
@@ -9,7 +9,7 @@ export interface Props {
baseColor: string;
decimals: number;
height: number;
mappings: Array<RangeMap | ValueMap>;
valueMappings: ValueMapping[];
maxValue: number;
minValue: number;
prefix: string;
@@ -29,7 +29,7 @@ export class Gauge extends PureComponent<Props> {
static defaultProps = {
baseColor: BasicGaugeColor.Green,
maxValue: 100,
mappings: [],
valueMappings: [],
minValue: 0,
prefix: '',
showThresholdMarkers: true,
@@ -64,20 +64,17 @@ export class Gauge extends PureComponent<Props> {
}
})[0];
return {
rangeMap,
valueMap,
};
return { rangeMap, valueMap };
}
formatValue(value) {
const { decimals, mappings, prefix, suffix, unit } = this.props;
const { decimals, valueMappings, prefix, suffix, unit } = this.props;
const formatFunc = kbn.valueFormats[unit];
const formattedValue = formatFunc(value, decimals);
if (mappings.length > 0) {
const { rangeMap, valueMap } = this.formatWithMappings(mappings, formattedValue);
if (valueMappings.length > 0) {
const { rangeMap, valueMap } = this.formatWithMappings(valueMappings, formattedValue);
if (valueMap) {
return `${prefix} ${valueMap} ${suffix}`;
@@ -148,10 +145,7 @@ export class Gauge extends PureComponent<Props> {
color: index === 0 ? threshold.color : thresholds[index].color,
};
}),
{
value: maxValue,
color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor,
},
{ value: maxValue, color: thresholds.length > 0 ? BasicGaugeColor.Red : baseColor },
];
const options = {
@@ -184,19 +178,14 @@ export class Gauge extends PureComponent<Props> {
formatter: () => {
return this.formatValue(value);
},
font: {
size: fontSize,
family: '"Helvetica Neue", Helvetica, Arial, sans-serif',
},
font: { size: fontSize, family: '"Helvetica Neue", Helvetica, Arial, sans-serif' },
},
show: true,
},
},
};
const plotSeries = {
data: [[0, value]],
};
const plotSeries = { data: [[0, value]] };
try {
$.plot(this.canvasElement, [plotSeries], options);

View File

@@ -1,4 +1,4 @@
// DEPENDENCIES
// DEPENDENCIES
@import '../../node_modules/react-table/react-table.css';
// VENDOR
@@ -97,7 +97,6 @@
@import 'components/add_data_source.scss';
@import 'components/page_loader';
@import 'components/toggle_button_group';
@import 'components/value-mappings';
@import 'components/popover-box';
// LOAD @grafana/ui components

View File

@@ -38,6 +38,14 @@
}
}
.is-react .footer {
display: none;
}
.is-react .custom-scrollbars .footer {
display: block;
}
// Keeping footer inside the graphic on Login screen
.login-page {
.footer {

View File

@@ -20,7 +20,23 @@
}
}
.page-scrollbar-wrapper {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
}
.page-scrollbar-content {
display: flex;
min-height: 100%;
flex-direction: column;
width: 100%;
}
.page-container {
flex-grow: 1;
width: 100%;
margin-left: auto;
margin-right: auto;
padding-left: $spacer*2;
@@ -78,7 +94,6 @@
.page-body {
padding-top: $spacer*2;
min-height: 500px;
}
.page-heading {

View File

@@ -8,8 +8,6 @@ RUN git clone https://github.com/aptly-dev/aptly $GOPATH/src/github.com/aptly-de
FROM circleci/python:2.7-stretch
ENV PATH=$PATH:/opt/google-cloud-sdk/bin
USER root
RUN pip install awscli && \
@@ -18,7 +16,9 @@ RUN pip install awscli && \
apt update && \
apt install -y createrepo expect && \
apt-get autoremove -y && \
rm -rf /var/lib/apt/lists/*
rm -rf /var/lib/apt/lists/* && \
ln -s /opt/google-cloud-sdk/bin/gsutil /usr/bin/gsutil && \
ln -s /opt/google-cloud-sdk/bin/gcloud /usr/bin/gcloud
COPY --from=0 /go/bin/aptly /usr/local/bin/aptly

View File

@@ -1,6 +1,6 @@
#!/bin/bash
_version="1.1.0"
_version="1.2.0"
_tag="grafana/grafana-ci-deploy:${_version}"
docker build -t $_tag .