diff --git a/conf/defaults.ini b/conf/defaults.ini index df02e01235b..044d8e59a7a 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -231,6 +231,7 @@ verify_email_enabled = false # Background text for the user field on the login page login_hint = email or username +password_hint = password # Default UI theme ("dark" or "light") default_theme = dark diff --git a/conf/sample.ini b/conf/sample.ini index 57ff82181de..dc1e4fbde8e 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -211,6 +211,7 @@ log_queries = # Background text for the user field on the login page ;login_hint = email or username +;password_hint = password # Default UI theme ("dark" or "light") ;default_theme = dark diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index f0418ad31a6..3d1b25979c3 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -162,9 +162,9 @@ executed with working directory set to the installation path. ### enable_gzip -Set this option to `true` to enable HTTP compression, this can improve -transfer speed and bandwidth utilization. It is recommended that most -users set it to `true`. By default it is set to `false` for compatibility +Set this option to `true` to enable HTTP compression, this can improve +transfer speed and bandwidth utilization. It is recommended that most +users set it to `true`. By default it is set to `false` for compatibility reasons. ### cert_file @@ -342,6 +342,14 @@ options are `Admin` and `Editor`. e.g. : Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard. Defaults to `false`. +### login_hint + +Text used as placeholder text on login page for login/username input. + +### password_hint + +Text used as placeholder text on login page for password input. +
## [auth] diff --git a/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss index 4ce9c5264ea..2b1a401d1e4 100644 --- a/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss +++ b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss @@ -53,7 +53,7 @@ } .panel-options-group__title { - font-size: 1.1rem; + font-size: 16px; position: relative; top: 1px; } diff --git a/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts b/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts index ca210902ced..0ce4fac8182 100644 --- a/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts +++ b/packages/grafana-ui/src/themes/_variables.scss.tmpl.ts @@ -17,7 +17,7 @@ $enable-hover-media-query: false !default; // Control the default styling of most Bootstrap elements by modifying these // variables. Mostly focused on spacing. -$spacer: 1rem !default; +$spacer: ${theme.spacing.m} !default; $spacer-x: $spacer !default; $spacer-y: $spacer !default; $spacers: ( @@ -46,7 +46,7 @@ $spacers: ( ), ), ) !default; -$border-width: 1px !default; +$border-width: ${theme.border.width.s} !default; // Grid breakpoints // @@ -54,11 +54,11 @@ $border-width: 1px !default; // adapting to different screen sizes, for use in media queries. $grid-breakpoints: ( - xs: 0, - sm: 544px, - md: 768px, - lg: 992px, - xl: 1200px, + xs: ${theme.breakpoints.xs}, + sm: ${theme.breakpoints.s}, + md: ${theme.breakpoints.m}, + lg: ${theme.breakpoints.l}, + xl: ${theme.breakpoints.xl}, ) !default; // Grid containers @@ -84,46 +84,32 @@ $enable-flex: true; // Typography // ------------------------- -$font-family-sans-serif: 'Roboto', Helvetica, Arial, sans-serif; -$font-family-serif: Georgia, 'Times New Roman', Times, serif; -$font-family-monospace: Menlo, Monaco, Consolas, 'Courier New', monospace; +$font-family-sans-serif: ${theme.typography.fontFamily.sansSerif}; +$font-family-monospace: ${theme.typography.fontFamily.monospace}; $font-family-base: $font-family-sans-serif !default; -$font-size-root: 14px !default; -$font-size-base: 13px !default; +$font-size-root: ${theme.typography.size.root} !default; +$font-size-base: ${theme.typography.size.base} !default; -$font-size-lg: 18px !default; -$font-size-md: 14px !default; -$font-size-sm: 12px !default; -$font-size-xs: 10px !default; +$font-size-lg: ${theme.typography.size.l} !default; +$font-size-md: ${theme.typography.size.m} !default; +$font-size-sm: ${theme.typography.size.s} !default; +$font-size-xs: ${theme.typography.size.xs} !default; -$line-height-base: 1.5 !default; -$font-weight-semi-bold: 500; +$line-height-base: ${theme.typography.lineHeight.l} !default; +$font-weight-semi-bold: ${theme.typography.weight.semibold}; -$font-size-h1: 2rem !default; -$font-size-h2: 1.75rem !default; -$font-size-h3: 1.5rem !default; -$font-size-h4: 1.3rem !default; -$font-size-h5: 1.2rem !default; -$font-size-h6: 1rem !default; - -$display1-size: 6rem !default; -$display2-size: 5.5rem !default; -$display3-size: 4.5rem !default; -$display4-size: 3.5rem !default; - -$display1-weight: 400 !default; -$display2-weight: 400 !default; -$display3-weight: 400 !default; -$display4-weight: 400 !default; - -$lead-font-size: 1.25rem !default; -$lead-font-weight: 300 !default; +$font-size-h1: ${theme.typography.heading.h1} !default; +$font-size-h2: ${theme.typography.heading.h2} !default; +$font-size-h3: ${theme.typography.heading.h3} !default; +$font-size-h4: ${theme.typography.heading.h4} !default; +$font-size-h5: ${theme.typography.heading.h5} !default; +$font-size-h6: ${theme.typography.heading.h6} !default; $headings-margin-bottom: ($spacer / 2) !default; $headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif; -$headings-font-weight: 400 !default; -$headings-line-height: 1.1 !default; +$headings-font-weight: ${theme.typography.weight.normal} !default; +$headings-line-height: ${theme.typography.lineHeight.s} !default; $hr-border-width: $border-width !default; $dt-font-weight: bold !default; @@ -141,8 +127,8 @@ $border-radius-sm: 2px !default; // Page -$page-sidebar-width: 11rem; -$page-sidebar-margin: 4rem; +$page-sidebar-width: 154px; +$page-sidebar-margin: 56px; // Links // ------------------------- @@ -174,7 +160,7 @@ $input-padding-y-lg: 10px !default; $input-height: 35px !default; -$gf-form-margin: 0.2rem; +$gf-form-margin: 3px; $gf-form-input-height: 35px; $cursor-disabled: not-allowed !default; @@ -199,13 +185,13 @@ $zindex-typeahead: 1060; // Buttons // -$btn-padding-x: 1rem !default; -$btn-padding-y: 0.7rem !default; +$btn-padding-x: 14px !default; +$btn-padding-y: 10px !default; $btn-line-height: 1 !default; -$btn-font-weight: 500 !default; +$btn-font-weight: ${theme.typography.weight.semibold} !default; -$btn-padding-x-sm: 0.5rem !default; -$btn-padding-y-sm: 0.25rem !default; +$btn-padding-x-sm: 7px !default; +$btn-padding-y-sm: 4px !default; $btn-padding-x-lg: 21px !default; $btn-padding-y-lg: 11px !default; diff --git a/packages/grafana-ui/src/themes/default.ts b/packages/grafana-ui/src/themes/default.ts index 104edced000..60ebcfa7aa0 100644 --- a/packages/grafana-ui/src/themes/default.ts +++ b/packages/grafana-ui/src/themes/default.ts @@ -5,10 +5,10 @@ const theme: GrafanaThemeCommons = { typography: { fontFamily: { sansSerif: "'Roboto', Helvetica, Arial, sans-serif", - serif: "Georgia, 'Times New Roman', Times, serif", monospace: "Menlo, Monaco, Consolas, 'Courier New', monospace", }, size: { + root: '14px', base: '13px', xs: '10px', s: '12px', @@ -16,12 +16,12 @@ const theme: GrafanaThemeCommons = { l: '18px', }, heading: { - h1: '2rem', - h2: '1.75rem', - h3: '1.5rem', - h4: '1.3rem', - h5: '1.2rem', - h6: '1rem', + h1: '28px', + h2: '24px', + h3: '21px', + h4: '18px', + h5: '16px', + h6: '14px', }, weight: { light: 300, @@ -35,7 +35,7 @@ const theme: GrafanaThemeCommons = { l: 1.5, }, }, - brakpoints: { + breakpoints: { xs: '0', s: '544px', m: '768px', @@ -44,9 +44,9 @@ const theme: GrafanaThemeCommons = { }, spacing: { xs: '0', - s: '0.2rem', - m: '1rem', - l: '1.5rem', + s: '3px', + m: '14px', + l: '21px', gutter: '30px', }, border: { @@ -55,6 +55,9 @@ const theme: GrafanaThemeCommons = { s: '3px', m: '5px', }, + width: { + s: '1px', + }, }, }; diff --git a/packages/grafana-ui/src/types/theme.ts b/packages/grafana-ui/src/types/theme.ts index 30f1bf4685b..1226b841836 100644 --- a/packages/grafana-ui/src/types/theme.ts +++ b/packages/grafana-ui/src/types/theme.ts @@ -6,7 +6,7 @@ export enum GrafanaThemeType { export interface GrafanaThemeCommons { name: string; // TODO: not sure if should be a part of theme - brakpoints: { + breakpoints: { xs: string; s: string; m: string; @@ -16,10 +16,10 @@ export interface GrafanaThemeCommons { typography: { fontFamily: { sansSerif: string; - serif: string; monospace: string; }; size: { + root: string; base: string; xs: string; s: string; @@ -60,6 +60,9 @@ export interface GrafanaThemeCommons { s: string; m: string; }; + width: { + s: string; + }; }; } diff --git a/pkg/api/login.go b/pkg/api/login.go index 1445463852b..f6c3e802988 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -36,6 +36,7 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) { viewData.Settings["oauth"] = enabledOAuths viewData.Settings["disableUserSignUp"] = !setting.AllowUserSignUp viewData.Settings["loginHint"] = setting.LoginHint + viewData.Settings["passwordHint"] = setting.PasswordHint viewData.Settings["disableLoginForm"] = setting.DisableLoginForm if loginError, ok := tryGetEncryptedCookie(c, LoginErrorCookieName); ok { diff --git a/pkg/services/alerting/test_notification.go b/pkg/services/alerting/test_notification.go index 5ffc8dc58fc..22f6a2118b7 100644 --- a/pkg/services/alerting/test_notification.go +++ b/pkg/services/alerting/test_notification.go @@ -56,7 +56,7 @@ func createTestEvalContext(cmd *NotificationTestCommand) *EvalContext { ctx := NewEvalContext(context.Background(), testRule) if cmd.Settings.Get("uploadImage").MustBool(true) { - ctx.ImagePublicUrl = "http://grafana.org/assets/img/blog/mixed_styles.png" + ctx.ImagePublicUrl = "https://grafana.com/assets/img/blog/mixed_styles.png" } ctx.IsTestRun = true ctx.Firing = true diff --git a/pkg/services/sqlstore/datasource.go b/pkg/services/sqlstore/datasource.go index 36fd7c5b023..071577eb6d6 100644 --- a/pkg/services/sqlstore/datasource.go +++ b/pkg/services/sqlstore/datasource.go @@ -174,6 +174,11 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error { Version: cmd.Version + 1, } + sess.UseBool("is_default") + sess.UseBool("basic_auth") + sess.UseBool("with_credentials") + sess.UseBool("read_only") + var updateSession *xorm.Session if cmd.Version != 0 { // the reason we allow cmd.version > db.version is make it possible for people to force @@ -185,7 +190,7 @@ func UpdateDataSource(cmd *m.UpdateDataSourceCommand) error { updateSession = sess.Where("id=? and org_id=?", ds.Id, ds.OrgId) } - affected, err := updateSession.AllCols().Omit("created").Update(ds) + affected, err := updateSession.Update(ds) if err != nil { return err } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 5d44a3585dc..7b6e99255aa 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -109,6 +109,7 @@ var ( AutoAssignOrgRole string VerifyEmailEnabled bool LoginHint string + PasswordHint string DefaultTheme string DisableLoginForm bool DisableSignoutMenu bool @@ -656,6 +657,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Viewer"}) VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false) LoginHint = users.Key("login_hint").String() + PasswordHint = users.Key("password_hint").String() DefaultTheme = users.Key("default_theme").String() ExternalUserMngLinkUrl = users.Key("external_manage_link_url").String() ExternalUserMngLinkName = users.Key("external_manage_link_name").String() diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 278025db75a..6d68e6902e5 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -496,9 +496,6 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) { } alias := model.Get("alias").MustString() - if alias == "" { - alias = "{{metric}}_{{stat}}" - } returnData := model.Get("returnData").MustBool(false) highResolution := model.Get("highResolution").MustBool(false) @@ -521,7 +518,11 @@ func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) { func formatAlias(query *CloudWatchQuery, stat string, dimensions map[string]string) string { if len(query.Id) > 0 && len(query.Expression) > 0 { - return query.Id + if len(query.Alias) > 0 { + return query.Alias + } else { + return query.Id + } } data := map[string]string{} diff --git a/pkg/tsdb/elasticsearch/client/index_pattern.go b/pkg/tsdb/elasticsearch/client/index_pattern.go index 952b5c4f806..da3c471424d 100644 --- a/pkg/tsdb/elasticsearch/client/index_pattern.go +++ b/pkg/tsdb/elasticsearch/client/index_pattern.go @@ -279,7 +279,7 @@ func formatDate(t time.Time, pattern string) string { isoYearShort := fmt.Sprintf("%d", isoYear)[2:4] formatted = strings.Replace(formatted, "", fmt.Sprintf("%d", isoYear), -1) formatted = strings.Replace(formatted, "", isoYearShort, -1) - formatted = strings.Replace(formatted, "", fmt.Sprintf("%d", isoWeek), -1) + formatted = strings.Replace(formatted, "", fmt.Sprintf("%02d", isoWeek), -1) formatted = strings.Replace(formatted, "", fmt.Sprintf("%d", t.Unix()), -1) diff --git a/pkg/tsdb/elasticsearch/client/index_pattern_test.go b/pkg/tsdb/elasticsearch/client/index_pattern_test.go index ca20b39d532..a3a056da04f 100644 --- a/pkg/tsdb/elasticsearch/client/index_pattern_test.go +++ b/pkg/tsdb/elasticsearch/client/index_pattern_test.go @@ -76,6 +76,15 @@ func TestIndexPattern(t *testing.T) { So(indices, ShouldHaveLength, 1) So(indices[0], ShouldEqual, "2018-data") }) + + Convey("Should return 01 week", func() { + from = fmt.Sprintf("%d", time.Date(2018, 1, 15, 17, 50, 0, 0, time.UTC).UnixNano()/int64(time.Millisecond)) + to = fmt.Sprintf("%d", time.Date(2018, 1, 15, 17, 55, 0, 0, time.UTC).UnixNano()/int64(time.Millisecond)) + indexPatternScenario(intervalWeekly, "[data-]GGGG.WW", tsdb.NewTimeRange(from, to), func(indices []string) { + So(indices, ShouldHaveLength, 1) + So(indices[0], ShouldEqual, "data-2018.03") + }) + }) }) Convey("Hourly interval", t, func() { diff --git a/public/app/core/components/sidemenu/SideMenu.test.tsx b/public/app/core/components/sidemenu/SideMenu.test.tsx index f7b7df69cfc..6352833490a 100644 --- a/public/app/core/components/sidemenu/SideMenu.test.tsx +++ b/public/app/core/components/sidemenu/SideMenu.test.tsx @@ -2,7 +2,6 @@ import React from 'react'; import { shallow } from 'enzyme'; import { SideMenu } from './SideMenu'; import appEvents from '../../app_events'; -import { contextSrv } from 'app/core/services/context_srv'; jest.mock('../../app_events', () => ({ emit: jest.fn(), @@ -26,7 +25,6 @@ jest.mock('app/core/services/context_srv', () => ({ isGrafanaAdmin: false, isEditor: false, hasEditPermissionFolders: false, - toggleSideMenu: jest.fn(), }, })); @@ -54,20 +52,6 @@ describe('Render', () => { }); describe('Functions', () => { - describe('toggle side menu', () => { - const wrapper = setup(); - const instance = wrapper.instance() as SideMenu; - instance.toggleSideMenu(); - - it('should call contextSrv.toggleSideMenu', () => { - expect(contextSrv.toggleSideMenu).toHaveBeenCalled(); - }); - - it('should emit toggle sidemenu event', () => { - expect(appEvents.emit).toHaveBeenCalledWith('toggle-sidemenu'); - }); - }); - describe('toggle side menu on mobile', () => { const wrapper = setup(); const instance = wrapper.instance() as SideMenu; diff --git a/public/app/core/components/sidemenu/SideMenu.tsx b/public/app/core/components/sidemenu/SideMenu.tsx index 1428ae181f5..b0ef053f746 100644 --- a/public/app/core/components/sidemenu/SideMenu.tsx +++ b/public/app/core/components/sidemenu/SideMenu.tsx @@ -1,31 +1,21 @@ import React, { PureComponent } from 'react'; import appEvents from '../../app_events'; -import { contextSrv } from 'app/core/services/context_srv'; import TopSection from './TopSection'; import BottomSection from './BottomSection'; -import { store } from 'app/store/store'; +import config from 'app/core/config'; + +const homeUrl = config.appSubUrl || '/'; export class SideMenu extends PureComponent { - toggleSideMenu = () => { - // ignore if we just made a location change, stops hiding sidemenu on double clicks of back button - const timeSinceLocationChanged = new Date().getTime() - store.getState().location.lastUpdated; - if (timeSinceLocationChanged < 1000) { - return; - } - - contextSrv.toggleSideMenu(); - appEvents.emit('toggle-sidemenu'); - }; - toggleSideMenuSmallBreakpoint = () => { appEvents.emit('toggle-sidemenu-mobile'); }; render() { return [ -
+ Grafana -
, + ,
diff --git a/public/app/core/components/sidemenu/__snapshots__/SideMenu.test.tsx.snap b/public/app/core/components/sidemenu/__snapshots__/SideMenu.test.tsx.snap index ec2fa845c6d..8d23cdc1565 100644 --- a/public/app/core/components/sidemenu/__snapshots__/SideMenu.test.tsx.snap +++ b/public/app/core/components/sidemenu/__snapshots__/SideMenu.test.tsx.snap @@ -2,16 +2,16 @@ exports[`Render should render component 1`] = ` Array [ -
Grafana -
, + ,
{ + const rowCopy = { ...row }; const previous = result[result.length - 1]; if (index > 0 && isDuplicateRow(row, previous, strategy)) { previous.duplicates++; } else { - row.duplicates = 0; - result.push(row); + rowCopy.duplicates = 0; + result.push(rowCopy); } return result; }, []); diff --git a/public/app/core/services/context_srv.ts b/public/app/core/services/context_srv.ts index 7bb753e6f71..e3b10f129d1 100644 --- a/public/app/core/services/context_srv.ts +++ b/public/app/core/services/context_srv.ts @@ -1,7 +1,6 @@ import config from 'app/core/config'; import _ from 'lodash'; import coreModule from 'app/core/core_module'; -import store from 'app/core/store'; export class User { isGrafanaAdmin: any; @@ -29,13 +28,10 @@ export class ContextSrv { isSignedIn: any; isGrafanaAdmin: any; isEditor: any; - sidemenu: any; sidemenuSmallBreakpoint = false; hasEditPermissionInFolders: boolean; constructor() { - this.sidemenu = store.getBool('grafana.sidemenu', true); - if (!config.bootData) { config.bootData = { user: {}, settings: {} }; } @@ -55,11 +51,6 @@ export class ContextSrv { return !!(document.visibilityState === undefined || document.visibilityState === 'visible'); } - toggleSideMenu() { - this.sidemenu = !this.sidemenu; - store.set('grafana.sidemenu', this.sidemenu); - } - hasAccessToExplore() { return (this.isEditor || config.viewersCanEdit) && config.exploreEnabled; } diff --git a/public/app/core/specs/logs_model.test.ts b/public/app/core/specs/logs_model.test.ts index c59d18e8b7d..dcd1e483330 100644 --- a/public/app/core/specs/logs_model.test.ts +++ b/public/app/core/specs/logs_model.test.ts @@ -113,6 +113,34 @@ describe('dedupLogRows()', () => { }, ]); }); + + test('should return to non-deduped state on same log result', () => { + const logs = { + rows: [ + { + entry: 'INFO 123', + }, + { + entry: 'WARN 123', + }, + { + entry: 'WARN 123', + }, + ], + }; + expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.exact).rows).toEqual([ + { + duplicates: 0, + entry: 'INFO 123', + }, + { + duplicates: 1, + entry: 'WARN 123', + }, + ]); + + expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.none).rows).toEqual(logs.rows); + }); }); describe('calculateFieldStats()', () => { diff --git a/public/app/features/explore/LogLabels.tsx b/public/app/features/explore/LogLabels.tsx index 7105a2a5370..24d6e1ec23c 100644 --- a/public/app/features/explore/LogLabels.tsx +++ b/public/app/features/explore/LogLabels.tsx @@ -13,8 +13,19 @@ interface Props { export class LogLabels extends PureComponent { render() { const { getRows, labels, onClickLabel, plain } = this.props; - return Object.keys(labels).map(key => ( - - )); + return ( + + {Object.keys(labels).map(key => ( + + ))} + + ); } } diff --git a/public/app/features/explore/LogRow.tsx b/public/app/features/explore/LogRow.tsx index d7615446b21..e45864f7b54 100644 --- a/public/app/features/explore/LogRow.tsx +++ b/public/app/features/explore/LogRow.tsx @@ -150,7 +150,7 @@ export class LogRow extends PureComponent {
)} {showLocalTime && ( -
+
{row.timeLocal}
)} diff --git a/public/app/features/explore/state/selectors.test.ts b/public/app/features/explore/state/selectors.test.ts new file mode 100644 index 00000000000..3a1fa5102ae --- /dev/null +++ b/public/app/features/explore/state/selectors.test.ts @@ -0,0 +1,108 @@ +import { deduplicatedLogsSelector } from './selectors'; +import { LogsDedupStrategy } from 'app/core/logs_model'; +import { ExploreItemState } from 'app/types'; + +const state = { + logsResult: { + rows: [ + { + entry: '2019-03-05T11:00:56Z sntpc sntpc[1]: offset=-0.033938, delay=0.000649', + }, + { + entry: '2019-03-05T11:00:26Z sntpc sntpc[1]: offset=-0.033730, delay=0.000581', + }, + { + entry: '2019-03-05T10:59:56Z sntpc sntpc[1]: offset=-0.034184, delay=0.001089', + }, + { + entry: '2019-03-05T10:59:26Z sntpc sntpc[1]: offset=-0.033972, delay=0.000582', + }, + { + entry: '2019-03-05T10:58:56Z sntpc sntpc[1]: offset=-0.033955, delay=0.000606', + }, + { + entry: '2019-03-05T10:58:26Z sntpc sntpc[1]: offset=-0.034067, delay=0.000616', + }, + { + entry: '2019-03-05T10:57:56Z sntpc sntpc[1]: offset=-0.034155, delay=0.001021', + }, + { + entry: '2019-03-05T10:57:26Z sntpc sntpc[1]: offset=-0.035797, delay=0.000883', + }, + { + entry: '2019-03-05T10:56:56Z sntpc sntpc[1]: offset=-0.046818, delay=0.000605', + }, + { + entry: '2019-03-05T10:56:26Z sntpc sntpc[1]: offset=-0.049200, delay=0.000584', + }, + ], + }, + hiddenLogLevels: undefined, + dedupStrategy: LogsDedupStrategy.none, +}; + +describe('Deduplication selector', () => { + it('should correctly deduplicate log rows when changing strategy multiple times', () => { + // Simulating sequence of UI actions that was causing a problem with deduplication counter being visible when unnecessary. + // The sequence was changing dedup strategy: (none -> exact -> numbers -> signature -> none) *2 -> exact. After that the first + // row contained information that was deduped, while it shouldn't be. + // Problem was caused by mutating the log results entries in redux state. The memoisation hash for deduplicatedLogsSelector + // was changing depending on duplicates information from log row state, while should be dependand on log row only. + + let dedups = deduplicatedLogsSelector(state as ExploreItemState); + expect(dedups.rows.length).toBe(10); + + deduplicatedLogsSelector({ + ...state, + dedupStrategy: LogsDedupStrategy.none, + } as ExploreItemState); + + deduplicatedLogsSelector({ + ...state, + dedupStrategy: LogsDedupStrategy.exact, + } as ExploreItemState); + + deduplicatedLogsSelector({ + ...state, + dedupStrategy: LogsDedupStrategy.numbers, + } as ExploreItemState); + + deduplicatedLogsSelector({ + ...state, + dedupStrategy: LogsDedupStrategy.signature, + } as ExploreItemState); + + deduplicatedLogsSelector({ + ...state, + dedupStrategy: LogsDedupStrategy.none, + } as ExploreItemState); + + deduplicatedLogsSelector({ + ...state, + dedupStrategy: LogsDedupStrategy.exact, + } as ExploreItemState); + + deduplicatedLogsSelector({ + ...state, + dedupStrategy: LogsDedupStrategy.numbers, + } as ExploreItemState); + + deduplicatedLogsSelector({ + ...state, + dedupStrategy: LogsDedupStrategy.signature, + } as ExploreItemState); + + deduplicatedLogsSelector({ + ...state, + dedupStrategy: LogsDedupStrategy.none, + } as ExploreItemState); + + dedups = deduplicatedLogsSelector({ + ...state, + dedupStrategy: LogsDedupStrategy.exact, + } as ExploreItemState); + + // Expecting that no row has duplicates now + expect(dedups.rows.reduce((acc, row) => acc + row.duplicates, 0)).toBe(0); + }); +}); diff --git a/public/app/partials/login.html b/public/app/partials/login.html index 674c9581ce5..33872c7d6d5 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -13,7 +13,7 @@