mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
084542a006
commit
1abbb477cf
1
.gitignore
vendored
1
.gitignore
vendored
@ -47,6 +47,7 @@ public/css/*.min.css
|
||||
.DS_Store
|
||||
.vscode/
|
||||
.vs/
|
||||
.eslintcache
|
||||
|
||||
/data/*
|
||||
/bin/*
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
}, {});
|
||||
})();
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
`;
|
@ -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
|
||||
),
|
||||
};
|
||||
};
|
@ -24,6 +24,7 @@ export const basic = () => {
|
||||
{(value, updateValue) => {
|
||||
return (
|
||||
<TimeRangePicker
|
||||
onChangeTimeZone={() => {}}
|
||||
timeZone="browser"
|
||||
value={value}
|
||||
onChange={timeRange => {
|
||||
|
@ -17,6 +17,7 @@ describe('TimePicker', () => {
|
||||
it('renders buttons correctly', () => {
|
||||
const wrapper = mount(
|
||||
<UnthemedTimeRangePicker
|
||||
onChangeTimeZone={() => {}}
|
||||
onChange={value => {}}
|
||||
value={value}
|
||||
onMoveBackward={() => {}}
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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;
|
@ -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}
|
@ -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,27 +153,30 @@ export const TimePickerContentWithScreenSize: React.FC<PropsWithScreenSize> = pr
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<div className={styles.leftSide}>
|
||||
<FullScreenForm {...props} visible={isFullscreen} historyOptions={historyOptions} />
|
||||
<div className={styles.body}>
|
||||
<div className={styles.leftSide}>
|
||||
<FullScreenForm {...props} visible={isFullscreen} historyOptions={historyOptions} />
|
||||
</div>
|
||||
<CustomScrollbar className={styles.rightSide}>
|
||||
<NarrowScreenForm {...props} visible={!isFullscreen} historyOptions={historyOptions} />
|
||||
<TimeRangeList
|
||||
title="Relative time ranges"
|
||||
options={quickOptions}
|
||||
onSelect={props.onChange}
|
||||
value={props.value}
|
||||
timeZone={props.timeZone}
|
||||
/>
|
||||
<div className={styles.spacing} />
|
||||
<TimeRangeList
|
||||
title="Other quick ranges"
|
||||
options={otherOptions}
|
||||
onSelect={props.onChange}
|
||||
value={props.value}
|
||||
timeZone={props.timeZone}
|
||||
/>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
<CustomScrollbar className={styles.rightSide}>
|
||||
<NarrowScreenForm {...props} visible={!isFullscreen} historyOptions={historyOptions} />
|
||||
<TimeRangeList
|
||||
title="Relative time ranges"
|
||||
options={quickOptions}
|
||||
onSelect={props.onChange}
|
||||
value={props.value}
|
||||
timeZone={props.timeZone}
|
||||
/>
|
||||
<div className={styles.spacing} />
|
||||
<TimeRangeList
|
||||
title="Other quick ranges"
|
||||
options={otherOptions}
|
||||
onSelect={props.onChange}
|
||||
value={props.value}
|
||||
timeZone={props.timeZone}
|
||||
/>
|
||||
</CustomScrollbar>
|
||||
{isFullscreen && <TimePickerFooter timeZone={props.timeZone} onChangeTimeZone={props.onChangeTimeZone} />}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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;
|
||||
`,
|
||||
};
|
||||
});
|
@ -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>
|
||||
`;
|
@ -23,6 +23,9 @@ export const basic = () => {
|
||||
<TimeZonePicker
|
||||
value={value.value}
|
||||
onChange={newValue => {
|
||||
if (!newValue) {
|
||||
return;
|
||||
}
|
||||
action('on selected')(newValue);
|
||||
updateValue({ value: newValue });
|
||||
}}
|
||||
|
@ -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]);
|
||||
};
|
||||
|
@ -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;
|
||||
`,
|
||||
};
|
||||
});
|
@ -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};
|
||||
`,
|
||||
};
|
||||
});
|
@ -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;
|
||||
`,
|
||||
};
|
||||
});
|
@ -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;
|
||||
`,
|
||||
};
|
||||
});
|
@ -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;
|
||||
`,
|
||||
};
|
||||
});
|
@ -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';
|
||||
|
@ -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 });
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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}
|
||||
|
@ -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">
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -63,6 +63,7 @@ function createToolbar(supportedModes: ExploreMode[]) {
|
||||
setDashboardQueriesToUpdateOnLoad={(() => {}) as any}
|
||||
exploreId={ExploreId.left}
|
||||
onChangeTime={(() => {}) as any}
|
||||
onChangeTimeZone={(() => {}) as any}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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 };
|
||||
|
Loading…
Reference in New Issue
Block a user