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

View File

@@ -7,7 +7,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
Object { Object {
"height": "auto", "height": "auto",
"maxHeight": "100%", "maxHeight": "100%",
"minHeight": "0", "minHeight": 0,
"overflow": "hidden", "overflow": "hidden",
"position": "relative", "position": "relative",
"width": "100%", "width": "100%",
@@ -24,7 +24,7 @@ exports[`CustomScrollbar renders correctly 1`] = `
"marginBottom": 0, "marginBottom": 0,
"marginRight": 0, "marginRight": 0,
"maxHeight": "calc(100% + 0px)", "maxHeight": "calc(100% + 0px)",
"minHeight": "calc(0 + 0px)", "minHeight": 0,
"overflow": "scroll", "overflow": "scroll",
"position": "relative", "position": "relative",
"right": undefined, "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 { .panel-options-group__header {
padding: 4px 20px; padding: 4px 8px;
font-size: 1.1rem; font-size: 1.1rem;
background: $panel-options-group-header-bg; background: $panel-options-group-header-bg;
position: relative; position: relative;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,3 +6,5 @@
@import 'PanelOptionsGroup/PanelOptionsGroup'; @import 'PanelOptionsGroup/PanelOptionsGroup';
@import 'PanelOptionsGrid/PanelOptionsGrid'; @import 'PanelOptionsGrid/PanelOptionsGrid';
@import 'ColorPicker/ColorPicker'; @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 { NoOptionsMessage } from './Select/NoOptionsMessage';
export { default as resetSelectStyles } from './Select/resetSelectStyles'; export { default as resetSelectStyles } from './Select/resetSelectStyles';
// Forms
export { FormLabel } from './FormLabel/FormLabel';
export { FormField } from './FormField/FormField';
export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder'; export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder';
export { ColorPicker } from './ColorPicker/ColorPicker'; export { ColorPicker } from './ColorPicker/ColorPicker';
export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover'; export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover';
export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker'; export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker';
export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor'; export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor';
export { GfFormLabel } from './GfFormLabel/GfFormLabel';
export { Graph } from './Graph/Graph'; export { Graph } from './Graph/Graph';
export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup'; export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup';
export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid'; 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 './series';
export * from './time'; export * from './time';
export * from './panel'; export * from './panel';
export * from './gauge';

View File

@@ -56,6 +56,8 @@ interface BaseMap {
type: MappingType; type: MappingType;
} }
export type ValueMapping = ValueMap | RangeMap;
export interface ValueMap extends BaseMap { export interface ValueMap extends BaseMap {
value: string; 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'; import Transition from 'react-transition-group/Transition';
interface Props { interface Props {
@@ -8,7 +8,7 @@ interface Props {
unmountOnExit?: boolean; unmountOnExit?: boolean;
} }
export const FadeIn: SFC<Props> = props => { export const FadeIn: FC<Props> = props => {
const defaultStyle = { const defaultStyle = {
transition: `opacity ${props.duration}ms linear`, transition: `opacity ${props.duration}ms linear`,
opacity: 0, 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; export type LayoutMode = LayoutModes.Grid | LayoutModes.List;
@@ -12,7 +12,7 @@ interface Props {
onLayoutModeChanged: (mode: LayoutMode) => {}; onLayoutModeChanged: (mode: LayoutMode) => {};
} }
const LayoutSelector: SFC<Props> = props => { const LayoutSelector: FC<Props> = props => {
const { mode, onLayoutModeChanged } = props; const { mode, onLayoutModeChanged } = props;
return ( return (
<div className="layout-selector"> <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 { NavModel, NavModelItem } from 'app/types';
import classNames from 'classnames'; import classNames from 'classnames';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
@@ -12,8 +12,8 @@ const SelectNav = ({ main, customCss }: { main: NavModelItem; customCss: string
return navItem.active === true; return navItem.active === true;
}); });
const gotoUrl = evt => { const gotoUrl = (evt: FormEvent) => {
const element = evt.target; const element = evt.target as HTMLSelectElement;
const url = element.options[element.selectedIndex].value; const url = element.options[element.selectedIndex].value;
appEvents.emit('location-change', { href: url }); appEvents.emit('location-change', { href: url });
}; };

View File

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

View File

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

View File

@@ -1,10 +1,10 @@
import React, { SFC } from 'react'; import React, { FC } from 'react';
export interface Props { export interface Props {
child: any; child: any;
} }
const DropDownChild: SFC<Props> = props => { const DropDownChild: FC<Props> = props => {
const { child } = props; const { child } = props;
const listItemClassName = child.divider ? 'divider' : ''; 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'; import DropDownChild from './DropDownChild';
interface Props { interface Props {
link: any; link: any;
} }
const SideMenuDropDown: SFC<Props> = props => { const SideMenuDropDown: FC<Props> = props => {
const { link } = props; const { link } = props;
return ( return (
<ul className="dropdown-menu dropdown-menu--sidemenu" role="menu"> <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)}`; const loginUrl = `login?redirect=${encodeURIComponent(window.location.pathname)}`;
return ( return (
<div className="sidemenu-item"> <div className="sidemenu-item">

View File

@@ -1,9 +1,9 @@
import React, { SFC } from 'react'; import React, { FC } from 'react';
import _ from 'lodash'; import _ from 'lodash';
import TopSectionItem from './TopSectionItem'; import TopSectionItem from './TopSectionItem';
import config from '../../config'; import config from '../../config';
const TopSection: SFC<any> = () => { const TopSection: FC<any> = () => {
const navTree = _.cloneDeep(config.bootData.navTree); const navTree = _.cloneDeep(config.bootData.navTree);
const mainLinks = _.filter(navTree, item => !item.hideFromMenu); 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'; import SideMenuDropDown from './SideMenuDropDown';
export interface Props { export interface Props {
link: any; link: any;
} }
const TopSectionItem: SFC<Props> = props => { const TopSectionItem: FC<Props> = props => {
const { link } = props; const { link } = props;
return ( return (
<div className="sidemenu-item dropdown"> <div className="sidemenu-item dropdown">

View File

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

View File

@@ -41,3 +41,7 @@ export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel)
return getNotFoundModel(); 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 setup = (propOverrides?: object) => {
const props: Props = { const props: Props = {
navModel: {} as NavModel, navModel: {
main: {
text: 'Configuration'
},
node: {
text: 'Api Keys'
}
} as NavModel,
apiKeys: [] as ApiKey[], apiKeys: [] as ApiKey[],
searchQuery: '', searchQuery: '',
hasFetched: false, hasFetched: false,

View File

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

View File

@@ -1,132 +1,152 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Render should render API keys table if there are any keys 1`] = ` exports[`Render should render API keys table if there are any keys 1`] = `
<div> <Page
<PageHeader navModel={
model={Object {}} Object {
"main": Object {
"text": "Configuration",
},
"node": Object {
"text": "Api Keys",
},
}
}
>
<PageContents
isLoading={true}
/> />
<PageLoader </Page>
pageName="Api keys"
/>
</div>
`; `;
exports[`Render should render CTA if there are no API keys 1`] = ` exports[`Render should render CTA if there are no API keys 1`] = `
<div> <Page
<PageHeader navModel={
model={Object {}} Object {
/> "main": Object {
<div "text": "Configuration",
className="page-container page-body" },
"node": Object {
"text": "Api Keys",
},
}
}
>
<PageContents
isLoading={false}
> >
<EmptyListCTA <div
model={ className="page-container page-body"
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 <EmptyListCTA
className="cta-form" 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 <div
className="cta-form__close btn btn-transparent" className="cta-form"
onClick={[Function]}
> >
<i <button
className="fa fa-close" className="cta-form__close btn btn-transparent"
/> onClick={[Function]}
</button> >
<h5> <i
Add API Key className="fa fa-close"
</h5> />
<form </button>
className="gf-form-group" <h5>
onSubmit={[Function]} Add API Key
> </h5>
<div <form
className="gf-form-inline" className="gf-form-group"
onSubmit={[Function]}
> >
<div <div
className="gf-form max-width-21" className="gf-form-inline"
> >
<span <div
className="gf-form-label" className="gf-form max-width-21"
> >
Key name <span
</span> className="gf-form-label"
<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"
> >
<option Key name
key="Viewer" </span>
label="Viewer" <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" value="Viewer"
> >
Viewer <option
</option> key="Viewer"
<option label="Viewer"
key="Editor" value="Viewer"
label="Editor" >
value="Editor" Viewer
> </option>
Editor <option
</option> key="Editor"
<option label="Editor"
key="Admin" value="Editor"
label="Admin" >
value="Admin" Editor
> </option>
Admin <option
</option> key="Admin"
</select> label="Admin"
</span> value="Admin"
</div> >
<div Admin
className="gf-form" </option>
> </select>
<button </span>
className="btn gf-form-btn btn-success" </div>
<div
className="gf-form"
> >
Add <button
</button> className="btn gf-form-btn btn-success"
>
Add
</button>
</div>
</div> </div>
</div> </form>
</form> </div>
</div> </Component>
</Component> </div>
</div> </PageContents>
</div> </Page>
`; `;

View File

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

View File

@@ -1,4 +1,4 @@
import React, { SFC } from 'react'; import React, { FC } from 'react';
import { Tooltip } from '@grafana/ui'; import { Tooltip } from '@grafana/ui';
interface Props { interface Props {
@@ -10,7 +10,7 @@ interface Props {
tooltipInfo?: any; 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 = ( const dsOption = (
<div className="gf-form gf-form--flex-end"> <div className="gf-form gf-form--flex-end">
<label className="gf-form-label">{label}</label> <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 { EventsWithValidation } from 'app/core/components/Form/Input';
import { InputStatus } from 'app/core/components/Form/Input'; import { InputStatus } from 'app/core/components/Form/Input';
import DataSourceOption from './DataSourceOption'; import DataSourceOption from './DataSourceOption';
import { GfFormLabel } from '@grafana/ui'; import { FormLabel } from '@grafana/ui';
// Types // Types
import { PanelModel } from '../panel_model'; import { PanelModel } from '../panel_model';
@@ -164,7 +164,7 @@ export class QueryOptions extends PureComponent<Props, State> {
{this.renderOptions()} {this.renderOptions()}
<div className="gf-form"> <div className="gf-form">
<GfFormLabel>Relative time</GfFormLabel> <FormLabel>Relative time</FormLabel>
<Input <Input
type="text" type="text"
className="width-6" className="width-6"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import React, { SFC } from 'react'; import React, { FC } from 'react';
export interface Props { export interface Props {
isReadOnly: boolean; isReadOnly: boolean;
@@ -6,7 +6,7 @@ export interface Props {
onSubmit: (event) => void; onSubmit: (event) => void;
} }
const ButtonRow: SFC<Props> = ({ isReadOnly, onDelete, onSubmit }) => { const ButtonRow: FC<Props> = ({ isReadOnly, onDelete, onSubmit }) => {
return ( return (
<div className="gf-form-button-row"> <div className="gf-form-button-row">
<button type="submit" className="btn btn-success" disabled={isReadOnly} onClick={event => onSubmit(event)}> <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 { interface Props {
message: any; message: any;
} }
export const Alert: SFC<Props> = props => { export const Alert: FC<Props> = props => {
const { message } = props; const { message } = props;
return ( return (
<div className="gf-form-group section"> <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; let instance;
if (datasource) { if (datasource) {
instance = await getDatasourceSrv().get(datasource); try {
} else { 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(); instance = await getDatasourceSrv().get();
} }
dispatch(loadDatasource(exploreId, instance)); dispatch(loadDatasource(exploreId, instance));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; 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 { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences';
import { updateTeam } from './state/actions'; import { updateTeam } from './state/actions';
import { getRouteParamsId } from 'app/core/selectors/location'; 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> <h3 className="page-sub-heading">Team Settings</h3>
<form name="teamDetailsForm" className="gf-form-group" onSubmit={this.onUpdate}> <form name="teamDetailsForm" className="gf-form-group" onSubmit={this.onUpdate}>
<div className="gf-form max-width-30"> <div className="gf-form max-width-30">
<Label>Name</Label> <FormLabel>Name</FormLabel>
<input <input
type="text" type="text"
required required
@@ -62,9 +62,9 @@ export class TeamSettings extends React.Component<Props, State> {
</div> </div>
<div className="gf-form max-width-30"> <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 Email
</Label> </FormLabel>
<input <input
type="email" type="email"
className="gf-form-input max-width-22" className="gf-form-input max-width-22"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
import React, { SFC } from 'react'; import React, { FC } from 'react';
import _ from 'lodash'; import _ from 'lodash';
import { MetricSelect } from 'app/core/components/Select/MetricSelect'; import { MetricSelect } from 'app/core/components/Select/MetricSelect';
@@ -12,7 +12,7 @@ export interface Props {
perSeriesAligner: string; perSeriesAligner: string;
} }
export const Alignments: SFC<Props> = ({ perSeriesAligner, templateSrv, onChange, alignOptions }) => { export const Alignments: FC<Props> = ({ perSeriesAligner, templateSrv, onChange, alignOptions }) => {
return ( return (
<> <>
<div className="gf-form-group"> <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 ( return (
<div className="gf-form grafana-info-box" style={{ padding: 0 }}> <div className="gf-form grafana-info-box" style={{ padding: 0 }}>
<pre className="gf-form-pre alert alert-info" style={{ marginRight: 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 { interface Props {
onValueChange: (e) => void; onValueChange: (e) => void;
@@ -7,7 +7,7 @@ interface Props {
label: string; label: string;
} }
const SimpleSelect: SFC<Props> = props => { const SimpleSelect: FC<Props> = props => {
const { label, onValueChange, value, options } = props; const { label, onValueChange, value, options } = props;
return ( return (
<div className="gf-form max-width-21"> <div className="gf-form max-width-21">

View File

@@ -1,8 +1,8 @@
import React, { PureComponent } from 'react'; 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 { 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>> { export default class GaugeOptionsEditor extends PureComponent<PanelOptionsProps<GaugeOptions>> {
onToggleThresholdLabels = () => onToggleThresholdLabels = () =>
@@ -21,14 +21,8 @@ export default class GaugeOptionsEditor extends PureComponent<PanelOptionsProps<
return ( return (
<PanelOptionsGroup title="Gauge"> <PanelOptionsGroup title="Gauge">
<div className="gf-form"> <FormField label="Min value" labelWidth={8} onChange={this.onMinValueChange} value={minValue} />
<Label width={8}>Min value</Label> <FormField label="Max value" labelWidth={8} onChange={this.onMaxValueChange} value={maxValue} />
<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>
<Switch <Switch
label="Show labels" label="Show labels"
labelClass="width-8" labelClass="width-8"

View File

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

View File

@@ -1,16 +1,17 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { import {
BasicGaugeColor, BasicGaugeColor,
GaugeOptions,
PanelOptionsProps, PanelOptionsProps,
ThresholdsEditor, ThresholdsEditor,
Threshold, Threshold,
PanelOptionsGrid, PanelOptionsGrid,
ValueMappingsEditor,
ValueMapping,
} from '@grafana/ui'; } from '@grafana/ui';
import ValueOptions from 'app/plugins/panel/gauge/ValueOptions'; import ValueOptions from 'app/plugins/panel/gauge/ValueOptions';
import ValueMappings from 'app/plugins/panel/gauge/ValueMappings';
import GaugeOptionsEditor from './GaugeOptionsEditor'; import GaugeOptionsEditor from './GaugeOptionsEditor';
import { GaugeOptions } from './types';
export const defaultProps = { export const defaultProps = {
options: { options: {
@@ -24,7 +25,7 @@ export const defaultProps = {
decimals: 0, decimals: 0,
stat: 'avg', stat: 'avg',
unit: 'none', unit: 'none',
mappings: [], valueMappings: [],
thresholds: [], thresholds: [],
}, },
}; };
@@ -32,7 +33,17 @@ export const defaultProps = {
export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<GaugeOptions>> { export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<GaugeOptions>> {
static defaultProps = defaultProps; 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() { render() {
const { onChange, options } = this.props; const { onChange, options } = this.props;
@@ -44,7 +55,7 @@ export default class GaugePanelOptions extends PureComponent<PanelOptionsProps<G
<ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} /> <ThresholdsEditor onChange={this.onThresholdsChanged} thresholds={options.thresholds} />
</PanelOptionsGrid> </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 React, { PureComponent } from 'react';
import { GaugeOptions, PanelOptionsProps, PanelOptionsGroup } from '@grafana/ui'; import { FormField, FormLabel, PanelOptionsProps, PanelOptionsGroup, Select } from '@grafana/ui';
import { Label } from 'app/core/components/Label/Label';
import { Select} from '@grafana/ui';
import UnitPicker from 'app/core/components/Select/UnitPicker'; import UnitPicker from 'app/core/components/Select/UnitPicker';
import { GaugeOptions } from './types';
const statOptions = [ const statOptions = [
{ value: 'min', label: 'Min' }, { value: 'min', label: 'Min' },
@@ -42,7 +40,7 @@ export default class ValueOptions extends PureComponent<PanelOptionsProps<GaugeO
return ( return (
<PanelOptionsGroup title="Value"> <PanelOptionsGroup title="Value">
<div className="gf-form"> <div className="gf-form">
<Label width={labelWidth}>Stat</Label> <FormLabel width={labelWidth}>Stat</FormLabel>
<Select <Select
width={12} width={12}
options={statOptions} options={statOptions}
@@ -51,27 +49,19 @@ export default class ValueOptions extends PureComponent<PanelOptionsProps<GaugeO
/> />
</div> </div>
<div className="gf-form"> <div className="gf-form">
<Label width={labelWidth}>Unit</Label> <FormLabel width={labelWidth}>Unit</FormLabel>
<UnitPicker defaultValue={unit} onChange={this.onUnitChange} /> <UnitPicker defaultValue={unit} onChange={this.onUnitChange} />
</div> </div>
<div className="gf-form"> <FormField
<Label width={labelWidth}>Decimals</Label> label="Decimals"
<input labelWidth={labelWidth}
className="gf-form-input width-12" placeholder="auto"
type="number" onChange={this.onDecimalChange}
placeholder="auto" value={decimals || ''}
value={decimals || ''} type="number"
onChange={this.onDecimalChange} />
/> <FormField label="Prefix" labelWidth={labelWidth} onChange={this.onPrefixChange} value={prefix || ''} />
</div> <FormField label="Suffix" labelWidth={labelWidth} onChange={this.onSuffixChange} value={suffix || ''} />
<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>
</PanelOptionsGroup> </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 = { const props: Props = {
baseColor: BasicGaugeColor.Green, baseColor: BasicGaugeColor.Green,
maxValue: 100, maxValue: 100,
mappings: [], valueMappings: [],
minValue: 0, minValue: 0,
prefix: '', prefix: '',
showThresholdMarkers: true, showThresholdMarkers: true,

View File

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

View File

@@ -1,4 +1,4 @@
// DEPENDENCIES // DEPENDENCIES
@import '../../node_modules/react-table/react-table.css'; @import '../../node_modules/react-table/react-table.css';
// VENDOR // VENDOR
@@ -97,7 +97,6 @@
@import 'components/add_data_source.scss'; @import 'components/add_data_source.scss';
@import 'components/page_loader'; @import 'components/page_loader';
@import 'components/toggle_button_group'; @import 'components/toggle_button_group';
@import 'components/value-mappings';
@import 'components/popover-box'; @import 'components/popover-box';
// LOAD @grafana/ui components // 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 // Keeping footer inside the graphic on Login screen
.login-page { .login-page {
.footer { .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 { .page-container {
flex-grow: 1;
width: 100%;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
padding-left: $spacer*2; padding-left: $spacer*2;
@@ -78,7 +94,6 @@
.page-body { .page-body {
padding-top: $spacer*2; padding-top: $spacer*2;
min-height: 500px;
} }
.page-heading { .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 FROM circleci/python:2.7-stretch
ENV PATH=$PATH:/opt/google-cloud-sdk/bin
USER root USER root
RUN pip install awscli && \ RUN pip install awscli && \
@@ -18,7 +16,9 @@ RUN pip install awscli && \
apt update && \ apt update && \
apt install -y createrepo expect && \ apt install -y createrepo expect && \
apt-get autoremove -y && \ 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 COPY --from=0 /go/bin/aptly /usr/local/bin/aptly

View File

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