This is an automated email from the ASF dual-hosted git repository.

vogievetsky pushed a commit to branch segment_timeline2
in repository https://gitbox.apache.org/repos/asf/druid.git

commit 8ff3f44ec39e13fca3d2cec73a9682f00616987f
Author: Vadim Ogievetsky <[email protected]>
AuthorDate: Wed Oct 30 11:55:22 2024 -0700

    better timezone menu
---
 web-console/jest.common.config.js                  |   2 +
 .../__snapshots__/bar-unit.spec.tsx.snap           |  13 -
 .../__snapshots__/segment-timeline.spec.tsx.snap   | 293 ++++++++++-----------
 web-console/src/utils/general.tsx                  |   8 +
 .../__snapshots__/datasources-view.spec.tsx.snap   |  15 +-
 .../__snapshots__/segments-view.spec.tsx.snap      |  50 ++--
 .../views/workbench-view/run-panel/run-panel.tsx   |  71 +----
 .../timezone-menu-items.spec.tsx}                  |  30 ++-
 .../timezone-menu-items/timezone-menu-items.tsx    | 135 ++++++++++
 9 files changed, 355 insertions(+), 262 deletions(-)

diff --git a/web-console/jest.common.config.js 
b/web-console/jest.common.config.js
index 89e3dab5852..1ea8f55ad18 100644
--- a/web-console/jest.common.config.js
+++ b/web-console/jest.common.config.js
@@ -18,6 +18,8 @@
 
 const { createJsWithTsPreset } = require('ts-jest');
 
+process.env.TZ = 'UTC';
+
 module.exports = {
   testEnvironment: 'jsdom',
   transformIgnorePatterns: ['/node_modules/(?!(d3-.+)/)'],
diff --git 
a/web-console/src/components/segment-timeline/__snapshots__/bar-unit.spec.tsx.snap
 
b/web-console/src/components/segment-timeline/__snapshots__/bar-unit.spec.tsx.snap
deleted file mode 100644
index 7d98145d4b5..00000000000
--- 
a/web-console/src/components/segment-timeline/__snapshots__/bar-unit.spec.tsx.snap
+++ /dev/null
@@ -1,13 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`BarUnit matches snapshot 1`] = `
-<svg>
-  <rect
-    class="bar-unit"
-    height="10"
-    width="10"
-    x="10"
-    y="10"
-  />
-</svg>
-`;
diff --git 
a/web-console/src/components/segment-timeline/__snapshots__/segment-timeline.spec.tsx.snap
 
