From 1abbb477cf618ad0a31ba0ff568e45367ab48217 Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Fri, 26 Jun 2020 09:08:15 +0200 Subject: [PATCH] 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. --- .gitignore | 1 + e2e/suite1/specs/dashboard-time-zone.spec.ts | 2 +- .../grafana-data/src/datetime/timezones.ts | 797 +++++++++--------- .../grafana-ui/src/components/Select/types.ts | 2 +- .../TimePickerContent.test.tsx.snap | 344 -------- .../TimePicker/TimePickerContent/colors.ts | 17 - .../TimePicker/TimeRangePicker.story.tsx | 1 + .../TimePicker/TimeRangePicker.test.tsx | 1 + .../components/TimePicker/TimeRangePicker.tsx | 7 +- .../TimePickerCalendar.tsx | 33 +- .../TimePickerContent.test.tsx | 17 +- .../TimePickerContent.tsx | 76 +- .../TimeRangePicker/TimePickerFooter.tsx | 113 +++ .../TimePickerTitle.tsx | 0 .../TimeRangeForm.tsx | 0 .../TimeRangeList.tsx | 0 .../TimeRangeOption.tsx | 0 .../TimePickerContent.test.tsx.snap | 370 ++++++++ .../mapper.ts | 0 .../TimePicker/TimeZonePicker.story.tsx | 3 + .../components/TimePicker/TimeZonePicker.tsx | 169 +++- .../TimeZonePicker/TimeZoneDescription.tsx | 49 ++ .../TimeZonePicker/TimeZoneGroup.tsx | 45 + .../TimeZonePicker/TimeZoneOffset.tsx | 72 ++ .../TimeZonePicker/TimeZoneOption.tsx | 159 ++++ .../TimeZonePicker/TimeZoneTitle.tsx | 28 + packages/grafana-ui/src/components/index.ts | 2 +- public/app/app.ts | 2 +- .../SharedPreferences/SharedPreferences.tsx | 28 +- .../dashboard/components/DashNav/DashNav.tsx | 22 +- .../DashNav/DashNavTimeControls.tsx | 13 +- .../DashboardSettings/TimePickerSettings.tsx | 25 +- .../components/PanelEditor/PanelEditor.tsx | 11 +- .../dashboard/services/ChangeTracker.ts | 1 + .../features/explore/ExploreTimeControls.tsx | 4 +- .../features/explore/ExploreToolbar.test.tsx | 1 + .../app/features/explore/ExploreToolbar.tsx | 5 + public/app/features/profile/state/reducers.ts | 37 +- 38 files changed, 1559 insertions(+), 898 deletions(-) delete mode 100644 packages/grafana-ui/src/components/TimePicker/TimePickerContent/__snapshots__/TimePickerContent.test.tsx.snap delete mode 100644 packages/grafana-ui/src/components/TimePicker/TimePickerContent/colors.ts rename packages/grafana-ui/src/components/TimePicker/{TimePickerContent => TimeRangePicker}/TimePickerCalendar.tsx (92%) rename packages/grafana-ui/src/components/TimePicker/{TimePickerContent => TimeRangePicker}/TimePickerContent.test.tsx (77%) rename packages/grafana-ui/src/components/TimePicker/{TimePickerContent => TimeRangePicker}/TimePickerContent.tsx (78%) create mode 100644 packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerFooter.tsx rename packages/grafana-ui/src/components/TimePicker/{TimePickerContent => TimeRangePicker}/TimePickerTitle.tsx (100%) rename packages/grafana-ui/src/components/TimePicker/{TimePickerContent => TimeRangePicker}/TimeRangeForm.tsx (100%) rename packages/grafana-ui/src/components/TimePicker/{TimePickerContent => TimeRangePicker}/TimeRangeList.tsx (100%) rename packages/grafana-ui/src/components/TimePicker/{TimePickerContent => TimeRangePicker}/TimeRangeOption.tsx (100%) create mode 100644 packages/grafana-ui/src/components/TimePicker/TimeRangePicker/__snapshots__/TimePickerContent.test.tsx.snap rename packages/grafana-ui/src/components/TimePicker/{TimePickerContent => TimeRangePicker}/mapper.ts (100%) create mode 100644 packages/grafana-ui/src/components/TimePicker/TimeZonePicker/TimeZoneDescription.tsx create mode 100644 packages/grafana-ui/src/components/TimePicker/TimeZonePicker/TimeZoneGroup.tsx create mode 100644 packages/grafana-ui/src/components/TimePicker/TimeZonePicker/TimeZoneOffset.tsx create mode 100644 packages/grafana-ui/src/components/TimePicker/TimeZonePicker/TimeZoneOption.tsx create mode 100644 packages/grafana-ui/src/components/TimePicker/TimeZonePicker/TimeZoneTitle.tsx diff --git a/.gitignore b/.gitignore index f2188d44197..ea2f09b7b5f 100644 --- a/.gitignore +++ b/.gitignore @@ -47,6 +47,7 @@ public/css/*.min.css .DS_Store .vscode/ .vs/ +.eslintcache /data/* /bin/* diff --git a/e2e/suite1/specs/dashboard-time-zone.spec.ts b/e2e/suite1/specs/dashboard-time-zone.spec.ts index fc627f6af2e..d7828bab906 100644 --- a/e2e/suite1/specs/dashboard-time-zone.spec.ts +++ b/e2e/suite1/specs/dashboard-time-zone.spec.ts @@ -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; diff --git a/packages/grafana-data/src/datetime/timezones.ts b/packages/grafana-data/src/datetime/timezones.ts index d763fb3a995..46a4c37eb3e 100644 --- a/packages/grafana-data/src/datetime/timezones.ts +++ b/packages/grafana-data/src/datetime/timezones.ts @@ -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, 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 = { + 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 => { + return moment.tz.countries().reduce((all: Record, code) => { + const timeZones = moment.tz.zonesForCountry(code); + return timeZones.reduce((all: Record, timeZone) => { + if (!all[timeZone]) { + all[timeZone] = []; + } + + const name = countryByCode[code]; + + if (!name) { + return all; + } + + all[timeZone].push({ code, name }); + return all; + }, all); + }, {}); +})(); diff --git a/packages/grafana-ui/src/components/Select/types.ts b/packages/grafana-ui/src/components/Select/types.ts index 38bfb4b6cdd..01cd4541e56 100644 --- a/packages/grafana-ui/src/components/Select/types.ts +++ b/packages/grafana-ui/src/components/Select/types.ts @@ -17,7 +17,7 @@ export interface SelectCommonProps { 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) => string; diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/__snapshots__/TimePickerContent.test.tsx.snap b/packages/grafana-ui/src/components/TimePicker/TimePickerContent/__snapshots__/TimePickerContent.test.tsx.snap deleted file mode 100644 index f0b7e2f672c..00000000000 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/__snapshots__/TimePickerContent.test.tsx.snap +++ /dev/null @@ -1,344 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`TimePickerContent renders correctly in full screen 1`] = ` -
-
- -
- - - -
- - -
-`; - -exports[`TimePickerContent renders correctly in narrow screen 1`] = ` -
-
- -
- - - -
- - -
-`; - -exports[`TimePickerContent renders recent absolute ranges correctly 1`] = ` -
-
- -
- - - -
- - -
-`; diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/colors.ts b/packages/grafana-ui/src/components/TimePicker/TimePickerContent/colors.ts deleted file mode 100644 index 7896d9871f1..00000000000 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/colors.ts +++ /dev/null @@ -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 - ), - }; -}; diff --git a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.story.tsx b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.story.tsx index 4fecc03c640..945d4b8c3dd 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.story.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.story.tsx @@ -24,6 +24,7 @@ export const basic = () => { {(value, updateValue) => { return ( {}} timeZone="browser" value={value} onChange={timeRange => { diff --git a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.test.tsx b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.test.tsx index 710dd887dcd..dff479a68b4 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.test.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.test.tsx @@ -17,6 +17,7 @@ describe('TimePicker', () => { it('renders buttons correctly', () => { const wrapper = mount( {}} onChange={value => {}} value={value} onMoveBackward={() => {}} diff --git a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.tsx b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.tsx index e3fc1e64ef0..371b55f5efe 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker.tsx @@ -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 { isSynced, theme, history, + onChangeTimeZone, } = this.props; const { isOpen } = this.state; @@ -168,7 +170,7 @@ export class UnthemedTimeRangePicker extends PureComponent { {isOpen && ( - + { otherOptions={otherOptions} quickOptions={quickOptions} history={history} + onChangeTimeZone={onChangeTimeZone} /> )} diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimePickerCalendar.tsx b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerCalendar.tsx similarity index 92% rename from packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimePickerCalendar.tsx rename to packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerCalendar.tsx index 0ef60953e15..44e224693cd 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimePickerCalendar.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerCalendar.tsx @@ -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; diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimePickerContent.test.tsx b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerContent.test.tsx similarity index 77% rename from packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimePickerContent.test.tsx rename to packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerContent.test.tsx index 936559926ff..40ab824eddc 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimePickerContent.test.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerContent.test.tsx @@ -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( - {}} timeZone="utc" value={value} isFullscreen={true} /> + {}} + 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( - {}} timeZone="utc" value={value} isFullscreen={false} /> + {}} + onChange={value => {}} + timeZone="utc" + value={value} + isFullscreen={false} + /> ); expect(wrapper).toMatchSnapshot(); }); @@ -29,6 +41,7 @@ describe('TimePickerContent', () => { const wrapper = shallow( {}} onChange={value => {}} timeZone="utc" value={value} diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimePickerContent.tsx b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerContent.tsx similarity index 78% rename from packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimePickerContent.tsx rename to packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerContent.tsx index f5bc500518c..8d4fa1479a6 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimePickerContent.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerContent.tsx @@ -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 = pr return (
-
- +
+
+ +
+ + + +
+ +
- - - -
- - + {isFullscreen && }
); }; diff --git a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerFooter.tsx b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerFooter.tsx new file mode 100644 index 00000000000..0dc959c3344 --- /dev/null +++ b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerFooter.tsx @@ -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 => { + 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 ( +
+
+ { + onToggleChangeTz(); + + if (isString(timeZone)) { + onChangeTimeZone(timeZone); + } + }} + autoFocus={true} + onBlur={onToggleChangeTz} + /> +
+
+ ); + } + + return ( +
+
+
+ +
+ +
+ +
+
+ +
+ ); +}; + +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; + `, + }; +}); diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimePickerTitle.tsx b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerTitle.tsx similarity index 100% rename from packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimePickerTitle.tsx rename to packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimePickerTitle.tsx diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimeRangeForm.tsx b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimeRangeForm.tsx similarity index 100% rename from packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimeRangeForm.tsx rename to packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimeRangeForm.tsx diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimeRangeList.tsx b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimeRangeList.tsx similarity index 100% rename from packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimeRangeList.tsx rename to packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimeRangeList.tsx diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimeRangeOption.tsx b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimeRangeOption.tsx similarity index 100% rename from packages/grafana-ui/src/components/TimePicker/TimePickerContent/TimeRangeOption.tsx rename to packages/grafana-ui/src/components/TimePicker/TimeRangePicker/TimeRangeOption.tsx diff --git a/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/__snapshots__/TimePickerContent.test.tsx.snap b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/__snapshots__/TimePickerContent.test.tsx.snap new file mode 100644 index 00000000000..671c9b9a0cd --- /dev/null +++ b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/__snapshots__/TimePickerContent.test.tsx.snap @@ -0,0 +1,370 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TimePickerContent renders correctly in full screen 1`] = ` +
+
+
+ +
+ + + +
+ + +
+ +
+`; + +exports[`TimePickerContent renders correctly in narrow screen 1`] = ` +
+
+
+ +
+ + + +
+ + +
+
+`; + +exports[`TimePickerContent renders recent absolute ranges correctly 1`] = ` +
+
+
+ +
+ + + +
+ + +
+ +
+`; diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerContent/mapper.ts b/packages/grafana-ui/src/components/TimePicker/TimeRangePicker/mapper.ts similarity index 100% rename from packages/grafana-ui/src/components/TimePicker/TimePickerContent/mapper.ts rename to packages/grafana-ui/src/components/TimePicker/TimeRangePicker/mapper.ts diff --git a/packages/grafana-ui/src/components/TimePicker/TimeZonePicker.story.tsx b/packages/grafana-ui/src/components/TimePicker/TimeZonePicker.story.tsx index 8f19391a9ce..9d8735f0098 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimeZonePicker.story.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimeZonePicker.story.tsx @@ -23,6 +23,9 @@ export const basic = () => { { + if (!newValue) { + return; + } action('on selected')(newValue); updateValue({ value: newValue }); }} diff --git a/packages/grafana-ui/src/components/TimePicker/TimeZonePicker.tsx b/packages/grafana-ui/src/components/TimePicker/TimeZonePicker.tsx index 9b63d747669..14b3134b454 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimeZonePicker.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimeZonePicker.tsx @@ -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 = ({ onChange, value, width }) => { - const timeZoneGroups = getTimeZoneGroups(); +export const TimeZonePicker: React.FC = 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) => { + if (!selectable || !isString(selectable.value)) { + return onChange(value); + } + onChange(selectable.value); }, - { value: '' } + [onChange, value] ); return ( - onChange(newValue)} + item.value === timezone)} - onChange={this.onTimeZoneChanged} - options={timeZones} - /> +
diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index c100415aaa4..5d769e27b06 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -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>; @@ -48,7 +53,7 @@ export interface StateProps { location: any; } -type Props = StateProps & OwnProps; +type Props = StateProps & OwnProps & DispatchProps; class DashNav extends PureComponent { playlistSrv: PlaylistSrv; @@ -277,7 +282,7 @@ class DashNav extends PureComponent { } render() { - const { dashboard, location, isFullscreen } = this.props; + const { dashboard, location, isFullscreen, updateTimeZoneForSession } = this.props; return (
@@ -315,7 +320,11 @@ class DashNav extends PureComponent { {!dashboard.timepicker.hidden && (
- +
)}
@@ -327,8 +336,9 @@ const mapStateToProps = (state: StoreState) => ({ location: state.location, }); -const mapDispatchToProps = { +const mapDispatchToProps: MapDispatchToProps = { updateLocation, + updateTimeZoneForSession, }; export default connect(mapStateToProps, mapDispatchToProps)(DashNav); diff --git a/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx b/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx index 6e5f1721afd..6b75bc6d2ef 100644 --- a/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx @@ -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 { componentDidMount() { @@ -87,6 +87,12 @@ class UnthemedDashNavTimeControls extends Component { 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 { onMoveBackward={this.onMoveBack} onMoveForward={this.onMoveForward} onZoom={this.onZoom} + onChangeTimeZone={this.onChangeTimeZone} /> { - 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 { this.forceUpdate(); }; - onTimeZoneChange = (timeZone: SelectableValue) => { - 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 (
@@ -115,7 +102,7 @@ export class TimePickerSettings extends PureComponent {
-