Skip to content

Commit 2a2e793

Browse files
authored
Timezone Support (#3139)
1 parent 4c73f33 commit 2a2e793

File tree

19 files changed

+204
-27
lines changed

19 files changed

+204
-27
lines changed

apps/zui/src/components/format.ts

+15-1
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,22 @@ export const getFormatConfig = createSelector(
2828
)
2929

3030
const getTimeZone = createSelector(getFormatConfig, (config) => config.timeZone)
31+
const getTimeFormat = createSelector(
32+
getFormatConfig,
33+
(config) => config.timeFormat
34+
)
35+
36+
export const useTimeZone = () => {
37+
const zone = useSelector(getTimeZone)
38+
zed.Time.config.zone = zone
39+
return zone
40+
}
3141

32-
export const useTimeZone = () => useSelector(getTimeZone)
42+
export const useTimeFormat = () => {
43+
const format = useSelector(getTimeFormat)
44+
zed.Time.config.format = format
45+
return format
46+
}
3347

3448
export function formatValue(
3549
data: zed.Value,

apps/zui/src/electron/run-main/run-configurations.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@ export function runConfigurations() {
4747
label: "Time Format",
4848
type: "string",
4949
defaultValue: "",
50+
placeholder: "%Y-%m-%dT%H:%M:%S.%L%:z",
5051
helpLink: {
5152
label: "docs",
52-
url: "https://momentjs.com/docs/#/displaying/format/",
53+
url: "https://github.com/samsonjs/strftime?tab=readme-ov-file#supported-specifiers",
5354
},
5455
},
5556
thousandsSeparator: {

apps/zui/src/views/detail-pane/handler.ts

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import LogDetails from "src/js/state/LogDetails"
55
import {useSelector} from "react-redux"
66
import {LogDetailHistory} from "src/js/state/LogDetails/reducer"
77
import {StateObject, useStateObject} from "src/core/state-object"
8+
import {useTimeFormat, useTimeZone} from "src/components/format"
89

910
const initial = {expanded: {}, page: {}}
1011

@@ -14,6 +15,8 @@ export class DetailPaneHandler extends ViewHandler {
1415

1516
constructor(public value: zed.Value) {
1617
super()
18+
useTimeZone()
19+
useTimeFormat()
1720
this.history = useSelector(LogDetails.getHistory)
1821
this.state = useStateObject(initial)
1922
}

apps/zui/src/views/export-modal/index.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export function ExportModal() {
3939

4040
<section className="flow region region-space-xl">
4141
<label>Export To</label>
42-
<div className="cluster">
42+
<div className="cluster gap-s">
4343
<label htmlFor="dest_file">
4444
<input
4545
type="radio"

apps/zui/src/views/results-pane/index.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ import {Inspector} from "./inspector"
66
import {Table} from "./table"
77
import {TableInspector} from "./table-inspector"
88
import styles from "./results-pane.module.css"
9+
import {useTimeFormat, useTimeZone} from "src/components/format"
910

1011
export function ResultsPane() {
12+
useTimeZone()
13+
useTimeFormat()
1114
const ref = useRef()
1215
return (
1316
<div

apps/zui/src/views/results-pane/table.tsx

+4
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {BareStringView} from "./bare-string-view"
1414
import {PathView} from "./path-view"
1515
import {showMenu} from "src/core/menu"
1616
import Selection from "src/js/state/Selection"
17+
import {useTimeFormat, useTimeZone} from "src/components/format"
1718

1819
// 1. Don't forget to save the shape using zed.typeunder
1920

@@ -24,6 +25,8 @@ export function Table() {
2425
const settings = useSelector(Slice.getShapeSettings)
2526
const shape = useSelector(Slice.getShape)
2627
const initialScrollPosition = useScrollPosition(table)
28+
const format = useTimeFormat()
29+
const zone = useTimeZone()
2730

2831
return (
2932
<TableView
@@ -33,6 +36,7 @@ export function Table() {
3336
width={ctx.width}
3437
height={ctx.height}
3538
initialScrollPosition={initialScrollPosition}
39+
deps={[format, zone]}
3640
valuePageState={{
3741
value: settings.valuePage,
3842
onChange: (next) => dispatch(Slice.setValuePage(next)),

apps/zui/src/views/settings-modal/input.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {invoke} from "src/core/invoke"
88
export function Input(props: SettingProps) {
99
const dispatch = useDispatch()
1010
const {field} = props
11-
const {name, defaultValue} = field
11+
const {name, defaultValue, placeholder} = field
1212
const value = useSelector(ConfigPropValues.get(props.sectionName, field.name))
1313
const update = (value) =>
1414
dispatch(
@@ -29,6 +29,7 @@ export function Input(props: SettingProps) {
2929
id={name}
3030
name={name}
3131
onBlur={onChange}
32+
placeholder={placeholder}
3233
defaultValue={value === undefined ? defaultValue : value}
3334
/>
3435
)
@@ -109,7 +110,7 @@ export function Input(props: SettingProps) {
109110
name={name}
110111
onBlur={onChange}
111112
type="text"
112-
placeholder=""
113+
placeholder={placeholder}
113114
defaultValue={value === undefined ? defaultValue : value}
114115
/>
115116
)

apps/zui/src/zui-kit/react/table-view.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export const TableView = forwardRef(function TableView(
3737
shape,
3838
args.columnVisibleState.value,
3939
args.columnExpandedState.value,
40+
...args.deps,
4041
]
4142
)
4243

apps/zui/src/zui-kit/react/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export type ReactAdapterProps = {
77
innerRef?: Ref<HTMLDivElement>
88
outerRef?: Ref<HTMLDivElement>
99
initialScrollPosition?: {top?: number; left?: number}
10+
deps?: any[]
1011
}

packages/zed-js/jest.config.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
1+
const esModules = ['d3-time-format', 'd3-time', 'd3-array', 'internmap'].join(
2+
'|'
3+
);
4+
15
export default {
26
displayName: 'zed-js',
37
preset: '../../jest.preset.js',
48
transform: {
59
'^.+\\.[tj]s$': ['@swc/jest'],
610
},
11+
transformIgnorePatterns: [`/node_modules/(?!${esModules})`],
712
moduleFileExtensions: ['ts', 'js', 'html'],
813
};

packages/zed-js/package.json

+6-2
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
"main": "./dist/cjs/src/index.js",
66
"jsdelivr": "./dist/browser/index.js",
77
"devDependencies": {
8-
"@types/event-source-polyfill": "^1.0.1"
8+
"@types/event-source-polyfill": "^1.0.1",
9+
"@types/strftime": "^0.9.8"
910
},
1011
"dependencies": {
12+
"date-fns": "^3.6.0",
13+
"date-fns-tz": "^3.1.3",
1114
"event-source-polyfill": "^1.0.31",
12-
"events": "^3.3.0"
15+
"events": "^3.3.0",
16+
"strftime": "^0.10.3"
1317
}
1418
}

packages/zed-js/src/query/result-stream.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ export class ResultStream extends EventEmitter {
3434
}
3535

3636
get shapes() {
37-
return this.channel("main").shapes;
37+
return this.channel('main').shapes;
3838
}
3939

4040
get rows() {
41-
return this.channel("main").rows;
41+
return this.channel('main').rows;
4242
}
4343

4444
channel(name: string | undefined = this.currentChannel) {
@@ -54,21 +54,21 @@ export class ResultStream extends EventEmitter {
5454
// eslint-disable-next-line @typescript-eslint/no-explicit-any
5555
async js(opts: JSOptions = {}): Promise<any> {
5656
this.consume();
57-
const channel = this.channel("main");
57+
const channel = this.channel('main');
5858
await this.promise;
5959
return channel.rows.map((r) => r.toJS(opts));
6060
}
6161

6262
async zed() {
6363
this.consume();
64-
const channel = this.channel("main");
64+
const channel = this.channel('main');
6565
await this.promise;
6666
return channel.rows;
6767
}
6868

6969
collect(collector: Collector) {
7070
this.consume();
71-
this.channel("main").collect(collector);
71+
this.channel('main').collect(collector);
7272
return this.promise;
7373
}
7474

@@ -111,19 +111,19 @@ export class ResultStream extends EventEmitter {
111111
// XXX This is here to support backwards compatibility for the channel name
112112
// in the query API. This can be removed after a reasonable period from
113113
// 8/2024.
114-
if ("channel_id" in o) {
115-
return o.channel_id === 0 ? "main" : o.channel_id.toString()
114+
if ('channel_id' in o) {
115+
return o.channel_id === 0 ? 'main' : o.channel_id.toString();
116116
}
117-
return o.channel
117+
return o.channel;
118118
}
119119

120120
private consumeLine(json: zjson.QueryObject) {
121121
switch (json.type) {
122122
case 'QueryChannelSet':
123-
this.currentChannel = this.getChannel(json.value)
123+
this.currentChannel = this.getChannel(json.value);
124124
break;
125125
case 'QueryChannelEnd':
126-
this.currentChannel = this.getChannel(json.value)
126+
this.currentChannel = this.getChannel(json.value);
127127
this.channel().done();
128128
break;
129129
case 'QueryStats':

packages/zed-js/src/values/primitive.ts

+2
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Type } from '../types/types';
22
import { isNull } from '../utils/is-null';
33
import { Value } from './types';
44

5+
6+
57
export abstract class Primitive implements Value {
68
abstract type: Type;
79
// eslint-disable-next-line @typescript-eslint/no-explicit-any

packages/zed-js/src/values/record.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ export class Record implements Value {
5151
return this.fields.map((f) => stream.encodeValue(f.value));
5252
}
5353

54-
at(index: number | number[]) {
55-
return this.fieldAt(index)?.value ?? null;
54+
at<T = Value>(index: number | number[]) {
55+
return (this.fieldAt(index)?.value ?? null) as T;
5656
}
5757

5858
fieldAt(index: number | number[]): null | Field {

packages/zed-js/src/values/time.test.ts

+54-1
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,64 @@ test('toDate()', () => {
1515
});
1616

1717
test('create record with time field', () => {
18-
const t = createData(new Date(0)) as Time;
18+
const t = createData(new Date(0)) as unknown as Time;
1919
expect(t.toDate()).toEqual(new Date(0));
2020
});
2121

2222
test('keeps the milliseconds', () => {
2323
const date = new Time('2020-02-25T16:03:17.838527Z').toDate();
2424
expect(date?.toISOString()).toEqual('2020-02-25T16:03:17.838Z');
2525
});
26+
27+
test('format with strftime with default', () => {
28+
const time = new Time('2000-01-01T00:00:00Z');
29+
expect(time.format()).toBe('2000-01-01T00:00:00.000+00:00');
30+
});
31+
32+
test('format in new york', () => {
33+
const time = new Time('2000-01-01T00:00:00Z');
34+
time.zone = 'America/New_York';
35+
expect(time.format()).toBe('1999-12-31T19:00:00.000-05:00');
36+
});
37+
38+
test('format in los angeles', () => {
39+
const time = new Time('2000-01-01T00:00:00Z');
40+
Time.config.zone = 'America/Los_Angeles';
41+
expect(time.format()).toEqual('1999-12-31T16:00:00.000-08:00');
42+
});
43+
44+
test('format using local specifier and static ime zone', () => {
45+
const time = new Time('2000-01-01T00:00:00Z');
46+
Time.config.zone = 'Asia/Bangkok';
47+
expect(time.format('%a, %B %d %Y at %H:%M, %z')).toEqual(
48+
'Sat, January 01 2000 at 07:00, +0700'
49+
);
50+
});
51+
52+
test('format using static format', () => {
53+
const time = new Time('2000-01-01T00:00:00Z');
54+
Time.config.zone = 'America/New_York';
55+
Time.config.format = '%a, %B %d %Y at %H:%M, %z';
56+
expect(time.format()).toEqual('Fri, December 31 1999 at 19:00, -0500');
57+
});
58+
59+
test('toString when nothing is set', () => {
60+
Time.config.zone = null;
61+
Time.config.format = null;
62+
const time = new Time('2000-01-01T00:00:00Z');
63+
expect(time.toString()).toBe(time.value);
64+
});
65+
66+
test('toString when zone is set', () => {
67+
Time.config.zone = 'America/New_York';
68+
Time.config.format = null;
69+
const time = new Time('1999-12-31T19:00:00.000-05:00');
70+
expect(time.toString()).toBe(time.value);
71+
});
72+
73+
test('toString when format is set', () => {
74+
Time.config.zone = null;
75+
Time.config.format = '%A';
76+
const time = new Time('1999-12-31T19:00:00.000-05:00');
77+
expect(time.toString()).toBe('Saturday');
78+
});

packages/zed-js/src/values/time.ts

+55-6
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,70 @@
1+
import { toZonedTime } from 'date-fns-tz';
12
import { TypeTime } from '../types/type-time';
2-
import { isNull } from '../utils/is-null';
33
import { Primitive } from './primitive';
4+
import { JSOptions } from './types';
5+
import strftime from 'strftime';
46

57
export class Time extends Primitive {
68
type: typeof TypeTime = TypeTime;
7-
_date: Date | null;
9+
_zone?: string;
10+
11+
static config: { zone?: string | null; format?: string | null } = {};
812

913
constructor(value: string) {
1014
super(value);
11-
this._date = isNull(value) ? null : new Date(value);
1215
}
1316

1417
toDate() {
15-
return this._date;
18+
if (!this.value) return null;
19+
return new Date(this.value);
20+
}
21+
22+
toJS(opts: JSOptions = {}) {
23+
if (opts.zonedDates) {
24+
return this.toZonedDate();
25+
} else {
26+
return this.toDate();
27+
}
28+
}
29+
30+
toZonedDate() {
31+
if (!this.value) return null;
32+
return toZonedTime(this.value, this.zone);
33+
}
34+
35+
format(specifier = this.formatSpecifier) {
36+
if (!this.value) return 'null';
37+
return strftime.timezone(this.offset)(specifier, this.toDate()!);
38+
}
39+
40+
override toString() {
41+
if (!this.value) return 'null';
42+
console.log(Time.config);
43+
if (Time.config.format || this.zone != 'UTC') {
44+
return this.format();
45+
} else {
46+
return this.value;
47+
}
48+
}
49+
50+
get offset() {
51+
const timeZone = this.zone;
52+
const date = this.toDate();
53+
if (!date) return 0;
54+
const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' }));
55+
const tzDate = new Date(date.toLocaleString('en-US', { timeZone }));
56+
return (tzDate.getTime() - utcDate.getTime()) / 6e4;
57+
}
58+
59+
get zone() {
60+
return this._zone || Time.config.zone || 'UTC';
61+
}
62+
63+
set zone(value: string) {
64+
this._zone = value;
1665
}
1766

18-
toJS() {
19-
return this.toDate();
67+
get formatSpecifier() {
68+
return Time.config.format || '%Y-%m-%dT%H:%M:%S.%L%:z';
2069
}
2170
}

0 commit comments

Comments
 (0)