b/web-console/src/components/segment-timeline/__snapshots__/segment-timeline.spec.tsx.snap
index ae30df246c2..b82bfbc10f2 100644
--- 
a/web-console/src/components/segment-timeline/__snapshots__/segment-timeline.spec.tsx.snap
+++ 
b/web-console/src/components/segment-timeline/__snapshots__/segment-timeline.spec.tsx.snap
@@ -2,190 +2,179 @@
 
 exports[`SegmentTimeline matches snapshot 1`] = `
 <div
-  class="segment-timeline"
+  class="splitter-layout segment-timeline splitter-layout-horizontal"
 >
-  <div>
+  <div
+    class="layout-pane layout-pane-primary"
+  >
     <div
-      class="loader"
-    >
-      <div
-        class="loader-logo"
-      >
-        <svg
-          viewBox="0 0 100 100"
-        >
-          <path
-            class="one"
-            
d="M54.2,69.8h-2.7c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h2.7c11.5,0,23.8-7.4,23.8-23.7
-          
c0-9.1-6.9-15.8-16.4-15.8H38c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h23.6c5.3,0,10.1,1.9,13.6,5.3c3.5,3.4,5.4,8,5.4,13.1
-          c0,6.6-2.3,13-6.3,17.7C69.5,66.8,62.5,69.8,54.2,69.8z"
-          />
-          <path
-            class="two"
-            
d="M55.7,59.5h-26c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h26c7.5,0,11.5-5.8,11.5-11.5
-            
c0-4.2-3.2-7.3-7.7-7.3h-26c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h26c5.9,0,10.3,4.3,10.3,9.9c0,3.7-1.3,7.2-3.7,9.8
-            C63.5,58,59.9,59.5,55.7,59.5z"
-          />
-          <path
-            class="three"
-            
d="M27.2,38h-6.3c-0.7,0-1.3-0.6-1.3-1.3s0.6-1.3,1.3-1.3h6.3c0.7,0,1.3,0.6,1.3,1.3S27.9,38,27.2,38z"
-          />
-          <path
-            class="four"
-            
d="M45.1,69.8h-5.8c-0.7,0-1.3-0.6-1.3-1.3c0-0.7,0.6-1.3,1.3-1.3h5.8c0.7,0,1.3,0.6,1.3,1.3
-            C46.4,69.2,45.8,69.8,45.1,69.8z"
-          />
-        </svg>
-      </div>
-    </div>
+      class="chart-container"
+    />
   </div>
   <div
-    class="side-control"
+    class="layout-splitter"
+    role="separator"
+  />
+  <div
+    class="layout-pane"
+    style="width: 220px;"
   >
     <div
-      class="bp5-form-group"
+      class="side-control"
     >
-      <label
-        class="bp5-label"
-      >
-        Show
-         
-        <span
-          class="bp5-text-muted"
-        />
-      </label>
       <div
-        class="bp5-form-content"
+        class="bp5-form-group"
       >
+        <label
+          class="bp5-label"
+        >
+          Show
+           
+          <span
+            class="bp5-text-muted"
+          />
+        </label>
         <div
-          class="bp5-segmented-control bp5-fill"
-          role="radiogroup"
+          class="bp5-form-content"
         >
-          <button
-            aria-checked="true"
-            class="bp5-button"
-            role="radio"
-            tabindex="0"
-            type="button"
+          <div
+            class="bp5-segmented-control bp5-fill"
+            role="radiogroup"
           >
-            <span
-              class="bp5-button-text"
+            <button
+              aria-checked="true"
+              class="bp5-button"
+              role="radio"
+              tabindex="0"
+              type="button"
             >
-              Total size
-            </span>
-          </button>
-          <button
-            aria-checked="false"
-            class="bp5-button bp5-minimal"
-            role="radio"
-            tabindex="-1"
-            type="button"
-          >
-            <span
-              class="bp5-button-text"
+              <span
+                class="bp5-button-text"
+              >
+                Size
+              </span>
+            </button>
+            <button
+              aria-checked="false"
+              class="bp5-button bp5-minimal"
+              role="radio"
+              tabindex="-1"
+              type="button"
             >
-              Segment count
-            </span>
-          </button>
+              <span
+                class="bp5-button-text"
+              >
+                Rows
+              </span>
+            </button>
+            <button
+              aria-checked="false"
+              class="bp5-button bp5-minimal"
+              role="radio"
+              tabindex="-1"
+              type="button"
+            >
+              <span
+                class="bp5-button-text"
+              >
+                Count
+              </span>
+            </button>
+          </div>
         </div>
       </div>
-    </div>
-    <div
-      class="bp5-form-group"
-    >
-      <label
-        class="bp5-label"
-      >
-        Interval
-         
-        <span
-          class="bp5-text-muted"
-        />
-      </label>
       <div
-        class="bp5-form-content"
+        class="bp5-form-group"
       >
+        <label
+          class="bp5-label"
+        >
+          Interval
+           
+          <span
+            class="bp5-text-muted"
+          />
+        </label>
         <div
-          aria-expanded="false"
-          aria-haspopup="menu"
-          class="bp5-control-group bp5-date-range-input bp5-popover-target 
bp5-fill"
+          class="bp5-form-content"
         >
           <div
-            class="bp5-input-group bp5-fill"
+            aria-expanded="false"
+            aria-haspopup="menu"
+            class="bp5-control-group bp5-date-range-input bp5-popover-target 
bp5-fill"
           >
-            <input
-              aria-disabled="false"
-              autocomplete="off"
-              class="bp5-input"
-              placeholder="Start date"
-              type="text"
-              value="2021-03-09"
-            />
-          </div>
-          <div
-            class="bp5-input-group bp5-fill"
-          >
-            <input
-              aria-disabled="false"
-              autocomplete="off"
-              class="bp5-input"
-              placeholder="End date"
-              type="text"
-              value="2021-06-09"
-            />
+            <div
+              class="bp5-input-group bp5-fill"
+            >
+              <input
+                aria-disabled="false"
+                autocomplete="off"
+                class="bp5-input"
+                placeholder="Start date"
+                type="text"
+                value="2024-07-31"
+              />
+            </div>
+            <div
+              class="bp5-input-group bp5-fill"
+            >
+              <input
+                aria-disabled="false"
+                autocomplete="off"
+                class="bp5-input"
+                placeholder="End date"
+                type="text"
+                value="2024-10-31"
+              />
+            </div>
           </div>
         </div>
       </div>
-    </div>
-    <div
-      class="bp5-form-group"
-    >
-      <label
-        class="bp5-label"
-      >
-        Datasource
-         
-        <span
-          class="bp5-text-muted"
-        />
-      </label>
       <div
-        class="bp5-form-content"
+        class="bp5-form-group"
       >
+        <label
+          class="bp5-label"
+        >
+          Datasource
+           
+          <span
+            class="bp5-text-muted"
+          />
+        </label>
         <div
-          aria-controls="listbox-1"
-          aria-expanded="false"
-          aria-haspopup="listbox"
-          class="bp5-popover-target bp5-fill"
-          role="combobox"
+          class="bp5-form-content"
         >
-          <button
-            class="bp5-button bp5-fill"
-            type="button"
+          <div
+            aria-controls="listbox-0"
+            aria-expanded="false"
+            aria-haspopup="listbox"
+            class="bp5-popover-target bp5-fill"
+            role="combobox"
           >
-            <span
-              class="bp5-button-text"
+            <button
+              class="bp5-button bp5-fill"
+              type="button"
             >
-              Show all
-            </span>
-            <span
-              aria-hidden="true"
-              class="bp5-icon bp5-icon-caret-down"
-            >
-              <svg
-                data-icon="caret-down"
-                height="16"
-                role="img"
-                viewBox="0 0 16 16"
-                width="16"
+              <span
+                aria-hidden="true"
+                class="bp5-icon bp5-icon-caret-down"
               >
-                <path
-                  d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 
4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
-                  fill-rule="evenodd"
-                />
-              </svg>
-            </span>
-          </button>
+                <svg
+                  data-icon="caret-down"
+                  height="16"
+                  role="img"
+                  viewBox="0 0 16 16"
+                  width="16"
+                >
+                  <path
+                    d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 00-.37.83l3.5 
4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
+                    fill-rule="evenodd"
+                  />
+                </svg>
+              </span>
+            </button>
+          </div>
         </div>
       </div>
     </div>
diff --git a/web-console/src/utils/general.tsx 
b/web-console/src/utils/general.tsx
index b742013b2e8..0bc959b6045 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -373,6 +373,14 @@ export function formatDurationHybrid(ms: NumberLike): 
string {
   }
 }
 
+export function timezoneOffsetInMinutesToString(offsetInMinutes: number, 
padHour: boolean): string {
+  const sign = offsetInMinutes < 0 ? '-' : '+';
+  const absOffset = Math.abs(offsetInMinutes);
+  const h = Math.floor(absOffset / 60);
+  const m = absOffset % 60;
+  return `${sign}${padHour ? pad2(h) : h}:${pad2(m)}`;
+}
+
 function pluralize(word: string): string {
   // Ignoring irregular plurals.
   if (/(s|x|z|ch|sh)$/.test(word)) {
diff --git 
a/web-console/src/views/datasources-view/__snapshots__/datasources-view.spec.tsx.snap
 
b/web-console/src/views/datasources-view/__snapshots__/datasources-view.spec.tsx.snap
index 6da41f6eb76..c4a548ad886 100644
--- 
a/web-console/src/views/datasources-view/__snapshots__/datasources-view.spec.tsx.snap
+++ 
b/web-console/src/views/datasources-view/__snapshots__/datasources-view.spec.tsx.snap
@@ -55,7 +55,7 @@ exports[`DatasourcesView matches snapshot 1`] = `
       onChange={[Function]}
     />
     <Blueprint5.Switch
