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

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


The following commit(s) were added to refs/heads/35.0.0 by this push:
     new a43e7773ca6 Display time in local time (#18455)
a43e7773ca6 is described below

commit a43e7773ca69635202fb953daf5bae5ef1ce31ec
Author: Gabriel Chang <[email protected]>
AuthorDate: Fri Oct 10 10:55:57 2025 +0800

    Display time in local time (#18455)
    
    * Display time in local time
    
    * Add config for displaying local time
    
    * Format code
    
    * Fix date format
    
    * Support local time in segments tab
    
    * Update menu icon
    
    * Fix bugs
    
    * Fix formatting
    
    * Update snapshots
    
    * Clean up files
    
    * Update snapshots
    
    * Format lastCompletedTaskTime and blacklistedUntil
---
 web-console/package-lock.json                      | 12 ++++
 web-console/package.json                           |  1 +
 .../__snapshots__/header-bar.spec.tsx.snap         | 10 ++++
 .../src/components/header-bar/header-bar.tsx       | 10 ++++
 .../table-filterable-cell.tsx                      |  6 +-
 .../web-console-config-dialog.tsx                  | 67 ++++++++++++++++++++++
 .../web-console-config/web-console-config.mock.tsx | 23 ++++++++
 .../web-console-config/web-console-config.tsx      | 37 ++++++++++++
 web-console/src/utils/date.ts                      | 14 +++++
 web-console/src/utils/local-storage-keys.tsx       |  2 +
 .../__snapshots__/segments-view.spec.tsx.snap      |  4 +-
 .../src/views/segments-view/segments-view.tsx      | 34 ++++++++---
 .../__snapshots__/services-view.spec.tsx.snap      |  4 +-
 .../src/views/services-view/services-view.tsx      | 47 ++++++++++-----
 .../__snapshots__/tasks-view.spec.tsx.snap         |  3 +-
 web-console/src/views/tasks-view/tasks-view.tsx    | 64 ++++++++++++++-------
 16 files changed, 290 insertions(+), 48 deletions(-)

diff --git a/web-console/package-lock.json b/web-console/package-lock.json
index 8c87c09706f..98cd3d23cb6 100644
--- a/web-console/package-lock.json
+++ b/web-console/package-lock.json
@@ -30,6 +30,7 @@
         "d3-shape": "^3.2.0",
         "d3-time-format": "^4.1.0",
         "date-fns": "^2.28.0",
+        "dayjs": "^1.11.15",
         "druid-query-toolkit": "^1.2.0",
         "echarts": "^5.5.1",
         "file-saver": "^2.0.5",
@@ -6978,6 +6979,12 @@
         "date-fns": "2.x"
       }
     },
+    "node_modules/dayjs": {
+      "version": "1.11.15",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.15.tgz";,
+      "integrity": 
"sha512-MC+DfnSWiM9APs7fpiurHGCoeIx0Gdl6QZBy+5lu8MbYKN5FZEXqOgrundfibdfhGZ15o9hzmZ2xJjZnbvgKXQ==",
+      "license": "MIT"
+    },
     "node_modules/debounce": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz";,
@@ -23482,6 +23489,11 @@
       "resolved": 
"https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-2.0.1.tgz";,
       "integrity": 
"sha512-fJCG3Pwx8HUoLhkepdsP7Z5RsucUi+ZBOxyM5d0ZZ6c4SdYustq0VMmOu6Wf7bli+yS/Jwp91TOCqn9jMcVrUA=="
     },
