mirror of
https://github.com/grafana/grafana.git
synced 2025-02-09 23:16:16 -06:00
Merge branch 'master' into 15330-vizpicker-red-when-0-hits
This commit is contained in:
commit
124b3486eb
8
.prettierignore
Normal file
8
.prettierignore
Normal file
@ -0,0 +1,8 @@
|
||||
.git
|
||||
.github
|
||||
dist/
|
||||
pkg/
|
||||
node_modules
|
||||
public/vendor/
|
||||
vendor/
|
||||
|
31
package.json
31
package.json
@ -65,7 +65,7 @@
|
||||
"html-loader": "^0.5.1",
|
||||
"html-webpack-harddisk-plugin": "^0.2.0",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"husky": "^0.14.3",
|
||||
"husky": "^1.3.1",
|
||||
"jest": "^23.6.0",
|
||||
"jest-date-mock": "^1.0.6",
|
||||
"lint-staged": "^8.1.3",
|
||||
@ -120,8 +120,8 @@
|
||||
"typecheck": "tsc --noEmit",
|
||||
"jest": "jest --notify --watch",
|
||||
"api-tests": "jest --notify --watch --config=tests/api/jest.js",
|
||||
"precommit": "grunt precommit",
|
||||
"storybook": "cd packages/grafana-ui && yarn storybook"
|
||||
"storybook": "cd packages/grafana-ui && yarn storybook",
|
||||
"prettier:check": "prettier -- --list-different \"**/*.{ts,tsx,scss}\""
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
@ -129,18 +129,8 @@
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx}": [
|
||||
"prettier --write",
|
||||
"git add"
|
||||
],
|
||||
"*.scss": [
|
||||
"prettier --write",
|
||||
"git add"
|
||||
],
|
||||
"*pkg/**/*.go": [
|
||||
"gofmt -w -s",
|
||||
"git add"
|
||||
]
|
||||
"*.{ts,tsx,json,scss}": ["prettier --write", "git add"],
|
||||
"*pkg/**/*.go": ["gofmt -w -s", "git add"]
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "es5",
|
||||
@ -151,6 +141,7 @@
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "^7.0.0",
|
||||
"@torkelo/react-select": "2.1.1",
|
||||
"@types/reselect": "^2.2.0",
|
||||
"angular": "1.6.6",
|
||||
"angular-bindonce": "0.3.1",
|
||||
"angular-native-dragdrop": "1.2.2",
|
||||
@ -187,6 +178,7 @@
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.3.0",
|
||||
"remarkable": "^1.7.1",
|
||||
"reselect": "^4.0.0",
|
||||
"rst2html": "github:thoward/rst2html#990cb89",
|
||||
"rxjs": "^6.3.3",
|
||||
"slate": "^0.33.4",
|
||||
@ -203,12 +195,7 @@
|
||||
"**/@types/react": "16.7.6"
|
||||
},
|
||||
"workspaces": {
|
||||
"packages": [
|
||||
"packages/*"
|
||||
],
|
||||
"nohoist": [
|
||||
"**/@types/*",
|
||||
"**/@types/*/**"
|
||||
]
|
||||
"packages": ["packages/*"],
|
||||
"nohoist": ["**/@types/*", "**/@types/*/**"]
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
import { SeriesColorPicker, ColorPicker } from './ColorPicker';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
|
@ -30,7 +30,7 @@ export const warnAboutColorPickerPropsDeprecation = (componentName: string, prop
|
||||
|
||||
export const colorPickerFactory = <T extends ColorPickerProps>(
|
||||
popover: React.ComponentType<T>,
|
||||
displayName = 'ColorPicker',
|
||||
displayName = 'ColorPicker'
|
||||
) => {
|
||||
return class ColorPicker extends Component<T, any> {
|
||||
static displayName = displayName;
|
||||
|
@ -15,7 +15,7 @@ describe('ColorPickerPopover', () => {
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should render provided color as selected if color provided by name', () => {
|
||||
const wrapper = mount(<ColorPickerPopover color={BasicGreen.name} onChange={() => {}} theme={getTheme()}/>);
|
||||
const wrapper = mount(<ColorPickerPopover color={BasicGreen.name} onChange={() => {}} theme={getTheme()} />);
|
||||
const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
|
||||
const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false);
|
||||
|
||||
@ -25,7 +25,9 @@ describe('ColorPickerPopover', () => {
|
||||
});
|
||||
|
||||
it('should render provided color as selected if color provided by hex', () => {
|
||||
const wrapper = mount(<ColorPickerPopover color={BasicGreen.variants.dark} onChange={() => {}} theme={getTheme()} />);
|
||||
const wrapper = mount(
|
||||
<ColorPickerPopover color={BasicGreen.variants.dark} onChange={() => {}} theme={getTheme()} />
|
||||
);
|
||||
const selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
|
||||
const notSelectedSwatches = wrapper.find(ColorSwatch).filterWhere(node => node.prop('isSelected') === false);
|
||||
|
||||
@ -46,7 +48,11 @@ describe('ColorPickerPopover', () => {
|
||||
|
||||
it('should pass hex color value to onChange prop by default', () => {
|
||||
wrapper = mount(
|
||||
<ColorPickerPopover color={BasicGreen.variants.dark} onChange={onChangeSpy} theme={getTheme(GrafanaThemeType.Light)} />
|
||||
<ColorPickerPopover
|
||||
color={BasicGreen.variants.dark}
|
||||
onChange={onChangeSpy}
|
||||
theme={getTheme(GrafanaThemeType.Light)}
|
||||
/>
|
||||
);
|
||||
const basicBlueSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicBlue.name);
|
||||
|
||||
|
@ -119,4 +119,3 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,7 +7,6 @@ import { getTheme } from '../../themes';
|
||||
import { GrafanaThemeType } from '../../types';
|
||||
|
||||
describe('NamedColorsPalette', () => {
|
||||
|
||||
const BasicGreen = getColorDefinitionByName('green');
|
||||
|
||||
describe('theme support for named colors', () => {
|
||||
@ -23,13 +22,15 @@ describe('NamedColorsPalette', () => {
|
||||
expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);
|
||||
|
||||
wrapper.unmount();
|
||||
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} theme={getTheme(GrafanaThemeType.Light)} onChange={() => {}} />);
|
||||
wrapper = mount(
|
||||
<NamedColorsPalette color={BasicGreen.name} theme={getTheme(GrafanaThemeType.Light)} onChange={() => {}} />
|
||||
);
|
||||
selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
|
||||
expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.light);
|
||||
});
|
||||
|
||||
it('should render dar variant of provided color when theme not provided', () => {
|
||||
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} onChange={() => {}} theme={getTheme()}/>);
|
||||
wrapper = mount(<NamedColorsPalette color={BasicGreen.name} onChange={() => {}} theme={getTheme()} />);
|
||||
selectedSwatch = wrapper.find(ColorSwatch).findWhere(node => node.key() === BasicGreen.name);
|
||||
expect(selectedSwatch.prop('color')).toBe(BasicGreen.variants.dark);
|
||||
});
|
||||
|
@ -15,7 +15,6 @@ const SpectrumPalettePointer: React.FunctionComponent<SpectrumPalettePointerProp
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
const pointerColor = selectThemeVariant(
|
||||
{
|
||||
light: theme.colors.dark3,
|
||||
|
@ -37,4 +37,4 @@
|
||||
@include gradient-horizontal($scrollbarBackground, $scrollbarBackground2);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
.empty-search-result {
|
||||
background-color: $empty-list-cta-bg;
|
||||
padding: $spacer;
|
||||
border-radius: $border-radius;
|
||||
margin-bottom: $spacer*2;
|
||||
}
|
||||
.empty-search-result {
|
||||
border-left: 3px solid $info-box-border-color;
|
||||
background-color: $empty-list-cta-bg;
|
||||
padding: $spacer;
|
||||
min-width: 350px;
|
||||
border-radius: $border-radius;
|
||||
margin-bottom: $spacer*2;
|
||||
}
|
||||
|
@ -31,7 +31,7 @@ export const FormLabel: FunctionComponent<Props> = ({
|
||||
<label className={classes} {...rest} htmlFor={htmlFor}>
|
||||
{children}
|
||||
{tooltip && (
|
||||
<Tooltip placement="top" content={tooltip} theme={"info"}>
|
||||
<Tooltip placement="top" content={tooltip} theme={'info'}>
|
||||
<div className="gf-form-help-icon gf-form-help-icon--right-normal">
|
||||
<i className="fa fa-info-circle" />
|
||||
</div>
|
||||
|
@ -25,7 +25,7 @@ const setup = (propOverrides?: object) => {
|
||||
width: 300,
|
||||
value: 25,
|
||||
decimals: 0,
|
||||
theme: getTheme()
|
||||
theme: getTheme(),
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
|
@ -6,10 +6,5 @@ interface Props {
|
||||
}
|
||||
|
||||
export const PanelOptionsGrid: SFC<Props> = ({ children }) => {
|
||||
|
||||
return (
|
||||
<div className="panel-options-grid">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
return <div className="panel-options-grid">{children}</div>;
|
||||
};
|
||||
|
@ -61,7 +61,9 @@ interface AsyncProps {
|
||||
export const MenuList = (props: any) => {
|
||||
return (
|
||||
<components.MenuList {...props}>
|
||||
<CustomScrollbar autoHide={false} autoHeightMax="inherit">{props.children}</CustomScrollbar>
|
||||
<CustomScrollbar autoHide={false} autoHeightMax="inherit">
|
||||
{props.children}
|
||||
</CustomScrollbar>
|
||||
</components.MenuList>
|
||||
);
|
||||
};
|
||||
|
@ -21,7 +21,7 @@ export const Tooltip = ({ children, theme, ...controllerProps }: TooltipProps) =
|
||||
onMouseEnter={showPopper}
|
||||
onMouseLeave={hidePopper}
|
||||
referenceElement={tooltipTriggerRef.current}
|
||||
wrapperClassName='popper'
|
||||
wrapperClassName="popper"
|
||||
className={popperBackgroundClassName}
|
||||
renderArrow={({ arrowProps, placement }) => (
|
||||
<div className="popper__arrow" data-placement={placement} {...arrowProps} />
|
||||
|
@ -31,7 +31,7 @@ $popper-margin-from-ref: 5px;
|
||||
|
||||
// Themes
|
||||
&.popper__background--error {
|
||||
@include popper-theme($tooltipBackgroundError, $tooltipBackgroundError);
|
||||
@include popper-theme($tooltipBackgroundError, $white);
|
||||
}
|
||||
|
||||
&.popper__background--info {
|
||||
|
@ -82,15 +82,15 @@ export class ValueMappingsEditor extends PureComponent<Props, State> {
|
||||
|
||||
return (
|
||||
<PanelOptionsGroup title="Add value mapping" onAdd={this.addMapping}>
|
||||
{valueMappings.length > 0 &&
|
||||
valueMappings.map((valueMapping, index) => (
|
||||
<MappingRow
|
||||
key={`${valueMapping.text}-${index}`}
|
||||
valueMapping={valueMapping}
|
||||
updateValueMapping={this.updateGauge}
|
||||
removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
|
||||
/>
|
||||
))}
|
||||
{valueMappings.length > 0 &&
|
||||
valueMappings.map((valueMapping, index) => (
|
||||
<MappingRow
|
||||
key={`${valueMapping.text}-${index}`}
|
||||
valueMapping={valueMapping}
|
||||
updateValueMapping={this.updateGauge}
|
||||
removeValueMapping={() => this.onRemoveMapping(valueMapping.id)}
|
||||
/>
|
||||
))}
|
||||
</PanelOptionsGroup>
|
||||
);
|
||||
}
|
||||
|
@ -7,5 +7,5 @@
|
||||
@import 'PanelOptionsGrid/PanelOptionsGrid';
|
||||
@import 'ColorPicker/ColorPicker';
|
||||
@import 'ValueMappingsEditor/ValueMappingsEditor';
|
||||
@import "FormField/FormField";
|
||||
@import "EmptySearchResult/EmptySearchResult";
|
||||
@import 'EmptySearchResult/EmptySearchResult';
|
||||
@import 'FormField/FormField';
|
||||
|
@ -1,4 +1,4 @@
|
||||
import tinycolor from 'tinycolor2';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import defaultTheme from './default';
|
||||
import { GrafanaTheme, GrafanaThemeType } from '../types/theme';
|
||||
|
||||
|
@ -1,12 +1,10 @@
|
||||
|
||||
|
||||
const theme = {
|
||||
name: 'Grafana Default',
|
||||
typography: {
|
||||
fontFamily: {
|
||||
sansSerif: "'Roboto', Helvetica, Arial, sans-serif;",
|
||||
serif: "Georgia, 'Times New Roman', Times, serif;",
|
||||
monospace: "Menlo, Monaco, Consolas, 'Courier New', monospace;"
|
||||
monospace: "Menlo, Monaco, Consolas, 'Courier New', monospace;",
|
||||
},
|
||||
size: {
|
||||
base: '13px',
|
||||
@ -31,16 +29,16 @@ const theme = {
|
||||
lineHeight: {
|
||||
xs: 1,
|
||||
s: 1.1,
|
||||
m: 4/3,
|
||||
l: 1.5
|
||||
}
|
||||
m: 4 / 3,
|
||||
l: 1.5,
|
||||
},
|
||||
},
|
||||
brakpoints: {
|
||||
xs: '0',
|
||||
s: '544px',
|
||||
m: '768px',
|
||||
l: '992px',
|
||||
xl: '1200px'
|
||||
xl: '1200px',
|
||||
},
|
||||
spacing: {
|
||||
xs: '0',
|
||||
@ -55,8 +53,8 @@ const theme = {
|
||||
xs: '2px',
|
||||
s: '3px',
|
||||
m: '5px',
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default theme;
|
||||
|
@ -40,12 +40,10 @@ describe('Theme variable variant selector', () => {
|
||||
it('return dark theme variant if no theme given', () => {
|
||||
const theme = lightThemeMock;
|
||||
|
||||
const selectedValue = selectThemeVariant(
|
||||
{
|
||||
dark: theme.color.red,
|
||||
light: theme.color.green,
|
||||
}
|
||||
);
|
||||
const selectedValue = selectThemeVariant({
|
||||
dark: theme.color.red,
|
||||
light: theme.color.green,
|
||||
});
|
||||
|
||||
expect(selectedValue).toBe(lightThemeMock.color.red);
|
||||
});
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { GrafanaThemeType } from '../types/theme';
|
||||
import { GrafanaThemeType } from '../types/theme';
|
||||
|
||||
type VariantDescriptor = {
|
||||
[key in GrafanaThemeType]: string | number;
|
||||
};
|
||||
type VariantDescriptor = { [key in GrafanaThemeType]: string | number };
|
||||
|
||||
export const selectThemeVariant = (variants: VariantDescriptor, currentTheme?: GrafanaThemeType) => {
|
||||
return variants[currentTheme || GrafanaThemeType.Dark];
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
export * from './data';
|
||||
export * from './time';
|
||||
export * from './panel';
|
||||
|
@ -45,7 +45,7 @@ describe('colors', () => {
|
||||
|
||||
describe('getColorFromHexRgbOrName', () => {
|
||||
it('returns black for unknown color', () => {
|
||||
expect(getColorFromHexRgbOrName('aruba-sunshine')).toBe("#000000");
|
||||
expect(getColorFromHexRgbOrName('aruba-sunshine')).toBe('#000000');
|
||||
});
|
||||
|
||||
it('returns dark hex variant for known color if theme not specified', () => {
|
||||
|
@ -70,7 +70,9 @@ export const getColorDefinitionByName = (name: Color): ColorDefinition => {
|
||||
};
|
||||
|
||||
export const getColorDefinition = (hex: string, theme: GrafanaThemeType): ColorDefinition | undefined => {
|
||||
return flatten(Array.from(getNamedColorPalette().values())).filter(definition => definition.variants[theme] === hex)[0];
|
||||
return flatten(Array.from(getNamedColorPalette().values())).filter(
|
||||
definition => definition.variants[theme] === hex
|
||||
)[0];
|
||||
};
|
||||
|
||||
const isHex = (color: string) => {
|
||||
@ -95,7 +97,9 @@ export const getColorName = (color?: string, theme?: GrafanaThemeType): Color |
|
||||
};
|
||||
|
||||
export const getColorByName = (colorName: string) => {
|
||||
const definition = flatten(Array.from(getNamedColorPalette().values())).filter(definition => definition.name === colorName);
|
||||
const definition = flatten(Array.from(getNamedColorPalette().values())).filter(
|
||||
definition => definition.name === colorName
|
||||
);
|
||||
return definition.length > 0 ? definition[0] : undefined;
|
||||
};
|
||||
|
||||
|
@ -51,7 +51,7 @@ export function processTimeSeries({ timeSeries, nullValueMode }: Options): TimeS
|
||||
}
|
||||
|
||||
if (currentValue !== null && typeof currentValue !== 'number') {
|
||||
throw {message: 'Time series contains non number values'};
|
||||
throw { message: 'Time series contains non number values' };
|
||||
}
|
||||
|
||||
// Due to missing values we could have different timeStep all along the series
|
||||
|
@ -15,12 +15,7 @@ const ThemableStory: React.FunctionComponent<{}> = ({ children }) => {
|
||||
GrafanaThemeType.Dark
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={getTheme(themeKnob)}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
|
||||
);
|
||||
return <ThemeContext.Provider value={getTheme(themeKnob)}>{children}</ThemeContext.Provider>;
|
||||
};
|
||||
|
||||
// Temporary solution. When we update to Storybook V5 we will be able to pass data from decorator to story
|
||||
|
@ -306,7 +306,7 @@ export const getCategories = (): ValueFormatCategory[] => [
|
||||
{ name: 'kilometers/hour (km/h)', id: 'velocitykmh', fn: toFixedUnit('km/h') },
|
||||
{ name: 'miles/hour (mph)', id: 'velocitymph', fn: toFixedUnit('mph') },
|
||||
{ name: 'knot (kn)', id: 'velocityknot', fn: toFixedUnit('kn') },
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Volume',
|
||||
@ -318,5 +318,5 @@ export const getCategories = (): ValueFormatCategory[] => [
|
||||
{ name: 'cubic decimetre', id: 'dm3', fn: toFixedUnit('dm³') },
|
||||
{ name: 'gallons', id: 'gallons', fn: toFixedUnit('gal') },
|
||||
],
|
||||
}
|
||||
},
|
||||
];
|
||||
|
@ -16,7 +16,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
reqOrgAdmin := middleware.ReqOrgAdmin
|
||||
redirectFromLegacyDashboardURL := middleware.RedirectFromLegacyDashboardURL()
|
||||
redirectFromLegacyDashboardSoloURL := middleware.RedirectFromLegacyDashboardSoloURL()
|
||||
quota := middleware.Quota
|
||||
quota := middleware.Quota(hs.QuotaService)
|
||||
bind := binding.Bind
|
||||
|
||||
r := hs.RouteRegister
|
||||
@ -286,7 +286,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
|
||||
dashboardRoute.Post("/calculate-diff", bind(dtos.CalculateDiffOptions{}), Wrap(CalculateDashboardDiff))
|
||||
|
||||
dashboardRoute.Post("/db", bind(m.SaveDashboardCommand{}), Wrap(PostDashboard))
|
||||
dashboardRoute.Post("/db", bind(m.SaveDashboardCommand{}), Wrap(hs.PostDashboard))
|
||||
dashboardRoute.Get("/home", Wrap(GetHomeDashboard))
|
||||
dashboardRoute.Get("/tags", GetDashboardTags)
|
||||
dashboardRoute.Post("/import", bind(dtos.ImportDashboardCommand{}), Wrap(ImportDashboard))
|
||||
@ -294,7 +294,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
dashboardRoute.Group("/id/:dashboardId", func(dashIdRoute routing.RouteRegister) {
|
||||
dashIdRoute.Get("/versions", Wrap(GetDashboardVersions))
|
||||
dashIdRoute.Get("/versions/:id", Wrap(GetDashboardVersion))
|
||||
dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), Wrap(RestoreDashboardVersion))
|
||||
dashIdRoute.Post("/restore", bind(dtos.RestoreDashboardVersionCommand{}), Wrap(hs.RestoreDashboardVersion))
|
||||
|
||||
dashIdRoute.Group("/permissions", func(dashboardPermissionRoute routing.RouteRegister) {
|
||||
dashboardPermissionRoute.Get("/", Wrap(GetDashboardPermissionList))
|
||||
|
@ -18,7 +18,6 @@ import (
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/guardian"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
@ -208,14 +207,14 @@ func DeleteDashboardByUID(c *m.ReqContext) Response {
|
||||
})
|
||||
}
|
||||
|
||||
func PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
|
||||
func (hs *HTTPServer) PostDashboard(c *m.ReqContext, cmd m.SaveDashboardCommand) Response {
|
||||
cmd.OrgId = c.OrgId
|
||||
cmd.UserId = c.UserId
|
||||
|
||||
dash := cmd.GetDashboardModel()
|
||||
|
||||
if dash.Id == 0 && dash.Uid == "" {
|
||||
limitReached, err := quota.QuotaReached(c, "dashboard")
|
||||
limitReached, err := hs.QuotaService.QuotaReached(c, "dashboard")
|
||||
if err != nil {
|
||||
return Error(500, "failed to get quota", err)
|
||||
}
|
||||
@ -463,7 +462,7 @@ func CalculateDashboardDiff(c *m.ReqContext, apiOptions dtos.CalculateDiffOption
|
||||
}
|
||||
|
||||
// RestoreDashboardVersion restores a dashboard to the given version.
|
||||
func RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersionCommand) Response {
|
||||
func (hs *HTTPServer) RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersionCommand) Response {
|
||||
dash, rsp := getDashboardHelper(c.OrgId, "", c.ParamsInt64(":dashboardId"), "")
|
||||
if rsp != nil {
|
||||
return rsp
|
||||
@ -490,7 +489,7 @@ func RestoreDashboardVersion(c *m.ReqContext, apiCmd dtos.RestoreDashboardVersio
|
||||
saveCmd.Dashboard.Set("uid", dash.Uid)
|
||||
saveCmd.Message = fmt.Sprintf("Restored from version %d", version.Version)
|
||||
|
||||
return PostDashboard(c, saveCmd)
|
||||
return hs.PostDashboard(c, saveCmd)
|
||||
}
|
||||
|
||||
func GetDashboardTags(c *m.ReqContext) {
|
||||
|
@ -881,12 +881,16 @@ func postDashboardScenario(desc string, url string, routePattern string, mock *d
|
||||
Convey(desc+" "+url, func() {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
hs := HTTPServer{
|
||||
Bus: bus.GetBus(),
|
||||
}
|
||||
|
||||
sc := setupScenarioContext(url)
|
||||
sc.defaultHandler = Wrap(func(c *m.ReqContext) Response {
|
||||
sc.context = c
|
||||
sc.context.SignedInUser = &m.SignedInUser{OrgId: cmd.OrgId, UserId: cmd.UserId}
|
||||
|
||||
return PostDashboard(c, cmd)
|
||||
return hs.PostDashboard(c, cmd)
|
||||
})
|
||||
|
||||
origNewDashboardService := dashboards.NewService
|
||||
|
@ -145,6 +145,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf
|
||||
"info": panel.Info,
|
||||
"hideFromList": panel.HideFromList,
|
||||
"sort": getPanelSort(panel.Id),
|
||||
"dataFormats": panel.DataFormats,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,6 +24,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/cache"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/hooks"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
"github.com/grafana/grafana/pkg/services/rendering"
|
||||
"github.com/grafana/grafana/pkg/services/session"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -55,6 +56,7 @@ type HTTPServer struct {
|
||||
CacheService *cache.CacheService `inject:""`
|
||||
DatasourceCache datasources.CacheService `inject:""`
|
||||
AuthTokenService models.UserTokenService `inject:""`
|
||||
QuotaService *quota.QuotaService `inject:""`
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) Init() error {
|
||||
|
@ -4,18 +4,30 @@ import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
)
|
||||
|
||||
func init() {
|
||||
bus.AddHandler("auth", UpsertUser)
|
||||
registry.RegisterService(&LoginService{})
|
||||
}
|
||||
|
||||
var (
|
||||
logger = log.New("login.ext_user")
|
||||
)
|
||||
|
||||
func UpsertUser(cmd *m.UpsertUserCommand) error {
|
||||
type LoginService struct {
|
||||
Bus bus.Bus `inject:""`
|
||||
QuotaService *quota.QuotaService `inject:""`
|
||||
}
|
||||
|
||||
func (ls *LoginService) Init() error {
|
||||
ls.Bus.AddHandler(ls.UpsertUser)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ls *LoginService) UpsertUser(cmd *m.UpsertUserCommand) error {
|
||||
extUser := cmd.ExternalUser
|
||||
|
||||
userQuery := &m.GetUserByAuthInfoQuery{
|
||||
@ -37,7 +49,7 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
|
||||
limitReached, err := quota.QuotaReached(cmd.ReqContext, "user")
|
||||
limitReached, err := ls.QuotaService.QuotaReached(cmd.ReqContext, "user")
|
||||
if err != nil {
|
||||
log.Warn("Error getting user quota. error: %v", err)
|
||||
return ErrGettingUserQuota
|
||||
@ -57,7 +69,7 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
|
||||
AuthModule: extUser.AuthModule,
|
||||
AuthId: extUser.AuthId,
|
||||
}
|
||||
if err := bus.Dispatch(cmd2); err != nil {
|
||||
if err := ls.Bus.Dispatch(cmd2); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@ -78,12 +90,12 @@ func UpsertUser(cmd *m.UpsertUserCommand) error {
|
||||
|
||||
// Sync isGrafanaAdmin permission
|
||||
if extUser.IsGrafanaAdmin != nil && *extUser.IsGrafanaAdmin != cmd.Result.IsAdmin {
|
||||
if err := bus.Dispatch(&m.UpdateUserPermissionsCommand{UserId: cmd.Result.Id, IsGrafanaAdmin: *extUser.IsGrafanaAdmin}); err != nil {
|
||||
if err := ls.Bus.Dispatch(&m.UpdateUserPermissionsCommand{UserId: cmd.Result.Id, IsGrafanaAdmin: *extUser.IsGrafanaAdmin}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err = bus.Dispatch(&m.SyncTeamsCommand{
|
||||
err = ls.Bus.Dispatch(&m.SyncTeamsCommand{
|
||||
User: cmd.Result,
|
||||
ExternalUser: extUser,
|
||||
})
|
||||
|
@ -395,8 +395,11 @@ func ldapAutherScenario(desc string, fn scenarioFunc) {
|
||||
defer bus.ClearBusHandlers()
|
||||
|
||||
sc := &scenarioContext{}
|
||||
loginService := &LoginService{
|
||||
Bus: bus.GetBus(),
|
||||
}
|
||||
|
||||
bus.AddHandler("test", UpsertUser)
|
||||
bus.AddHandler("test", loginService.UpsertUser)
|
||||
|
||||
bus.AddHandlerCtx("test", func(ctx context.Context, cmd *m.SyncTeamsCommand) error {
|
||||
return nil
|
||||
|
@ -682,6 +682,7 @@ type fakeUserAuthTokenService struct {
|
||||
tryRotateTokenProvider func(token *m.UserToken, clientIP, userAgent string) (bool, error)
|
||||
lookupTokenProvider func(unhashedToken string) (*m.UserToken, error)
|
||||
revokeTokenProvider func(token *m.UserToken) error
|
||||
activeAuthTokenCount func() (int64, error)
|
||||
}
|
||||
|
||||
func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
|
||||
@ -704,6 +705,9 @@ func newFakeUserAuthTokenService() *fakeUserAuthTokenService {
|
||||
revokeTokenProvider: func(token *m.UserToken) error {
|
||||
return nil
|
||||
},
|
||||
activeAuthTokenCount: func() (int64, error) {
|
||||
return 10, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@ -722,3 +726,7 @@ func (s *fakeUserAuthTokenService) TryRotateToken(token *m.UserToken, clientIP,
|
||||
func (s *fakeUserAuthTokenService) RevokeToken(token *m.UserToken) error {
|
||||
return s.revokeTokenProvider(token)
|
||||
}
|
||||
|
||||
func (s *fakeUserAuthTokenService) ActiveTokenCount() (int64, error) {
|
||||
return s.activeAuthTokenCount()
|
||||
}
|
||||
|
@ -9,16 +9,20 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
)
|
||||
|
||||
func Quota(target string) macaron.Handler {
|
||||
return func(c *m.ReqContext) {
|
||||
limitReached, err := quota.QuotaReached(c, target)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "failed to get quota", err)
|
||||
return
|
||||
}
|
||||
if limitReached {
|
||||
c.JsonApiErr(403, fmt.Sprintf("%s Quota reached", target), nil)
|
||||
return
|
||||
// Quota returns a function that returns a function used to call quotaservice based on target name
|
||||
func Quota(quotaService *quota.QuotaService) func(target string) macaron.Handler {
|
||||
//https://open.spotify.com/track/7bZSoBEAEEUsGEuLOf94Jm?si=T1Tdju5qRSmmR0zph_6RBw fuuuuunky
|
||||
return func(target string) macaron.Handler {
|
||||
return func(c *m.ReqContext) {
|
||||
limitReached, err := quotaService.QuotaReached(c, target)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "failed to get quota", err)
|
||||
return
|
||||
}
|
||||
if limitReached {
|
||||
c.JsonApiErr(403, fmt.Sprintf("%s Quota reached", target), nil)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,9 +3,10 @@ package middleware
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/quota"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/session"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
@ -13,10 +14,6 @@ import (
|
||||
func TestMiddlewareQuota(t *testing.T) {
|
||||
|
||||
Convey("Given the grafana quota middleware", t, func() {
|
||||
session.GetSessionCount = func() int {
|
||||
return 4
|
||||
}
|
||||
|
||||
setting.AnonymousEnabled = false
|
||||
setting.Quota = setting.QuotaSettings{
|
||||
Enabled: true,
|
||||
@ -39,6 +36,12 @@ func TestMiddlewareQuota(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
fakeAuthTokenService := newFakeUserAuthTokenService()
|
||||
qs := "a.QuotaService{
|
||||
AuthTokenService: fakeAuthTokenService,
|
||||
}
|
||||
QuotaFn := Quota(qs)
|
||||
|
||||
middlewareScenario("with user not logged in", func(sc *scenarioContext) {
|
||||
bus.AddHandler("globalQuota", func(query *m.GetGlobalQuotaByTargetQuery) error {
|
||||
query.Result = &m.GlobalQuotaDTO{
|
||||
@ -48,26 +51,30 @@ func TestMiddlewareQuota(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey("global quota not reached", func() {
|
||||
sc.m.Get("/user", Quota("user"), sc.defaultHandler)
|
||||
sc.m.Get("/user", QuotaFn("user"), sc.defaultHandler)
|
||||
sc.fakeReq("GET", "/user").exec()
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
Convey("global quota reached", func() {
|
||||
setting.Quota.Global.User = 4
|
||||
sc.m.Get("/user", Quota("user"), sc.defaultHandler)
|
||||
sc.m.Get("/user", QuotaFn("user"), sc.defaultHandler)
|
||||
sc.fakeReq("GET", "/user").exec()
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
Convey("global session quota not reached", func() {
|
||||
setting.Quota.Global.Session = 10
|
||||
sc.m.Get("/user", Quota("session"), sc.defaultHandler)
|
||||
sc.m.Get("/user", QuotaFn("session"), sc.defaultHandler)
|
||||
sc.fakeReq("GET", "/user").exec()
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
Convey("global session quota reached", func() {
|
||||
setting.Quota.Global.Session = 1
|
||||
sc.m.Get("/user", Quota("session"), sc.defaultHandler)
|
||||
sc.m.Get("/user", QuotaFn("session"), sc.defaultHandler)
|
||||
sc.fakeReq("GET", "/user").exec()
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
@ -95,6 +102,7 @@ func TestMiddlewareQuota(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("userQuota", func(query *m.GetUserQuotaByTargetQuery) error {
|
||||
query.Result = &m.UserQuotaDTO{
|
||||
Target: query.Target,
|
||||
@ -103,6 +111,7 @@ func TestMiddlewareQuota(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
bus.AddHandler("orgQuota", func(query *m.GetOrgQuotaByTargetQuery) error {
|
||||
query.Result = &m.OrgQuotaDTO{
|
||||
Target: query.Target,
|
||||
@ -111,45 +120,49 @@ func TestMiddlewareQuota(t *testing.T) {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
Convey("global datasource quota reached", func() {
|
||||
setting.Quota.Global.DataSource = 4
|
||||
sc.m.Get("/ds", Quota("data_source"), sc.defaultHandler)
|
||||
sc.m.Get("/ds", QuotaFn("data_source"), sc.defaultHandler)
|
||||
sc.fakeReq("GET", "/ds").exec()
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
Convey("user Org quota not reached", func() {
|
||||
setting.Quota.User.Org = 5
|
||||
sc.m.Get("/org", Quota("org"), sc.defaultHandler)
|
||||
sc.m.Get("/org", QuotaFn("org"), sc.defaultHandler)
|
||||
sc.fakeReq("GET", "/org").exec()
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
Convey("user Org quota reached", func() {
|
||||
setting.Quota.User.Org = 4
|
||||
sc.m.Get("/org", Quota("org"), sc.defaultHandler)
|
||||
sc.m.Get("/org", QuotaFn("org"), sc.defaultHandler)
|
||||
sc.fakeReq("GET", "/org").exec()
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
Convey("org dashboard quota not reached", func() {
|
||||
setting.Quota.Org.Dashboard = 10
|
||||
sc.m.Get("/dashboard", Quota("dashboard"), sc.defaultHandler)
|
||||
sc.m.Get("/dashboard", QuotaFn("dashboard"), sc.defaultHandler)
|
||||
sc.fakeReq("GET", "/dashboard").exec()
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
Convey("org dashboard quota reached", func() {
|
||||
setting.Quota.Org.Dashboard = 4
|
||||
sc.m.Get("/dashboard", Quota("dashboard"), sc.defaultHandler)
|
||||
sc.m.Get("/dashboard", QuotaFn("dashboard"), sc.defaultHandler)
|
||||
sc.fakeReq("GET", "/dashboard").exec()
|
||||
So(sc.resp.Code, ShouldEqual, 403)
|
||||
})
|
||||
|
||||
Convey("org dashboard quota reached but quotas disabled", func() {
|
||||
setting.Quota.Org.Dashboard = 4
|
||||
setting.Quota.Enabled = false
|
||||
sc.m.Get("/dashboard", Quota("dashboard"), sc.defaultHandler)
|
||||
sc.m.Get("/dashboard", QuotaFn("dashboard"), sc.defaultHandler)
|
||||
sc.fakeReq("GET", "/dashboard").exec()
|
||||
So(sc.resp.Code, ShouldEqual, 200)
|
||||
})
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ import (
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/session"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
macaron "gopkg.in/macaron.v1"
|
||||
@ -66,7 +65,6 @@ func recoveryScenario(desc string, url string, fn scenarioFunc) {
|
||||
sc.userAuthTokenService = newFakeUserAuthTokenService()
|
||||
sc.m.Use(GetContextHandler(sc.userAuthTokenService))
|
||||
// mock out gc goroutine
|
||||
session.StartSessionGC = func() {}
|
||||
sc.m.Use(OrgRedirect())
|
||||
sc.m.Use(AddDefaultResponseHeaders())
|
||||
|
||||
|
@ -29,4 +29,5 @@ type UserTokenService interface {
|
||||
LookupToken(unhashedToken string) (*UserToken, error)
|
||||
TryRotateToken(token *UserToken, clientIP, userAgent string) (bool, error)
|
||||
RevokeToken(token *UserToken) error
|
||||
ActiveTokenCount() (int64, error)
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import "encoding/json"
|
||||
|
||||
type PanelPlugin struct {
|
||||
FrontendPluginBase
|
||||
DataFormats []string `json:"dataFormats"`
|
||||
}
|
||||
|
||||
func (p *PanelPlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
||||
@ -15,6 +16,10 @@ func (p *PanelPlugin) Load(decoder *json.Decoder, pluginDir string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if p.DataFormats == nil {
|
||||
p.DataFormats = []string{"time_series", "table"}
|
||||
}
|
||||
|
||||
Panels[p.Id] = p
|
||||
return nil
|
||||
}
|
||||
|
@ -35,6 +35,13 @@ func (s *UserAuthTokenService) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) ActiveTokenCount() (int64, error) {
|
||||
var model userAuthToken
|
||||
count, err := s.SQLStore.NewSession().Where(`created_at > ? AND rotated_at > ?`, s.createdAfterParam(), s.rotatedAfterParam()).Count(&model)
|
||||
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*models.UserToken, error) {
|
||||
clientIP = util.ParseIPAddress(clientIP)
|
||||
token, err := util.RandomHex(16)
|
||||
@ -79,13 +86,8 @@ func (s *UserAuthTokenService) LookupToken(unhashedToken string) (*models.UserTo
|
||||
s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken)
|
||||
}
|
||||
|
||||
tokenMaxLifetime := time.Duration(s.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour
|
||||
tokenMaxInactiveLifetime := time.Duration(s.Cfg.LoginMaxInactiveLifetimeDays) * 24 * time.Hour
|
||||
createdAfter := getTime().Add(-tokenMaxLifetime).Unix()
|
||||
rotatedAfter := getTime().Add(-tokenMaxInactiveLifetime).Unix()
|
||||
|
||||
var model userAuthToken
|
||||
exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ? AND rotated_at > ?", hashedToken, hashedToken, createdAfter, rotatedAfter).Get(&model)
|
||||
exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ? AND rotated_at > ?", hashedToken, hashedToken, s.createdAfterParam(), s.rotatedAfterParam()).Get(&model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -219,6 +221,16 @@ func (s *UserAuthTokenService) RevokeToken(token *models.UserToken) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) createdAfterParam() int64 {
|
||||
tokenMaxLifetime := time.Duration(s.Cfg.LoginMaxLifetimeDays) * 24 * time.Hour
|
||||
return getTime().Add(-tokenMaxLifetime).Unix()
|
||||
}
|
||||
|
||||
func (s *UserAuthTokenService) rotatedAfterParam() int64 {
|
||||
tokenMaxInactiveLifetime := time.Duration(s.Cfg.LoginMaxInactiveLifetimeDays) * 24 * time.Hour
|
||||
return getTime().Add(-tokenMaxInactiveLifetime).Unix()
|
||||
}
|
||||
|
||||
func hashToken(token string) string {
|
||||
hashBytes := sha256.Sum256([]byte(token + setting.SecretKey))
|
||||
return hex.EncodeToString(hashBytes[:])
|
||||
|
@ -31,6 +31,12 @@ func TestUserAuthToken(t *testing.T) {
|
||||
So(userToken, ShouldNotBeNil)
|
||||
So(userToken.AuthTokenSeen, ShouldBeFalse)
|
||||
|
||||
Convey("Can count active tokens", func() {
|
||||
count, err := userAuthTokenService.ActiveTokenCount()
|
||||
So(err, ShouldBeNil)
|
||||
So(count, ShouldEqual, 1)
|
||||
})
|
||||
|
||||
Convey("When lookup unhashed token should return user auth token", func() {
|
||||
userToken, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
|
||||
So(err, ShouldBeNil)
|
||||
@ -114,6 +120,12 @@ func TestUserAuthToken(t *testing.T) {
|
||||
notGood, err := userAuthTokenService.LookupToken(userToken.UnhashedToken)
|
||||
So(err, ShouldEqual, models.ErrUserTokenNotFound)
|
||||
So(notGood, ShouldBeNil)
|
||||
|
||||
Convey("should not find active token when expired", func() {
|
||||
count, err := userAuthTokenService.ActiveTokenCount()
|
||||
So(err, ShouldBeNil)
|
||||
So(count, ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("when rotated_at is 5 days ago and created_at is 29 days and 23:59:59 ago should not find token", func() {
|
||||
|
@ -3,11 +3,23 @@ package quota
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/session"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
func QuotaReached(c *m.ReqContext, target string) (bool, error) {
|
||||
func init() {
|
||||
registry.RegisterService(&QuotaService{})
|
||||
}
|
||||
|
||||
type QuotaService struct {
|
||||
AuthTokenService m.UserTokenService `inject:""`
|
||||
}
|
||||
|
||||
func (qs *QuotaService) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qs *QuotaService) QuotaReached(c *m.ReqContext, target string) (bool, error) {
|
||||
if !setting.Quota.Enabled {
|
||||
return false, nil
|
||||
}
|
||||
@ -30,7 +42,12 @@ func QuotaReached(c *m.ReqContext, target string) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
if target == "session" {
|
||||
usedSessions := session.GetSessionCount()
|
||||
|
||||
usedSessions, err := qs.AuthTokenService.ActiveTokenCount()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if int64(usedSessions) > scope.DefaultLimit {
|
||||
c.Logger.Debug("Sessions limit reached", "active", usedSessions, "limit", scope.DefaultLimit)
|
||||
return true, nil
|
||||
|
@ -19,7 +19,7 @@ const (
|
||||
|
||||
var sessionManager *ms.Manager
|
||||
var sessionOptions *ms.Options
|
||||
var StartSessionGC func()
|
||||
var StartSessionGC func() = func() {}
|
||||
var GetSessionCount func() int
|
||||
var sessionLogger = log.New("session")
|
||||
var sessionConnMaxLifetime int64
|
||||
|
@ -27,7 +27,7 @@ class ErrorBoundary extends Component<Props, State> {
|
||||
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||
this.setState({
|
||||
error: error,
|
||||
errorInfo: errorInfo
|
||||
errorInfo: errorInfo,
|
||||
});
|
||||
}
|
||||
|
||||
|
51
public/app/core/components/FilterInput/FilterInput.tsx
Normal file
51
public/app/core/components/FilterInput/FilterInput.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
const specialChars = ['(', '[', '{', '}', ']', ')', '|', '*', '+', '-', '.', '?', '<', '>', '#', '&', '^', '$'];
|
||||
|
||||
export const escapeStringForRegex = (value: string) => {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const newValue = specialChars.reduce(
|
||||
(escaped, currentChar) => escaped.replace(currentChar, '\\' + currentChar),
|
||||
value
|
||||
);
|
||||
|
||||
return newValue;
|
||||
};
|
||||
|
||||
export const unEscapeStringFromRegex = (value: string) => {
|
||||
if (!value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
const newValue = specialChars.reduce(
|
||||
(escaped, currentChar) => escaped.replace('\\' + currentChar, currentChar),
|
||||
value
|
||||
);
|
||||
|
||||
return newValue;
|
||||
};
|
||||
|
||||
export interface Props {
|
||||
value: string | undefined;
|
||||
placeholder?: string;
|
||||
labelClassName?: string;
|
||||
inputClassName?: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export const FilterInput = forwardRef<HTMLInputElement, Props>((props, ref) => (
|
||||
<label className={props.labelClassName}>
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
className={props.inputClassName}
|
||||
value={unEscapeStringFromRegex(props.value)}
|
||||
onChange={event => props.onChange(escapeStringForRegex(event.target.value))}
|
||||
placeholder={props.placeholder ? props.placeholder : null}
|
||||
/>
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
</label>
|
||||
));
|
@ -9,42 +9,49 @@ interface Props {
|
||||
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 && (
|
||||
export const Footer: FC<Props> = React.memo(
|
||||
({ appName, buildVersion, buildCommit, newGrafanaVersionExists, newGrafanaVersion }) => {
|
||||
return (
|
||||
<footer className="footer">
|
||||
<div className="text-center">
|
||||
<ul>
|
||||
<li>
|
||||
<Tooltip placement="auto" content={newGrafanaVersion}>
|
||||
<a href="https://grafana.com/get" target="_blank">
|
||||
New version available!
|
||||
</a>
|
||||
</Tooltip>
|
||||
<a href="http://docs.grafana.org" target="_blank">
|
||||
<i className="fa fa-file-code-o" /> Docs
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
});
|
||||
<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;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import LayoutSelector, { LayoutMode } from '../LayoutSelector/LayoutSelector';
|
||||
import { FilterInput } from '../FilterInput/FilterInput';
|
||||
|
||||
export interface Props {
|
||||
searchQuery: string;
|
||||
@ -22,16 +23,13 @@ export default class OrgActionBar extends PureComponent<Props> {
|
||||
return (
|
||||
<div className="page-action-bar">
|
||||
<div className="gf-form gf-form--grow">
|
||||
<label className="gf-form--has-input-icon">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input width-20"
|
||||
value={searchQuery}
|
||||
onChange={event => setSearchQuery(event.target.value)}
|
||||
placeholder="Filter by name or type"
|
||||
/>
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
</label>
|
||||
<FilterInput
|
||||
labelClassName="gf-form--has-input-icon"
|
||||
inputClassName="gf-form-input width-20"
|
||||
value={searchQuery}
|
||||
onChange={setSearchQuery}
|
||||
placeholder={'Filter by name or type'}
|
||||
/>
|
||||
<LayoutSelector mode={layoutMode} onLayoutModeChanged={(mode: LayoutMode) => onSetLayoutMode(mode)} />
|
||||
</div>
|
||||
<div className="page-action-bar__spacer" />
|
||||
|
@ -7,20 +7,13 @@ exports[`Render should render component 1`] = `
|
||||
<div
|
||||
className="gf-form gf-form--grow"
|
||||
>
|
||||
<label
|
||||
className="gf-form--has-input-icon"
|
||||
>
|
||||
<input
|
||||
className="gf-form-input width-20"
|
||||
onChange={[Function]}
|
||||
placeholder="Filter by name or type"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<i
|
||||
className="gf-form-input-icon fa fa-search"
|
||||
/>
|
||||
</label>
|
||||
<ForwardRef
|
||||
inputClassName="gf-form-input width-20"
|
||||
labelClassName="gf-form--has-input-icon"
|
||||
onChange={[MockFunction]}
|
||||
placeholder="Filter by name or type"
|
||||
value=""
|
||||
/>
|
||||
<LayoutSelector
|
||||
onLayoutModeChanged={[Function]}
|
||||
/>
|
||||
|
@ -33,9 +33,9 @@ class Page extends Component<Props> {
|
||||
updateTitle = () => {
|
||||
const title = this.getPageTitle;
|
||||
document.title = title ? title + ' - Grafana' : 'Grafana';
|
||||
}
|
||||
};
|
||||
|
||||
get getPageTitle () {
|
||||
get getPageTitle() {
|
||||
const { navModel } = this.props;
|
||||
if (navModel) {
|
||||
return getTitleFromNavModel(navModel) || undefined;
|
||||
@ -47,20 +47,21 @@ class Page extends Component<Props> {
|
||||
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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,6 @@ interface Props {
|
||||
}
|
||||
|
||||
class PageContents extends Component<Props> {
|
||||
|
||||
render() {
|
||||
const { isLoading } = this.props;
|
||||
|
||||
|
@ -5,6 +5,7 @@ import AsyncSelect from '@torkelo/react-select/lib/Async';
|
||||
import { TagOption } from './TagOption';
|
||||
import { TagBadge } from './TagBadge';
|
||||
import { components } from '@torkelo/react-select';
|
||||
import { escapeStringForRegex } from '../FilterInput/FilterInput';
|
||||
|
||||
export interface Props {
|
||||
tags: string[];
|
||||
@ -51,7 +52,7 @@ export class TagFilter extends React.Component<Props, any> {
|
||||
value: tags,
|
||||
styles: resetSelectStyles(),
|
||||
filterOption: (option, searchQuery) => {
|
||||
const regex = RegExp(searchQuery, 'i');
|
||||
const regex = RegExp(escapeStringForRegex(searchQuery), 'i');
|
||||
return regex.test(option.value);
|
||||
},
|
||||
components: {
|
||||
|
@ -13,9 +13,9 @@ jest.mock('app/store/store', () => ({
|
||||
getState: jest.fn().mockReturnValue({
|
||||
location: {
|
||||
lastUpdated: 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('app/core/services/context_srv', () => ({
|
||||
|
@ -57,7 +57,7 @@ export class Settings {
|
||||
isEnterprise: false,
|
||||
},
|
||||
viewersCanEdit: false,
|
||||
disableSanitizeHtml: false
|
||||
disableSanitizeHtml: false,
|
||||
};
|
||||
|
||||
_.extend(this, defaults, options);
|
||||
|
@ -28,7 +28,7 @@ export function autofillEventFix($compile) {
|
||||
input.removeEventListener('animationstart', onAnimationStart);
|
||||
// input.removeEventListener('change', onChange);
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -142,7 +142,7 @@ export function dropdownTypeahead2($compile) {
|
||||
const $input = $(inputTemplate);
|
||||
const $button = $(buttonTemplate);
|
||||
const timeoutId = {
|
||||
blur: null
|
||||
blur: null,
|
||||
};
|
||||
$input.appendTo(elem);
|
||||
$button.appendTo(elem);
|
||||
|
@ -344,7 +344,7 @@ export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): Time
|
||||
datapoints: series.datapoints,
|
||||
target: series.alias,
|
||||
alias: series.alias,
|
||||
color: series.color
|
||||
color: series.color,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
export class Profiler {
|
||||
panelsRendered: number;
|
||||
enabled: boolean;
|
||||
|
@ -43,5 +43,5 @@ export function getNavModel(navIndex: NavIndex, id: string, fallback?: NavModel)
|
||||
}
|
||||
|
||||
export const getTitleFromNavModel = (navModel: NavModel) => {
|
||||
return `${navModel.main.text}${navModel.node.text ? ': ' + navModel.node.text : '' }`;
|
||||
return `${navModel.main.text}${navModel.node.text ? ': ' + navModel.node.text : ''}`;
|
||||
};
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
const backendSrv = {
|
||||
get: jest.fn(),
|
||||
getDashboard: jest.fn(),
|
||||
@ -10,5 +9,3 @@ const backendSrv = {
|
||||
export function getBackendSrv() {
|
||||
return backendSrv;
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,24 +1,21 @@
|
||||
import React from 'react';
|
||||
import {shallow} from 'enzyme';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import {PasswordStrength} from '../components/PasswordStrength';
|
||||
import { PasswordStrength } from '../components/PasswordStrength';
|
||||
|
||||
describe('PasswordStrength', () => {
|
||||
|
||||
it('should have class bad if length below 4', () => {
|
||||
const wrapper = shallow(<PasswordStrength password="asd" />);
|
||||
expect(wrapper.find(".password-strength-bad")).toHaveLength(1);
|
||||
expect(wrapper.find('.password-strength-bad')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should have class ok if length below 8', () => {
|
||||
const wrapper = shallow(<PasswordStrength password="asdasd" />);
|
||||
expect(wrapper.find(".password-strength-ok")).toHaveLength(1);
|
||||
expect(wrapper.find('.password-strength-ok')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should have class good if length above 8', () => {
|
||||
const wrapper = shallow(<PasswordStrength password="asdaasdda" />);
|
||||
expect(wrapper.find(".password-strength-good")).toHaveLength(1);
|
||||
expect(wrapper.find('.password-strength-good')).toHaveLength(1);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
@ -19,7 +19,7 @@ const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
|
||||
showingTable: true,
|
||||
showingLogs: true,
|
||||
dedupStrategy: LogsDedupStrategy.none,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
describe('state functions', () => {
|
||||
|
@ -207,7 +207,14 @@ export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: bo
|
||||
urlState.range.to,
|
||||
urlState.datasource,
|
||||
...urlState.queries,
|
||||
{ ui: [!!urlState.ui.showingGraph, !!urlState.ui.showingLogs, !!urlState.ui.showingTable, urlState.ui.dedupStrategy] },
|
||||
{
|
||||
ui: [
|
||||
!!urlState.ui.showingGraph,
|
||||
!!urlState.ui.showingLogs,
|
||||
!!urlState.ui.showingTable,
|
||||
urlState.ui.dedupStrategy,
|
||||
],
|
||||
},
|
||||
]);
|
||||
}
|
||||
return JSON.stringify(urlState);
|
||||
|
@ -289,21 +289,21 @@ kbn.getUnitFormats = () => {
|
||||
//
|
||||
// Backward compatible layer for value formats to support old plugins
|
||||
//
|
||||
if (typeof Proxy !== "undefined") {
|
||||
if (typeof Proxy !== 'undefined') {
|
||||
kbn.valueFormats = new Proxy(kbn.valueFormats, {
|
||||
get(target, name, receiver) {
|
||||
if (typeof name !== 'string') {
|
||||
throw {message: `Value format ${String(name)} is not a string` };
|
||||
throw { message: `Value format ${String(name)} is not a string` };
|
||||
}
|
||||
|
||||
const formatter = getValueFormat(name);
|
||||
if (formatter) {
|
||||
if (formatter) {
|
||||
return formatter;
|
||||
}
|
||||
|
||||
// default to look here
|
||||
return Reflect.get(target, name, receiver);
|
||||
}
|
||||
},
|
||||
});
|
||||
} else {
|
||||
kbn.valueFormats = getValueFormatterIndex();
|
||||
|
5
public/app/core/utils/reselect.ts
Normal file
5
public/app/core/utils/reselect.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import { memoize } from 'lodash';
|
||||
import { createSelectorCreator } from 'reselect';
|
||||
|
||||
const hashFn = (...args) => args.reduce((acc, val) => acc + '-' + JSON.stringify(val), '');
|
||||
export const createLodashMemoizedSelector = createSelectorCreator(memoize, hashFn);
|
@ -15,7 +15,7 @@ export default function getScrollbarWidth() {
|
||||
position: 'absolute',
|
||||
top: '-9999px',
|
||||
overflow: 'scroll',
|
||||
MsOverflowStyle: 'scrollbar'
|
||||
MsOverflowStyle: 'scrollbar',
|
||||
};
|
||||
|
||||
Object.keys(newStyles).map(style => {
|
||||
@ -23,7 +23,7 @@ export default function getScrollbarWidth() {
|
||||
});
|
||||
|
||||
document.body.appendChild(div);
|
||||
scrollbarWidth = (div.offsetWidth - div.clientWidth);
|
||||
scrollbarWidth = div.offsetWidth - div.clientWidth;
|
||||
document.body.removeChild(div);
|
||||
} else {
|
||||
scrollbarWidth = 0;
|
||||
|
@ -50,7 +50,7 @@ const XSSWL = Object.keys(xss.whiteList).reduce((acc, element) => {
|
||||
}, {});
|
||||
|
||||
const sanitizeXSS = new xss.FilterXSS({
|
||||
whiteList: XSSWL
|
||||
whiteList: XSSWL,
|
||||
});
|
||||
|
||||
/**
|
||||
@ -60,7 +60,7 @@ const sanitizeXSS = new xss.FilterXSS({
|
||||
* Info: https://github.com/leizongmin/js-xss#customize-css-filter
|
||||
* Whitelist: https://github.com/leizongmin/js-css-filter/blob/master/lib/default.js
|
||||
*/
|
||||
export function sanitize (unsanitizedString: string): string {
|
||||
export function sanitize(unsanitizedString: string): string {
|
||||
try {
|
||||
return sanitizeXSS.process(unsanitizedString);
|
||||
} catch (error) {
|
||||
|
@ -12,13 +12,13 @@ export function renderUrl(path: string, query: UrlQueryMap | undefined): string
|
||||
}
|
||||
|
||||
export function encodeURIComponentAsAngularJS(val, pctEncodeSpaces) {
|
||||
return encodeURIComponent(val).
|
||||
replace(/%40/gi, '@').
|
||||
replace(/%3A/gi, ':').
|
||||
replace(/%24/g, '$').
|
||||
replace(/%2C/gi, ',').
|
||||
replace(/%3B/gi, ';').
|
||||
replace(/%20/g, (pctEncodeSpaces ? '%20' : '+'));
|
||||
return encodeURIComponent(val)
|
||||
.replace(/%40/gi, '@')
|
||||
.replace(/%3A/gi, ':')
|
||||
.replace(/%24/g, '$')
|
||||
.replace(/%2C/gi, ',')
|
||||
.replace(/%3B/gi, ';')
|
||||
.replace(/%20/g, pctEncodeSpaces ? '%20' : '+');
|
||||
}
|
||||
|
||||
export function toUrlParams(a) {
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
export default class AdminEditOrgCtrl {
|
||||
/** @ngInject */
|
||||
constructor($scope, $routeParams, backendSrv, $location, navModelSrv) {
|
||||
@ -46,4 +45,3 @@ export default class AdminEditOrgCtrl {
|
||||
$scope.init();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
export default class AdminListOrgsCtrl {
|
||||
/** @ngInject */
|
||||
constructor($scope, backendSrv, navModelSrv) {
|
||||
@ -31,4 +30,3 @@ export default class AdminListOrgsCtrl {
|
||||
$scope.init();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ export class ServerStats extends PureComponent<Props, State> {
|
||||
|
||||
this.state = {
|
||||
stats: [],
|
||||
isLoading: false
|
||||
isLoading: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -25,4 +25,3 @@ export default class StyleGuideCtrl {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,8 +4,7 @@
|
||||
<div class="page-action-bar">
|
||||
<div class="page-action-bar__spacer"></div>
|
||||
<a class="page-header__cta btn btn-primary" href="org/new">
|
||||
<i class="fa fa-plus"></i>
|
||||
New Org
|
||||
New org
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
@ -8,8 +8,7 @@
|
||||
</label>
|
||||
<div class="page-action-bar__spacer"></div>
|
||||
<a class="btn btn-primary" href="admin/users/create">
|
||||
<i class="fa fa-plus"></i>
|
||||
Add new user
|
||||
New user
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
@ -18,7 +18,7 @@ const setup = (propOverrides?: object) => {
|
||||
togglePauseAlertRule: jest.fn(),
|
||||
stateFilter: '',
|
||||
search: '',
|
||||
isLoading: false
|
||||
isLoading: false,
|
||||
};
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
@ -147,9 +147,8 @@ describe('Functions', () => {
|
||||
describe('Search query change', () => {
|
||||
it('should set search query', () => {
|
||||
const { instance } = setup();
|
||||
const mockEvent = { target: { value: 'dashboard' } } as React.ChangeEvent<HTMLInputElement>;
|
||||
|
||||
instance.onSearchQueryChange(mockEvent);
|
||||
instance.onSearchQueryChange('dashboard');
|
||||
|
||||
expect(instance.props.setSearchQuery).toHaveBeenCalledWith('dashboard');
|
||||
});
|
||||
|
@ -9,6 +9,7 @@ import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { NavModel, StoreState, AlertRule } from 'app/types';
|
||||
import { getAlertRulesAsync, setSearchQuery, togglePauseAlertRule } from './state/actions';
|
||||
import { getAlertRuleItems, getSearchQuery } from './state/selectors';
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
@ -69,8 +70,7 @@ export class AlertRuleList extends PureComponent<Props, any> {
|
||||
});
|
||||
};
|
||||
|
||||
onSearchQueryChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = evt.target;
|
||||
onSearchQueryChange = (value: string) => {
|
||||
this.props.setSearchQuery(value);
|
||||
};
|
||||
|
||||
@ -78,7 +78,7 @@ export class AlertRuleList extends PureComponent<Props, any> {
|
||||
this.props.togglePauseAlertRule(rule.id, { paused: rule.state !== 'paused' });
|
||||
};
|
||||
|
||||
alertStateFilterOption = ({ text, value }: { text: string; value: string; }) => {
|
||||
alertStateFilterOption = ({ text, value }: { text: string; value: string }) => {
|
||||
return (
|
||||
<option key={value} value={value}>
|
||||
{text}
|
||||
@ -94,16 +94,13 @@ export class AlertRuleList extends PureComponent<Props, any> {
|
||||
<Page.Contents isLoading={isLoading}>
|
||||
<div className="page-action-bar">
|
||||
<div className="gf-form gf-form--grow">
|
||||
<label className="gf-form--has-input-icon gf-form--grow">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
placeholder="Search alerts"
|
||||
value={search}
|
||||
onChange={this.onSearchQueryChange}
|
||||
/>
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
</label>
|
||||
<FilterInput
|
||||
labelClassName="gf-form--has-input-icon gf-form--grow"
|
||||
inputClassName="gf-form-input"
|
||||
placeholder="Search alerts"
|
||||
value={search}
|
||||
onChange={this.onSearchQueryChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form">
|
||||
<label className="gf-form-label">States</label>
|
||||
@ -142,7 +139,7 @@ const mapStateToProps = (state: StoreState) => ({
|
||||
alertRules: getAlertRuleItems(state.alertRules),
|
||||
stateFilter: state.location.query.state,
|
||||
search: getSearchQuery(state.alertRules),
|
||||
isLoading: state.alertRules.isLoading
|
||||
isLoading: state.alertRules.isLoading,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = {
|
||||
|
@ -140,7 +140,7 @@ export class AlertTabCtrl {
|
||||
name: model.name,
|
||||
iconClass: this.getNotificationIcon(model.type),
|
||||
isDefault: false,
|
||||
uid: model.uid
|
||||
uid: model.uid,
|
||||
});
|
||||
|
||||
// avoid duplicates using both id and uid to be backwards compatible.
|
||||
@ -157,8 +157,8 @@ export class AlertTabCtrl {
|
||||
removeNotification(an) {
|
||||
// remove notifiers refeered to by id and uid to support notifiers added
|
||||
// before and after we added support for uid
|
||||
_.remove(this.alert.notifications, n => n.uid === an.uid || n.id === an.id);
|
||||
_.remove(this.alertNotifications, n => n.uid === an.uid || n.id === an.id);
|
||||
_.remove(this.alert.notifications, n => n.uid === an.uid || n.id === an.id);
|
||||
_.remove(this.alertNotifications, n => n.uid === an.uid || n.id === an.id);
|
||||
}
|
||||
|
||||
initModel() {
|
||||
|
@ -13,20 +13,13 @@ exports[`Render should render alert rules 1`] = `
|
||||
<div
|
||||
className="gf-form gf-form--grow"
|
||||
>
|
||||
<label
|
||||
className="gf-form--has-input-icon gf-form--grow"
|
||||
>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
placeholder="Search alerts"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<i
|
||||
className="gf-form-input-icon fa fa-search"
|
||||
/>
|
||||
</label>
|
||||
<ForwardRef
|
||||
inputClassName="gf-form-input"
|
||||
labelClassName="gf-form--has-input-icon gf-form--grow"
|
||||
onChange={[Function]}
|
||||
placeholder="Search alerts"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form"
|
||||
@ -167,20 +160,13 @@ exports[`Render should render component 1`] = `
|
||||
<div
|
||||
className="gf-form gf-form--grow"
|
||||
>
|
||||
<label
|
||||
className="gf-form--has-input-icon gf-form--grow"
|
||||
>
|
||||
<input
|
||||
className="gf-form-input"
|
||||
onChange={[Function]}
|
||||
placeholder="Search alerts"
|
||||
type="text"
|
||||
value=""
|
||||
/>
|
||||
<i
|
||||
className="gf-form-input-icon fa fa-search"
|
||||
/>
|
||||
</label>
|
||||
<ForwardRef
|
||||
inputClassName="gf-form-input"
|
||||
labelClassName="gf-form--has-input-icon gf-form--grow"
|
||||
onChange={[Function]}
|
||||
placeholder="Search alerts"
|
||||
value=""
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="gf-form"
|
||||
|
@ -8,7 +8,7 @@
|
||||
</div>
|
||||
|
||||
<a href="alerting/notification/new" class="btn btn-primary">
|
||||
New Channel
|
||||
New channel
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
@ -8,11 +8,11 @@ const setup = (propOverrides?: object) => {
|
||||
const props: Props = {
|
||||
navModel: {
|
||||
main: {
|
||||
text: 'Configuration'
|
||||
text: 'Configuration',
|
||||
},
|
||||
node: {
|
||||
text: 'Api Keys'
|
||||
}
|
||||
text: 'Api Keys',
|
||||
},
|
||||
} as NavModel,
|
||||
apiKeys: [] as ApiKey[],
|
||||
searchQuery: '',
|
||||
@ -78,9 +78,8 @@ describe('Functions', () => {
|
||||
describe('on search query change', () => {
|
||||
it('should call setSearchQuery', () => {
|
||||
const { instance } = setup();
|
||||
const mockEvent = { target: { value: 'test' } };
|
||||
|
||||
instance.onSearchQueryChange(mockEvent);
|
||||
instance.onSearchQueryChange('test');
|
||||
|
||||
expect(instance.props.setSearchQuery).toHaveBeenCalledWith('test');
|
||||
});
|
||||
|
@ -13,6 +13,7 @@ import config from 'app/core/config';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import { DeleteButton } from '@grafana/ui';
|
||||
import { FilterInput } from 'app/core/components/FilterInput/FilterInput';
|
||||
|
||||
export interface Props {
|
||||
navModel: NavModel;
|
||||
@ -59,8 +60,8 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
this.props.deleteApiKey(key.id);
|
||||
}
|
||||
|
||||
onSearchQueryChange = evt => {
|
||||
this.props.setSearchQuery(evt.target.value);
|
||||
onSearchQueryChange = (value: string) => {
|
||||
this.props.setSearchQuery(value);
|
||||
};
|
||||
|
||||
onToggleAdding = () => {
|
||||
@ -186,21 +187,18 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
<>
|
||||
<div className="page-action-bar">
|
||||
<div className="gf-form gf-form--grow">
|
||||
<label className="gf-form--has-input-icon gf-form--grow">
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
placeholder="Search keys"
|
||||
value={searchQuery}
|
||||
onChange={this.onSearchQueryChange}
|
||||
/>
|
||||
<i className="gf-form-input-icon fa fa-search" />
|
||||
</label>
|
||||
<FilterInput
|
||||
labelClassName="gf-form--has-input-icon gf-form--grow"
|
||||
inputClassName="gf-form-input"
|
||||
placeholder="Search keys"
|
||||
value={searchQuery}
|
||||
onChange={this.onSearchQueryChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="page-action-bar__spacer" />
|
||||
<button className="btn btn-primary pull-right" onClick={this.onToggleAdding} disabled={isAdding}>
|
||||
Add API Key
|
||||
Add API key
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -241,13 +239,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
|
||||
return (
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents isLoading={!hasFetched}>
|
||||
{hasFetched && (
|
||||
apiKeysCount > 0 ? (
|
||||
this.renderApiKeyList()
|
||||
) : (
|
||||
this.renderEmptyList()
|
||||
)
|
||||
)}
|
||||
{hasFetched && (apiKeysCount > 0 ? this.renderApiKeyList() : this.renderEmptyList())}
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
|
@ -10,13 +10,7 @@ export class AdHocFiltersCtrl {
|
||||
removeTagFilterSegment: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
private uiSegmentSrv,
|
||||
private datasourceSrv,
|
||||
private $q,
|
||||
private variableSrv,
|
||||
$scope,
|
||||
) {
|
||||
constructor(private uiSegmentSrv, private datasourceSrv, private $q, private variableSrv, $scope) {
|
||||
this.removeTagFilterSegment = uiSegmentSrv.newSegment({
|
||||
fake: true,
|
||||
value: '-- remove filter --',
|
||||
|
@ -87,7 +87,7 @@ export class AddPanelWidget extends React.Component<Props, State> {
|
||||
|
||||
if (tab === 'visualization') {
|
||||
location.query.tab = 'visualization';
|
||||
location.query.openVizPicker = true;
|
||||
location.query.openVizPicker = true;
|
||||
}
|
||||
|
||||
reduxStore.dispatch(updateLocation(location));
|
||||
@ -161,7 +161,9 @@ export class AddPanelWidget extends React.Component<Props, State> {
|
||||
)}
|
||||
</div>
|
||||
<div className="add-panel-widget__actions">
|
||||
<button className="btn btn-inverse add-panel-widget__action" onClick={this.onCreateNewRow}>Convert to row</button>
|
||||
<button className="btn btn-inverse add-panel-widget__action" onClick={this.onCreateNewRow}>
|
||||
Convert to row
|
||||
</button>
|
||||
{copiedPanelPlugins.length === 1 && (
|
||||
<button
|
||||
className="btn btn-inverse add-panel-widget__action"
|
||||
|
@ -76,7 +76,9 @@ export class DashboardPermissions extends PureComponent<Props, State> {
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div className="page-action-bar__spacer" />
|
||||
<button className="btn btn-primary pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>Add Permission</button>
|
||||
<button className="btn btn-primary pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>
|
||||
Add Permission
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<SlideDown in={isAdding}>
|
||||
|
@ -27,7 +27,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
|
||||
onVariableUpdated = () => {
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
onToggle = () => {
|
||||
this.props.dashboard.toggleRow(this.props.panel);
|
||||
@ -35,12 +35,12 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
this.setState(prevState => {
|
||||
return { collapsed: !prevState.collapsed };
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onUpdate = () => {
|
||||
this.props.dashboard.processRepeats();
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
onOpenSettings = () => {
|
||||
appEvents.emit('show-modal', {
|
||||
@ -51,7 +51,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
onUpdated: this.onUpdate,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
onDelete = () => {
|
||||
appEvents.emit('confirm-modal', {
|
||||
@ -66,7 +66,7 @@ export class DashboardRow extends React.Component<DashboardRowProps, any> {
|
||||
this.props.dashboard.removeRow(this.props.panel, false);
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const classes = classNames({
|
||||
|
@ -31,6 +31,6 @@ export class DashboardSettings extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div className="panel-height-helper" ref={element => this.element = element} />;
|
||||
return <div className="panel-height-helper" ref={element => (this.element = element)} />;
|
||||
}
|
||||
}
|
||||
|
@ -38,7 +38,7 @@ export class SettingsCtrl {
|
||||
});
|
||||
});
|
||||
|
||||
this.canSaveAs = this.dashboard.meta.canEdit && contextSrv.hasEditPermissionInFolders;
|
||||
this.canSaveAs = contextSrv.hasEditPermissionInFolders;
|
||||
this.canSave = this.dashboard.meta.canSave;
|
||||
this.canDelete = this.dashboard.meta.canSave;
|
||||
|
||||
|
@ -11,14 +11,12 @@
|
||||
|
||||
<div class="dashboard-settings__aside-actions">
|
||||
<button class="btn btn-primary" ng-click="ctrl.saveDashboard()" ng-show="ctrl.canSave">
|
||||
<i class="fa fa-save"></i> Save
|
||||
Save
|
||||
</button>
|
||||
<button class="btn btn-inverse" ng-click="ctrl.openSaveAsModal()" ng-show="ctrl.canSaveAs">
|
||||
<i class="fa fa-copy"></i>
|
||||
Save As...
|
||||
</button>
|
||||
<button class="btn btn-danger" ng-click="ctrl.deleteDashboard()" ng-show="ctrl.canDelete">
|
||||
<i class="fa fa-trash"></i>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
@ -101,7 +99,7 @@
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-primary" ng-click="ctrl.saveDashboardJson()" ng-show="ctrl.canSave">
|
||||
<i class="fa fa-save"></i> Save Changes
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -31,6 +31,6 @@ export class SubMenu extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
render() {
|
||||
return <div ref={element => this.element = element} />;
|
||||
return <div ref={element => (this.element = element)} />;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { DashboardPage, Props, State } from './DashboardPage';
|
||||
import { DashboardModel } from '../state';
|
||||
import { cleanUpDashboard } from '../state/actions';
|
||||
import { getNoPayloadActionCreatorMock, NoPayloadActionCreatorMock } from 'app/core/redux';
|
||||
import { getNoPayloadActionCreatorMock, NoPayloadActionCreatorMock } from 'app/core/redux';
|
||||
import { DashboardRouteInfo, DashboardInitPhase } from 'app/types';
|
||||
|
||||
jest.mock('sass/_variables.scss', () => ({
|
||||
@ -23,17 +23,20 @@ interface ScenarioContext {
|
||||
}
|
||||
|
||||
function getTestDashboard(overrides?: any, metaOverrides?: any): DashboardModel {
|
||||
const data = Object.assign({
|
||||
title: 'My dashboard',
|
||||
panels: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'graph',
|
||||
title: 'My graph',
|
||||
gridPos: { x: 0, y: 0, w: 1, h: 1 },
|
||||
},
|
||||
],
|
||||
}, overrides);
|
||||
const data = Object.assign(
|
||||
{
|
||||
title: 'My dashboard',
|
||||
panels: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'graph',
|
||||
title: 'My graph',
|
||||
gridPos: { x: 0, y: 0, w: 1, h: 1 },
|
||||
},
|
||||
],
|
||||
},
|
||||
overrides
|
||||
);
|
||||
|
||||
const meta = Object.assign({ canSave: true, canEdit: true }, metaOverrides);
|
||||
return new DashboardModel(data, meta);
|
||||
@ -74,7 +77,7 @@ function dashboardPageScenario(description, scenarioFn: (ctx: ScenarioContext) =
|
||||
|
||||
ctx.dashboard = props.dashboard;
|
||||
ctx.wrapper = shallow(<DashboardPage {...props} />);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@ -86,8 +89,7 @@ function dashboardPageScenario(description, scenarioFn: (ctx: ScenarioContext) =
|
||||
}
|
||||
|
||||
describe('DashboardPage', () => {
|
||||
|
||||
dashboardPageScenario("Given initial state", (ctx) => {
|
||||
dashboardPageScenario('Given initial state', ctx => {
|
||||
ctx.setup(() => {
|
||||
ctx.mount();
|
||||
});
|
||||
@ -97,7 +99,7 @@ describe('DashboardPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
dashboardPageScenario("Dashboard is fetching slowly", (ctx) => {
|
||||
dashboardPageScenario('Dashboard is fetching slowly', ctx => {
|
||||
ctx.setup(() => {
|
||||
ctx.mount();
|
||||
ctx.wrapper.setProps({
|
||||
@ -111,7 +113,7 @@ describe('DashboardPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
dashboardPageScenario("Dashboard init completed ", (ctx) => {
|
||||
dashboardPageScenario('Dashboard init completed ', ctx => {
|
||||
ctx.setup(() => {
|
||||
ctx.mount();
|
||||
ctx.setDashboardProp();
|
||||
@ -126,7 +128,7 @@ describe('DashboardPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
dashboardPageScenario("When user goes into panel edit", (ctx) => {
|
||||
dashboardPageScenario('When user goes into panel edit', ctx => {
|
||||
ctx.setup(() => {
|
||||
ctx.mount();
|
||||
ctx.setDashboardProp();
|
||||
@ -149,7 +151,7 @@ describe('DashboardPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
dashboardPageScenario("When user goes back to dashboard from panel edit", (ctx) => {
|
||||
dashboardPageScenario('When user goes back to dashboard from panel edit', ctx => {
|
||||
ctx.setup(() => {
|
||||
ctx.mount();
|
||||
ctx.setDashboardProp();
|
||||
@ -179,7 +181,7 @@ describe('DashboardPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
dashboardPageScenario("When dashboard has editview url state", (ctx) => {
|
||||
dashboardPageScenario('When dashboard has editview url state', ctx => {
|
||||
ctx.setup(() => {
|
||||
ctx.mount();
|
||||
ctx.setDashboardProp();
|
||||
@ -197,7 +199,7 @@ describe('DashboardPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
dashboardPageScenario("When adding panel", (ctx) => {
|
||||
dashboardPageScenario('When adding panel', ctx => {
|
||||
ctx.setup(() => {
|
||||
ctx.mount();
|
||||
ctx.setDashboardProp();
|
||||
@ -214,37 +216,37 @@ describe('DashboardPage', () => {
|
||||
});
|
||||
});
|
||||
|
||||
dashboardPageScenario("Given panel with id 0", (ctx) => {
|
||||
dashboardPageScenario('Given panel with id 0', ctx => {
|
||||
ctx.setup(() => {
|
||||
ctx.mount();
|
||||
ctx.setDashboardProp({
|
||||
panels: [{ id: 0, type: 'graph'}],
|
||||
panels: [{ id: 0, type: 'graph' }],
|
||||
schemaVersion: 17,
|
||||
});
|
||||
ctx.wrapper.setProps({
|
||||
urlEdit: true,
|
||||
urlFullscreen: true,
|
||||
urlPanelId: '0'
|
||||
urlPanelId: '0',
|
||||
});
|
||||
});
|
||||
|
||||
it('Should go into edit mode' , () => {
|
||||
it('Should go into edit mode', () => {
|
||||
expect(ctx.wrapper.state().isEditing).toBe(true);
|
||||
expect(ctx.wrapper.state().fullscreenPanel.id).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
dashboardPageScenario("When dashboard unmounts", (ctx) => {
|
||||
dashboardPageScenario('When dashboard unmounts', ctx => {
|
||||
ctx.setup(() => {
|
||||
ctx.mount();
|
||||
ctx.setDashboardProp({
|
||||
panels: [{ id: 0, type: 'graph'}],
|
||||
panels: [{ id: 0, type: 'graph' }],
|
||||
schemaVersion: 17,
|
||||
});
|
||||
ctx.wrapper.unmount();
|
||||
});
|
||||
|
||||
it('Should call clean up action' , () => {
|
||||
it('Should call clean up action', () => {
|
||||
expect(ctx.cleanUpDashboardMock.calls).toBe(1);
|
||||
});
|
||||
});
|
||||
|
@ -14,7 +14,7 @@ let lastGridWidth = 1200;
|
||||
let ignoreNextWidthChange = false;
|
||||
|
||||
interface GridWrapperProps {
|
||||
size: { width: number; };
|
||||
size: { width: number };
|
||||
layout: ReactGridLayout.Layout[];
|
||||
onLayoutChange: (layout: ReactGridLayout.Layout[]) => void;
|
||||
children: JSX.Element | JSX.Element[];
|
||||
@ -41,7 +41,7 @@ function GridWrapper({
|
||||
isResizable,
|
||||
isDraggable,
|
||||
isFullscreen,
|
||||
}: GridWrapperProps) {
|
||||
}: GridWrapperProps) {
|
||||
const width = size.width > 0 ? size.width : lastGridWidth;
|
||||
|
||||
// logic to ignore width changes (optimization)
|
||||
@ -149,21 +149,21 @@ export class DashboardGrid extends PureComponent<Props> {
|
||||
}
|
||||
|
||||
this.props.dashboard.sortPanelsByGridPos();
|
||||
}
|
||||
};
|
||||
|
||||
triggerForceUpdate = () => {
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
onWidthChange = () => {
|
||||
for (const panel of this.props.dashboard.panels) {
|
||||
panel.resizeDone();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onViewModeChanged = () => {
|
||||
ignoreNextWidthChange = true;
|
||||
}
|
||||
};
|
||||
|
||||
updateGridPos = (item: ReactGridLayout.Layout, layout: ReactGridLayout.Layout[]) => {
|
||||
this.panelMap[item.i].updateGridPos(item);
|
||||
@ -171,21 +171,21 @@ export class DashboardGrid extends PureComponent<Props> {
|
||||
// react-grid-layout has a bug (#670), and onLayoutChange() is only called when the component is mounted.
|
||||
// So it's required to call it explicitly when panel resized or moved to save layout changes.
|
||||
this.onLayoutChange(layout);
|
||||
}
|
||||
};
|
||||
|
||||
onResize: ItemCallback = (layout, oldItem, newItem) => {
|
||||
console.log();
|
||||
this.panelMap[newItem.i].updateGridPos(newItem);
|
||||
}
|
||||
};
|
||||
|
||||
onResizeStop: ItemCallback = (layout, oldItem, newItem) => {
|
||||
this.updateGridPos(newItem, layout);
|
||||
this.panelMap[newItem.i].resizeDone();
|
||||
}
|
||||
};
|
||||
|
||||
onDragStop: ItemCallback = (layout, oldItem, newItem) => {
|
||||
this.updateGridPos(newItem, layout);
|
||||
}
|
||||
};
|
||||
|
||||
renderPanels() {
|
||||
const panelElements = [];
|
||||
|
@ -10,14 +10,14 @@ import { PanelHeader } from './PanelHeader/PanelHeader';
|
||||
import { DataPanel } from './DataPanel';
|
||||
|
||||
// Utils
|
||||
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
|
||||
import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel';
|
||||
import { PANEL_HEADER_HEIGHT } from 'app/core/constants';
|
||||
import { profiler } from 'app/core/profiler';
|
||||
|
||||
// Types
|
||||
import { DashboardModel, PanelModel } from '../state';
|
||||
import { PanelPlugin } from 'app/types';
|
||||
import { TimeRange, LoadingState } from '@grafana/ui';
|
||||
import { TimeRange, LoadingState, PanelData } from '@grafana/ui';
|
||||
|
||||
import variables from 'sass/_variables.scss';
|
||||
import templateSrv from 'app/features/templating/template_srv';
|
||||
@ -94,7 +94,20 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
return !this.props.dashboard.otherPanelInFullscreen(this.props.panel);
|
||||
}
|
||||
|
||||
renderPanel(loading, panelData, width, height): JSX.Element {
|
||||
get hasPanelSnapshot() {
|
||||
const { panel } = this.props;
|
||||
return panel.snapshotData && panel.snapshotData.length;
|
||||
}
|
||||
|
||||
get needsQueryExecution() {
|
||||
return this.hasPanelSnapshot || this.props.plugin.dataFormats.length > 0;
|
||||
}
|
||||
|
||||
get getDataForPanel() {
|
||||
return this.hasPanelSnapshot ? snapshotDataToPanelData(this.props.panel) : null;
|
||||
}
|
||||
|
||||
renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element {
|
||||
const { panel, plugin } = this.props;
|
||||
const { timeRange, renderCounter } = this.state;
|
||||
const PanelComponent = plugin.exports.Panel;
|
||||
@ -121,11 +134,39 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { panel, dashboard } = this.props;
|
||||
const { refreshCounter, timeRange, timeInfo } = this.state;
|
||||
renderPanelBody = (width: number, height: number): JSX.Element => {
|
||||
const { panel } = this.props;
|
||||
const { refreshCounter, timeRange } = this.state;
|
||||
const { datasource, targets } = panel;
|
||||
return (
|
||||
<>
|
||||
{this.needsQueryExecution ? (
|
||||
<DataPanel
|
||||
panelId={panel.id}
|
||||
datasource={datasource}
|
||||
queries={targets}
|
||||
timeRange={timeRange}
|
||||
isVisible={this.isVisible}
|
||||
widthPixels={width}
|
||||
refreshCounter={refreshCounter}
|
||||
onDataResponse={this.onDataResponse}
|
||||
>
|
||||
{({ loading, panelData }) => {
|
||||
return this.renderPanelPlugin(loading, panelData, width, height);
|
||||
}}
|
||||
</DataPanel>
|
||||
) : (
|
||||
this.renderPanelPlugin(LoadingState.Done, this.getDataForPanel, width, height)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { dashboard, panel } = this.props;
|
||||
const { timeInfo } = this.state;
|
||||
const { transparent } = panel;
|
||||
|
||||
const { datasource, targets, transparent } = panel;
|
||||
const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`;
|
||||
return (
|
||||
<AutoSizer>
|
||||
@ -145,24 +186,7 @@ export class PanelChrome extends PureComponent<Props, State> {
|
||||
scopedVars={panel.scopedVars}
|
||||
links={panel.links}
|
||||
/>
|
||||
{panel.snapshotData ? (
|
||||
this.renderPanel(false, panel.snapshotData, width, height)
|
||||
) : (
|
||||
<DataPanel
|
||||
panelId={panel.id}
|
||||
datasource={datasource}
|
||||
queries={targets}
|
||||
timeRange={timeRange}
|
||||
isVisible={this.isVisible}
|
||||
widthPixels={width}
|
||||
refreshCounter={refreshCounter}
|
||||
onDataResponse={this.onDataResponse}
|
||||
>
|
||||
{({ loading, panelData }) => {
|
||||
return this.renderPanel(loading, panelData, width, height);
|
||||
}}
|
||||
</DataPanel>
|
||||
)}
|
||||
{this.renderPanelBody(width, height)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
|
@ -30,18 +30,18 @@ interface State {
|
||||
}
|
||||
|
||||
export class PanelHeader extends Component<Props, State> {
|
||||
clickCoordinates: ClickCoordinates = {x: 0, y: 0};
|
||||
clickCoordinates: ClickCoordinates = { x: 0, y: 0 };
|
||||
state = {
|
||||
panelMenuOpen: false,
|
||||
clickCoordinates: {x: 0, y: 0}
|
||||
clickCoordinates: { x: 0, y: 0 },
|
||||
};
|
||||
|
||||
eventToClickCoordinates = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
return {
|
||||
x: event.clientX,
|
||||
y: event.clientY
|
||||
y: event.clientY,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
onMouseDown = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
this.clickCoordinates = this.eventToClickCoordinates(event);
|
||||
@ -49,7 +49,7 @@ export class PanelHeader extends Component<Props, State> {
|
||||
|
||||
isClick = (clickCoordinates: ClickCoordinates) => {
|
||||
return isEqual(clickCoordinates, this.clickCoordinates);
|
||||
}
|
||||
};
|
||||
|
||||
onMenuToggle = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (this.isClick(this.eventToClickCoordinates(event))) {
|
||||
|
@ -76,13 +76,8 @@ export class PanelHeaderCorner extends Component<Props> {
|
||||
return (
|
||||
<>
|
||||
{infoMode === InfoModes.Info || infoMode === InfoModes.Links ? (
|
||||
<Tooltip
|
||||
content={this.getInfoContent()}
|
||||
placement="bottom-start"
|
||||
>
|
||||
<div
|
||||
className={`panel-info-corner panel-info-corner--${infoMode.toLowerCase()}`}
|
||||
>
|
||||
<Tooltip content={this.getInfoContent()} placement="bottom-start">
|
||||
<div className={`panel-info-corner panel-info-corner--${infoMode.toLowerCase()}`}>
|
||||
<i className="fa" />
|
||||
<span className="panel-info-corner-inner" />
|
||||
</div>
|
||||
|
@ -46,6 +46,7 @@ export function getPanelPluginNotFound(id: string): PanelPlugin {
|
||||
sort: 100,
|
||||
module: '',
|
||||
baseUrl: '',
|
||||
dataFormats: [],
|
||||
info: {
|
||||
author: {
|
||||
name: '',
|
||||
|
@ -66,7 +66,7 @@ export class PanelResizer extends PureComponent<Props, State> {
|
||||
|
||||
return (
|
||||
<>
|
||||
{render(isEditing ? {height: editorHeight} : this.noStyles)}
|
||||
{render(isEditing ? { height: editorHeight } : this.noStyles)}
|
||||
{isEditing && (
|
||||
<div className="panel-editor-container__resizer">
|
||||
<Draggable axis="y" grid={[100, 1]} onDrag={this.onDrag} position={{ x: 0, y: 0 }}>
|
||||
|
@ -27,4 +27,3 @@ import DashboardPermissions from './components/DashboardPermissions/DashboardPer
|
||||
import { react2AngularDirective } from 'app/core/utils/react2angular';
|
||||
|
||||
react2AngularDirective('dashboardPermissions', DashboardPermissions, ['dashboardId', 'folder']);
|
||||
|
||||
|
@ -30,6 +30,32 @@ interface PanelEditorTab {
|
||||
text: string;
|
||||
}
|
||||
|
||||
enum PanelEditorTabIds {
|
||||
Queries = 'queries',
|
||||
Visualization = 'visualization',
|
||||
Advanced = 'advanced',
|
||||
Alert = 'alert',
|
||||
}
|
||||
|
||||
interface PanelEditorTab {
|
||||
id: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const panelEditorTabTexts = {
|
||||
[PanelEditorTabIds.Queries]: 'Queries',
|
||||
[PanelEditorTabIds.Visualization]: 'Visualization',
|
||||
[PanelEditorTabIds.Advanced]: 'Panel Options',
|
||||
[PanelEditorTabIds.Alert]: 'Alert',
|
||||
};
|
||||
|
||||
const getPanelEditorTab = (tabId: PanelEditorTabIds): PanelEditorTab => {
|
||||
return {
|
||||
id: tabId,
|
||||
text: panelEditorTabTexts[tabId],
|
||||
};
|
||||
};
|
||||
|
||||
export class PanelEditor extends PureComponent<PanelEditorProps> {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
@ -72,31 +98,26 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
|
||||
|
||||
render() {
|
||||
const { plugin } = this.props;
|
||||
let activeTab = store.getState().location.query.tab || 'queries';
|
||||
let activeTab: PanelEditorTabIds = store.getState().location.query.tab || PanelEditorTabIds.Queries;
|
||||
|
||||
const tabs: PanelEditorTab[] = [
|
||||
{ id: 'queries', text: 'Queries' },
|
||||
{ id: 'visualization', text: 'Visualization' },
|
||||
{ id: 'advanced', text: 'Panel Options' },
|
||||
getPanelEditorTab(PanelEditorTabIds.Queries),
|
||||
getPanelEditorTab(PanelEditorTabIds.Visualization),
|
||||
getPanelEditorTab(PanelEditorTabIds.Advanced),
|
||||
];
|
||||
|
||||
// handle panels that do not have queries tab
|
||||
if (plugin.exports.PanelCtrl) {
|
||||
if (!plugin.exports.PanelCtrl.prototype.onDataReceived) {
|
||||
// remove queries tab
|
||||
tabs.shift();
|
||||
// switch tab
|
||||
if (activeTab === 'queries') {
|
||||
activeTab = 'visualization';
|
||||
}
|
||||
if (plugin.dataFormats.length === 0) {
|
||||
// remove queries tab
|
||||
tabs.shift();
|
||||
// switch tab
|
||||
if (activeTab === PanelEditorTabIds.Queries) {
|
||||
activeTab = PanelEditorTabIds.Visualization;
|
||||
}
|
||||
}
|
||||
|
||||
if (config.alertingEnabled && plugin.id === 'graph') {
|
||||
tabs.push({
|
||||
id: 'alert',
|
||||
text: 'Alert',
|
||||
});
|
||||
tabs.push(getPanelEditorTab(PanelEditorTabIds.Alert));
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -131,14 +131,7 @@ export class QueryEditorRow extends PureComponent<Props, State> {
|
||||
|
||||
if (datasource.pluginExports.QueryEditor) {
|
||||
const QueryEditor = datasource.pluginExports.QueryEditor;
|
||||
return (
|
||||
<QueryEditor
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
onRunQuery={this.onRunQuery}
|
||||
/>
|
||||
);
|
||||
return <QueryEditor query={query} datasource={datasource} onChange={onChange} onRunQuery={this.onRunQuery} />;
|
||||
}
|
||||
|
||||
return <div>Data source plugin does not export any Query Editor component</div>;
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user