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 "Tasks",
"Segments"
+ and "Services" 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]