mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboard: Add week start option to global and dashboard preferences (#40010)
* Add global week start option to shared preferences * Add default_week_start to configuration docs * Add week start option to dashboards * Add week start argument to tsdb time range parser * Fix strict check issues * Add tests for week start * Change wording on default_week_start documentation Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * Update week_start column to be a nullable field Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com> * Update configuration to include browser option * Update WeekStartPicker container selector Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com> * Add menuShouldPortal to WeekStartPicker to remove deprecation warning Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com> * Add inputId to WeekStartPicker * Use e2e selector on WeekStartPicker aria-label * Simplify WeekStartPicker onChange condition * Specify value type on WeekStartPicker weekStarts * Remove setWeekStart side effect from reducer * Fix updateLocale failing to reset week start * Store week start as string to handle empty values Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: Emil Tullstedt <emil.tullstedt@grafana.com> Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com> Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
db62ce477d
commit
a9faab6b09
@@ -1785,6 +1785,10 @@ Set this to `true` to have date formats automatically derived from your browser
|
||||
|
||||
Used as the default time zone for user preferences. Can be either `browser` for the browser local time zone or a time zone name from the IANA Time Zone database, such as `UTC` or `Europe/Amsterdam`.
|
||||
|
||||
### default_week_start
|
||||
|
||||
Set the default start of the week, valid values are: `saturday`, `sunday`, `monday` or `browser` to use the browser locale to define the first day of the week. Default is `browser`.
|
||||
|
||||
## [expressions]
|
||||
|
||||
> **Note:** This feature is available in Grafana v7.4 and later versions.
|
||||
|
||||
@@ -120,3 +120,23 @@ export const dateTimeForTimeZone = (
|
||||
|
||||
return dateTime(input, formatInput);
|
||||
};
|
||||
|
||||
export const getWeekdayIndex = (day: string) => {
|
||||
return moment.weekdays().findIndex((wd) => wd.toLowerCase() === day.toLowerCase());
|
||||
};
|
||||
|
||||
export const setWeekStart = (weekStart?: string) => {
|
||||
const suffix = '-weekStart';
|
||||
const language = getLocale().replace(suffix, '');
|
||||
const dow = weekStart ? getWeekdayIndex(weekStart) : -1;
|
||||
if (dow !== -1) {
|
||||
moment.locale(language + suffix, {
|
||||
parentLocale: language,
|
||||
week: {
|
||||
dow,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
setLocale(language);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -219,6 +219,9 @@ export const Components = {
|
||||
TimeZonePicker: {
|
||||
container: 'Time zone picker select container',
|
||||
},
|
||||
WeekStartPicker: {
|
||||
container: 'Choose starting day of the week',
|
||||
},
|
||||
TraceViewer: {
|
||||
spanBar: () => '[data-test-id="SpanBar--wrapper"]',
|
||||
},
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import { WeekStartPicker } from '@grafana/ui';
|
||||
import { UseState } from '../../utils/storybook/UseState';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
|
||||
export default {
|
||||
title: 'Pickers and Editors/TimePickers/WeekStartPicker',
|
||||
component: WeekStartPicker,
|
||||
decorators: [withCenteredStory],
|
||||
};
|
||||
|
||||
export const basic = () => {
|
||||
return (
|
||||
<UseState
|
||||
initialState={{
|
||||
value: '',
|
||||
}}
|
||||
>
|
||||
{(value, updateValue) => {
|
||||
return (
|
||||
<WeekStartPicker
|
||||
value={value.value}
|
||||
onChange={(newValue: string) => {
|
||||
if (!newValue) {
|
||||
return;
|
||||
}
|
||||
action('on selected')(newValue);
|
||||
updateValue({ value: newValue });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</UseState>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { Select } from '../Select/Select';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
|
||||
export interface Props {
|
||||
onChange: (weekStart: string) => void;
|
||||
value: string;
|
||||
width?: number;
|
||||
autoFocus?: boolean;
|
||||
onBlur?: () => void;
|
||||
includeInternal?: boolean;
|
||||
disabled?: boolean;
|
||||
inputId?: string;
|
||||
}
|
||||
|
||||
const weekStarts: Array<SelectableValue<string>> = [
|
||||
{ value: '', label: 'Default' },
|
||||
{ value: 'saturday', label: 'Saturday' },
|
||||
{ value: 'sunday', label: 'Sunday' },
|
||||
{ value: 'monday', label: 'Monday' },
|
||||
];
|
||||
|
||||
export const WeekStartPicker: React.FC<Props> = (props) => {
|
||||
const { onChange, width, autoFocus = false, onBlur, value, disabled = false, inputId } = props;
|
||||
|
||||
const onChangeWeekStart = useCallback(
|
||||
(selectable: SelectableValue<string>) => {
|
||||
if (selectable.value !== undefined) {
|
||||
onChange(selectable.value);
|
||||
}
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
inputId={inputId}
|
||||
value={weekStarts.find((item) => item.value === value)?.value}
|
||||
placeholder="Choose starting day of the week"
|
||||
autoFocus={autoFocus}
|
||||
openMenuOnFocus={true}
|
||||
width={width}
|
||||
options={weekStarts}
|
||||
onChange={onChangeWeekStart}
|
||||
onBlur={onBlur}
|
||||
disabled={disabled}
|
||||
aria-label={selectors.components.WeekStartPicker.container}
|
||||
menuShouldPortal={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -24,6 +24,7 @@ export { RefreshPicker, defaultIntervals } from './RefreshPicker/RefreshPicker';
|
||||
export { TimeRangePicker, TimeRangePickerProps } from './DateTimePickers/TimeRangePicker';
|
||||
export { TimeOfDayPicker } from './DateTimePickers/TimeOfDayPicker';
|
||||
export { TimeZonePicker } from './DateTimePickers/TimeZonePicker';
|
||||
export { WeekStartPicker } from './DateTimePickers/WeekStartPicker';
|
||||
export { DatePicker, DatePickerProps } from './DateTimePickers/DatePicker/DatePicker';
|
||||
export {
|
||||
DatePickerWithInput,
|
||||
|
||||
@@ -38,6 +38,7 @@ type CurrentUser struct {
|
||||
IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
|
||||
GravatarUrl string `json:"gravatarUrl"`
|
||||
Timezone string `json:"timezone"`
|
||||
WeekStart string `json:"weekStart"`
|
||||
Locale string `json:"locale"`
|
||||
HelpFlags1 models.HelpFlags1 `json:"helpFlags1"`
|
||||
HasEditPermissionInFolders bool `json:"hasEditPermissionInFolders"`
|
||||
|
||||
@@ -4,10 +4,12 @@ type Prefs struct {
|
||||
Theme string `json:"theme"`
|
||||
HomeDashboardID int64 `json:"homeDashboardId"`
|
||||
Timezone string `json:"timezone"`
|
||||
WeekStart string `json:"weekStart"`
|
||||
}
|
||||
|
||||
type UpdatePrefsCmd struct {
|
||||
Theme string `json:"theme"`
|
||||
HomeDashboardID int64 `json:"homeDashboardId"`
|
||||
Timezone string `json:"timezone"`
|
||||
WeekStart string `json:"weekStart"`
|
||||
}
|
||||
|
||||
@@ -484,6 +484,7 @@ func (hs *HTTPServer) setIndexViewData(c *models.ReqContext) (*dtos.IndexViewDat
|
||||
IsGrafanaAdmin: c.IsGrafanaAdmin,
|
||||
LightTheme: prefs.Theme == lightName,
|
||||
Timezone: prefs.Timezone,
|
||||
WeekStart: prefs.WeekStart,
|
||||
Locale: locale,
|
||||
HelpFlags1: c.HelpFlags1,
|
||||
HasEditPermissionInFolders: hasEditPerm,
|
||||
|
||||
@@ -41,6 +41,7 @@ func getPreferencesFor(orgID, userID, teamID int64) response.Response {
|
||||
Theme: prefsQuery.Result.Theme,
|
||||
HomeDashboardID: prefsQuery.Result.HomeDashboardId,
|
||||
Timezone: prefsQuery.Result.Timezone,
|
||||
WeekStart: prefsQuery.Result.WeekStart,
|
||||
}
|
||||
|
||||
return response.JSON(200, &dto)
|
||||
@@ -61,6 +62,7 @@ func updatePreferencesFor(orgID, userID, teamId int64, dtoCmd *dtos.UpdatePrefsC
|
||||
TeamId: teamId,
|
||||
Theme: dtoCmd.Theme,
|
||||
Timezone: dtoCmd.Timezone,
|
||||
WeekStart: dtoCmd.WeekStart,
|
||||
HomeDashboardId: dtoCmd.HomeDashboardID,
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ type Preferences struct {
|
||||
Version int
|
||||
HomeDashboardId int64
|
||||
Timezone string
|
||||
WeekStart string
|
||||
Theme string
|
||||
Created time.Time
|
||||
Updated time.Time
|
||||
@@ -44,5 +45,6 @@ type SavePreferencesCommand struct {
|
||||
|
||||
HomeDashboardId int64 `json:"homeDashboardId"`
|
||||
Timezone string `json:"timezone"`
|
||||
WeekStart string `json:"weekStart"`
|
||||
Theme string `json:"theme"`
|
||||
}
|
||||
|
||||
@@ -42,4 +42,8 @@ func addPreferencesMigrations(mg *Migrator) {
|
||||
SQLite("UPDATE preferences SET team_id=0 WHERE team_id IS NULL;").
|
||||
Postgres("UPDATE preferences SET team_id=0 WHERE team_id IS NULL;").
|
||||
Mysql("UPDATE preferences SET team_id=0 WHERE team_id IS NULL;"))
|
||||
|
||||
mg.AddMigration("Add column week_start in preferences", NewAddColumnMigration(preferencesV2, &Column{
|
||||
Name: "week_start", Type: DB_NVarchar, Length: 10, Nullable: true,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ func (ss *SQLStore) GetPreferencesWithDefaults(ctx context.Context, query *model
|
||||
res := &models.Preferences{
|
||||
Theme: ss.Cfg.DefaultTheme,
|
||||
Timezone: ss.Cfg.DateFormats.DefaultTimezone,
|
||||
WeekStart: ss.Cfg.DateFormats.DefaultWeekStart,
|
||||
HomeDashboardId: 0,
|
||||
}
|
||||
|
||||
@@ -54,6 +55,9 @@ func (ss *SQLStore) GetPreferencesWithDefaults(ctx context.Context, query *model
|
||||
if p.Timezone != "" {
|
||||
res.Timezone = p.Timezone
|
||||
}
|
||||
if p.WeekStart != "" {
|
||||
res.WeekStart = p.WeekStart
|
||||
}
|
||||
if p.HomeDashboardId != 0 {
|
||||
res.HomeDashboardId = p.HomeDashboardId
|
||||
}
|
||||
@@ -96,6 +100,7 @@ func SavePreferences(cmd *models.SavePreferencesCommand) error {
|
||||
TeamId: cmd.TeamId,
|
||||
HomeDashboardId: cmd.HomeDashboardId,
|
||||
Timezone: cmd.Timezone,
|
||||
WeekStart: cmd.WeekStart,
|
||||
Theme: cmd.Theme,
|
||||
Created: time.Now(),
|
||||
Updated: time.Now(),
|
||||
@@ -104,7 +109,7 @@ func SavePreferences(cmd *models.SavePreferencesCommand) error {
|
||||
return err
|
||||
}
|
||||
prefs.HomeDashboardId = cmd.HomeDashboardId
|
||||
prefs.Timezone = cmd.Timezone
|
||||
prefs.WeekStart = cmd.WeekStart
|
||||
prefs.Theme = cmd.Theme
|
||||
prefs.Updated = time.Now()
|
||||
prefs.Version += 1
|
||||
|
||||
@@ -11,6 +11,7 @@ type DateFormats struct {
|
||||
UseBrowserLocale bool `json:"useBrowserLocale"`
|
||||
Interval DateFormatIntervals `json:"interval"`
|
||||
DefaultTimezone string `json:"defaultTimezone"`
|
||||
DefaultWeekStart string `json:"defaultWeekStart"`
|
||||
}
|
||||
|
||||
type DateFormatIntervals struct {
|
||||
@@ -22,17 +23,17 @@ type DateFormatIntervals struct {
|
||||
Year string `json:"year"`
|
||||
}
|
||||
|
||||
const localBrowserTimezone = "browser"
|
||||
const localBrowser = "browser"
|
||||
|
||||
func valueAsTimezone(section *ini.Section, keyName string) (string, error) {
|
||||
timezone := section.Key(keyName).MustString(localBrowserTimezone)
|
||||
if timezone == localBrowserTimezone {
|
||||
return localBrowserTimezone, nil
|
||||
timezone := section.Key(keyName).MustString(localBrowser)
|
||||
if timezone == localBrowser {
|
||||
return localBrowser, nil
|
||||
}
|
||||
|
||||
location, err := time.LoadLocation(timezone)
|
||||
if err != nil {
|
||||
return localBrowserTimezone, err
|
||||
return localBrowser, err
|
||||
}
|
||||
|
||||
return location.String(), nil
|
||||
@@ -54,4 +55,5 @@ func (cfg *Cfg) readDateFormats() {
|
||||
cfg.Logger.Warn("Unknown timezone as default_timezone", "err", err)
|
||||
}
|
||||
cfg.DateFormats.DefaultTimezone = timezone
|
||||
cfg.DateFormats.DefaultWeekStart = valueAsString(dateFormats, "default_week_start", "browser")
|
||||
}
|
||||
|
||||
@@ -79,22 +79,30 @@ func tryParseUnixMsEpoch(val string) (time.Time, bool) {
|
||||
}
|
||||
|
||||
func (tr *TimeRange) ParseFrom() (time.Time, error) {
|
||||
return parse(tr.From, tr.now, false, nil)
|
||||
return parse(tr.From, tr.now, false, nil, -1)
|
||||
}
|
||||
|
||||
func (tr *TimeRange) ParseTo() (time.Time, error) {
|
||||
return parse(tr.To, tr.now, true, nil)
|
||||
return parse(tr.To, tr.now, true, nil, -1)
|
||||
}
|
||||
|
||||
func (tr *TimeRange) ParseFromWithLocation(location *time.Location) (time.Time, error) {
|
||||
return parse(tr.From, tr.now, false, location)
|
||||
return parse(tr.From, tr.now, false, location, -1)
|
||||
}
|
||||
|
||||
func (tr *TimeRange) ParseToWithLocation(location *time.Location) (time.Time, error) {
|
||||
return parse(tr.To, tr.now, true, location)
|
||||
return parse(tr.To, tr.now, true, location, -1)
|
||||
}
|
||||
|
||||
func parse(s string, now time.Time, withRoundUp bool, location *time.Location) (time.Time, error) {
|
||||
func (tr *TimeRange) ParseFromWithWeekStart(location *time.Location, weekstart time.Weekday) (time.Time, error) {
|
||||
return parse(tr.From, tr.now, false, location, weekstart)
|
||||
}
|
||||
|
||||
func (tr *TimeRange) ParseToWithWeekStart(location *time.Location, weekstart time.Weekday) (time.Time, error) {
|
||||
return parse(tr.To, tr.now, true, location, weekstart)
|
||||
}
|
||||
|
||||
func parse(s string, now time.Time, withRoundUp bool, location *time.Location, weekstart time.Weekday) (time.Time, error) {
|
||||
if res, ok := tryParseUnixMsEpoch(s); ok {
|
||||
return res, nil
|
||||
}
|
||||
@@ -108,6 +116,12 @@ func parse(s string, now time.Time, withRoundUp bool, location *time.Location) (
|
||||
if location != nil {
|
||||
options = append(options, datemath.WithLocation(location))
|
||||
}
|
||||
if weekstart != -1 {
|
||||
if weekstart > now.Weekday() {
|
||||
weekstart = weekstart - 7
|
||||
}
|
||||
options = append(options, datemath.WithStartOfWeek(weekstart))
|
||||
}
|
||||
|
||||
return datemath.ParseAndEvaluate(s, options...)
|
||||
}
|
||||
|
||||
@@ -248,5 +248,120 @@ func TestTimeRange(t *testing.T) {
|
||||
So(res, ShouldEqual, expected)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Can parse now-1w/w, now-1w/w without timezone and week start on Monday", func() {
|
||||
tr := TimeRange{
|
||||
From: "now-1w/w",
|
||||
To: "now-1w/w",
|
||||
now: now,
|
||||
}
|
||||
weekstart := time.Monday
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("from now-1w/w ", func() {
|
||||
expected, err := time.Parse(time.RFC3339Nano, "2020-07-13T00:00:00.000Z")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
res, err := tr.ParseFromWithWeekStart(nil, weekstart)
|
||||
So(err, ShouldBeNil)
|
||||
So(res, ShouldEqual, expected)
|
||||
})
|
||||
|
||||
Convey("to now-1w/w ", func() {
|
||||
expected, err := time.Parse(time.RFC3339Nano, "2020-07-19T23:59:59.999Z")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
res, err := tr.ParseToWithWeekStart(nil, weekstart)
|
||||
So(err, ShouldBeNil)
|
||||
So(res, ShouldEqual, expected)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Can parse now-1w/w, now-1w/w with America/Chicago timezone and week start on Monday", func() {
|
||||
tr := TimeRange{
|
||||
From: "now-1w/w",
|
||||
To: "now-1w/w",
|
||||
now: now,
|
||||
}
|
||||
weekstart := time.Monday
|
||||
location, err := time.LoadLocation("America/Chicago")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("from now-1w/w ", func() {
|
||||
expected, err := time.Parse(time.RFC3339Nano, "2020-07-13T00:00:00.000-05:00")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
res, err := tr.ParseFromWithWeekStart(location, weekstart)
|
||||
So(err, ShouldBeNil)
|
||||
So(res, ShouldEqual, expected)
|
||||
})
|
||||
|
||||
Convey("to now-1w/w ", func() {
|
||||
expected, err := time.Parse(time.RFC3339Nano, "2020-07-19T23:59:59.999-05:00")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
res, err := tr.ParseToWithWeekStart(location, weekstart)
|
||||
So(err, ShouldBeNil)
|
||||
So(res, ShouldEqual, expected)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Can parse now-1w/w, now-1w/w with America/Chicago timezone and week start on Sunday", func() {
|
||||
tr := TimeRange{
|
||||
From: "now-1w/w",
|
||||
To: "now-1w/w",
|
||||
now: now,
|
||||
}
|
||||
weekstart := time.Sunday
|
||||
location, err := time.LoadLocation("America/Chicago")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("from now-1w/w ", func() {
|
||||
expected, err := time.Parse(time.RFC3339Nano, "2020-07-19T00:00:00.000-05:00")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
res, err := tr.ParseFromWithWeekStart(location, weekstart)
|
||||
So(err, ShouldBeNil)
|
||||
So(res, ShouldEqual, expected)
|
||||
})
|
||||
|
||||
Convey("to now-1w/w ", func() {
|
||||
expected, err := time.Parse(time.RFC3339Nano, "2020-07-25T23:59:59.999-05:00")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
res, err := tr.ParseToWithWeekStart(location, weekstart)
|
||||
So(err, ShouldBeNil)
|
||||
So(res, ShouldEqual, expected)
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Can parse now-1w/w, now-1w/w with America/Chicago timezone and week start on Saturday", func() {
|
||||
tr := TimeRange{
|
||||
From: "now-1w/w",
|
||||
To: "now-1w/w",
|
||||
now: now,
|
||||
}
|
||||
weekstart := time.Saturday
|
||||
location, err := time.LoadLocation("America/Chicago")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("from now-1w/w ", func() {
|
||||
expected, err := time.Parse(time.RFC3339Nano, "2020-07-18T00:00:00.000-05:00")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
res, err := tr.ParseFromWithWeekStart(location, weekstart)
|
||||
So(err, ShouldBeNil)
|
||||
So(res, ShouldEqual, expected)
|
||||
})
|
||||
|
||||
Convey("to now-1w/w ", func() {
|
||||
expected, err := time.Parse(time.RFC3339Nano, "2020-07-24T23:59:59.999-05:00")
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
res, err := tr.ParseToWithWeekStart(location, weekstart)
|
||||
So(err, ShouldBeNil)
|
||||
So(res, ShouldEqual, expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
monacoLanguageRegistry,
|
||||
setLocale,
|
||||
setTimeZoneResolver,
|
||||
setWeekStart,
|
||||
standardEditorsRegistry,
|
||||
standardFieldConfigEditorRegistry,
|
||||
standardTransformersRegistry,
|
||||
@@ -77,6 +78,7 @@ export class GrafanaApp {
|
||||
initEchoSrv();
|
||||
addClassIfNoOverlayScrollbar();
|
||||
setLocale(config.bootData.user.locale);
|
||||
setWeekStart(config.bootData.user.weekStart);
|
||||
setPanelRenderer(PanelRenderer);
|
||||
setTimeZoneResolver(() => config.bootData.user.timezone);
|
||||
// Important that extensions are initialized before store
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
Select,
|
||||
stylesFactory,
|
||||
TimeZonePicker,
|
||||
WeekStartPicker,
|
||||
Tooltip,
|
||||
} from '@grafana/ui';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
@@ -29,6 +30,7 @@ export interface State {
|
||||
homeDashboardId: number;
|
||||
theme: string;
|
||||
timezone: string;
|
||||
weekStart: string;
|
||||
dashboards: DashboardSearchHit[];
|
||||
}
|
||||
|
||||
@@ -49,6 +51,7 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
||||
homeDashboardId: 0,
|
||||
theme: '',
|
||||
timezone: '',
|
||||
weekStart: '',
|
||||
dashboards: [],
|
||||
};
|
||||
}
|
||||
@@ -84,13 +87,14 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
||||
homeDashboardId: prefs.homeDashboardId,
|
||||
theme: prefs.theme,
|
||||
timezone: prefs.timezone,
|
||||
weekStart: prefs.weekStart,
|
||||
dashboards: [defaultDashboardHit, ...dashboards],
|
||||
});
|
||||
}
|
||||
|
||||
onSubmitForm = async () => {
|
||||
const { homeDashboardId, theme, timezone } = this.state;
|
||||
await this.service.update({ homeDashboardId, theme, timezone });
|
||||
const { homeDashboardId, theme, timezone, weekStart } = this.state;
|
||||
await this.service.update({ homeDashboardId, theme, timezone, weekStart });
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
@@ -105,6 +109,10 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
||||
this.setState({ timezone: timezone });
|
||||
};
|
||||
|
||||
onWeekStartChanged = (weekStart: string) => {
|
||||
this.setState({ weekStart: weekStart });
|
||||
};
|
||||
|
||||
onHomeDashboardChanged = (dashboardId: number) => {
|
||||
this.setState({ homeDashboardId: dashboardId });
|
||||
};
|
||||
@@ -117,7 +125,7 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
render() {
|
||||
const { theme, timezone, homeDashboardId, dashboards } = this.state;
|
||||
const { theme, timezone, weekStart, homeDashboardId, dashboards } = this.state;
|
||||
const styles = getStyles();
|
||||
|
||||
return (
|
||||
@@ -161,6 +169,10 @@ export class SharedPreferences extends PureComponent<Props, State> {
|
||||
<Field label="Timezone" aria-label={selectors.components.TimeZonePicker.container}>
|
||||
<TimeZonePicker includeInternal={true} value={timezone} onChange={this.onTimeZoneChanged} />
|
||||
</Field>
|
||||
|
||||
<Field label="Week start" aria-label={selectors.components.WeekStartPicker.container}>
|
||||
<WeekStartPicker value={weekStart} onChange={this.onWeekStartChanged} />
|
||||
</Field>
|
||||
<div className="gf-form-button-row">
|
||||
<Button variant="primary" aria-label="User preferences save button">
|
||||
Save
|
||||
|
||||
@@ -23,6 +23,7 @@ const setupTestContext = (options: Partial<Props>) => {
|
||||
timezone: 'utc',
|
||||
} as unknown) as DashboardModel,
|
||||
updateTimeZone: jest.fn(),
|
||||
updateWeekStart: jest.fn(),
|
||||
};
|
||||
|
||||
const props = { ...defaults, ...options };
|
||||
|
||||
@@ -8,7 +8,7 @@ import { DashboardModel } from '../../state/DashboardModel';
|
||||
import { DeleteDashboardButton } from '../DeleteDashboard/DeleteDashboardButton';
|
||||
import { TimePickerSettings } from './TimePickerSettings';
|
||||
|
||||
import { updateTimeZoneDashboard } from 'app/features/dashboard/state/actions';
|
||||
import { updateTimeZoneDashboard, updateWeekStartDashboard } from 'app/features/dashboard/state/actions';
|
||||
|
||||
interface OwnProps {
|
||||
dashboard: DashboardModel;
|
||||
@@ -22,7 +22,7 @@ const GRAPH_TOOLTIP_OPTIONS = [
|
||||
{ value: 2, label: 'Shared Tooltip' },
|
||||
];
|
||||
|
||||
export function GeneralSettingsUnconnected({ dashboard, updateTimeZone }: Props): JSX.Element {
|
||||
export function GeneralSettingsUnconnected({ dashboard, updateTimeZone, updateWeekStart }: Props): JSX.Element {
|
||||
const [renderCounter, setRenderCounter] = useState(0);
|
||||
|
||||
const onFolderChange = (folder: { id: number; title: string }) => {
|
||||
@@ -64,6 +64,12 @@ export function GeneralSettingsUnconnected({ dashboard, updateTimeZone }: Props)
|
||||
updateTimeZone(timeZone);
|
||||
};
|
||||
|
||||
const onWeekStartChange = (weekStart: string) => {
|
||||
dashboard.weekStart = weekStart;
|
||||
setRenderCounter(renderCounter + 1);
|
||||
updateWeekStart(weekStart);
|
||||
};
|
||||
|
||||
const onTagsChange = (tags: string[]) => {
|
||||
dashboard.tags = tags;
|
||||
setRenderCounter(renderCounter + 1);
|
||||
@@ -116,6 +122,7 @@ export function GeneralSettingsUnconnected({ dashboard, updateTimeZone }: Props)
|
||||
|
||||
<TimePickerSettings
|
||||
onTimeZoneChange={onTimeZoneChange}
|
||||
onWeekStartChange={onWeekStartChange}
|
||||
onRefreshIntervalChange={onRefreshIntervalChange}
|
||||
onNowDelayChange={onNowDelayChange}
|
||||
onHideTimePickerChange={onHideTimePickerChange}
|
||||
@@ -124,6 +131,7 @@ export function GeneralSettingsUnconnected({ dashboard, updateTimeZone }: Props)
|
||||
timePickerHidden={dashboard.timepicker.hidden}
|
||||
nowDelay={dashboard.timepicker.nowDelay}
|
||||
timezone={dashboard.timezone}
|
||||
weekStart={dashboard.weekStart}
|
||||
liveNow={dashboard.liveNow}
|
||||
/>
|
||||
|
||||
@@ -145,6 +153,7 @@ export function GeneralSettingsUnconnected({ dashboard, updateTimeZone }: Props)
|
||||
|
||||
const mapDispatchToProps = {
|
||||
updateTimeZone: updateTimeZoneDashboard,
|
||||
updateWeekStart: updateWeekStartDashboard,
|
||||
};
|
||||
|
||||
const connector = connect(null, mapDispatchToProps);
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { Input, TimeZonePicker, Field, Switch, CollapsableSection } from '@grafana/ui';
|
||||
import { Input, TimeZonePicker, Field, Switch, CollapsableSection, WeekStartPicker } from '@grafana/ui';
|
||||
import { rangeUtil, TimeZone } from '@grafana/data';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { AutoRefreshIntervals } from './AutoRefreshIntervals';
|
||||
|
||||
interface Props {
|
||||
onWeekStartChange: (weekStart: string) => void;
|
||||
onTimeZoneChange: (timeZone: TimeZone) => void;
|
||||
onRefreshIntervalChange: (interval: string[]) => void;
|
||||
onNowDelayChange: (nowDelay: string) => void;
|
||||
@@ -15,6 +16,7 @@ interface Props {
|
||||
timePickerHidden: boolean;
|
||||
nowDelay: string;
|
||||
timezone: TimeZone;
|
||||
weekStart: string;
|
||||
liveNow: boolean;
|
||||
}
|
||||
|
||||
@@ -56,6 +58,10 @@ export class TimePickerSettings extends PureComponent<Props, State> {
|
||||
this.props.onTimeZoneChange(timeZone);
|
||||
};
|
||||
|
||||
onWeekStartChange = (weekStart: string) => {
|
||||
this.props.onWeekStartChange(weekStart);
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<CollapsableSection label="Time options" isOpen={true}>
|
||||
@@ -67,6 +73,9 @@ export class TimePickerSettings extends PureComponent<Props, State> {
|
||||
width={40}
|
||||
/>
|
||||
</Field>
|
||||
<Field label="Week start" aria-label={selectors.components.WeekStartPicker.container}>
|
||||
<WeekStartPicker width={40} value={this.props.weekStart} onChange={this.onWeekStartChange} />
|
||||
</Field>
|
||||
<AutoRefreshIntervals
|
||||
refreshIntervals={this.props.refreshIntervals}
|
||||
onRefreshIntervalChange={this.props.onRefreshIntervalChange}
|
||||
|
||||
@@ -78,6 +78,7 @@ export class DashboardModel {
|
||||
tags: any;
|
||||
style: any;
|
||||
timezone: any;
|
||||
weekStart: any;
|
||||
editable: any;
|
||||
graphTooltip: DashboardCursorSync;
|
||||
time: any;
|
||||
@@ -139,6 +140,7 @@ export class DashboardModel {
|
||||
this.tags = data.tags ?? [];
|
||||
this.style = data.style ?? 'dark';
|
||||
this.timezone = data.timezone ?? '';
|
||||
this.weekStart = data.weekStart ?? '';
|
||||
this.editable = data.editable !== false;
|
||||
this.graphTooltip = data.graphTooltip || 0;
|
||||
this.time = data.time ?? { from: 'now-6h', to: 'now' };
|
||||
|
||||
@@ -5,7 +5,7 @@ import { createSuccessNotification } from 'app/core/copy/appNotification';
|
||||
import { loadPluginDashboards } from '../../plugins/state/actions';
|
||||
import { cleanUpDashboard, loadDashboardPermissions } from './reducers';
|
||||
import { notifyApp } from 'app/core/actions';
|
||||
import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
|
||||
import { updateTimeZoneForSession, updateWeekStartForSession } from 'app/features/profile/state/reducers';
|
||||
// Types
|
||||
import { DashboardAcl, DashboardAclUpdateDTO, NewDashboardAclItem, PermissionLevel, ThunkResult } from 'app/types';
|
||||
import { cancelVariables } from '../../variables/state/actions';
|
||||
@@ -131,3 +131,8 @@ export const updateTimeZoneDashboard = (timeZone: TimeZone): ThunkResult<void> =
|
||||
dispatch(updateTimeZoneForSession(timeZone));
|
||||
getTimeSrv().refreshDashboard();
|
||||
};
|
||||
|
||||
export const updateWeekStartDashboard = (weekStart: string): ThunkResult<void> => (dispatch) => {
|
||||
dispatch(updateWeekStartForSession(weekStart));
|
||||
getTimeSrv().refreshDashboard();
|
||||
};
|
||||
|
||||
@@ -18,11 +18,11 @@ import {
|
||||
// Types
|
||||
import { DashboardDTO, DashboardInitPhase, DashboardRoutes, StoreState, ThunkDispatch, ThunkResult } from 'app/types';
|
||||
import { DashboardModel } from './DashboardModel';
|
||||
import { DataQuery, locationUtil } from '@grafana/data';
|
||||
import { DataQuery, locationUtil, setWeekStart } from '@grafana/data';
|
||||
import { initVariablesTransaction } from '../../variables/state/actions';
|
||||
import { emitDashboardViewEvent } from './analyticsProcessor';
|
||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { createDashboardQueryRunner } from '../../query/state/DashboardQueryRunner/DashboardQueryRunner';
|
||||
|
||||
export interface InitDashboardArgs {
|
||||
@@ -210,6 +210,13 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
|
||||
dashboardWatcher.leave();
|
||||
}
|
||||
|
||||
// set week start
|
||||
if (dashboard.weekStart !== '') {
|
||||
setWeekStart(dashboard.weekStart);
|
||||
} else {
|
||||
setWeekStart(config.bootData.user.weekStart);
|
||||
}
|
||||
|
||||
// yay we are done
|
||||
dispatch(dashboardInitCompleted(dashboard));
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
setUpdating,
|
||||
teamsLoaded,
|
||||
updateTimeZone,
|
||||
updateWeekStart,
|
||||
userLoaded,
|
||||
userReducer,
|
||||
userSessionRevoked,
|
||||
@@ -33,6 +34,15 @@ describe('userReducer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updateWeekStart is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<UserState>()
|
||||
.givenReducer(userReducer, { ...initialUserState })
|
||||
.whenActionIsDispatched(updateWeekStart({ weekStart: 'xyz' }))
|
||||
.thenStateShouldEqual({ ...initialUserState, weekStart: 'xyz' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('when setUpdating is dispatched', () => {
|
||||
it('then state should be correct', () => {
|
||||
reducerTester<UserState>()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isEmpty, isString, set } from 'lodash';
|
||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||
import { dateTimeFormat, dateTimeFormatTimeAgo, TimeZone } from '@grafana/data';
|
||||
import { dateTimeFormat, dateTimeFormatTimeAgo, setWeekStart, TimeZone } from '@grafana/data';
|
||||
|
||||
import { Team, ThunkResult, UserDTO, UserOrg, UserSession } from 'app/types';
|
||||
import config from 'app/core/config';
|
||||
@@ -9,6 +9,7 @@ import { contextSrv } from 'app/core/core';
|
||||
export interface UserState {
|
||||
orgId: number;
|
||||
timeZone: TimeZone;
|
||||
weekStart: string;
|
||||
fiscalYearStartMonth: number;
|
||||
user: UserDTO | null;
|
||||
teams: Team[];
|
||||
@@ -23,6 +24,7 @@ export interface UserState {
|
||||
export const initialUserState: UserState = {
|
||||
orgId: config.bootData.user.orgId,
|
||||
timeZone: config.bootData.user.timezone,
|
||||
weekStart: config.bootData.user.weekStart,
|
||||
fiscalYearStartMonth: 0,
|
||||
orgsAreLoading: false,
|
||||
sessionsAreLoading: false,
|
||||
@@ -41,6 +43,9 @@ export const slice = createSlice({
|
||||
updateTimeZone: (state, action: PayloadAction<{ timeZone: TimeZone }>) => {
|
||||
state.timeZone = action.payload.timeZone;
|
||||
},
|
||||
updateWeekStart: (state, action: PayloadAction<{ weekStart: string }>) => {
|
||||
state.weekStart = action.payload.weekStart;
|
||||
},
|
||||
updateFiscalYearStartMonth: (state, action: PayloadAction<{ fiscalYearStartMonth: number }>) => {
|
||||
state.fiscalYearStartMonth = action.payload.fiscalYearStartMonth;
|
||||
},
|
||||
@@ -110,6 +115,18 @@ export const updateTimeZoneForSession = (timeZone: TimeZone): ThunkResult<void>
|
||||
};
|
||||
};
|
||||
|
||||
export const updateWeekStartForSession = (weekStart: string): ThunkResult<void> => {
|
||||
return async (dispatch) => {
|
||||
if (!isString(weekStart) || isEmpty(weekStart)) {
|
||||
weekStart = config?.bootData?.user?.weekStart;
|
||||
}
|
||||
|
||||
set(contextSrv, 'user.weekStart', weekStart);
|
||||
dispatch(updateWeekStart({ weekStart }));
|
||||
setWeekStart(weekStart);
|
||||
};
|
||||
};
|
||||
|
||||
export const {
|
||||
setUpdating,
|
||||
initLoadOrgs,
|
||||
@@ -121,6 +138,7 @@ export const {
|
||||
initLoadSessions,
|
||||
sessionsLoaded,
|
||||
updateTimeZone,
|
||||
updateWeekStart,
|
||||
updateFiscalYearStartMonth,
|
||||
} = slice.actions;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TimeZone } from '@grafana/data';
|
||||
|
||||
export interface UserPreferencesDTO {
|
||||
timezone: TimeZone;
|
||||
weekStart: string;
|
||||
homeDashboardId: number;
|
||||
theme: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user