TimeZone: unify the time zone pickers to one that can rule them all. (#24803)

* draft on a unified TimeZonePicker.

* most of the data structures is in place.

* wip.

* wip.

* wip: timezone selector in progress.2

* fixed so we have proper data on all timezones.

* started to add timezone into time picker.

* addeing time zone footer.

* footer is working.

* fixed so we use the timeZone picker in shared preferences.

* Added so we can change timeZone from picker.

* did some styling changes.

* will update timezone on all places that we need to update it.

* removed console.log

* removed magic string.

* fixed border on calendar.

* ignoring eslint cache.

* cleaned up the code a bit.

* made the default selectable.

* corrected so the behaviour about default works as expected.

* excluded timezone from change tracker.

* revert so default will always be the intial value.

* default will always fallback to the one in the config.

* do the country mapping on startup.

* fixed nit.

* updated snapshots for timepicker.

* fixed build errors.

* updating so snapshot tests is in sync.

* removed Date.now from prop since it will change each run in the snapshot tests.

* fixed so e2e tests works as before.

* moved files into separate folders.
This commit is contained in:
Marcus Andersson 2020-06-26 09:08:15 +02:00 committed by GitHub
parent 084542a006
commit 1abbb477cf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
38 changed files with 1559 additions and 898 deletions

1
.gitignore vendored
View File

@ -47,6 +47,7 @@ public/css/*.min.css
.DS_Store
.vscode/
.vs/
.eslintcache
/data/*
/bin/*

View File

@ -9,7 +9,7 @@ e2e.scenario({
scenario: () => {
e2e.flows.openDashboard('5SdHCasdf');
const fromTimeZone = 'UTC';
const fromTimeZone = 'Coordinated Universal Time';
const toTimeZone = 'America/Chicago';
const offset = -5;

View File

@ -1,6 +1,14 @@
import moment from 'moment-timezone';
import { memoize } from 'lodash';
import { TimeZone } from '../types';
import { getTimeZone } from './common';
export enum InternalTimeZones {
default = '',
localBrowserTime = 'browser',
utc = 'utc',
}
export const timeZoneFormatUserFriendly = (timeZone: TimeZone | undefined) => {
switch (getTimeZone({ timeZone })) {
case 'browser':
@ -12,392 +20,413 @@ export const timeZoneFormatUserFriendly = (timeZone: TimeZone | undefined) => {
}
};
// List taken from https://stackoverflow.com/questions/38399465/how-to-get-list-of-all-timezones-in-javascript
export const getTimeZoneGroups = () => {
const europeZones = [
'Europe/Amsterdam',
'Europe/Andorra',
'Europe/Astrakhan',
'Europe/Athens',
'Europe/Belgrade',
'Europe/Berlin',
'Europe/Brussels',
'Europe/Bucharest',
'Europe/Budapest',
'Europe/Chisinau',
'Europe/Copenhagen',
'Europe/Dublin',
'Europe/Gibraltar',
'Europe/Helsinki',
'Europe/Istanbul',
'Europe/Kaliningrad',
'Europe/Kiev',
'Europe/Kirov',
'Europe/Lisbon',
'Europe/London',
'Europe/Luxembourg',
'Europe/Madrid',
'Europe/Malta',
'Europe/Minsk',
'Europe/Monaco',
'Europe/Moscow',
'Europe/Oslo',
'Europe/Paris',
'Europe/Prague',
'Europe/Riga',
'Europe/Rome',
'Europe/Samara',
'Europe/Saratov',
'Europe/Simferopol',
'Europe/Sofia',
'Europe/Stockholm',
'Europe/Tallinn',
'Europe/Tirane',
'Europe/Ulyanovsk',
'Europe/Uzhgorod',
'Europe/Vienna',
'Europe/Vilnius',
'Europe/Volgograd',
'Europe/Warsaw',
'Europe/Zaporozhye',
'Europe/Zurich',
];
export interface TimeZoneCountry {
code: string;
name: string;
}
export interface TimeZoneInfo {
name: string;
zone: string;
countries: TimeZoneCountry[];
abbreviation: string;
offsetInMins: number;
}
const africaZones = [
'Africa/Abidjan',
'Africa/Accra',
'Africa/Algiers',
'Africa/Bissau',
'Africa/Cairo',
'Africa/Casablanca',
'Africa/Ceuta',
'Africa/El_Aaiun',
'Africa/Johannesburg',
'Africa/Juba',
'Africa/Khartoum',
'Africa/Lagos',
'Africa/Maputo',
'Africa/Monrovia',
'Africa/Nairobi',
'Africa/Ndjamena',
'Africa/Sao_Tome',
'Africa/Tripoli',
'Africa/Tunis',
'Africa/Windhoek',
];
export interface GroupedTimeZones {
name: string;
zones: TimeZone[];
}
const asiaZones = [
'Asia/Almaty',
'Asia/Amman',
'Asia/Anadyr',
'Asia/Aqtau',
'Asia/Aqtobe',
'Asia/Ashgabat',
'Asia/Atyrau',
'Asia/Baghdad',
'Asia/Baku',
'Asia/Bangkok',
'Asia/Barnaul',
'Asia/Beirut',
'Asia/Bishkek',
'Asia/Brunei',
'Asia/Chita',
'Asia/Choibalsan',
'Asia/Colombo',
'Asia/Damascus',
'Asia/Dhaka',
'Asia/Dili',
'Asia/Dubai',
'Asia/Dushanbe',
'Asia/Famagusta',
'Asia/Gaza',
'Asia/Hebron',
'Asia/Ho_Chi_Minh',
'Asia/Hong_Kong',
'Asia/Hovd',
'Asia/Irkutsk',
'Asia/Jakarta',
'Asia/Jayapura',
'Asia/Jerusalem',
'Asia/Kabul',
'Asia/Kamchatka',
'Asia/Karachi',
'Asia/Kathmandu',
'Asia/Khandyga',
'Asia/Kolkata',
'Asia/Krasnoyarsk',
'Asia/Kuala_Lumpur',
'Asia/Kuching',
'Asia/Macau',
'Asia/Magadan',
'Asia/Makassar',
'Asia/Manila',
'Asia/Nicosia',
'Asia/Novokuznetsk',
'Asia/Novosibirsk',
'Asia/Omsk',
'Asia/Oral',
'Asia/Pontianak',
'Asia/Pyongyang',
'Asia/Qatar',
'Asia/Qostanay',
'Asia/Qyzylorda',
'Asia/Riyadh',
'Asia/Sakhalin',
'Asia/Samarkand',
'Asia/Seoul',
'Asia/Shanghai',
'Asia/Singapore',
'Asia/Srednekolymsk',
'Asia/Taipei',
'Asia/Tashkent',
'Asia/Tbilisi',
'Asia/Tehran',
'Asia/Thimphu',
'Asia/Tokyo',
'Asia/Tomsk',
'Asia/Ulaanbaatar',
'Asia/Urumqi',
'Asia/Ust-Nera',
'Asia/Vladivostok',
'Asia/Yakutsk',
'Asia/Yangon',
'Asia/Yekaterinburg',
'Asia/Yerevan',
];
export const getTimeZoneInfo = (zone: string, timestamp: number): TimeZoneInfo | undefined => {
const internal = mapInternal(zone, timestamp);
const antarcticaZones = [
'Antarctica/Casey',
'Antarctica/Davis',
'Antarctica/DumontDUrville',
'Antarctica/Macquarie',
'Antarctica/Mawson',
'Antarctica/Palmer',
'Antarctica/Rothera',
'Antarctica/Syowa',
'Antarctica/Troll',
'Antarctica/Vostok',
];
if (internal) {
return internal;
}
const americaZones = [
'America/Adak',
'America/Anchorage',
'America/Araguaina',
'America/Argentina/Buenos_Aires',
'America/Argentina/Catamarca',
'America/Argentina/Cordoba',
'America/Argentina/Jujuy',
'America/Argentina/La_Rioja',
'America/Argentina/Mendoza',
'America/Argentina/Rio_Gallegos',
'America/Argentina/Salta',
'America/Argentina/San_Juan',
'America/Argentina/San_Luis',
'America/Argentina/Tucuman',
'America/Argentina/Ushuaia',
'America/Asuncion',
'America/Atikokan',
'America/Bahia',
'America/Bahia_Banderas',
'America/Barbados',
'America/Belem',
'America/Belize',
'America/Blanc-Sablon',
'America/Boa_Vista',
'America/Bogota',
'America/Boise',
'America/Cambridge_Bay',
'America/Campo_Grande',
'America/Cancun',
'America/Caracas',
'America/Cayenne',
'America/Chicago',
'America/Chihuahua',
'America/Costa_Rica',
'America/Creston',
'America/Cuiaba',
'America/Curacao',
'America/Danmarkshavn',
'America/Dawson',
'America/Dawson_Creek',
'America/Denver',
'America/Detroit',
'America/Edmonton',
'America/Eirunepe',
'America/El_Salvador',
'America/Fort_Nelson',
'America/Fortaleza',
'America/Glace_Bay',
'America/Godthab',
'America/Goose_Bay',
'America/Grand_Turk',
'America/Guatemala',
'America/Guayaquil',
'America/Guyana',
'America/Halifax',
'America/Havana',
'America/Hermosillo',
'America/Indiana/Indianapolis',
'America/Indiana/Knox',
'America/Indiana/Marengo',
'America/Indiana/Petersburg',
'America/Indiana/Tell_City',
'America/Indiana/Vevay',
'America/Indiana/Vincennes',
'America/Indiana/Winamac',
'America/Inuvik',
'America/Iqaluit',
'America/Jamaica',
'America/Juneau',
'America/Kentucky/Louisville',
'America/Kentucky/Monticello',
'America/La_Paz',
'America/Lima',
'America/Los_Angeles',
'America/Maceio',
'America/Managua',
'America/Manaus',
'America/Martinique',
'America/Matamoros',
'America/Mazatlan',
'America/Menominee',
'America/Merida',
'America/Metlakatla',
'America/Mexico_City',
'America/Miquelon',
'America/Moncton',
'America/Monterrey',
'America/Montevideo',
'America/Nassau',
'America/New_York',
'America/Nipigon',
'America/Nome',
'America/Noronha',
'America/North_Dakota/Beulah',
'America/North_Dakota/Center',
'America/North_Dakota/New_Salem',
'America/Ojinaga',
'America/Panama',
'America/Pangnirtung',
'America/Paramaribo',
'America/Phoenix',
'America/Port-au-Prince',
'America/Port_of_Spain',
'America/Porto_Velho',
'America/Puerto_Rico',
'America/Punta_Arenas',
'America/Rainy_River',
'America/Rankin_Inlet',
'America/Recife',
'America/Regina',
'America/Resolute',
'America/Rio_Branco',
'America/Santarem',
'America/Santiago',
'America/Santo_Domingo',
'America/Sao_Paulo',
'America/Scoresbysund',
'America/Sitka',
'America/St_Johns',
'America/Swift_Current',
'America/Tegucigalpa',
'America/Thule',
'America/Thunder_Bay',
'America/Tijuana',
'America/Toronto',
'America/Vancouver',
'America/Whitehorse',
'America/Winnipeg',
'America/Yakutat',
'America/Yellowknife',
];
const pacificZones = [
'Pacific/Apia',
'Pacific/Auckland',
'Pacific/Bougainville',
'Pacific/Chatham',
'Pacific/Chuuk',
'Pacific/Easter',
'Pacific/Efate',
'Pacific/Enderbury',
'Pacific/Fakaofo',
'Pacific/Fiji',
'Pacific/Funafuti',
'Pacific/Galapagos',
'Pacific/Gambier',
'Pacific/Guadalcanal',
'Pacific/Guam',
'Pacific/Honolulu',
'Pacific/Kiritimati',
'Pacific/Kosrae',
'Pacific/Kwajalein',
'Pacific/Majuro',
'Pacific/Marquesas',
'Pacific/Nauru',
'Pacific/Niue',
'Pacific/Norfolk',
'Pacific/Noumea',
'Pacific/Pago_Pago',
'Pacific/Palau',
'Pacific/Pitcairn',
'Pacific/Pohnpei',
'Pacific/Port_Moresby',
'Pacific/Rarotonga',
'Pacific/Tahiti',
'Pacific/Tarawa',
'Pacific/Tongatapu',
'Pacific/Wake',
'Pacific/Wallis',
];
const australiaZones = [
'Australia/Adelaide',
'Australia/Brisbane',
'Australia/Broken_Hill',
'Australia/Currie',
'Australia/Darwin',
'Australia/Eucla',
'Australia/Hobart',
'Australia/Lindeman',
'Australia/Lord_Howe',
'Australia/Melbourne',
'Australia/Perth',
'Australia/Sydney',
];
const atlanticZones = [
'Atlantic/Azores',
'Atlantic/Bermuda',
'Atlantic/Canary',
'Atlantic/Cape_Verde',
'Atlantic/Faroe',
'Atlantic/Madeira',
'Atlantic/Reykjavik',
'Atlantic/South_Georgia',
'Atlantic/Stanley',
];
const indianZones = [
'Indian/Chagos',
'Indian/Christmas',
'Indian/Cocos',
'Indian/Kerguelen',
'Indian/Mahe',
'Indian/Maldives',
'Indian/Mauritius',
'Indian/Reunion',
];
return [
{ label: 'Africa', options: africaZones },
{ label: 'America', options: americaZones },
{ label: 'Antarctica', options: antarcticaZones },
{ label: 'Asia', options: asiaZones },
{ label: 'Atlantic', options: atlanticZones },
{ label: 'Australia', options: australiaZones },
{ label: 'Europe', options: europeZones },
{ label: 'Indian', options: indianZones },
{ label: 'Pacific', options: pacificZones },
];
return mapToInfo(zone, timestamp);
};
export const getTimeZones = memoize((includeInternal = false): TimeZone[] => {
const initial: TimeZone[] = [];
if (includeInternal) {
initial.push.apply(initial, [InternalTimeZones.default, InternalTimeZones.localBrowserTime, InternalTimeZones.utc]);
}
return moment.tz.names().reduce((zones: TimeZone[], zone: string) => {
const countriesForZone = countriesByTimeZone[zone];
if (!Array.isArray(countriesForZone) || countriesForZone.length === 0) {
return zones;
}
zones.push(zone);
return zones;
}, initial);
});
export const getTimeZoneGroups = memoize((includeInternal = false): GroupedTimeZones[] => {
const timeZones = getTimeZones(includeInternal);
const groups = timeZones.reduce((groups: Record<string, TimeZone[]>, zone: TimeZone) => {
const delimiter = zone.indexOf('/');
if (delimiter === -1) {
const group = '';
groups[group] = groups[group] ?? [];
groups[group].push(zone);
return groups;
}
const group = zone.substr(0, delimiter);
groups[group] = groups[group] ?? [];
groups[group].push(zone);
return groups;
}, {});
return Object.keys(groups).map(name => ({
name,
zones: groups[name],
}));
});
const mapInternal = (zone: string, timestamp: number): TimeZoneInfo | undefined => {
switch (zone) {
case InternalTimeZones.utc: {
return {
name: 'Coordinated Universal Time',
zone,
countries: [],
abbreviation: 'UTC, GMT',
offsetInMins: 0,
};
}
case InternalTimeZones.default: {
const tz = getTimeZone();
const isInternal = tz === 'browser' || tz === 'utc';
const info = (isInternal ? mapInternal(tz, timestamp) : mapToInfo(tz, timestamp)) ?? {};
return {
countries: countriesByTimeZone[tz] ?? [],
abbreviation: '',
offsetInMins: 0,
...info,
name: 'Default',
zone,
};
}
case InternalTimeZones.localBrowserTime: {
const tz = moment.tz.guess(true);
const info = mapToInfo(tz, timestamp) ?? {};
return {
countries: countriesByTimeZone[tz] ?? [],
abbreviation: 'Your local time',
offsetInMins: new Date().getTimezoneOffset(),
...info,
name: 'Browser Time',
zone,
};
}
default:
return undefined;
}
};
const abbrevationWithoutOffset = (abbrevation: string): string => {
if (/^(\+|\-).+/.test(abbrevation)) {
return '';
}
return abbrevation;
};
const mapToInfo = (timeZone: TimeZone, timestamp: number): TimeZoneInfo | undefined => {
const momentTz = moment.tz.zone(timeZone);
if (!momentTz) {
return undefined;
}
return {
name: timeZone,
zone: timeZone,
countries: countriesByTimeZone[timeZone] ?? [],
abbreviation: abbrevationWithoutOffset(momentTz.abbr(timestamp)),
offsetInMins: momentTz.utcOffset(timestamp),
};
};
// Country names by ISO 3166-1-alpha-2 code
const countryByCode: Record<string, string> = {
AF: 'Afghanistan',
AX: 'Aland Islands',
AL: 'Albania',
DZ: 'Algeria',
AS: 'American Samoa',
AD: 'Andorra',
AO: 'Angola',
AI: 'Anguilla',
AQ: 'Antarctica',
AG: 'Antigua And Barbuda',
AR: 'Argentina',
AM: 'Armenia',
AW: 'Aruba',
AU: 'Australia',
AT: 'Austria',
AZ: 'Azerbaijan',
BS: 'Bahamas',
BH: 'Bahrain',
BD: 'Bangladesh',
BB: 'Barbados',
BY: 'Belarus',
BE: 'Belgium',
BZ: 'Belize',
BJ: 'Benin',
BM: 'Bermuda',
BT: 'Bhutan',
BO: 'Bolivia',
BA: 'Bosnia And Herzegovina',
BW: 'Botswana',
BV: 'Bouvet Island',
BR: 'Brazil',
IO: 'British Indian Ocean Territory',
BN: 'Brunei Darussalam',
BG: 'Bulgaria',
BF: 'Burkina Faso',
BI: 'Burundi',
KH: 'Cambodia',
CM: 'Cameroon',
CA: 'Canada',
CV: 'Cape Verde',
KY: 'Cayman Islands',
CF: 'Central African Republic',
TD: 'Chad',
CL: 'Chile',
CN: 'China',
CX: 'Christmas Island',
CC: 'Cocos (Keeling) Islands',
CO: 'Colombia',
KM: 'Comoros',
CG: 'Congo',
CD: 'Congo, Democratic Republic',
CK: 'Cook Islands',
CR: 'Costa Rica',
CI: "Cote D'Ivoire",
HR: 'Croatia',
CU: 'Cuba',
CY: 'Cyprus',
CZ: 'Czech Republic',
DK: 'Denmark',
DJ: 'Djibouti',
DM: 'Dominica',
DO: 'Dominican Republic',
EC: 'Ecuador',
EG: 'Egypt',
SV: 'El Salvador',
GQ: 'Equatorial Guinea',
ER: 'Eritrea',
EE: 'Estonia',
ET: 'Ethiopia',
FK: 'Falkland Islands (Malvinas)',
FO: 'Faroe Islands',
FJ: 'Fiji',
FI: 'Finland',
FR: 'France',
GF: 'French Guiana',
PF: 'French Polynesia',
TF: 'French Southern Territories',
GA: 'Gabon',
GM: 'Gambia',
GE: 'Georgia',
DE: 'Germany',
GH: 'Ghana',
GI: 'Gibraltar',
GR: 'Greece',
GL: 'Greenland',
GD: 'Grenada',
GP: 'Guadeloupe',
GU: 'Guam',
GT: 'Guatemala',
GG: 'Guernsey',
GN: 'Guinea',
GW: 'Guinea-Bissau',
GY: 'Guyana',
HT: 'Haiti',
HM: 'Heard Island & Mcdonald Islands',
VA: 'Holy See (Vatican City State)',
HN: 'Honduras',
HK: 'Hong Kong',
HU: 'Hungary',
IS: 'Iceland',
IN: 'India',
ID: 'Indonesia',
IR: 'Iran (Islamic Republic Of)',
IQ: 'Iraq',
IE: 'Ireland',
IM: 'Isle Of Man',
IL: 'Israel',
IT: 'Italy',
JM: 'Jamaica',
JP: 'Japan',
JE: 'Jersey',
JO: 'Jordan',
KZ: 'Kazakhstan',
KE: 'Kenya',
KI: 'Kiribati',
KR: 'Korea',
KW: 'Kuwait',
KG: 'Kyrgyzstan',
LA: "Lao People's Democratic Republic",
LV: 'Latvia',
LB: 'Lebanon',
LS: 'Lesotho',
LR: 'Liberia',
LY: 'Libyan Arab Jamahiriya',
LI: 'Liechtenstein',
LT: 'Lithuania',
LU: 'Luxembourg',
MO: 'Macao',
MK: 'Macedonia',
MG: 'Madagascar',
MW: 'Malawi',
MY: 'Malaysia',
MV: 'Maldives',
ML: 'Mali',
MT: 'Malta',
MH: 'Marshall Islands',
MQ: 'Martinique',
MR: 'Mauritania',
MU: 'Mauritius',
YT: 'Mayotte',
MX: 'Mexico',
FM: 'Micronesia (Federated States Of)',
MD: 'Moldova',
MC: 'Monaco',
MN: 'Mongolia',
ME: 'Montenegro',
MS: 'Montserrat',
MA: 'Morocco',
MZ: 'Mozambique',
MM: 'Myanmar',
NA: 'Namibia',
NR: 'Nauru',
NP: 'Nepal',
NL: 'Netherlands',
AN: 'Netherlands Antilles',
NC: 'New Caledonia',
NZ: 'New Zealand',
NI: 'Nicaragua',
NE: 'Niger',
NG: 'Nigeria',
NU: 'Niue',
NF: 'Norfolk Island',
MP: 'Northern Mariana Islands',
NO: 'Norway',
OM: 'Oman',
PK: 'Pakistan',
PW: 'Palau',
PS: 'Palestinian Territory (Occupied)',
PA: 'Panama',
PG: 'Papua New Guinea',
PY: 'Paraguay',
PE: 'Peru',
PH: 'Philippines',
PN: 'Pitcairn',
PL: 'Poland',
PT: 'Portugal',
PR: 'Puerto Rico',
QA: 'Qatar',
RE: 'Reunion',
RO: 'Romania',
RU: 'Russian Federation',
RW: 'Rwanda',
BL: 'Saint Barthelemy',
SH: 'Saint Helena',
KN: 'Saint Kitts And Nevis',
LC: 'Saint Lucia',
MF: 'Saint Martin',
PM: 'Saint Pierre And Miquelon',
VC: 'Saint Vincent And Grenadines',
WS: 'Samoa',
SM: 'San Marino',
ST: 'Sao Tome And Principe',
SA: 'Saudi Arabia',
SN: 'Senegal',
RS: 'Serbia',
SC: 'Seychelles',
SL: 'Sierra Leone',
SG: 'Singapore',
SK: 'Slovakia',
SI: 'Slovenia',
SB: 'Solomon Islands',
SO: 'Somalia',
ZA: 'South Africa',
GS: 'South Georgia And Sandwich Isl.',
ES: 'Spain',
LK: 'Sri Lanka',
SD: 'Sudan',
SR: 'Suriname',
SJ: 'Svalbard And Jan Mayen',
SZ: 'Swaziland',
SE: 'Sweden',
CH: 'Switzerland',
SY: 'Syrian Arab Republic',
TW: 'Taiwan',
TJ: 'Tajikistan',
TZ: 'Tanzania',
TH: 'Thailand',
TL: 'Timor-Leste',
TG: 'Togo',
TK: 'Tokelau',
TO: 'Tonga',
TT: 'Trinidad And Tobago',
TN: 'Tunisia',
TR: 'Turkey',
TM: 'Turkmenistan',
TC: 'Turks And Caicos Islands',
TV: 'Tuvalu',
UG: 'Uganda',
UA: 'Ukraine',
AE: 'United Arab Emirates',
GB: 'United Kingdom',
US: 'United States',
UM: 'United States Outlying Islands',
UY: 'Uruguay',
UZ: 'Uzbekistan',
VU: 'Vanuatu',
VE: 'Venezuela',
VN: 'Viet Nam',
VG: 'Virgin Islands, British',
VI: 'Virgin Islands, U.S.',
WF: 'Wallis And Futuna',
EH: 'Western Sahara',
YE: 'Yemen',
ZM: 'Zambia',
ZW: 'Zimbabwe',
};
const countriesByTimeZone = ((): Record<string, TimeZoneCountry[]> => {
return moment.tz.countries().reduce((all: Record<string, TimeZoneCountry[]>, code) => {
const timeZones = moment.tz.zonesForCountry(code);
return timeZones.reduce((all: Record<string, TimeZoneCountry[]>, timeZone) => {
if (!all[timeZone]) {
all[timeZone] = [];
}
const name = countryByCode[code];
if (!name) {
return all;
}
all[timeZone].push({ code, name });
return all;
}, all);
}, {});
})();

View File

@ -17,7 +17,7 @@ export interface SelectCommonProps<T> {
components?: any;
defaultValue?: any;
disabled?: boolean;
filterOption?: (option: SelectableValue, searchQuery: string) => void;
filterOption?: (option: SelectableValue, searchQuery: string) => boolean;
/** Function for formatting the text that is displayed when creating a new value*/
formatCreateLabel?: (input: string) => string;
getOptionLabel?: (item: SelectableValue<T>) => string;

View File

@ -1,344 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TimePickerContent renders correctly in full screen 1`] = `
<div
className="css-1py091y"
>
<div
className="css-dlnzj7"
>
<FullScreenForm
historyOptions={Array []}
isFullscreen={true}
onChange={[Function]}
timeZone="utc"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
visible={true}
/>
</div>
<CustomScrollbar
autoHeightMax="100%"
autoHeightMin="0"
autoHide={false}
autoHideDuration={200}
autoHideTimeout={200}
className="css-1o1b8dr"
hideTracksWhenNotNeeded={false}
setScrollTop={[Function]}
>
<NarrowScreenForm
historyOptions={Array []}
isFullscreen={true}
onChange={[Function]}
timeZone="utc"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
visible={false}
/>
<Component
onSelect={[Function]}
options={Array []}
timeZone="utc"
title="Relative time ranges"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
/>
<div
className="css-1ogeuxc"
/>
<Component
onSelect={[Function]}
options={Array []}
timeZone="utc"
title="Other quick ranges"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
/>
</CustomScrollbar>
</div>
`;
exports[`TimePickerContent renders correctly in narrow screen 1`] = `
<div
className="css-1py091y"
>
<div
className="css-dlnzj7"
>
<FullScreenForm
historyOptions={Array []}
isFullscreen={false}
onChange={[Function]}
timeZone="utc"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
visible={false}
/>
</div>
<CustomScrollbar
autoHeightMax="100%"
autoHeightMin="0"
autoHide={false}
autoHideDuration={200}
autoHideTimeout={200}
className="css-1o1b8dr"
hideTracksWhenNotNeeded={false}
setScrollTop={[Function]}
>
<NarrowScreenForm
historyOptions={Array []}
isFullscreen={false}
onChange={[Function]}
timeZone="utc"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
visible={true}
/>
<Component
onSelect={[Function]}
options={Array []}
timeZone="utc"
title="Relative time ranges"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
/>
<div
className="css-1ogeuxc"
/>
<Component
onSelect={[Function]}
options={Array []}
timeZone="utc"
title="Other quick ranges"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
/>
</CustomScrollbar>
</div>
`;
exports[`TimePickerContent renders recent absolute ranges correctly 1`] = `
<div
className="css-1py091y"
>
<div
className="css-dlnzj7"
>
<FullScreenForm
history={
Array [
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
},
Object {
"from": "2019-10-17T07:48:27.433Z",
"raw": Object {
"from": "2019-10-17T07:48:27.433Z",
"to": "2019-10-18T07:48:27.433Z",
},
"to": "2019-10-18T07:48:27.433Z",
},
]
}
historyOptions={
Array [
Object {
"display": "2019-12-17 07:48:27 to 2019-12-18 07:48:27",
"from": "2019-12-17T07:48:27Z",
"section": 3,
"to": "2019-12-18T07:48:27Z",
},
Object {
"display": "2019-10-17 07:48:27 to 2019-10-18 07:48:27",
"from": "2019-10-17T07:48:27Z",
"section": 3,
"to": "2019-10-18T07:48:27Z",
},
]
}
isFullscreen={true}
onChange={[Function]}
timeZone="utc"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
visible={true}
/>
</div>
<CustomScrollbar
autoHeightMax="100%"
autoHeightMin="0"
autoHide={false}
autoHideDuration={200}
autoHideTimeout={200}
className="css-1o1b8dr"
hideTracksWhenNotNeeded={false}
setScrollTop={[Function]}
>
<NarrowScreenForm
history={
Array [
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
},
Object {
"from": "2019-10-17T07:48:27.433Z",
"raw": Object {
"from": "2019-10-17T07:48:27.433Z",
"to": "2019-10-18T07:48:27.433Z",
},
"to": "2019-10-18T07:48:27.433Z",
},
]
}
historyOptions={
Array [
Object {
"display": "2019-12-17 07:48:27 to 2019-12-18 07:48:27",
"from": "2019-12-17T07:48:27Z",
"section": 3,
"to": "2019-12-18T07:48:27Z",
},
Object {
"display": "2019-10-17 07:48:27 to 2019-10-18 07:48:27",
"from": "2019-10-17T07:48:27Z",
"section": 3,
"to": "2019-10-18T07:48:27Z",
},
]
}
isFullscreen={true}
onChange={[Function]}
timeZone="utc"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
visible={false}
/>
<Component
onSelect={[Function]}
options={Array []}
timeZone="utc"
title="Relative time ranges"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
/>
<div
className="css-1ogeuxc"
/>
<Component
onSelect={[Function]}
options={Array []}
timeZone="utc"
title="Other quick ranges"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
/>
</CustomScrollbar>
</div>
`;

View File

@ -1,17 +0,0 @@
import { GrafanaTheme } from '@grafana/data';
import { selectThemeVariant } from '../../../themes/selectThemeVariant';
export const getThemeColors = (theme: GrafanaTheme) => {
return {
border: theme.colors.border1,
background: theme.colors.bodyBg,
shadow: theme.colors.dropdownShadow,
formBackground: selectThemeVariant(
{
dark: theme.palette.gray15,
light: theme.palette.gray98,
},
theme.type
),
};
};

View File

@ -24,6 +24,7 @@ export const basic = () => {
{(value, updateValue) => {
return (
<TimeRangePicker
onChangeTimeZone={() => {}}
timeZone="browser"
value={value}
onChange={timeRange => {

View File

@ -17,6 +17,7 @@ describe('TimePicker', () => {
it('renders buttons correctly', () => {
const wrapper = mount(
<UnthemedTimeRangePicker
onChangeTimeZone={() => {}}
onChange={value => {}}
value={value}
onMoveBackward={() => {}}

View File

@ -5,7 +5,7 @@ import { css, cx } from 'emotion';
// Components
import { Tooltip } from '../Tooltip/Tooltip';
import { Icon } from '../Icon/Icon';
import { TimePickerContent } from './TimePickerContent/TimePickerContent';
import { TimePickerContent } from './TimeRangePicker/TimePickerContent';
import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper';
// Utils & Services
@ -98,6 +98,7 @@ export interface Props extends Themeable {
timeSyncButton?: JSX.Element;
isSynced?: boolean;
onChange: (timeRange: TimeRange) => void;
onChangeTimeZone: (timeZone: TimeZone) => void;
onMoveBackward: () => void;
onMoveForward: () => void;
onZoom: () => void;
@ -139,6 +140,7 @@ export class UnthemedTimeRangePicker extends PureComponent<Props, State> {
isSynced,
theme,
history,
onChangeTimeZone,
} = this.props;
const { isOpen } = this.state;
@ -168,7 +170,7 @@ export class UnthemedTimeRangePicker extends PureComponent<Props, State> {
</button>
</Tooltip>
{isOpen && (
<ClickOutsideWrapper onClick={this.onClose}>
<ClickOutsideWrapper includeButtonPress={false} onClick={this.onClose}>
<TimePickerContent
timeZone={timeZone}
value={value}
@ -176,6 +178,7 @@ export class UnthemedTimeRangePicker extends PureComponent<Props, State> {
otherOptions={otherOptions}
quickOptions={quickOptions}
history={history}
onChangeTimeZone={onChangeTimeZone}
/>
</ClickOutsideWrapper>
)}

View File

@ -7,31 +7,32 @@ import { TimePickerTitle } from './TimePickerTitle';
import { Button } from '../../Button';
import { Icon } from '../../Icon/Icon';
import { Portal } from '../../Portal/Portal';
import { getThemeColors } from './colors';
import { ClickOutsideWrapper } from '../../ClickOutsideWrapper/ClickOutsideWrapper';
const getStyles = stylesFactory((theme: GrafanaTheme) => {
const colors = getThemeColors(theme);
const containerBorder = theme.isDark ? theme.palette.dark9 : theme.palette.gray5;
return {
container: css`
top: 0;
top: -1px;
position: absolute;
right: 546px;
box-shadow: 0px 0px 20px ${colors.shadow};
background-color: ${colors.background};
right: 544px;
box-shadow: 0px 0px 20px ${theme.colors.dropdownShadow};
background-color: ${theme.colors.bodyBg};
z-index: -1;
border: 1px solid ${containerBorder};
border-radius: 2px 0 0 2px;
&:after {
display: block;
background-color: ${colors.background};
background-color: ${theme.colors.bodyBg};
width: 19px;
height: 381px;
height: 100%;
content: ' ';
position: absolute;
top: 0;
right: -19px;
border-left: 1px solid ${colors.border};
border-left: 1px solid ${theme.colors.border1};
}
`,
modal: css`
@ -59,11 +60,9 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
});
const getFooterStyles = stylesFactory((theme: GrafanaTheme) => {
const colors = getThemeColors(theme);
return {
container: css`
background-color: ${colors.background};
background-color: ${theme.colors.bodyBg};
display: flex;
justify-content: center;
padding: 10px;
@ -78,12 +77,10 @@ const getFooterStyles = stylesFactory((theme: GrafanaTheme) => {
});
const getBodyStyles = stylesFactory((theme: GrafanaTheme) => {
const colors = getThemeColors(theme);
return {
title: css`
color: ${theme.colors.text};
background-color: ${colors.background};
background-color: ${theme.colors.bodyBg};
font-size: ${theme.typography.size.md};
border: 1px solid transparent;
@ -93,7 +90,7 @@ const getBodyStyles = stylesFactory((theme: GrafanaTheme) => {
`,
body: css`
z-index: ${theme.zIndex.modal};
background-color: ${colors.background};
background-color: ${theme.colors.bodyBg};
width: 268px;
.react-calendar__navigation__label,
@ -177,11 +174,9 @@ const getBodyStyles = stylesFactory((theme: GrafanaTheme) => {
});
const getHeaderStyles = stylesFactory((theme: GrafanaTheme) => {
const colors = getThemeColors(theme);
return {
container: css`
background-color: ${colors.background};
background-color: ${theme.colors.bodyBg};
display: flex;
justify-content: space-between;
padding: 7px;

View File

@ -7,7 +7,13 @@ describe('TimePickerContent', () => {
it('renders correctly in full screen', () => {
const value = createTimeRange('2019-12-17T07:48:27.433Z', '2019-12-18T07:48:27.433Z');
const wrapper = shallow(
<TimePickerContentWithScreenSize onChange={value => {}} timeZone="utc" value={value} isFullscreen={true} />
<TimePickerContentWithScreenSize
onChangeTimeZone={() => {}}
onChange={value => {}}
timeZone="utc"
value={value}
isFullscreen={true}
/>
);
expect(wrapper).toMatchSnapshot();
});
@ -15,7 +21,13 @@ describe('TimePickerContent', () => {
it('renders correctly in narrow screen', () => {
const value = createTimeRange('2019-12-17T07:48:27.433Z', '2019-12-18T07:48:27.433Z');
const wrapper = shallow(
<TimePickerContentWithScreenSize onChange={value => {}} timeZone="utc" value={value} isFullscreen={false} />
<TimePickerContentWithScreenSize
onChangeTimeZone={() => {}}
onChange={value => {}}
timeZone="utc"
value={value}
isFullscreen={false}
/>
);
expect(wrapper).toMatchSnapshot();
});
@ -29,6 +41,7 @@ describe('TimePickerContent', () => {
const wrapper = shallow(
<TimePickerContentWithScreenSize
onChangeTimeZone={() => {}}
onChange={value => {}}
timeZone="utc"
value={value}

View File

@ -5,26 +5,26 @@ import { useMedia } from 'react-use';
import { stylesFactory, useTheme } from '../../../themes';
import { CustomScrollbar } from '../../CustomScrollbar/CustomScrollbar';
import { Icon } from '../../Icon/Icon';
import { getThemeColors } from './colors';
import { mapRangeToTimeOption } from './mapper';
import { TimePickerTitle } from './TimePickerTitle';
import { TimeRangeForm } from './TimeRangeForm';
import { TimeRangeList } from './TimeRangeList';
import { TimePickerFooter } from './TimePickerFooter';
const getStyles = stylesFactory((theme: GrafanaTheme) => {
const colors = getThemeColors(theme);
const containerBorder = theme.isDark ? theme.palette.dark9 : theme.palette.gray5;
return {
container: css`
display: flex;
background: ${colors.background};
box-shadow: 0px 0px 20px ${colors.shadow};
background: ${theme.colors.bodyBg};
box-shadow: 0px 0px 20px ${theme.colors.dropdownShadow};
position: absolute;
z-index: ${theme.zIndex.modal};
width: 546px;
height: 381px;
top: 116%;
margin-left: -322px;
border-radius: 2px;
border: 1px solid ${containerBorder};
@media only screen and (max-width: ${theme.breakpoints.lg}) {
width: 218px;
@ -36,10 +36,14 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
margin-left: -100px;
}
`,
body: css`
display: flex;
height: 381px;
`,
leftSide: css`
display: flex;
flex-direction: column;
border-right: 1px solid ${colors.border};
border-right: 1px solid ${theme.colors.border1};
width: 60%;
overflow: hidden;
@ -61,7 +65,7 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
});
const getNarrowScreenStyles = stylesFactory((theme: GrafanaTheme) => {
const colors = getThemeColors(theme);
const formBackground = theme.isDark ? theme.palette.gray15 : theme.palette.gray98;
return {
header: css`
@ -69,13 +73,13 @@ const getNarrowScreenStyles = stylesFactory((theme: GrafanaTheme) => {
flex-direction: row;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid ${colors.border};
border-bottom: 1px solid ${theme.colors.border1};
padding: 7px 9px 7px 9px;
`,
body: css`
border-bottom: 1px solid ${colors.border};
background: ${colors.formBackground};
box-shadow: inset 0px 2px 2px ${colors.shadow};
border-bottom: 1px solid ${theme.colors.border1};
background: ${formBackground};
box-shadow: inset 0px 2px 2px ${theme.colors.dropdownShadow};
`,
form: css`
padding: 7px 9px 7px 9px;
@ -103,11 +107,11 @@ const getFullScreenStyles = stylesFactory((theme: GrafanaTheme) => {
});
const getEmptyListStyles = stylesFactory((theme: GrafanaTheme) => {
const colors = getThemeColors(theme);
const formBackground = theme.isDark ? theme.palette.gray15 : theme.palette.gray98;
return {
container: css`
background-color: ${colors.formBackground};
background-color: ${formBackground};
padding: 12px;
margin: 12px;
@ -125,6 +129,7 @@ const getEmptyListStyles = stylesFactory((theme: GrafanaTheme) => {
interface Props {
value: TimeRange;
onChange: (timeRange: TimeRange) => void;
onChangeTimeZone: (timeZone: TimeZone) => void;
timeZone?: TimeZone;
quickOptions?: TimeOption[];
otherOptions?: TimeOption[];
@ -148,6 +153,7 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = pr
return (
<div className={styles.container}>
<div className={styles.body}>
<div className={styles.leftSide}>
<FullScreenForm {...props} visible={isFullscreen} historyOptions={historyOptions} />
</div>
@ -170,6 +176,8 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = pr
/>
</CustomScrollbar>
</div>
{isFullscreen && <TimePickerFooter timeZone={props.timeZone} onChangeTimeZone={props.onChangeTimeZone} />}
</div>
);
};

View File

@ -0,0 +1,113 @@
import React, { FC, useState, useCallback } from 'react';
import { css, cx } from 'emotion';
import { TimeZone, GrafanaTheme, getTimeZoneInfo } from '@grafana/data';
import { stylesFactory, useTheme } from '../../../themes';
import { TimeZoneTitle } from '../TimeZonePicker/TimeZoneTitle';
import { TimeZoneDescription } from '../TimeZonePicker/TimeZoneDescription';
import { TimeZoneOffset } from '../TimeZonePicker/TimeZoneOffset';
import { Button } from '../../Button';
import { TimeZonePicker } from '../TimeZonePicker';
import isString from 'lodash/isString';
interface Props {
timeZone?: TimeZone;
timestamp?: number;
onChangeTimeZone: (timeZone: TimeZone) => void;
}
export const TimePickerFooter: FC<Props> = props => {
const { timeZone, timestamp = Date.now(), onChangeTimeZone } = props;
const [isEditing, setEditing] = useState(false);
const onToggleChangeTz = useCallback(
(event?: React.MouseEvent) => {
if (event) {
event.stopPropagation();
}
setEditing(!isEditing);
},
[isEditing, setEditing]
);
const theme = useTheme();
const style = getStyle(theme);
if (!isString(timeZone)) {
return null;
}
const info = getTimeZoneInfo(timeZone, timestamp);
if (!info) {
return null;
}
if (isEditing) {
return (
<div className={cx(style.container, style.editContainer)}>
<div className={style.timeZoneContainer}>
<TimeZonePicker
onChange={timeZone => {
onToggleChangeTz();
if (isString(timeZone)) {
onChangeTimeZone(timeZone);
}
}}
autoFocus={true}
onBlur={onToggleChangeTz}
/>
</div>
</div>
);
}
return (
<div className={style.container}>
<div className={style.timeZoneContainer}>
<div className={style.timeZone}>
<TimeZoneTitle title={info.name} />
<div className={style.spacer} />
<TimeZoneDescription info={info} />
</div>
<TimeZoneOffset timeZone={timeZone} timestamp={timestamp} />
</div>
<div className={style.spacer} />
<Button variant="secondary" onClick={onToggleChangeTz} size="sm">
Change time zone
</Button>
</div>
);
};
const getStyle = stylesFactory((theme: GrafanaTheme) => {
return {
container: css`
border-top: 1px solid ${theme.colors.border1};
padding: 11px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
`,
editContainer: css`
padding: 7px;
`,
spacer: css`
margin-left: 7px;
`,
timeZoneContainer: css`
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
flex-grow: 1;
`,
timeZone: css`
display: flex;
flex-direction: row;
align-items: baseline;
flex-grow: 1;
`,
};
});

View File

@ -0,0 +1,370 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TimePickerContent renders correctly in full screen 1`] = `
<div
className="css-1py9bjs"
>
<div
className="css-ooqtr4"
>
<div
className="css-dlnzj7"
>
<FullScreenForm
historyOptions={Array []}
isFullscreen={true}
onChange={[Function]}
onChangeTimeZone={[Function]}
timeZone="utc"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
visible={true}
/>
</div>
<CustomScrollbar
autoHeightMax="100%"
autoHeightMin="0"
autoHide={false}
autoHideDuration={200}
autoHideTimeout={200}
className="css-1o1b8dr"
hideTracksWhenNotNeeded={false}
setScrollTop={[Function]}
>
<NarrowScreenForm
historyOptions={Array []}
isFullscreen={true}
onChange={[Function]}
onChangeTimeZone={[Function]}
timeZone="utc"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
visible={false}
/>
<Component
onSelect={[Function]}
options={Array []}
timeZone="utc"
title="Relative time ranges"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
/>
<div
className="css-1ogeuxc"
/>
<Component
onSelect={[Function]}
options={Array []}
timeZone="utc"
title="Other quick ranges"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
/>
</CustomScrollbar>
</div>
<Component
onChangeTimeZone={[Function]}
timeZone="utc"
/>
</div>
`;
exports[`TimePickerContent renders correctly in narrow screen 1`] = `
<div
className="css-1py9bjs"
>
<div
className="css-ooqtr4"
>
<div
className="css-dlnzj7"
>
<FullScreenForm
historyOptions={Array []}
isFullscreen={false}
onChange={[Function]}
onChangeTimeZone={[Function]}
timeZone="utc"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
visible={false}
/>
</div>
<CustomScrollbar
autoHeightMax="100%"
autoHeightMin="0"
autoHide={false}
autoHideDuration={200}
autoHideTimeout={200}
className="css-1o1b8dr"
hideTracksWhenNotNeeded={false}
setScrollTop={[Function]}
>
<NarrowScreenForm
historyOptions={Array []}
isFullscreen={false}
onChange={[Function]}
onChangeTimeZone={[Function]}
timeZone="utc"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
visible={true}
/>
<Component
onSelect={[Function]}
options={Array []}
timeZone="utc"
title="Relative time ranges"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
/>
<div
className="css-1ogeuxc"
/>
<Component
onSelect={[Function]}
options={Array []}
timeZone="utc"
title="Other quick ranges"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
/>
</CustomScrollbar>
</div>
</div>
`;
exports[`TimePickerContent renders recent absolute ranges correctly 1`] = `
<div
className="css-1py9bjs"
>
<div
className="css-ooqtr4"
>
<div
className="css-dlnzj7"
>
<FullScreenForm
history={
Array [
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
},
Object {
"from": "2019-10-17T07:48:27.433Z",
"raw": Object {
"from": "2019-10-17T07:48:27.433Z",
"to": "2019-10-18T07:48:27.433Z",
},
"to": "2019-10-18T07:48:27.433Z",
},
]
}
historyOptions={
Array [
Object {
"display": "2019-12-17 07:48:27 to 2019-12-18 07:48:27",
"from": "2019-12-17T07:48:27Z",
"section": 3,
"to": "2019-12-18T07:48:27Z",
},
Object {
"display": "2019-10-17 07:48:27 to 2019-10-18 07:48:27",
"from": "2019-10-17T07:48:27Z",
"section": 3,
"to": "2019-10-18T07:48:27Z",
},
]
}
isFullscreen={true}
onChange={[Function]}
onChangeTimeZone={[Function]}
timeZone="utc"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
visible={true}
/>
</div>
<CustomScrollbar
autoHeightMax="100%"
autoHeightMin="0"
autoHide={false}
autoHideDuration={200}
autoHideTimeout={200}
className="css-1o1b8dr"
hideTracksWhenNotNeeded={false}
setScrollTop={[Function]}
>
<NarrowScreenForm
history={
Array [
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
},
Object {
"from": "2019-10-17T07:48:27.433Z",
"raw": Object {
"from": "2019-10-17T07:48:27.433Z",
"to": "2019-10-18T07:48:27.433Z",
},
"to": "2019-10-18T07:48:27.433Z",
},
]
}
historyOptions={
Array [
Object {
"display": "2019-12-17 07:48:27 to 2019-12-18 07:48:27",
"from": "2019-12-17T07:48:27Z",
"section": 3,
"to": "2019-12-18T07:48:27Z",
},
Object {
"display": "2019-10-17 07:48:27 to 2019-10-18 07:48:27",
"from": "2019-10-17T07:48:27Z",
"section": 3,
"to": "2019-10-18T07:48:27Z",
},
]
}
isFullscreen={true}
onChange={[Function]}
onChangeTimeZone={[Function]}
timeZone="utc"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
visible={false}
/>
<Component
onSelect={[Function]}
options={Array []}
timeZone="utc"
title="Relative time ranges"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
/>
<div
className="css-1ogeuxc"
/>
<Component
onSelect={[Function]}
options={Array []}
timeZone="utc"
title="Other quick ranges"
value={
Object {
"from": "2019-12-17T07:48:27.433Z",
"raw": Object {
"from": "2019-12-17T07:48:27.433Z",
"to": "2019-12-18T07:48:27.433Z",
},
"to": "2019-12-18T07:48:27.433Z",
}
}
/>
</CustomScrollbar>
</div>
<Component
onChangeTimeZone={[Function]}
timeZone="utc"
/>
</div>
`;

View File

@ -23,6 +23,9 @@ export const basic = () => {
<TimeZonePicker
value={value.value}
onChange={newValue => {
if (!newValue) {
return;
}
action('on selected')(newValue);
updateValue({ value: newValue });
}}

View File

@ -1,47 +1,150 @@
import React, { FC } from 'react';
import { getTimeZoneGroups } from '@grafana/data';
import { Cascader } from '../index';
import React, { useMemo, useCallback } from 'react';
import { toLower, isEmpty, isString } from 'lodash';
import {
SelectableValue,
getTimeZoneInfo,
TimeZoneInfo,
getTimeZoneGroups,
GroupedTimeZones,
TimeZone,
InternalTimeZones,
} from '@grafana/data';
import { Select } from '../Select/Select';
import { CompactTimeZoneOption, WideTimeZoneOption, SelectableZone } from './TimeZonePicker/TimeZoneOption';
import { TimeZoneGroup } from './TimeZonePicker/TimeZoneGroup';
import { formatUtcOffset } from './TimeZonePicker/TimeZoneOffset';
export interface Props {
value: string;
value?: TimeZone;
width?: number;
onChange: (newValue: string) => void;
autoFocus?: boolean;
onChange: (timeZone: TimeZone | undefined) => void;
onBlur?: () => void;
}
export const TimeZonePicker: FC<Props> = ({ onChange, value, width }) => {
const timeZoneGroups = getTimeZoneGroups();
export const TimeZonePicker: React.FC<Props> = props => {
const { onChange, width, autoFocus = false, onBlur, value } = props;
const groupedTimeZones = useTimeZones();
const selected = useSelectedTimeZone(groupedTimeZones, value);
const filterBySearchIndex = useFilterBySearchIndex();
const TimeZoneOption = width && width <= 45 ? CompactTimeZoneOption : WideTimeZoneOption;
const groupOptions = timeZoneGroups.map(group => {
const options = group.options.map(timeZone => {
return {
label: timeZone,
value: timeZone,
};
});
return {
label: group.label,
value: group.label,
items: options,
};
});
const selectedValue = groupOptions.reduce(
(acc, group) => {
const found = group.items.find(option => option.value === value);
return found || acc;
const onChangeTz = useCallback(
(selectable: SelectableValue<string>) => {
if (!selectable || !isString(selectable.value)) {
return onChange(value);
}
onChange(selectable.value);
},
{ value: '' }
[onChange, value]
);
return (
<Cascader
options={groupOptions}
initialValue={selectedValue?.value}
onSelect={(newValue: string) => onChange(newValue)}
<Select
value={selected}
placeholder="Type to search (country, city, abbreviation)"
autoFocus={autoFocus}
openMenuOnFocus={true}
width={width}
placeholder="Select timezone"
filterOption={filterBySearchIndex}
options={groupedTimeZones}
onChange={onChangeTz}
onBlur={onBlur}
components={{ Option: TimeZoneOption, Group: TimeZoneGroup }}
/>
);
};
interface SelectableZoneGroup extends SelectableValue<string> {
options: SelectableZone[];
}
const useTimeZones = (): SelectableZoneGroup[] => {
const now = Date.now();
return getTimeZoneGroups(true).map((group: GroupedTimeZones) => {
const options = group.zones.reduce((options: SelectableZone[], zone) => {
const info = getTimeZoneInfo(zone, now);
if (!info) {
return options;
}
options.push({
label: info.name,
value: info.zone,
searchIndex: useSearchIndex(info, now),
});
return options;
}, []);
return {
label: group.name,
options,
};
});
};
const useSelectedTimeZone = (
groups: SelectableZoneGroup[],
timeZone: TimeZone | undefined
): SelectableZone | undefined => {
return useMemo(() => {
if (timeZone === undefined) {
return undefined;
}
const group = groups.find(group => {
if (!group.label) {
return isInternal(timeZone);
}
return timeZone.startsWith(group.label);
});
return group?.options.find(option => {
if (isEmpty(timeZone)) {
return option.value === InternalTimeZones.default;
}
return toLower(option.value) === timeZone;
});
}, [groups, timeZone]);
};
const isInternal = (timeZone: TimeZone): boolean => {
switch (timeZone) {
case InternalTimeZones.default:
case InternalTimeZones.localBrowserTime:
case InternalTimeZones.utc:
return true;
default:
return false;
}
};
const useFilterBySearchIndex = () => {
return useCallback((option: SelectableValue, searchQuery: string) => {
if (!searchQuery || !option.data || !option.data.searchIndex) {
return true;
}
return option.data.searchIndex.indexOf(toLower(searchQuery)) > -1;
}, []);
};
const useSearchIndex = (info: TimeZoneInfo, timestamp: number): string => {
return useMemo(() => {
const parts: string[] = [
toLower(info.name),
toLower(info.abbreviation),
toLower(formatUtcOffset(timestamp, info.zone)),
];
for (const country of info.countries) {
parts.push(toLower(country.name));
parts.push(toLower(country.code));
}
return parts.join('|');
}, [info.zone, info.abbreviation, info.offsetInMins]);
};

View File

@ -0,0 +1,49 @@
import React, { PropsWithChildren, useMemo } from 'react';
import { css } from 'emotion';
import { GrafanaTheme, TimeZoneInfo } from '@grafana/data';
import { useTheme, stylesFactory } from '../../../themes';
interface Props {
info?: TimeZoneInfo;
}
export const TimeZoneDescription: React.FC<PropsWithChildren<Props>> = ({ info }) => {
if (!info) {
return null;
}
const theme = useTheme();
const styles = getStyles(theme);
const description = useDescription(info);
return <div className={styles.description}>{description}</div>;
};
const useDescription = (info: TimeZoneInfo): string => {
return useMemo(() => {
const parts: string[] = [];
if (info.countries.length > 0) {
const country = info.countries[0];
parts.push(country.name);
}
if (info.abbreviation) {
parts.push(info.abbreviation);
}
return parts.join(', ');
}, [info.zone]);
};
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
description: css`
font-weight: normal;
font-size: ${theme.typography.size.sm};
color: ${theme.colors.textWeak};
white-space: normal;
text-overflow: ellipsis;
`,
};
});

View File

@ -0,0 +1,45 @@
import React, { PropsWithChildren } from 'react';
import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { useTheme, stylesFactory } from '../../../themes';
interface Props {
label: string | undefined;
}
const stopPropagation = (event: React.MouseEvent) => event.stopPropagation();
export const TimeZoneGroup: React.FC<PropsWithChildren<Props>> = props => {
const theme = useTheme();
const { children, label } = props;
const styles = getStyles(theme);
if (!label) {
return <div onClick={stopPropagation}>{children}</div>;
}
return (
<div onClick={stopPropagation}>
<div className={styles.header}>
<span className={styles.label}>{label}</span>
</div>
{children}
</div>
);
};
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
header: css`
padding: 7px 10px;
width: 100%;
border-top: 1px solid ${theme.colors.border1};
text-transform: capitalize;
`,
label: css`
font-size: ${theme.typography.size.sm};
color: ${theme.colors.textWeak};
font-weight: ${theme.typography.weight.semibold};
`,
};
});

View File

@ -0,0 +1,72 @@
import React, { PropsWithChildren } from 'react';
import { css, cx } from 'emotion';
import { GrafanaTheme, TimeZone, dateTimeFormat } from '@grafana/data';
import { useTheme, stylesFactory } from '../../../themes';
import isString from 'lodash/isString';
interface Props {
timestamp: number;
timeZone: TimeZone | undefined;
className?: string;
}
export const TimeZoneOffset: React.FC<PropsWithChildren<Props>> = props => {
const theme = useTheme();
const { timestamp, timeZone, className } = props;
const styles = getStyles(theme);
if (!isString(timeZone)) {
return null;
}
return (
<>
<span className={styles.localTime}>{formatLocalTime(timestamp, timeZone)}</span>
<span className={cx(styles.offset, className)}>{formatUtcOffset(timestamp, timeZone)}</span>
</>
);
};
export const formatUtcOffset = (timestamp: number, timeZone: TimeZone): string => {
const offset = dateTimeFormat(timestamp, {
timeZone,
format: 'Z',
});
if (offset === '+00:00') {
return 'UTC';
}
return `UTC${offset}`;
};
const formatLocalTime = (timestamp: number, timeZone: TimeZone): string => {
return dateTimeFormat(timestamp, {
timeZone,
format: 'HH:mm',
});
};
const getStyles = stylesFactory((theme: GrafanaTheme) => {
const textBase = css`
font-weight: normal;
font-size: ${theme.typography.size.sm};
color: ${theme.colors.textWeak};
white-space: normal;
`;
return {
localTime: css`
display: none;
${textBase};
color: ${theme.colors.text};
`,
offset: css`
${textBase};
color: ${theme.colors.text};
background: ${theme.colors.bg2};
padding: 2px 5px;
border-radius: 2px;
margin-left: 4px;
`,
};
});

View File

@ -0,0 +1,159 @@
import React, { PropsWithChildren } from 'react';
import { css, cx } from 'emotion';
import { GrafanaTheme, SelectableValue, getTimeZoneInfo } from '@grafana/data';
import { useTheme } from '../../../themes/ThemeContext';
import { stylesFactory } from '../../../themes/stylesFactory';
import { Icon } from '../../Icon/Icon';
import { TimeZoneOffset } from './TimeZoneOffset';
import { TimeZoneDescription } from './TimeZoneDescription';
import { TimeZoneTitle } from './TimeZoneTitle';
import isString from 'lodash/isString';
interface Props {
isFocused: boolean;
isSelected: boolean;
innerProps: any;
data: SelectableZone;
}
const offsetClassName = 'tz-utc-offset';
export interface SelectableZone extends SelectableValue<string> {
searchIndex: string;
}
export const WideTimeZoneOption: React.FC<PropsWithChildren<Props>> = (props, ref) => {
const { children, innerProps, data, isSelected, isFocused } = props;
const theme = useTheme();
const styles = getStyles(theme);
const timestamp = Date.now();
const containerStyles = cx(styles.container, isFocused && styles.containerFocused);
if (!isString(data.value)) {
return null;
}
return (
<div className={containerStyles} {...innerProps} aria-label="Select option">
<div className={cx(styles.leftColumn, styles.row)}>
<div className={cx(styles.leftColumn, styles.wideRow)}>
<TimeZoneTitle title={children} />
<div className={styles.spacer} />
<TimeZoneDescription info={getTimeZoneInfo(data.value, timestamp)} />
</div>
<div className={styles.rightColumn}>
<TimeZoneOffset timeZone={data.value} timestamp={timestamp} className={offsetClassName} />
{isSelected && (
<span>
<Icon name="check" />
</span>
)}
</div>
</div>
</div>
);
};
export const CompactTimeZoneOption: React.FC<PropsWithChildren<Props>> = (props, ref) => {
const { children, innerProps, data, isSelected, isFocused } = props;
const theme = useTheme();
const styles = getStyles(theme);
const timestamp = Date.now();
const containerStyles = cx(styles.container, isFocused && styles.containerFocused);
if (!isString(data.value)) {
return null;
}
return (
<div className={containerStyles} {...innerProps} aria-label="Select option">
<div className={styles.body}>
<div className={styles.row}>
<div className={styles.leftColumn}>
<TimeZoneTitle title={children} />
</div>
<div className={styles.rightColumn}>
{isSelected && (
<span>
<Icon name="check" />
</span>
)}
</div>
</div>
<div className={styles.row}>
<div className={styles.leftColumn}>
<TimeZoneDescription info={getTimeZoneInfo(data.value, timestamp)} />
</div>
<div className={styles.rightColumn}>
<TimeZoneOffset timestamp={timestamp} timeZone={data.value} className={offsetClassName} />
</div>
</div>
</div>
</div>
);
};
const getStyles = stylesFactory((theme: GrafanaTheme) => {
const offsetHoverBg = theme.isDark ? theme.palette.gray05 : theme.palette.white;
return {
container: css`
display: flex;
align-items: center;
flex-direction: row;
flex-shrink: 0;
white-space: nowrap;
cursor: pointer;
border-left: 2px solid transparent;
padding: 6px 8px 4px;
&:hover {
background: ${theme.colors.dropdownOptionHoverBg};
span.${offsetClassName} {
background: ${offsetHoverBg};
}
}
`,
containerFocused: css`
background: ${theme.colors.dropdownOptionHoverBg};
border-image: linear-gradient(#f05a28 30%, #fbca0a 99%);
border-image-slice: 1;
border-style: solid;
border-top: 0;
border-right: 0;
border-bottom: 0;
border-left-width: 2px;
span.${offsetClassName} {
background: ${offsetHoverBg};
}
`,
body: css`
display: flex;
font-weight: ${theme.typography.weight.semibold};
flex-direction: column;
flex-grow: 1;
`,
row: css`
display: flex;
flex-direction: row;
`,
leftColumn: css`
flex-grow: 1;
text-overflow: ellipsis;
`,
rightColumn: css`
justify-content: flex-end;
align-items: center;
`,
wideRow: css`
display: flex;
flex-direction: row;
align-items: baseline;
`,
spacer: css`
margin-left: 6px;
`,
};
});

View File

@ -0,0 +1,28 @@
import React, { ReactNode } from 'react';
import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { useTheme, stylesFactory } from '../../../themes';
interface Props {
title: string | ReactNode;
}
export const TimeZoneTitle: React.FC<Props> = ({ title }) => {
const theme = useTheme();
const styles = getStyles(theme);
if (!title) {
return null;
}
return <span className={styles.title}>{title}</span>;
};
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
title: css`
font-weight: ${theme.typography.weight.regular};
text-overflow: ellipsis;
`,
};
});

View File

@ -23,8 +23,8 @@ export { UnitPicker } from './UnitPicker/UnitPicker';
export { StatsPicker } from './StatsPicker/StatsPicker';
export { RefreshPicker } from './RefreshPicker/RefreshPicker';
export { TimeRangePicker } from './TimePicker/TimeRangePicker';
export { TimeZonePicker } from './TimePicker/TimeZonePicker';
export { TimeOfDayPicker } from './TimePicker/TimeOfDayPicker';
export { TimeZonePicker } from './TimePicker/TimeZonePicker';
export { List } from './List/List';
export { TagsInput } from './TagsInput/TagsInput';
export { Pagination } from './Pagination/Pagination';

View File

@ -94,7 +94,7 @@ export class GrafanaApp {
addClassIfNoOverlayScrollbar();
setLocale(config.bootData.user.locale);
setTimeZoneResolver(() => config.bootData.user.timeZone);
setTimeZoneResolver(() => config.bootData.user.timezone);
setMarkdownOptions({ sanitize: !config.disableSanitizeHtml });

View File

@ -12,8 +12,9 @@ import {
Button,
RadioButtonGroup,
FieldSet,
TimeZonePicker,
} from '@grafana/ui';
import { getTimeZoneGroups, SelectableValue } from '@grafana/data';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
@ -36,18 +37,6 @@ const themes: SelectableValue[] = [
{ value: 'light', label: 'Light' },
];
const grafanaTimeZones = [
{ value: '', label: 'Default' },
{ value: 'browser', label: 'Local browser time' },
{ value: 'utc', label: 'UTC' },
];
const timeZones = getTimeZoneGroups().reduce((tzs, group) => {
const options = group.options.map(tz => ({ value: tz, label: tz }));
tzs.push.apply(tzs, options);
return tzs;
}, grafanaTimeZones);
export class SharedPreferences extends PureComponent<Props, State> {
backendSrv = backendSrv;
@ -112,11 +101,11 @@ export class SharedPreferences extends PureComponent<Props, State> {
this.setState({ theme: value });
};
onTimeZoneChanged = (timezone: SelectableValue<string>) => {
if (!timezone || typeof timezone.value !== 'string') {
onTimeZoneChanged = (timezone: string) => {
if (!timezone) {
return;
}
this.setState({ timezone: timezone.value });
this.setState({ timezone: timezone });
};
onHomeDashboardChanged = (dashboardId: number) => {
@ -168,12 +157,7 @@ export class SharedPreferences extends PureComponent<Props, State> {
</Field>
<Field label="Timezone" aria-label={selectors.components.TimeZonePicker.container}>
<Select
isSearchable={true}
value={timeZones.find(item => item.value === timezone)}
onChange={this.onTimeZoneChanged}
options={timeZones}
/>
<TimeZonePicker value={timezone} onChange={this.onTimeZoneChanged} />
</Field>
<div className="gf-form-button-row">
<Button variant="primary">Save</Button>

View File

@ -1,6 +1,6 @@
// Libaries
import React, { PureComponent, FC, ReactNode } from 'react';
import { connect } from 'react-redux';
import { connect, MapDispatchToProps } from 'react-redux';
import { css } from 'emotion';
// Utils & Services
import { appEvents } from 'app/core/app_events';
@ -13,6 +13,7 @@ import { textUtil } from '@grafana/data';
import { BackButton } from 'app/core/components/BackButton/BackButton';
// State
import { updateLocation } from 'app/core/actions';
import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
// Types
import { DashboardModel } from '../../state';
import { CoreEvents, StoreState } from 'app/types';
@ -23,10 +24,14 @@ export interface OwnProps {
dashboard: DashboardModel;
isFullscreen: boolean;
$injector: any;
updateLocation: typeof updateLocation;
onAddPanel: () => void;
}
interface DispatchProps {
updateTimeZoneForSession: typeof updateTimeZoneForSession;
updateLocation: typeof updateLocation;
}
interface DashNavButtonModel {
show: (props: Props) => boolean;
component: FC<Partial<Props>>;
@ -48,7 +53,7 @@ export interface StateProps {
location: any;
}
type Props = StateProps & OwnProps;
type Props = StateProps & OwnProps & DispatchProps;
class DashNav extends PureComponent<Props> {
playlistSrv: PlaylistSrv;
@ -277,7 +282,7 @@ class DashNav extends PureComponent<Props> {
}
render() {
const { dashboard, location, isFullscreen } = this.props;
const { dashboard, location, isFullscreen, updateTimeZoneForSession } = this.props;
return (
<div className="navbar">
@ -315,7 +320,11 @@ class DashNav extends PureComponent<Props> {
{!dashboard.timepicker.hidden && (
<div className="navbar-buttons">
<DashNavTimeControls dashboard={dashboard} location={location} updateLocation={updateLocation} />
<DashNavTimeControls
dashboard={dashboard}
location={location}
onChangeTimeZone={updateTimeZoneForSession}
/>
</div>
)}
</div>
@ -327,8 +336,9 @@ const mapStateToProps = (state: StoreState) => ({
location: state.location,
});
const mapDispatchToProps = {
const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
updateLocation,
updateTimeZoneForSession,
};
export default connect(mapStateToProps, mapDispatchToProps)(DashNav);

View File

@ -1,6 +1,6 @@
// Libaries
import React, { Component } from 'react';
import { dateMath, GrafanaTheme } from '@grafana/data';
import { dateMath, GrafanaTheme, TimeZone } from '@grafana/data';
import { css } from 'emotion';
// Types
@ -9,7 +9,7 @@ import { LocationState, CoreEvents } from 'app/types';
import { TimeRange } from '@grafana/data';
// State
import { updateLocation } from 'app/core/actions';
import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
// Components
import { RefreshPicker, withTheme, stylesFactory, Themeable } from '@grafana/ui';
@ -31,8 +31,8 @@ const getStyles = stylesFactory((theme: GrafanaTheme) => {
export interface Props extends Themeable {
dashboard: DashboardModel;
updateLocation: typeof updateLocation;
location: LocationState;
onChangeTimeZone: typeof updateTimeZoneForSession;
}
class UnthemedDashNavTimeControls extends Component<Props> {
componentDidMount() {
@ -87,6 +87,12 @@ class UnthemedDashNavTimeControls extends Component<Props> {
getTimeSrv().setTime(nextRange);
};
onChangeTimeZone = (timeZone: TimeZone) => {
this.props.dashboard.timezone = timeZone;
this.props.onChangeTimeZone(timeZone);
this.onRefresh();
};
onZoom = () => {
appEvents.emit(CoreEvents.zoomOut, 2);
};
@ -109,6 +115,7 @@ class UnthemedDashNavTimeControls extends Component<Props> {
onMoveBackward={this.onMoveBack}
onMoveForward={this.onMoveForward}
onZoom={this.onZoom}
onChangeTimeZone={this.onChangeTimeZone}
/>
<RefreshPicker
onIntervalChanged={this.onChangeRefreshInterval}

View File

@ -1,24 +1,12 @@
import React, { PureComponent } from 'react';
import { Select, Input, Tooltip, LegacyForms } from '@grafana/ui';
import { TimeZonePicker, Input, Tooltip, LegacyForms } from '@grafana/ui';
import { DashboardModel } from '../../state/DashboardModel';
import { getTimeZoneGroups, TimeZone, rangeUtil, SelectableValue } from '@grafana/data';
import { TimeZone, rangeUtil } from '@grafana/data';
import { config } from '@grafana/runtime';
import kbn from 'app/core/utils/kbn';
import isEmpty from 'lodash/isEmpty';
import { selectors } from '@grafana/e2e-selectors';
const grafanaTimeZones = [
{ value: '', label: 'Default' },
{ value: 'browser', label: 'Local browser time' },
{ value: 'utc', label: 'UTC' },
];
const timeZones = getTimeZoneGroups().reduce((tzs, group) => {
const options = group.options.map(tz => ({ value: tz, label: tz }));
tzs.push.apply(tzs, options);
return tzs;
}, grafanaTimeZones);
interface Props {
getDashboard: () => DashboardModel;
onTimeZoneChange: (timeZone: TimeZone) => void;
@ -97,17 +85,16 @@ export class TimePickerSettings extends PureComponent<Props, State> {
this.forceUpdate();
};
onTimeZoneChange = (timeZone: SelectableValue<string>) => {
if (!timeZone || typeof timeZone.value !== 'string') {
onTimeZoneChange = (timeZone: string) => {
if (typeof timeZone !== 'string') {
return;
}
this.props.onTimeZoneChange(timeZone.value);
this.props.onTimeZoneChange(timeZone);
this.forceUpdate();
};
render() {
const dashboard = this.props.getDashboard();
const value = timeZones.find(item => item.value === dashboard.timezone);
return (
<div className="editor-row">
@ -115,7 +102,7 @@ export class TimePickerSettings extends PureComponent<Props, State> {
<div className="gf-form-group">
<div className="gf-form" aria-label={selectors.components.TimeZonePicker.container}>
<label className="gf-form-label width-7">Timezone</label>
<Select isSearchable={true} value={value} onChange={this.onTimeZoneChange} options={timeZones} width={40} />
<TimeZonePicker value={dashboard.timezone} onChange={this.onTimeZoneChange} width={40} />
</div>
<div className="gf-form">

View File

@ -24,6 +24,7 @@ import { PanelEditorUIState, setDiscardChanges } from './state/reducers';
import { getPanelEditorTabs } from './state/selectors';
import { getPanelStateById } from '../../state/selectors';
import { OptionsPaneContent } from './OptionsPaneContent';
import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
import { DashNavButton } from 'app/features/dashboard/components/DashNav/DashNavButton';
import { VariableModel } from 'app/features/variables/types';
import { getVariables } from 'app/features/variables/state/selectors';
@ -54,6 +55,7 @@ interface DispatchProps {
panelEditorCleanUp: typeof panelEditorCleanUp;
setDiscardChanges: typeof setDiscardChanges;
updatePanelEditorUIState: typeof updatePanelEditorUIState;
updateTimeZoneForSession: typeof updateTimeZoneForSession;
}
type Props = OwnProps & ConnectedProps & DispatchProps;
@ -220,7 +222,7 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
}
renderPanelToolbar(styles: EditorStyles) {
const { dashboard, location, uiState, variables } = this.props;
const { dashboard, location, uiState, variables, updateTimeZoneForSession } = this.props;
return (
<div className={styles.panelToolbar}>
<HorizontalGroup justify={variables.length > 0 ? 'space-between' : 'flex-end'} align="flex-start">
@ -228,7 +230,11 @@ export class PanelEditorUnconnected extends PureComponent<Props> {
<HorizontalGroup>
<RadioButtonGroup value={uiState.mode} options={displayModes} onChange={this.onDiplayModeChange} />
<DashNavTimeControls dashboard={dashboard} location={location} updateLocation={updateLocation} />
<DashNavTimeControls
dashboard={dashboard}
location={location}
onChangeTimeZone={updateTimeZoneForSession}
/>
{!uiState.isPanelOptionsVisible && (
<DashNavButton
onClick={this.onTogglePanelOptions}
@ -362,6 +368,7 @@ const mapDispatchToProps: MapDispatchToProps<DispatchProps, OwnProps> = {
panelEditorCleanUp,
setDiscardChanges,
updatePanelEditorUIState,
updateTimeZoneForSession,
};
export const PanelEditor = connect(mapStateToProps, mapDispatchToProps)(PanelEditorUnconnected);

View File

@ -112,6 +112,7 @@ export class ChangeTracker {
dash.time = 0;
dash.refresh = 0;
dash.schemaVersion = 0;
dash.timezone = 0;
// ignore iteration property
delete dash.iteration;

View File

@ -23,6 +23,7 @@ export interface Props {
syncedTimes: boolean;
onChangeTimeSync: () => void;
onChangeTime: (range: RawTimeRange) => void;
onChangeTimeZone: (timeZone: TimeZone) => void;
}
export class ExploreTimeControls extends Component<Props> {
@ -56,7 +57,7 @@ export class ExploreTimeControls extends Component<Props> {
};
render() {
const { range, timeZone, splitted, syncedTimes, onChangeTimeSync, hideText } = this.props;
const { range, timeZone, splitted, syncedTimes, onChangeTimeSync, hideText, onChangeTimeZone } = this.props;
const timeSyncButton = splitted ? <TimeSyncButton onClick={onChangeTimeSync} isSynced={syncedTimes} /> : undefined;
const timePickerCommonProps = {
value: range,
@ -73,6 +74,7 @@ export class ExploreTimeControls extends Component<Props> {
timeSyncButton={timeSyncButton}
isSynced={syncedTimes}
onChange={this.onChangeTimePicker}
onChangeTimeZone={onChangeTimeZone}
/>
);
}

View File

@ -63,6 +63,7 @@ function createToolbar(supportedModes: ExploreMode[]) {
setDashboardQueriesToUpdateOnLoad={(() => {}) as any}
exploreId={ExploreId.left}
onChangeTime={(() => {}) as any}
onChangeTimeZone={(() => {}) as any}
/>
);
}

View File

@ -23,6 +23,7 @@ import {
} from './state/actions';
import { updateLocation } from 'app/core/actions';
import { getTimeZone } from '../profile/state/selectors';
import { updateTimeZoneForSession } from '../profile/state/reducers';
import { getDashboardSrv } from '../dashboard/services/DashboardSrv';
import kbn from '../../core/utils/kbn';
import { ExploreTimeControls } from './ExploreTimeControls';
@ -83,6 +84,7 @@ interface DispatchProps {
changeMode: typeof changeMode;
updateLocation: typeof updateLocation;
setDashboardQueriesToUpdateOnLoad: typeof setDashboardQueriesToUpdateOnLoad;
onChangeTimeZone: typeof updateTimeZoneForSession;
}
type Props = StateProps & DispatchProps & OwnProps;
@ -180,6 +182,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
originPanelId,
datasourceLoading,
containerWidth,
onChangeTimeZone,
} = this.props;
const styles = getStyles();
@ -303,6 +306,7 @@ export class UnConnectedExploreToolbar extends PureComponent<Props> {
syncedTimes={syncedTimes}
onChangeTimeSync={this.onChangeTimeSync}
hideText={showSmallTimePicker}
onChangeTimeZone={onChangeTimeZone}
/>
</div>
)}
@ -410,6 +414,7 @@ const mapDispatchToProps: DispatchProps = {
syncTimes,
changeMode: changeMode,
setDashboardQueriesToUpdateOnLoad,
onChangeTimeZone: updateTimeZoneForSession,
};
export const ExploreToolbar = hot(module)(connect(mapStateToProps, mapDispatchToProps)(UnConnectedExploreToolbar));

View File

@ -1,15 +1,40 @@
import { UserState } from 'app/types';
import _ from 'lodash';
import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import { UserState, ThunkResult } from 'app/types';
import config from 'app/core/config';
import { TimeZone } from '@grafana/data';
import { contextSrv } from 'app/core/core';
export const initialState: UserState = {
orgId: config.bootData.user.orgId,
timeZone: config.bootData.user.timezone,
};
export const userReducer = (state = initialState, action: any): UserState => {
return state;
export const slice = createSlice({
name: 'user/profile',
initialState,
reducers: {
updateTimeZone: (state, action: PayloadAction<TimeZone>): UserState => {
return {
...state,
timeZone: action.payload,
};
},
},
});
export const updateTimeZoneForSession = (timeZone: TimeZone): ThunkResult<void> => {
return async (dispatch, getState) => {
const { updateTimeZone } = slice.actions;
if (!_.isString(timeZone) || _.isEmpty(timeZone)) {
timeZone = config?.bootData?.user?.timezone;
}
_.set(contextSrv, 'user.timezone', timeZone);
dispatch(updateTimeZone(timeZone));
};
};
export default {
user: userReducer,
};
export const userReducer = slice.reducer;
export default { user: slice.reducer };