diff --git a/proto/buf.lock b/proto/buf.lock index 7a07371fd4c..2e630a3eeea 100644 --- a/proto/buf.lock +++ b/proto/buf.lock @@ -9,5 +9,5 @@ deps: - remote: buf.build owner: grpc-ecosystem repository: grpc-gateway - commit: 7dec1bb6f79840c5849dac18f642de9c - digest: shake256:aa37e1bbccaf5793f1c82af9e620e67220f390dc5cbab2b8b3091aa4c030f32d9144be4dbfb8a90d02d44cd6b781875959ab1d454742ebd3d586b1a7806a07ea + commit: ca289928665845a7b2f7285d052c739a + digest: shake256:67b115260e12cb2d6c5d5ce8dbbf3a095c86f0e52b84f9dbd16dec9433b218f8694bc9aadb1d45eb6fd52f5a7029977d460e2d58afb3208ab6c680e7b21c80e4 diff --git a/ui/package.json b/ui/package.json index 8439d800a2f..f6f31eee921 100644 --- a/ui/package.json +++ b/ui/package.json @@ -34,7 +34,7 @@ "dependencies": { "@parca/react-benchmark": "^5.3.0", "command-line-args": "^5.2.1", - "date-fns": "2.30.0", + "date-fns": "3.6.0", "not-a-log": "^1.0.1", "react": "18.2.0", "react-dom": "18.2.0", diff --git a/ui/packages/app/web/fork-ts-checker.config.js b/ui/packages/app/web/fork-ts-checker.config.js new file mode 100644 index 00000000000..743f80950ca --- /dev/null +++ b/ui/packages/app/web/fork-ts-checker.config.js @@ -0,0 +1,6 @@ +module.exports = { + logger: { + log: message => console.log(message), + error: message => console.error(message), + }, +}; diff --git a/ui/packages/shared/components/src/DateTimePicker/index.tsx b/ui/packages/shared/components/src/DateTimePicker/index.tsx index 4ee7946cd38..89b8bd9677c 100644 --- a/ui/packages/shared/components/src/DateTimePicker/index.tsx +++ b/ui/packages/shared/components/src/DateTimePicker/index.tsx @@ -23,6 +23,7 @@ import {convertLocalToUTCDate, convertUTCToLocalDate} from '@parca/utilities'; import {AbsoluteDate} from '../DateTimeRangePicker/utils'; import Input from '../Input'; +import {useParcaContext} from '../ParcaContext'; export const DATE_FORMAT = 'yyyy-MM-DD HH:mm:ss'; @@ -42,18 +43,19 @@ interface Props { } export const DateTimePicker = ({selected, onChange}: Props): JSX.Element => { + const {timezone} = useParcaContext(); const [referenceElement, setReferenceElement] = useState(); const [popperElement, setPopperElement] = useState(); const {styles, attributes} = usePopper(referenceElement, popperElement, { placement: 'bottom-end', strategy: 'absolute', }); - const [textInput, setTextInput] = useState(selected.getUIString()); + const [textInput, setTextInput] = useState(selected.getUIString(timezone)); const [isTextInputDirty, setIsTextInputDirty] = useState(false); useEffect(() => { - setTextInput(selected.getUIString()); - }, [selected]); + setTextInput(selected.getUIString(timezone)); + }, [selected, timezone]); return ( @@ -83,10 +85,12 @@ export const DateTimePicker = ({selected, onChange}: Props): JSX.Element => { } const date = new Date(textInput); if (isNaN(date.getTime())) { - setTextInput(selected.getUIString()); + setTextInput(selected.getUIString(timezone)); return; } - onChange(new AbsoluteDate(convertLocalToUTCDate(date))); + onChange( + new AbsoluteDate(timezone !== undefined ? date : convertLocalToUTCDate(date)) + ); }} onChange={e => { setTextInput(e.target.value); @@ -101,12 +105,18 @@ export const DateTimePicker = ({selected, onChange}: Props): JSX.Element => { className="z-10" > { if (date == null) { return; } - onChange(new AbsoluteDate(convertLocalToUTCDate(date))); + onChange( + new AbsoluteDate(timezone !== undefined ? date : convertLocalToUTCDate(date)) + ); setIsTextInputDirty(false); }} showTimeInput diff --git a/ui/packages/shared/components/src/DateTimeRangePicker/utils.ts b/ui/packages/shared/components/src/DateTimeRangePicker/utils.ts index de91cd02bfe..0f047fc1cda 100644 --- a/ui/packages/shared/components/src/DateTimeRangePicker/utils.ts +++ b/ui/packages/shared/components/src/DateTimeRangePicker/utils.ts @@ -74,10 +74,15 @@ export class AbsoluteDate implements BaseDate { return this.value; } - getUIString(): string { + getUIString(timezone?: string): string { if (typeof this.value === 'string') { return this.value; } + + if (timezone !== undefined) { + return getStringForDateInTimezone(this, timezone, DATE_FORMAT); + } + return getUtcStringForDate(this, DATE_FORMAT); } @@ -281,3 +286,11 @@ export const getUtcStringForDate = (date: AbsoluteDate, format = 'lll'): string .utc() .format(format); }; + +export const getStringForDateInTimezone = ( + date: AbsoluteDate, + timezone: string, + format = 'lll' +): string => { + return moment.tz(date.getTime().toISOString(), timezone).format(format); +}; diff --git a/ui/packages/shared/components/src/ParcaContext/index.tsx b/ui/packages/shared/components/src/ParcaContext/index.tsx index 80b9a50bf2f..545ad53c0b7 100644 --- a/ui/packages/shared/components/src/ParcaContext/index.tsx +++ b/ui/packages/shared/components/src/ParcaContext/index.tsx @@ -64,6 +64,7 @@ interface Props { profileViewExternalSubActions?: ReactNode; sourceViewContextMenuItems?: SourceViewContextMenuItem[]; additionalFlamegraphColorProfiles?: Record; + timezone?: string; } export const defaultValue: Props = { diff --git a/ui/packages/shared/profile/src/MetricsGraph/MetricsTooltip/index.tsx b/ui/packages/shared/profile/src/MetricsGraph/MetricsTooltip/index.tsx index 1cab3b860d0..2d100f70849 100644 --- a/ui/packages/shared/profile/src/MetricsGraph/MetricsTooltip/index.tsx +++ b/ui/packages/shared/profile/src/MetricsGraph/MetricsTooltip/index.tsx @@ -18,11 +18,10 @@ import type {VirtualElement} from '@popperjs/core'; import {usePopper} from 'react-popper'; import {Label} from '@parca/client'; -import {TextWithTooltip} from '@parca/components'; -import {formatDate, valueFormatter} from '@parca/utilities'; +import {TextWithTooltip, useParcaContext} from '@parca/components'; +import {formatDate, timePattern, valueFormatter} from '@parca/utilities'; import {HighlightedSeries} from '../'; -import {timeFormat} from '../../'; interface Props { x: number; @@ -69,6 +68,8 @@ const MetricsTooltip = ({ sampleUnit, delta, }: Props): JSX.Element => { + const {timezone} = useParcaContext(); + const [popperElement, setPopperElement] = useState(null); const {styles, attributes, ...popperProps} = usePopper(virtualElement, popperElement, { @@ -154,7 +155,13 @@ const MetricsTooltip = ({ )} At - {formatDate(highlighted.timestamp, timeFormat)} + + {formatDate( + highlighted.timestamp, + timePattern(timezone as string), + timezone + )} + diff --git a/ui/packages/shared/profile/src/MetricsGraph/index.tsx b/ui/packages/shared/profile/src/MetricsGraph/index.tsx index 8713898dc3a..a97d2492fb9 100644 --- a/ui/packages/shared/profile/src/MetricsGraph/index.tsx +++ b/ui/packages/shared/profile/src/MetricsGraph/index.tsx @@ -19,7 +19,7 @@ import throttle from 'lodash.throttle'; import {useContextMenu} from 'react-contexify'; import {Label, MetricsSample, MetricsSeries as MetricsSeriesPb} from '@parca/client'; -import {DateTimeRange} from '@parca/components'; +import {DateTimeRange, useParcaContext} from '@parca/components'; import { formatDate, formatForTimespan, @@ -132,6 +132,7 @@ export const RawMetricsGraph = ({ margin = 0, sampleUnit, }: Props): JSX.Element => { + const {timezone} = useParcaContext(); const graph = useRef(null); const [dragging, setDragging] = useState(false); const [hovering, setHovering] = useState(false); @@ -537,7 +538,7 @@ export const RawMetricsGraph = ({ > - {formatDate(d, formatForTimespan(from, to))} + {formatDate(d, formatForTimespan(from, to), timezone)} diff --git a/ui/packages/shared/profile/src/ProfileSource.tsx b/ui/packages/shared/profile/src/ProfileSource.tsx index 6783bc0ec05..211baff822c 100644 --- a/ui/packages/shared/profile/src/ProfileSource.tsx +++ b/ui/packages/shared/profile/src/ProfileSource.tsx @@ -27,8 +27,7 @@ export interface ProfileSource { QueryRequest: () => QueryRequest; ProfileType: () => ProfileType; DiffSelection: () => ProfileDiffSelection; - Describe: () => JSX.Element; - toString: () => string; + toString: (timezone?: string) => string; } export interface ProfileSelection { @@ -37,8 +36,13 @@ export interface ProfileSelection { ProfileSource: () => ProfileSource; Type: () => string; } +const timeFormat = (timezone?: string): string => { + if (timezone !== undefined) { + return 'yyyy-MM-dd HH:mm:ss'; + } -export const timeFormat = "yyyy-MM-dd HH:mm:ss '(UTC)'"; + return "yyyy-MM-dd HH:mm:ss '(UTC)'"; +}; export function ParamsString(params: {[key: string]: string}): string { return Object.keys(params) @@ -229,25 +233,17 @@ export class MergedProfileSource implements ProfileSource { return ProfileType.fromString(Query.parse(this.query.toString()).profileName()); } - Describe(): JSX.Element { - return ( - - Merge of "{this.query.toString()}" from {formatDate(this.mergeFrom, timeFormat)}{' '} - to {formatDate(this.mergeTo, timeFormat)} - - ); - } - stringMatchers(): string[] { return this.query.matchers .filter((m: Matcher) => m.key !== '__name__') .map((m: Matcher) => `${m.key}=${m.value}`); } - toString(): string { + toString(timezone?: string): string { return `merged profiles of query "${this.query.toString()}" from ${formatDate( this.mergeFrom, - timeFormat - )} to ${formatDate(this.mergeTo, timeFormat)}`; + timeFormat(timezone), + timezone + )} to ${formatDate(this.mergeTo, timeFormat(timezone), timezone)}`; } } diff --git a/ui/packages/shared/profile/src/ProfileView/index.tsx b/ui/packages/shared/profile/src/ProfileView/index.tsx index 5070e39b65d..ff45fd19b10 100644 --- a/ui/packages/shared/profile/src/ProfileView/index.tsx +++ b/ui/packages/shared/profile/src/ProfileView/index.tsx @@ -132,6 +132,7 @@ export const ProfileView = ({ pprofDownloading, compare, }: ProfileViewProps): JSX.Element => { + const {timezone} = useParcaContext(); const {ref, dimensions} = useContainerDimensions(); const [curPath, setCurPath] = useState([]); const [rawDashboardItems = ['icicle'], setDashboardItems] = useURLState({ @@ -325,7 +326,7 @@ export const ProfileView = ({ }; // TODO: this is just a placeholder, we need to replace with an actually informative and accurate title (cc @metalmatze) - const profileSourceString = profileSource?.toString(); + const profileSourceString = profileSource?.toString(timezone); const hasProfileSource = profileSource !== undefined && profileSourceString !== ''; const headerParts = profileSourceString?.split('"') ?? []; diff --git a/ui/packages/shared/utilities/package.json b/ui/packages/shared/utilities/package.json index 8a0027cbf5d..155c4a0f66d 100644 --- a/ui/packages/shared/utilities/package.json +++ b/ui/packages/shared/utilities/package.json @@ -13,7 +13,8 @@ "dependencies": { "@parca/client": "^0.16.107", "@rehooks/local-storage": "^2.4.4", - "tailwindcss": "3.2.4" + "tailwindcss": "3.2.4", + "date-fns-tz": "^3.1.3" }, "keywords": [], "author": "", diff --git a/ui/packages/shared/utilities/src/time.ts b/ui/packages/shared/utilities/src/time.ts index 4cf5d7e0fb2..232f3d1c2a4 100644 --- a/ui/packages/shared/utilities/src/time.ts +++ b/ui/packages/shared/utilities/src/time.ts @@ -11,11 +11,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -import format from 'date-fns/format'; +import * as DateFns from 'date-fns'; +import {toZonedTime} from 'date-fns-tz'; import intervalToDuration from 'date-fns/intervalToDuration'; import {Duration} from '@parca/client'; +export const timePattern = (timezone?: string): string => { + if (timezone !== undefined) { + return 'yyyy-MM-dd HH:mm:ss'; + } + + return "yyyy-MM-dd HH:mm:ss '(UTC)'"; +}; + export interface TimeObject { nanos?: number; micros?: number; @@ -107,13 +116,19 @@ export const formatDuration = (timeObject: TimeObject, to?: number): string => { return values.join(' '); }; -export const formatDate = (date: number | Date, timeFormat: string): string => { +export const formatDate = (date: number | Date, timeFormat: string, timezone?: string): string => { if (typeof date === 'number') { date = new Date(date); } const ISOString = date.toISOString().slice(0, -1); - return format(new Date(ISOString), timeFormat); + + if (timezone !== undefined) { + const zonedDate = toZonedTime(date, timezone); + return DateFns.format(zonedDate, timeFormat); + } + + return DateFns.format(new Date(ISOString), timeFormat); }; export const formatForTimespan = (from: number, to: number): string => { diff --git a/ui/yarn.lock b/ui/yarn.lock index c150be3a6fb..d8df08d2cff 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -1494,13 +1494,20 @@ resolved "https://registry.yarnpkg.com/@babel/regjsgen/-/regjsgen-0.8.0.tgz#f0ba69b075e1f05fb2825b7fad991e7adbb18310" integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.6", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.23.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.1", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.8", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.6", "@babel/runtime@^7.20.7", "@babel/runtime@^7.23.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.23.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885" integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg== dependencies: regenerator-runtime "^0.14.0" +"@babel/runtime@^7.21.0": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" + integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/template@^7.18.6", "@babel/template@^7.22.15", "@babel/template@^7.24.0", "@babel/template@^7.3.3": version "7.24.0" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.24.0.tgz#c6a524aa93a4a05d66aaf31654258fae69d87d50" @@ -10981,12 +10988,22 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" +date-fns-tz@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/date-fns-tz/-/date-fns-tz-3.1.3.tgz#643dfc7157008a3873cd717973e4074bb802f962" + integrity sha512-ZfbMu+nbzW0mEzC8VZrLiSWvUIaI3aRHeq33mTe7Y38UctKukgqPR4nTDwcwS4d64Gf8GghnVsroBuMY3eiTeA== + date-fns@2.29.3: version "2.29.3" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== -date-fns@2.30.0, date-fns@^2.0.1, date-fns@^2.30.0: +date-fns@3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf" + integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== + +date-fns@^2.0.1, date-fns@^2.30.0: version "2.30.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw== @@ -23457,9 +23474,9 @@ regenerator-runtime@^0.11.0: integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== regenerator-runtime@^0.14.0: - version "0.14.0" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" - integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== regenerator-transform@^0.15.2: version "0.15.2"