+    "dayjs": {
+      "version": "1.11.15",
+      "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.15.tgz";,
+      "integrity": 
"sha512-MC+DfnSWiM9APs7fpiurHGCoeIx0Gdl6QZBy+5lu8MbYKN5FZEXqOgrundfibdfhGZ15o9hzmZ2xJjZnbvgKXQ=="
+    },
     "debounce": {
       "version": "1.2.1",
       "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz";,
diff --git a/web-console/package.json b/web-console/package.json
index e7152234610..97008f824ae 100644
--- a/web-console/package.json
+++ b/web-console/package.json
@@ -72,6 +72,7 @@
     "d3-shape": "^3.2.0",
     "d3-time-format": "^4.1.0",
     "date-fns": "^2.28.0",
+    "dayjs": "^1.11.15",
     "druid-query-toolkit": "^1.2.0",
     "echarts": "^5.5.1",
     "file-saver": "^2.0.5",
diff --git 
a/web-console/src/components/header-bar/__snapshots__/header-bar.spec.tsx.snap 
b/web-console/src/components/header-bar/__snapshots__/header-bar.spec.tsx.snap
index e802272e4a2..03b1a6b06a3 100644
--- 
a/web-console/src/components/header-bar/__snapshots__/header-bar.spec.tsx.snap
+++ 
b/web-console/src/components/header-bar/__snapshots__/header-bar.spec.tsx.snap
@@ -274,6 +274,16 @@ exports[`HeaderBar matches snapshot 1`] = `
             shouldDismissPopover={true}
             text="Compaction dynamic config"
           />
+          <Blueprint5.MenuItem
+            active={false}
+            disabled={false}
+            icon="console"
+            multiline={false}
+            onClick={[Function]}
+            popoverProps={{}}
+            shouldDismissPopover={true}
+            text="Web console config"
+          />
           <Blueprint5.MenuDivider />
           <Blueprint5.MenuItem
             active={false}
diff --git a/web-console/src/components/header-bar/header-bar.tsx 
b/web-console/src/components/header-bar/header-bar.tsx
index cfa10d83a5b..d2e2bb23b13 100644
--- a/web-console/src/components/header-bar/header-bar.tsx
+++ b/web-console/src/components/header-bar/header-bar.tsx
@@ -41,6 +41,7 @@ import {
   DoctorDialog,
   OverlordDynamicConfigDialog,
 } from '../../dialogs';
+import { WebConsoleConfigDialog } from 
'../../dialogs/web-console-config-dialog/web-console-config-dialog';
 import type { ConsoleViewId } from '../../druid-models';
 import { getConsoleViewIcon } from '../../druid-models';
 import { Capabilities } from '../../helpers';
@@ -76,6 +77,7 @@ export const HeaderBar = React.memo(function HeaderBar(props: 
HeaderBarProps) {
     useState(false);
   const [overlordDynamicConfigDialogOpen, setOverlordDynamicConfigDialogOpen] 
= useState(false);
   const [compactionDynamicConfigDialogOpen, 
setCompactionDynamicConfigDialogOpen] = useState(false);
+  const [webConsoleConfigDialogOpen, setWebConsoleConfigDialogOpen] = 
useState(false);
 
   const showSplitDataLoaderMenu = capabilities.hasMultiStageQueryTask();
 
@@ -194,6 +196,11 @@ export const HeaderBar = React.memo(function 
HeaderBar(props: HeaderBarProps) {
         onClick={() => setCompactionDynamicConfigDialogOpen(true)}
         disabled={!capabilities.hasCoordinatorAccess()}
       />
+      <MenuItem
+        icon={IconNames.CONSOLE}
+        text="Web console config"
+        onClick={() => setWebConsoleConfigDialogOpen(true)}
+      />
       <MenuDivider />
       <MenuItem
         icon={IconNames.HIGH_PRIORITY}
@@ -401,6 +408,9 @@ export const HeaderBar = React.memo(function 
HeaderBar(props: HeaderBarProps) {
           onClose={() => setCompactionDynamicConfigDialogOpen(false)}
         />
       )}
+      {webConsoleConfigDialogOpen && (
+        <WebConsoleConfigDialog onClose={() => 
setWebConsoleConfigDialogOpen(false)} />
+      )}
     </Navbar>
   );
 });
diff --git 
a/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx 
b/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx
index 80d9cc83d59..1e2f3094ff3 100644
--- a/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx
+++ b/web-console/src/components/table-filterable-cell/table-filterable-cell.tsx
@@ -37,12 +37,14 @@ export interface TableFilterableCellProps {
   onFiltersChange(filters: Filter[]): void;
   enableComparisons?: boolean;
   children?: ReactNode;
+  displayValue?: string;
 }
 
 export const TableFilterableCell = React.memo(function TableFilterableCell(
   props: TableFilterableCellProps,
 ) {
-  const { field, value, children, filters, enableComparisons, onFiltersChange 
} = props;
+  const { field, value, children, filters, enableComparisons, onFiltersChange, 
displayValue } =
+    props;
 
   return (
     <Popover
@@ -56,7 +58,7 @@ export const TableFilterableCell = React.memo(function 
TableFilterableCell(
                 <MenuItem
                   key={i}
                   icon={filterModeToIcon(mode)}
-                  text={value}
+                  text={displayValue ?? value}
                   onClick={() =>
                     onFiltersChange(
                       addOrUpdateFilter(filters, {
diff --git 
a/web-console/src/dialogs/web-console-config-dialog/web-console-config-dialog.tsx
 
b/web-console/src/dialogs/web-console-config-dialog/web-console-config-dialog.tsx
new file mode 100644
index 00000000000..af316e15c61
--- /dev/null
+++ 
b/web-console/src/dialogs/web-console-config-dialog/web-console-config-dialog.tsx
@@ -0,0 +1,67 @@
+/*
+ * 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 { Button, Classes, Dialog, Intent } from '@blueprintjs/core';
+import { IconNames } from '@blueprintjs/icons';
+import React, { useState } from 'react';
+
+import { AutoForm } from '../../components';
+import {
+  WEB_CONSOLE_CONFIG_FIELDS,
+  type WebConsoleConfig,
+} from '../../druid-models/web-console-config/web-console-config';
+import { DEFAULT_WEB_CONSOLE_CONFIG } from 
'../../druid-models/web-console-config/web-console-config.mock';
+import { AppToaster } from '../../singletons';
+import { localStorageGetJson, LocalStorageKeys, localStorageSetJson } from 
'../../utils';
+
+export interface WebConsoleConfigDialogProps {
+  onClose(): void;
+}
+
+export const WebConsoleConfigDialog = React.memo(function 
WebConsoleConfigDialog(
+  props: WebConsoleConfigDialogProps,
+) {
+  const { onClose } = props;
+  const [config, setConfig] = useState<WebConsoleConfig>(
+    localStorageGetJson(LocalStorageKeys.WEB_CONSOLE_CONFIGS) || 
DEFAULT_WEB_CONSOLE_CONFIG,
+  );
+
+  function save() {
+    localStorageSetJson(LocalStorageKeys.WEB_CONSOLE_CONFIGS, config);
+    AppToaster.show({
+      message: 'Saved web console config',
+      intent: Intent.SUCCESS,
+    });
+    onClose();
+    location.reload();
+  }
+
+  return (
+    <Dialog isOpen title="Web console config" onClose={onClose} 
canOutsideClickClose={false}>
+      <div className={Classes.DIALOG_BODY}>
+        <p>Sets the local web console configuration.</p>
+        <AutoForm fields={WEB_CONSOLE_CONFIG_FIELDS} model={config} 
onChange={setConfig} />
+      </div>
+      <div className={Classes.DIALOG_FOOTER}>
+        <div className={Classes.DIALOG_FOOTER_ACTIONS}>
+          <Button text="Save" onClick={save} intent={Intent.PRIMARY} 
rightIcon={IconNames.TICK} />
+        </div>
+      </div>
+    </Dialog>
+  );
+});
diff --git 
a/web-console/src/druid-models/web-console-config/web-console-config.mock.tsx 
b/web-console/src/druid-models/web-console-config/web-console-config.mock.tsx
new file mode 100644
index 00000000000..13e65b0e2ef
--- /dev/null
+++ 
b/web-console/src/druid-models/web-console-config/web-console-config.mock.tsx
@@ -0,0 +1,23 @@
+/*
+ * 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 type { WebConsoleConfig } from './web-console-config';
+
+export const DEFAULT_WEB_CONSOLE_CONFIG: WebConsoleConfig = {
+  showLocalTime: false,
+};
diff --git 
a/web-console/src/druid-models/web-console-config/web-console-config.tsx 
b/web-console/src/druid-models/web-console-config/web-console-config.tsx
new file mode 100644
index 00000000000..1e6ebbf1d9f
--- /dev/null
+++ b/web-console/src/druid-models/web-console-config/web-console-config.tsx
@@ -0,0 +1,37 @@
+/*
+ * 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 type { Field } from '../../components';
+
+export interface WebConsoleConfig {
+  showLocalTime?: boolean;
+}
+
+export const WEB_CONSOLE_CONFIG_FIELDS: Field<WebConsoleConfig>[] = [
+  {
+    name: 'showLocalTime',
+    type: 'boolean',
+    defaultValue: false,
+    info: (
+      <>
+        Boolean flag for whether we show local time in the &quot;Tasks&quot;, 
&quot;Segments&quot;
+        and &quot;Services&quot; views.
+      </>
+    ),
+  },
+];
diff --git a/web-console/src/utils/date.ts b/web-console/src/utils/date.ts
index e240932d92b..5eaadc8c8ac 100644
--- a/web-console/src/utils/date.ts
+++ b/web-console/src/utils/date.ts
@@ -19,8 +19,14 @@
 import type { DateRange, NonNullDateRange } from '@blueprintjs/datetime';
 import { fromDate, toTimeZone } from '@internationalized/date';
 import type { Timezone } from 'chronoshift';
+import dayjs from 'dayjs';
+
+import type { WebConsoleConfig } from 
'../druid-models/web-console-config/web-console-config';
+
+import { localStorageGetJson, LocalStorageKeys } from './local-storage-keys';
 
 const CURRENT_YEAR = new Date().getUTCFullYear();
+export const DATE_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSSZ';
 
 export function isNonNullRange(range: DateRange): range is NonNullDateRange {
   return range[0] != null && range[1] != null;
@@ -94,3 +100,11 @@ export function maxDate(a: Date, b: Date): Date {
 export function minDate(a: Date, b: Date): Date {
   return a < b ? a : b;
 }
+
+export function formatDate(value: string) {
+  const webConsoleConfig: WebConsoleConfig | undefined = localStorageGetJson(
+    LocalStorageKeys.WEB_CONSOLE_CONFIGS,
+  );
+  const showLocalTime = webConsoleConfig?.showLocalTime;
+  return showLocalTime ? dayjs(value).format(DATE_FORMAT) : 
dayjs(value).toISOString();
+}
diff --git a/web-console/src/utils/local-storage-keys.tsx 
b/web-console/src/utils/local-storage-keys.tsx
index 9181e4be0d3..de1118c7ae1 100644
--- a/web-console/src/utils/local-storage-keys.tsx
+++ b/web-console/src/utils/local-storage-keys.tsx
@@ -61,6 +61,8 @@ export const LocalStorageKeys = {
 
   EXPLORE_STATE: 'explore-state' as const,
   EXPLORE_STICKY: 'explore-sticky' as const,
+
+  WEB_CONSOLE_CONFIGS: 'web-console-configs' as const,
 };
 export type LocalStorageKeys = (typeof LocalStorageKeys)[keyof typeof 
LocalStorageKeys];
 
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 0e6b64e844d..b30c6bd6226 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
@@ -178,7 +178,7 @@ exports[`SegmentsView matches snapshot 1`] = `
             "filterable": true,
             "headerClassName": "enable-comparisons",
             "show": true,
-            "width": 180,
+            "width": 220,
           },
           {
             "Cell": [Function],
@@ -188,7 +188,7 @@ exports[`SegmentsView matches snapshot 1`] = `
             "filterable": true,
             "headerClassName": "enable-comparisons",
             "show": true,
-            "width": 180,
+            "width": 220,
           },
           {
             "Cell": [Function],
diff --git a/web-console/src/views/segments-view/segments-view.tsx 
b/web-console/src/views/segments-view/segments-view.tsx
index 61ff034e7bb..8690023a844 100644
--- a/web-console/src/views/segments-view/segments-view.tsx
+++ b/web-console/src/views/segments-view/segments-view.tsx
@@ -18,6 +18,7 @@
 
 import { Button, ButtonGroup, Intent, Label, MenuItem, Switch, Tag } from 
'@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
+import dayjs from 'dayjs';
 import { C, L, SqlComparison, SqlExpression } from 'druid-query-toolkit';
 import * as JSONBig from 'json-bigint-native';
 import type { ReactNode } from 'react';
@@ -50,6 +51,7 @@ import type { Capabilities, CapabilitiesMode } from 
'../../helpers';
 import {
   booleanCustomTableFilter,
   BooleanFilterInput,
+  combineModeAndNeedle,
   parseFilterModeAndNeedle,
   sqlQueryCustomTableFilter,
   STANDARD_TABLE_PAGE_SIZE,
@@ -65,6 +67,7 @@ import {
   filterMap,
   findMap,
   formatBytes,
+  formatDate,
   formatInteger,
   getApiArray,
   hasOverlayOpen,
@@ -158,6 +161,20 @@ function formatRangeDimensionValue(dimension: any, value: 
any): string {
 function segmentFiltersToExpression(filters: Filter[]): SqlExpression {
   return SqlExpression.and(
     ...filterMap(filters, filter => {
+      if (filter.id === 'start' || filter.id === 'end') {
+        // Dates need to be converted to ISO string for the SQL query
+        const modeAndNeedle = parseFilterModeAndNeedle(filter);
+        if (!modeAndNeedle) return;
+        if (modeAndNeedle.mode === '~') {
+          return sqlQueryCustomTableFilter(filter);
+        }
+        const internalFilter = { ...filter };
+        const formattedDate = formatDate(modeAndNeedle.needle);
+        const filterDate = dayjs(formattedDate).toISOString();
+        filter.value = combineModeAndNeedle(modeAndNeedle.mode, formattedDate);
+        internalFilter.value = combineModeAndNeedle(modeAndNeedle.mode, 
filterDate);
+        return sqlQueryCustomTableFilter(internalFilter);
+      }
       if (filter.id === 'shard_type') {
         // Special handling for shard_type that needs to be searched for in 
the shard_spec
         // Creates filters like `shard_spec LIKE '%"type":"numbered"%'`
@@ -570,7 +587,8 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
   private renderFilterableCell(
     field: string,
     enableComparisons = false,
-    valueFn: (value: string) => ReactNode = String,
+    displayFn: (value: string) => ReactNode = String,
+    filterDisplayFn: (value: string) => string = String,
   ) {
     const { filters } = this.props;
     const { handleFilterChange } = this;
@@ -583,8 +601,9 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
           filters={filters}
           onFiltersChange={handleFilterChange}
           enableComparisons={enableComparisons}
+          displayValue={filterDisplayFn(row.value)}
         >
-          {valueFn(row.value)}
+          {displayFn(row.value)}
         </TableFilterableCell>
       );
     };
@@ -695,20 +714,20 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
             show: visibleColumns.shown('Start'),
             accessor: 'start',
             headerClassName: 'enable-comparisons',
-            width: 180,
+            width: 220,
             defaultSortDesc: true,
             filterable: allowGeneralFilter,
-            Cell: this.renderFilterableCell('start', true),
+            Cell: this.renderFilterableCell('start', true, formatDate, 
formatDate),
           },
           {
             Header: 'End',
             show: visibleColumns.shown('End'),
             accessor: 'end',
             headerClassName: 'enable-comparisons',
-            width: 180,
+            width: 220,
             defaultSortDesc: true,
             filterable: allowGeneralFilter,
-            Cell: this.renderFilterableCell('end', true),
+            Cell: this.renderFilterableCell('end', true, formatDate, 
formatDate),
           },
           {
             Header: 'Version',
@@ -724,7 +743,8 @@ export class SegmentsView extends 
React.PureComponent<SegmentsViewProps, Segment
             show: visibleColumns.shown('Time span'),
             id: 'time_span',
             className: 'padded',
-            accessor: ({ start, end }) => computeSegmentTimeSpan(start, end),
+            accessor: ({ start, end }) =>
+              computeSegmentTimeSpan(dayjs(start).toISOString(), 
dayjs(end).toISOString()),
             width: 100,
             sortable: false,
             filterable: false,
diff --git 
a/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap 
b/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap
index b2158116696..f17d35a1438 100644
--- 
a/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap
+++ 
b/web-console/src/views/services-view/__snapshots__/services-view.spec.tsx.snap
@@ -206,8 +206,10 @@ exports[`ServicesView renders data 1`] = `
             "Cell": [Function],
             "Header": "Start time",
             "accessor": "start_time",
+            "filterMethod": [Function],
+            "id": "start_time",
             "show": true,
-            "width": 200,
+            "width": 220,
           },
           {
             "Aggregated": [Function],
diff --git a/web-console/src/views/services-view/services-view.tsx 
b/web-console/src/views/services-view/services-view.tsx
index 1ac4e42c712..6fe1fe0236e 100644
--- a/web-console/src/views/services-view/services-view.tsx
+++ b/web-console/src/views/services-view/services-view.tsx
@@ -41,6 +41,9 @@ import type { QueryWithContext } from '../../druid-models';
 import { getConsoleViewIcon } from '../../druid-models';
 import type { Capabilities, CapabilitiesMode } from '../../helpers';
 import {
+  booleanCustomTableFilter,
+  combineModeAndNeedle,
+  parseFilterModeAndNeedle,
   STANDARD_TABLE_PAGE_SIZE,
   STANDARD_TABLE_PAGE_SIZE_OPTIONS,
   suggestibleFilterInput,
@@ -53,6 +56,7 @@ import {
   filterMap,
   formatBytes,
   formatBytesCompact,
+  formatDate,
   formatDurationWithMsIfNeeded,
   getApiArray,
   hasOverlayOpen,
@@ -61,7 +65,6 @@ import {
   lookupBy,
   oneOf,
   pluralIfNeeded,
-  prettyFormatIsoDateWithMsIfNeeded,
   queryDruidSql,
   QueryManager,
   QueryState,
@@ -201,6 +204,11 @@ function aggregateLoadQueueInfos(loadQueueInfos: 
LoadQueueInfo[]): LoadQueueInfo
   };
 }
 
+function defaultDisplayFn(value: any): string {
+  if (value === undefined || value === null) return '';
+  return String(value);
+}
+
 interface WorkerInfo {
   readonly availabilityGroups: string[];
   readonly blacklistedUntil: string | null;
@@ -385,7 +393,10 @@ ORDER BY
     this.serviceQueryManager.runQuery({ capabilities, visibleColumns });
   };
 
-  private renderFilterableCell(field: string) {
+  private renderFilterableCell(
+    field: string,
+    displayFn: (value: string) => string = defaultDisplayFn,
+  ) {
     const { filters, onFiltersChange } = this.props;
 
     return function FilterableCell(row: { value: any }) {
@@ -395,7 +406,10 @@ ORDER BY
           value={row.value}
           filters={filters}
           onFiltersChange={onFiltersChange}
-        />
+          displayValue={displayFn(row.value)}
+        >
+          {displayFn(row.value)}
+        </TableFilterableCell>
       );
     };
   }
@@ -439,6 +453,7 @@ ORDER BY
       workerInfoLookup: Record<string, WorkerInfo>,
     ): Column<ServiceResultRow>[] => {
       const { capabilities } = this.props;
+
       return [
         {
           Header: 'Service',
@@ -617,9 +632,21 @@ ORDER BY
           Header: 'Start time',
           show: visibleColumns.shown('Start time'),
           accessor: 'start_time',
-          width: 200,
-          Cell: this.renderFilterableCell('start_time'),
+          id: 'start_time',
+          width: 220,
+          Cell: this.renderFilterableCell('start_time', formatDate),
           Aggregated: () => '',
+          filterMethod: (filter: Filter, row: ServiceResultRow) => {
+            const modeAndNeedle = parseFilterModeAndNeedle(filter);
+            if (!modeAndNeedle) return true;
+            const parsedRowTime = formatDate(row.start_time);
+            if (modeAndNeedle.mode === '~') {
+              return booleanCustomTableFilter(filter, parsedRowTime);
+            }
+            const parsedFilterTime = formatDate(modeAndNeedle.needle);
+            filter.value = combineModeAndNeedle(modeAndNeedle.mode, 
parsedFilterTime);
+            return booleanCustomTableFilter(filter, parsedRowTime);
+          },
         },
         {
           Header: 'Version',
@@ -652,17 +679,11 @@ ORDER BY
                 const details: string[] = [];
                 if (workerInfo.lastCompletedTaskTime) {
                   details.push(
-                    `Last completed task: ${prettyFormatIsoDateWithMsIfNeeded(
-                      workerInfo.lastCompletedTaskTime,
-                    )}`,
+                    `Last completed task: 
${formatDate(workerInfo.lastCompletedTaskTime)}`,
                   );
                 }
                 if (workerInfo.blacklistedUntil) {
-                  details.push(
-                    `Blacklisted until: ${prettyFormatIsoDateWithMsIfNeeded(
-                      workerInfo.blacklistedUntil,
-                    )}`,
-                  );
+                  details.push(`Blacklisted until: 
${formatDate(workerInfo.blacklistedUntil)}`);
                 }
                 return details.join(' ') || null;
               }
diff --git 
a/web-console/src/views/tasks-view/__snapshots__/tasks-view.spec.tsx.snap 
b/web-console/src/views/tasks-view/__snapshots__/tasks-view.spec.tsx.snap
index 6fc2a4d3386..87744c0ac83 100644
--- a/web-console/src/views/tasks-view/__snapshots__/tasks-view.spec.tsx.snap
+++ b/web-console/src/views/tasks-view/__snapshots__/tasks-view.spec.tsx.snap
@@ -201,8 +201,9 @@ exports[`TasksView matches snapshot 1`] = `
           "Cell": [Function],
           "Header": "Created time",
           "accessor": "created_time",
+          "filterMethod": [Function],
           "show": true,
-          "width": 190,
+          "width": 220,
         },
         {
           "Aggregated": [Function],
diff --git a/web-console/src/views/tasks-view/tasks-view.tsx 
b/web-console/src/views/tasks-view/tasks-view.tsx
index bc32ba8c4a4..543a746d51e 100644
--- a/web-console/src/views/tasks-view/tasks-view.tsx
+++ b/web-console/src/views/tasks-view/tasks-view.tsx
@@ -18,7 +18,8 @@
 
 import { Button, ButtonGroup, Intent, Label, MenuItem, Tag } from 
'@blueprintjs/core';
 import { IconNames } from '@blueprintjs/icons';
-import { formatDistanceToNow } from 'date-fns';
+import dayjs from 'dayjs';
+import relativeTime from 'dayjs/plugin/relativeTime';
 import React, { type ReactNode } from 'react';
 import type { Filter } from 'react-table';
 import ReactTable from 'react-table';
@@ -44,12 +45,17 @@ import {
 } from '../../druid-models';
 import type { Capabilities } from '../../helpers';
 import {
+  booleanCustomTableFilter,
+  combineModeAndNeedle,
+  parseFilterModeAndNeedle,
   SMALL_TABLE_PAGE_SIZE,
   SMALL_TABLE_PAGE_SIZE_OPTIONS,
   suggestibleFilterInput,
 } from '../../react-table';
 import { Api, AppToaster } from '../../singletons';
 import {
+  DATE_FORMAT,
+  formatDate,
   formatDuration,
   getApiArray,
   getDruidErrorMessage,
@@ -66,6 +72,8 @@ import { ExecutionDetailsDialog } from 
'../workbench-view/execution-details-dial
 
 import './tasks-view.scss';
 
+dayjs.extend(relativeTime);
+
 const taskTableColumns: string[] = [
   'Task ID',
   'Group ID',
@@ -329,7 +337,8 @@ ORDER BY
   private renderTaskFilterableCell(
     field: string,
     enableComparisons = false,
-    valueFn: (value: string) => ReactNode = String,
+    displayFn: (value: string) => ReactNode = String,
+    filterDisplayFn: (value: string) => string = String,
   ) {
     const { filters, onFiltersChange } = this.props;
 
@@ -341,8 +350,9 @@ ORDER BY
           filters={filters}
           onFiltersChange={onFiltersChange}
           enableComparisons={enableComparisons}
+          displayValue={filterDisplayFn(row.value)}
         >
-          {valueFn(row.value)}
+          {displayFn(row.value)}
         </TableFilterableCell>
       );
     };
@@ -494,19 +504,33 @@ ORDER BY
           {
             Header: 'Created time',
             accessor: 'created_time',
-            width: 190,
-            Cell: this.renderTaskFilterableCell('created_time', true, value => 
{
-              const valueAsDate = new Date(value);
-              return isNaN(valueAsDate.valueOf()) ? (
-                String(value)
-              ) : (
-                <span data-tooltip={formatDistanceToNow(valueAsDate, { 
addSuffix: true })}>
-                  {value}
-                </span>
-              );
-            }),
+            width: 220,
+            Cell: this.renderTaskFilterableCell(
+              'created_time',
+              true,
+              value => {
+                const day = dayjs(value);
+                return day.isValid() ? (
+                  <span data-tooltip={day.fromNow()}>{formatDate(value)}</span>
+                ) : (
+                  String(value)
+                );
+              },
+              formatDate,
+            ),
             Aggregated: () => '',
             show: visibleColumns.shown('Created time'),
+            filterMethod: (filter: Filter, row: TaskQueryResultRow) => {
+              const modeAndNeedle = parseFilterModeAndNeedle(filter);
+              if (!modeAndNeedle) return true;
+              const parsedRowDate = formatDate(row.created_time);
+              if (modeAndNeedle.mode === '~') {
+                return booleanCustomTableFilter(filter, parsedRowDate);
+              }
+              const parsedFilterDate = formatDate(modeAndNeedle.needle);
+              filter.value = combineModeAndNeedle(modeAndNeedle.mode, 
parsedFilterDate);
+              return booleanCustomTableFilter(filter, parsedRowDate);
+            },
           },
           {
             Header: 'Duration',
@@ -519,16 +543,12 @@ ORDER BY
               if (value > 0) {
                 const shownDuration = formatDuration(value);
 
-                const start = new Date(original.created_time);
-                if (isNaN(start.valueOf())) return shownDuration;
+                const start = dayjs(original.created_time);
+                if (!start.isValid()) return shownDuration;
 
-                const end = new Date(start.valueOf() + value);
+                const end = start.add(value, 'ms');
                 return (
-                  <span
-                    data-tooltip={`End time: 
${end.toISOString()}\n(${formatDistanceToNow(end, {
-                      addSuffix: true,
-                    })})`}
-                  >
+                  <span data-tooltip={`End time: 
${end.format(DATE_FORMAT)}\n(${end.fromNow()})`}>
                     {shownDuration}
                   </span>
                 );


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

Reply via email to