-      checked={false}
+      checked={true}
       disabled={false}
       label="Show segment timeline"
       onChange={[Function]}
@@ -109,6 +109,7 @@ exports[`DatasourcesView matches snapshot 1`] = `
     />
   </Memo(ViewControlBar)>
   <SplitterLayout
+    className="timeline-datasources-splitter"
     percentage={true}
     primaryIndex={1}
     primaryMinSize={20}
@@ -116,6 +117,18 @@ exports[`DatasourcesView matches snapshot 1`] = `
     secondaryMinSize={10}
     vertical={true}
   >
+    <SegmentTimeline
+      capabilities={
+        Capabilities {
+          "coordinator": true,
+          "maxTaskSlots": undefined,
+          "multiStageQueryDart": true,
+          "multiStageQueryTask": true,
+          "overlord": true,
+          "queryType": "nativeAndSql",
+        }
+      }
+    />
     <ReactTable
       AggregatedComponent={[Function]}
       ExpanderComponent={[Function]}
diff --git 
a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap 
b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
index e177003b7ee..7fa71826cb1 100755
--- 
a/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
+++ 
b/web-console/src/views/segments-view/__snapshots__/segments-view.spec.tsx.snap
@@ -54,10 +54,7 @@ exports[`SegmentsView matches snapshot 1`] = `
           "Start",
           "End",
           "Version",
-          {
-            "label": "𝑓(sys.segments)",
-            "text": "Time span",
-          },
+          "Time span",
           "Shard type",
           "Shard spec",
           "Partition",
@@ -77,7 +74,6 @@ exports[`SegmentsView matches snapshot 1`] = `
       onClose={[Function]}
       tableColumnsHidden={
         [
-          "Time span",
           "Is published",
           "Is overshadowed",
         ]
@@ -85,6 +81,7 @@ exports[`SegmentsView matches snapshot 1`] = `
     />
   </Memo(ViewControlBar)>
   <SplitterLayout
+    className="timeline-segments-splitter"
     percentage={true}
     primaryIndex={1}
     primaryMinSize={20}
@@ -184,7 +181,7 @@ exports[`SegmentsView matches snapshot 1`] = `
             "headerClassName": "enable-comparisons",
             "show": true,
             "sortable": true,
-            "width": 160,
+            "width": 180,
           },
           {
             "Cell": [Function],
@@ -195,7 +192,7 @@ exports[`SegmentsView matches snapshot 1`] = `
             "headerClassName": "enable-comparisons",
             "show": true,
             "sortable": true,
-            "width": 160,
+            "width": 180,
           },
           {
             "Cell": [Function],
@@ -205,15 +202,16 @@ exports[`SegmentsView matches snapshot 1`] = `
             "filterable": true,
             "show": true,
             "sortable": true,
-            "width": 160,
+            "width": 180,
           },
           {
-            "Cell": [Function],
             "Header": "Time span",
-            "accessor": "time_span",
-            "filterable": true,
-            "show": false,
-            "sortable": true,
+            "accessor": [Function],
+            "className": "padded",
+            "filterable": false,
+            "id": "time_span",
+            "show": true,
+            "sortable": false,
             "width": 100,
           },
           {
@@ -374,18 +372,11 @@ exports[`SegmentsView matches snapshot 1`] = `
       defaultFilterMethod={[Function]}
       defaultFiltered={[]}
       defaultPage={0}
-      defaultPageSize={50}
+      defaultPageSize={20}
       defaultResized={[]}
       defaultSortDesc={false}
       defaultSortMethod={[Function]}
-      defaultSorted={
-        [
-          {
-            "desc": true,
-            "id": "start",
-          },
-        ]
-      }
+      defaultSorted={[]}
       expanderDefaults={
         {
           "filterable": false,
@@ -421,7 +412,7 @@ exports[`SegmentsView matches snapshot 1`] = `
       getTrProps={[Function]}
       groupedByPivotKey="_groupedByPivot"
       indexKey="_index"
-      loading={false}
+      loading={true}
       loadingText="Loading..."
       manual={true}
       multiSort={true}
@@ -431,8 +422,13 @@ exports[`SegmentsView matches snapshot 1`] = `
       ofText=""
       onFetchData={[Function]}
       onFilteredChange={[Function]}
+      onPageChange={[Function]}
+      onPageSizeChange={[Function]}
+      onSortedChange={[Function]}
       originalKey="_original"
+      page={0}
       pageJumpText="jump to page"
+      pageSize={50}
       pageSizeOptions={
         [
           50,
@@ -457,6 +453,14 @@ exports[`SegmentsView matches snapshot 1`] = `
       showPaginationBottom={true}
       showPaginationTop={false}
       sortable={true}
+      sorted={
+        [
+          {
+            "desc": true,
+            "id": "start",
+          },
+        ]
+      }
       style={{}}
       subRowsKey="_subRows"
     />
diff --git a/web-console/src/views/workbench-view/run-panel/run-panel.tsx 
b/web-console/src/views/workbench-view/run-panel/run-panel.tsx
index d586959357a..2747179d6d4 100644
--- a/web-console/src/views/workbench-view/run-panel/run-panel.tsx
+++ b/web-console/src/views/workbench-view/run-panel/run-panel.tsx
@@ -51,31 +51,10 @@ import { deepGet, pluralIfNeeded, removeUndefinedValues, 
tickIcon } from '../../
 import type { MaxTasksButtonProps } from 
'../max-tasks-button/max-tasks-button';
 import { MaxTasksButton } from '../max-tasks-button/max-tasks-button';
 import { QueryParametersDialog } from 
'../query-parameters-dialog/query-parameters-dialog';
+import { TimezoneMenuItems } from '../timezone-menu-items/timezone-menu-items';
 
 import './run-panel.scss';
 
-const NAMED_TIMEZONES: string[] = [
-  'America/Juneau', // -9.0
-  'America/Los_Angeles', // -8.0
-  'America/Yellowknife', // -7.0
-  'America/Phoenix', // -7.0
-  'America/Denver', // -7.0
-  'America/Mexico_City', // -6.0
-  'America/Chicago', // -6.0
-  'America/New_York', // -5.0
-  'America/Argentina/Buenos_Aires', // -4.0
-  'Etc/UTC', // +0.0
-  'Europe/London', // +0.0
-  'Europe/Paris', // +1.0
-  'Asia/Jerusalem', // +2.0
-  'Asia/Shanghai', // +8.0
-  'Asia/Hong_Kong', // +8.0
-  'Asia/Seoul', // +9.0
-  'Asia/Tokyo', // +9.0
-  'Pacific/Guam', // +10.0
-  'Australia/Sydney', // +11.0
-];
-
 const ARRAY_INGEST_MODE_LABEL: Record<ArrayIngestMode, string> = {
   array: 'Array',
   mvd: 'MVD',
@@ -314,25 +293,6 @@ export const RunPanel = React.memo(function 
RunPanel(props: RunPanelProps) {
     
onQueryChange(query.changeQueryContext(removeUndefinedValues(queryContext)));
   }
 
-  function offsetOptions(): JSX.Element[] {
-    const items: JSX.Element[] = [];
-
-    for (let i = -12; i <= 14; i++) {
-      const offset = `${i < 0 ? '-' : '+'}${String(Math.abs(i)).padStart(2, 
'0')}:00`;
-      items.push(
-        <MenuItem
-          key={offset}
-          icon={tickIcon(offset === sqlTimeZone)}
-          text={offset}
-          shouldDismissPopover={false}
-          onClick={() => changeQueryContext({ ...queryContext, sqlTimeZone: 
offset })}
-        />,
-      );
-    }
-
-    return items;
-  }
-
   const overloadWarning =
     query.unlimited &&
     (queryEngine === 'sql-native' ||
@@ -424,32 +384,13 @@ export const RunPanel = React.memo(function 
RunPanel(props: RunPanelProps) {
                     text="Timezone"
                     label={sqlTimeZone ?? defaultQueryContext.sqlTimeZone}
                   >
-                    <MenuDivider title="Timezone type" />
-                    <MenuItem
-                      icon={tickIcon(!sqlTimeZone)}
-                      text="Default"
-                      label={defaultQueryContext.sqlTimeZone}
-                      shouldDismissPopover={false}
-                      onClick={() =>
-                        changeQueryContext({ ...queryContext, sqlTimeZone: 
undefined })
+                    <TimezoneMenuItems
+                      sqlTimeZone={sqlTimeZone}
+                      setSqlTimeZone={sqlTimeZone =>
+                        changeQueryContext({ ...queryContext, sqlTimeZone })
                       }
+                      defaultSqlTimeZone={defaultQueryContext.sqlTimeZone}
                     />
-                    <MenuItem 
icon={tickIcon(String(sqlTimeZone).includes('/'))} text="Named">
-                      {NAMED_TIMEZONES.map(namedTimezone => (
-                        <MenuItem
-                          key={namedTimezone}
-                          icon={tickIcon(namedTimezone === sqlTimeZone)}
-                          text={namedTimezone}
-                          shouldDismissPopover={false}
-                          onClick={() =>
-                            changeQueryContext({ ...queryContext, sqlTimeZone: 
namedTimezone })
-                          }
-                        />
-                      ))}
-                    </MenuItem>
-                    <MenuItem 
icon={tickIcon(String(sqlTimeZone).includes(':'))} text="Offset">
-                      {offsetOptions()}
-                    </MenuItem>
                     <MenuItem
                       icon={IconNames.BLANK}
                       text="Custom"
diff --git a/web-console/jest.common.config.js 
b/web-console/src/views/workbench-view/timezone-menu-items/timezone-menu-items.spec.tsx
similarity index 58%
copy from web-console/jest.common.config.js
copy to 
web-console/src/views/workbench-view/timezone-menu-items/timezone-menu-items.spec.tsx
index 89e3dab5852..5a1a2b9ce51 100644
--- a/web-console/jest.common.config.js
+++ 
b/web-console/src/views/workbench-view/timezone-menu-items/timezone-menu-items.spec.tsx
@@ -16,12 +16,26 @@
  * limitations under the License.
  */
 
-const { createJsWithTsPreset } = require('ts-jest');
+import { shallow } from '../../../utils/shallow-renderer';
 
-module.exports = {
-  testEnvironment: 'jsdom',
-  transformIgnorePatterns: ['/node_modules/(?!(d3-.+)/)'],
-  ...createJsWithTsPreset({
-    tsconfig: './tsconfig.test.json',
-  }),
-};
+import { TimezoneMenuItems } from './timezone-menu-items';
+
+jest.useFakeTimers('modern').setSystemTime(Date.parse('2024-06-08T12:34:56Z'));
+
+describe('TimezoneMenuItems', () => {
+  it('ensure UTC', () => {
+    expect(new Date().getTimezoneOffset()).toBe(0);
+  });
+
+  it('matches snapshot', () => {
+    const comp = shallow(
+      <TimezoneMenuItems
+        sqlTimeZone="Blah"
+        setSqlTimeZone={() => {}}
+        defaultSqlTimeZone="Etc/UTC"
+      />,
+    );
+
+    expect(comp).toMatchSnapshot();
+  });
+});
diff --git 
a/web-console/src/views/workbench-view/timezone-menu-items/timezone-menu-items.tsx
 
b/web-console/src/views/workbench-view/timezone-menu-items/timezone-menu-items.tsx
new file mode 100644
index 00000000000..402620563ef
--- /dev/null
+++ 
b/web-console/src/views/workbench-view/timezone-menu-items/timezone-menu-items.tsx
@@ -0,0 +1,135 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Icon, MenuDivider, MenuItem } from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
+import { fromDate, getLocalTimeZone } from '@internationalized/date';
+
+import { minute, tickIcon, timezoneOffsetInMinutesToString } from 
'../../../utils';
+
+const n = new Date();
+const NAMED_TIMEZONES: { timezone: string; offsetInMinutes: number }[] = [
+  'America/Juneau', // -9:00
+  'America/Los_Angeles', // -8:00
+  'America/Yellowknife', // -7:00
+  'America/Phoenix', // -7:00
+  'America/Denver', // -7:00
+  'America/Mexico_City', // -6:00
+  'America/Chicago', // -6:00
+  'America/New_York', // -5:00
+  'America/Argentina/Buenos_Aires', // -4:00
+  'Etc/UTC', // +0:00
+  'Europe/London', // +0:00
+  'Europe/Paris', // +1:00
+  'Asia/Jerusalem', // +2:00
+  'Asia/Kolkata', // +5:30
+  'Asia/Shanghai', // +8:00
+  'Asia/Hong_Kong', // +8:00
+  'Asia/Seoul', // +9:00
+  'Asia/Tokyo', // +9:00
+  'Pacific/Guam', // +10:00
+  'Australia/Sydney', // +11:00
+].map(timezone => ({
+  timezone,
+  offsetInMinutes: fromDate(n, timezone).offset / minute.canonicalLength,
+}));
+const LOCAL_TIMEZONE = getLocalTimeZone();
+const LOCAL_OFFSET_IN_MINUTES = -n.getTimezoneOffset();
+
+// Make sure the browsers timezone is in the list
+if (!NAMED_TIMEZONES.find(({ timezone }) => timezone === LOCAL_TIMEZONE)) {
+  NAMED_TIMEZONES.push({ timezone: LOCAL_TIMEZONE, offsetInMinutes: 
LOCAL_OFFSET_IN_MINUTES });
+}
+NAMED_TIMEZONES.sort((a, b) => a.offsetInMinutes - b.offsetInMinutes);
+
+const OFFSETS_IN_MINUTES: number[] = [];
+for (let offsetInMinutes = -12 * 60; offsetInMinutes <= 14 * 60; 
offsetInMinutes += 60) {
+  OFFSETS_IN_MINUTES.push(offsetInMinutes);
+}
+
+// Make sure the browser offset is in the list
+if (!OFFSETS_IN_MINUTES.includes(LOCAL_OFFSET_IN_MINUTES)) {
+  OFFSETS_IN_MINUTES.push(LOCAL_OFFSET_IN_MINUTES);
+  OFFSETS_IN_MINUTES.sort((a, b) => a - b);
+}
+
+export interface TimezoneMenuItemsProps {
+  sqlTimeZone: string | undefined;
+  setSqlTimeZone(sqlTimeZone: string | undefined): void;
+  defaultSqlTimeZone: string | undefined;
+}
+
+export const TimezoneMenuItems = function TimezoneMenuItems(props: 
TimezoneMenuItemsProps) {
+  const { sqlTimeZone, setSqlTimeZone, defaultSqlTimeZone } = props;
+
+  const localOffset = new Date().getTimezoneOffset();
+
+  return [
+    <MenuDivider key="type" title="Timezone type" />,
+    <MenuItem
+      key="default"
+      icon={tickIcon(!sqlTimeZone)}
+      text="Default"
+      label={defaultSqlTimeZone}
+      shouldDismissPopover={false}
+      onClick={() => setSqlTimeZone(undefined)}
+    />,
+    <MenuItem key="named" icon={tickIcon(String(sqlTimeZone).includes('/'))} 
text="Named">
+      {NAMED_TIMEZONES.map(({ timezone, offsetInMinutes }) => (
+        <MenuItem
+          key={timezone}
+          icon={tickIcon(timezone === sqlTimeZone)}
+          text={
+            timezone === LOCAL_TIMEZONE ? (
+              <>
+                {timezone} <Icon icon={IconNames.STAR} data-tooltip="Browser 
timezone" />
+              </>
+            ) : (
+              timezone
+            )
+          }
+          label={`UTC${timezoneOffsetInMinutesToString(offsetInMinutes, 
false)}`}
+          shouldDismissPopover={false}
+          onClick={() => setSqlTimeZone(timezone)}
+        />
+      ))}
+    </MenuItem>,
+    <MenuItem key="offset" icon={tickIcon(String(sqlTimeZone).includes(':'))} 
text="Offset">
+      {OFFSETS_IN_MINUTES.map(offsetInMinutes => {
+        const offset = timezoneOffsetInMinutesToString(offsetInMinutes, true);
+        return (
+          <MenuItem
+            key={offset}
+            icon={tickIcon(offset === sqlTimeZone)}
+            text={
+              localOffset === offsetInMinutes ? (
+                <>
+                  {offset} <Icon icon={IconNames.STAR} data-tooltip="Browser 
offset" />
+                </>
+              ) : (
+                offset
+              )
+            }
+            shouldDismissPopover={false}
+            onClick={() => setSqlTimeZone(offset)}
+          />
+        );
+      })}
+    </MenuItem>,
+  ];
+};


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to