Merge branch 'master' into 15330-vizpicker-red-when-0-hits

This commit is contained in:
Torkel Ödegaard 2019-02-13 14:49:11 +01:00
commit 124b3486eb
216 changed files with 3223 additions and 2760 deletions

8
.prettierignore Normal file
View File

@ -0,0 +1,8 @@
.git
.github
dist/
pkg/
node_modules
public/vendor/
vendor/

View File

@ -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/*/**"]
}
}

View File

@ -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';

View File

@ -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;

View File

@ -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);

View File

@ -119,4 +119,3 @@ export class ColorPickerPopover<T extends CustomPickersDescriptor> extends React
);
}
}

View File

@ -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);
});

View File

@ -15,7 +15,6 @@ const SpectrumPalettePointer: React.FunctionComponent<SpectrumPalettePointerProp
},
};
const pointerColor = selectThemeVariant(
{
light: theme.colors.dark3,

View File

@ -37,4 +37,4 @@
@include gradient-horizontal($scrollbarBackground, $scrollbarBackground2);
border-radius: 6px;
}
}
}

View File

@ -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;
}

View File

@ -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>

View File

@ -25,7 +25,7 @@ const setup = (propOverrides?: object) => {
width: 300,
value: 25,
decimals: 0,
theme: getTheme()
theme: getTheme(),
};
Object.assign(props, propOverrides);

View File

@ -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>;
};

View File

@ -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>
);
};

View File

@ -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} />

View File

@ -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 {

View File

@ -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>
);
}

View File

@ -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';

View File

@ -1,4 +1,4 @@
import tinycolor from 'tinycolor2';
import tinycolor from 'tinycolor2';
import defaultTheme from './default';
import { GrafanaTheme, GrafanaThemeType } from '../types/theme';

View File

@ -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;

View File

@ -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);
});

View File

@ -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];

View File

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

View File

@ -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', () => {

View File

@ -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;
};

View File

@ -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

View File

@ -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

View File

@ -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') },
],
}
},
];

View File

@ -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))

View File

@ -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) {

View File

@ -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

View File

@ -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,
}
}

View File

@ -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 {

View File

@ -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,
})

View File

@ -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

View File

@ -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()
}

View File

@ -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
}
}
}
}

View File

@ -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 := &quota.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)
})
})
})
}

View File

@ -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())

View File

@ -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)
}

View File

@ -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
}

View File

@ -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[:])

View File

@ -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() {

View File

@ -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

View File

@ -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

View File

@ -27,7 +27,7 @@ class ErrorBoundary extends Component<Props, State> {
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.setState({
error: error,
errorInfo: errorInfo
errorInfo: errorInfo,
});
}

View 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>
));

View File

@ -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;

View File

@ -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" />

View File

@ -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]}
/>

View File

@ -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>
);
}
}

View File

@ -10,7 +10,6 @@ interface Props {
}
class PageContents extends Component<Props> {
render() {
const { isLoading } = this.props;

View File

@ -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: {

View File

@ -13,9 +13,9 @@ jest.mock('app/store/store', () => ({
getState: jest.fn().mockReturnValue({
location: {
lastUpdated: 0,
}
})
}
},
}),
},
}));
jest.mock('app/core/services/context_srv', () => ({

View File

@ -57,7 +57,7 @@ export class Settings {
isEnterprise: false,
},
viewersCanEdit: false,
disableSanitizeHtml: false
disableSanitizeHtml: false,
};
_.extend(this, defaults, options);

View File

@ -28,7 +28,7 @@ export function autofillEventFix($compile) {
input.removeEventListener('animationstart', onAnimationStart);
// input.removeEventListener('change', onChange);
});
}
},
};
}

View File

@ -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);

View File

@ -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,
};
});
}

View File

@ -1,4 +1,3 @@
export class Profiler {
panelsRendered: number;
enabled: boolean;

View File

@ -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 : ''}`;
};

View File

@ -1,4 +1,3 @@
const backendSrv = {
get: jest.fn(),
getDashboard: jest.fn(),
@ -10,5 +9,3 @@ const backendSrv = {
export function getBackendSrv() {
return backendSrv;
}

View File

@ -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);
});
});

View File

@ -19,7 +19,7 @@ const DEFAULT_EXPLORE_STATE: ExploreUrlState = {
showingTable: true,
showingLogs: true,
dedupStrategy: LogsDedupStrategy.none,
}
},
};
describe('state functions', () => {

View File

@ -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);

View File

@ -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();

View 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);

View File

@ -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;

View File

@ -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) {

View File

@ -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) {

View File

@ -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();
}
}

View File

@ -1,4 +1,3 @@
export default class AdminListOrgsCtrl {
/** @ngInject */
constructor($scope, backendSrv, navModelSrv) {
@ -31,4 +30,3 @@ export default class AdminListOrgsCtrl {
$scope.init();
}
}

View File

@ -22,7 +22,7 @@ export class ServerStats extends PureComponent<Props, State> {
this.state = {
stats: [],
isLoading: false
isLoading: false,
};
}

View File

@ -25,4 +25,3 @@ export default class StyleGuideCtrl {
});
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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');
});

View File

@ -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 = {

View File

@ -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() {

View File

@ -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"

View File

@ -8,7 +8,7 @@
</div>
<a href="alerting/notification/new" class="btn btn-primary">
New Channel
New channel
</a>
</div>

View File

@ -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');
});

View File

@ -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>
);

View File

@ -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 --',

View File

@ -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"

View File

@ -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}>

View File

@ -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({

View File

@ -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)} />;
}
}

View File

@ -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;

View File

@ -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>

View File

@ -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)} />;
}
}

View File

@ -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);
});
});

View File

@ -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 = [];

View File

@ -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>
);
}}

View File

@ -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))) {

View File

@ -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>

View File

@ -46,6 +46,7 @@ export function getPanelPluginNotFound(id: string): PanelPlugin {
sort: 100,
module: '',
baseUrl: '',
dataFormats: [],
info: {
author: {
name: '',

View File

@ -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 }}>

View File

@ -27,4 +27,3 @@ import DashboardPermissions from './components/DashboardPermissions/DashboardPer
import { react2AngularDirective } from 'app/core/utils/react2angular';
react2AngularDirective('dashboardPermissions', DashboardPermissions, ['dashboardId', 'folder']);

View File

@ -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 (

View File

@ -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