Add new useTimeQuery hook (#75)

## Context

This PR relates to https://github.com/hyperdxio/hyperdx/issues/53. From a chat with @MikeShi42 a while back we were discussing the UX of the time query presets (e.g. "Past 1h") and how it's kind of confusing atm. Once you click a preset, the label will remain as "Past 1h" and add a `from` and `to` param to the url. If after some time you refresh the page, the label will still say "Past 1h" but the `from` and `to` are the old values. At the same time, the existing useTimeQuery hook has a ton of complex logic which makes it very tricky to modify without causing regressions.

## This PR

Create a new version of the useTimeQuery hook (temporarily calling it useNewTimeQuery) and add detailed unit testing to it for all the different cases. This can eventually be used to replace all the existing callsites of the useTimeQuery hook. For the search page, we can move all the live tail functionality into a separate hook and integrate with this new time query hook to have better separation of concerns and make them both easier to maintain. I'll be making a separate PR for the live tail functionality.
This commit is contained in:
Joel Sequeira 2023-10-29 00:44:07 -04:00 committed by GitHub
parent f231d1f65f
commit 4d24bfac0a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 600 additions and 34 deletions

View file

@ -0,0 +1,5 @@
---
'@hyperdx/app': patch
---
Add new version of the useTimeQuery hook along with a testing suite

View file

@ -0,0 +1,3 @@
module.exports = async () => {
process.env.TZ = 'America/New_York';
};

View file

@ -2,7 +2,7 @@ module.exports = {
preset: 'ts-jest/presets/js-with-ts',
testEnvironment: '@deploysentinel/jest-rtl-debugger/environment',
setupFilesAfterEnv: ['<rootDir>/setup-jest.ts'],
globalSetup: '@deploysentinel/jest-rtl-debugger/globalSetup',
globalSetup: '<rootDir>/global-setup.js',
roots: ['<rootDir>/src'],
transform: {
'^.+\\.tsx?$': 'ts-jest',

View file

@ -85,6 +85,7 @@
},
"devDependencies": {
"@deploysentinel/jest-rtl-debugger": "^0.2.3",
"@jedmao/location": "^3.0.0",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^12.1.4",
"@testing-library/user-event": "^14.4.3",

View file

@ -34,7 +34,7 @@ import TabBar from './TabBar';
import HDXHistogramChart from './HDXHistogramChart';
import api from './api';
import { LogTableWithSidePanel } from './LogTableWithSidePanel';
import { parseTimeQuery, useTimeQuery } from './timeQuery';
import { parseTimeQuery, useNewTimeQuery, useTimeQuery } from './timeQuery';
import {
EditSearchChartForm,
EditMarkdownChartForm,
@ -535,7 +535,7 @@ function DashboardFilter({
}
// TODO: This is a hack to set the default time range
const defaultTimeRange = parseTimeQuery('Past 1h', false);
const defaultTimeRange = parseTimeQuery('Past 1h', false) as [Date, Date];
export default function DashboardPage() {
const { data: dashboardsData, isLoading: isDashboardsLoading } =
api.useDashboards();
@ -622,19 +622,17 @@ export default function DashboardPage() {
const [editedChart, setEditedChart] = useState<undefined | Chart>();
const {
searchedTimeRange,
displayedTimeInputValue,
setDisplayedTimeInputValue,
onSearch,
} = useTimeQuery({
isUTC: false,
defaultValue: 'Past 1h',
defaultTimeRange: [
defaultTimeRange?.[0]?.getTime() ?? -1,
defaultTimeRange?.[1]?.getTime() ?? -1,
],
});
const { searchedTimeRange, displayedTimeInputValue, onSearch } =
useNewTimeQuery({
isUTC: false,
initialDisplayValue: 'Past 1h',
initialTimeRange: defaultTimeRange,
});
const [input, setInput] = useState<string>(displayedTimeInputValue);
useEffect(() => {
setInput(displayedTimeInputValue);
}, [displayedTimeInputValue]);
const onAddChart = () => {
setEditedChart({
@ -766,13 +764,13 @@ export default function DashboardPage() {
className="d-flex align-items-center"
onSubmit={e => {
e.preventDefault();
onSearch(displayedTimeInputValue);
onSearch(input);
}}
style={{ height: 33 }}
>
<SearchTimeRangePicker
inputValue={displayedTimeInputValue}
setInputValue={setDisplayedTimeInputValue}
inputValue={input}
setInputValue={setInput}
onSearch={range => {
onSearch(range);
}}

View file

@ -0,0 +1,53 @@
import { Router } from 'next/router';
import { LocationMock } from '@jedmao/location';
import type { UrlObject } from 'url';
type PartialRouter = Partial<Router>;
export const BASE_URL = 'https://www.hyperdx.io';
/**
* A Router to be used for testing which provides the bare minimum needed
* for the useQueryParam(s) hook and NextAdapter to work.
*/
export class TestRouter implements PartialRouter {
isReady = true;
pathname = '/';
private currentUrl = '';
private history: string[] = [];
constructor(private locationMock: LocationMock) {}
replace = (url: string | UrlObject) => {
this.locationMock.assign(`${BASE_URL}${url}`);
this.currentUrl = TestRouter.getURLString(url);
this.locationMock.assign(`${BASE_URL}${this.currentUrl}`);
return Promise.resolve(true);
};
push = (url: string | UrlObject) => {
this.history.push(this.currentUrl);
this.currentUrl = TestRouter.getURLString(url);
this.locationMock.assign(`${BASE_URL}${this.currentUrl}`);
return Promise.resolve(true);
};
setIsReady = (isReady: boolean) => {
this.isReady = isReady;
};
get asPath() {
return this.pathname;
}
static getURLString(url: string | UrlObject): string {
if (typeof url === 'string') {
return url;
}
return `${url.pathname}${url.search}`;
}
getParams(): URLSearchParams {
return new URL(`${BASE_URL}${this.currentUrl}`).searchParams;
}
}

View file

@ -0,0 +1,361 @@
import {
getLiveTailTimeRange,
useNewTimeQuery,
type UseTimeQueryInputType,
type UseTimeQueryReturnType,
} from '../timeQuery';
import { useRouter } from 'next/router';
import { render } from '@testing-library/react';
import * as React from 'react';
import { useImperativeHandle } from 'react';
import { QueryParamProvider } from 'use-query-params';
import { NextAdapter } from 'next-query-params';
import { TestRouter } from './fixtures';
import { LocationMock } from '@jedmao/location';
// Setting a fixed time of 10/03/23 at 12pm EDT
const INITIAL_DATE_STRING =
'Tue Oct 03 2023 12:00:00 GMT-0400 (Eastern Daylight Time)';
jest.mock('next/router', () => ({
useRouter: jest.fn(),
}));
function TestWrapper({ children }: { children: React.ReactNode }) {
return (
<QueryParamProvider adapter={NextAdapter}>{children}</QueryParamProvider>
);
}
const TestComponent = React.forwardRef(function Component(
timeQueryInput: UseTimeQueryInputType,
ref: React.Ref<UseTimeQueryReturnType>,
) {
const timeQueryVal = useNewTimeQuery(timeQueryInput);
useImperativeHandle(ref, () => timeQueryVal);
return null;
});
const { location: savedLocation } = window;
describe('useTimeQuery tests', () => {
let testRouter: TestRouter;
let locationMock: LocationMock;
beforeAll(() => {
// @ts-ignore - This complains because we can only delete optional operands
delete window.location;
});
beforeEach(() => {
jest.resetAllMocks();
locationMock = new LocationMock('https://www.hyperdx.io/');
testRouter = new TestRouter(locationMock);
window.location = locationMock;
(useRouter as jest.Mock).mockReturnValue(testRouter);
jest.useFakeTimers().setSystemTime(new Date(INITIAL_DATE_STRING));
});
afterAll(() => {
window.location = savedLocation;
});
it('initializes successfully to a non-UTC time', async () => {
const timeQueryRef = React.createRef<UseTimeQueryReturnType>();
render(
<TestWrapper>
<TestComponent
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
</TestWrapper>,
);
// The live tail time range is 15 mins
expect(timeQueryRef.current?.displayedTimeInputValue).toMatchInlineSnapshot(
`"Oct 3 11:45:00 - Oct 3 12:00:00"`,
);
});
it('initializes successfully to a UTC time', async () => {
const timeQueryRef = React.createRef<UseTimeQueryReturnType>();
render(
<TestWrapper>
<TestComponent
isUTC={true}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
</TestWrapper>,
);
// The live tail time range is 15 mins
expect(timeQueryRef.current?.displayedTimeInputValue).toMatchInlineSnapshot(
`"Oct 3 15:45:00 - Oct 3 16:00:00"`,
);
});
it('can be overridden by `tq` url param', async () => {
const timeQueryRef = React.createRef<UseTimeQueryReturnType>();
testRouter.replace('/search?tq=Last+4H');
const { rerender } = render(
<TestWrapper>
<TestComponent
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
</TestWrapper>,
);
jest.runAllTimers();
rerender(
<TestWrapper>
<TestComponent
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
</TestWrapper>,
);
jest.runAllTimers();
// Once the hook runs, it will unset the `tq` param and replace it with
// a `from` and `to`
expect(locationMock.searchParams.get('tq')).toBeNull();
// `From` should be 10/03/23 at 8:00am EDT
expect(locationMock.searchParams.get('from')).toBe('1696334400000');
// `To` should be 10/03/23 at 12:00pm EDT
expect(locationMock.searchParams.get('to')).toBe('1696348800000');
expect(timeQueryRef.current?.displayedTimeInputValue).toMatchInlineSnapshot(
`"Oct 3 08:00:00 - Oct 3 12:00:00"`,
);
});
it('browser navigation of from/to qparmas updates the searched time range', async () => {
const timeQueryRef = React.createRef<UseTimeQueryReturnType>();
testRouter.setIsReady(false);
testRouter.replace('/search');
const result = render(
<TestWrapper>
<TestComponent
initialDisplayValue="Past 1h"
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
</TestWrapper>,
);
jest.runAllTimers();
testRouter.setIsReady(true);
result.rerender(
<TestWrapper>
<TestComponent
initialDisplayValue="Past 1h"
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
</TestWrapper>,
);
expect(timeQueryRef.current?.displayedTimeInputValue).toMatchInlineSnapshot(
`"Past 1h"`,
);
expect(timeQueryRef.current?.searchedTimeRange).toMatchInlineSnapshot(`
Array [
2023-10-03T15:45:00.000Z,
2023-10-03T16:00:00.000Z,
]
`);
// 10/03/23 from 04:00am EDT to 08:00am EDT
testRouter.replace('/search?from=1696320000000&to=1696334400000');
result.rerender(
<TestWrapper>
<TestComponent
initialDisplayValue="Past 1h"
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
</TestWrapper>,
);
result.rerender(
<TestWrapper>
<TestComponent
initialDisplayValue="Past 1h"
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
</TestWrapper>,
);
expect(timeQueryRef.current?.displayedTimeInputValue).toMatchInlineSnapshot(
`"Oct 3 04:00:00 - Oct 3 08:00:00"`,
);
expect(timeQueryRef.current?.searchedTimeRange).toMatchInlineSnapshot(`
Array [
2023-10-03T08:00:00.000Z,
2023-10-03T12:00:00.000Z,
]
`);
});
it('overrides initial value with async updated `from` and `to` params', async () => {
const timeQueryRef = React.createRef<UseTimeQueryReturnType>();
// 10/03/23 from 04:00am EDT to 08:00am EDT
testRouter.setIsReady(false);
testRouter.replace('/search');
const result = render(
<TestWrapper>
<TestComponent
initialDisplayValue="Past 1h"
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
</TestWrapper>,
);
jest.runAllTimers();
testRouter.replace('/search?from=1696320000000&to=1696334400000');
testRouter.setIsReady(true);
result.rerender(
<TestWrapper>
<TestComponent
initialDisplayValue="Past 1h"
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
</TestWrapper>,
);
expect(timeQueryRef.current?.displayedTimeInputValue).toMatchInlineSnapshot(
`"Oct 3 04:00:00 - Oct 3 08:00:00"`,
);
expect(timeQueryRef.current?.searchedTimeRange).toMatchInlineSnapshot(`
Array [
2023-10-03T08:00:00.000Z,
2023-10-03T12:00:00.000Z,
]
`);
});
it('accepts `from` and `to` url params', async () => {
const timeQueryRef = React.createRef<UseTimeQueryReturnType>();
// 10/03/23 from 04:00am EDT to 08:00am EDT
testRouter.replace('/search?from=1696320000000&to=1696334400000');
render(
<TestWrapper>
<TestComponent
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
</TestWrapper>,
);
jest.runAllTimers();
expect(timeQueryRef.current?.displayedTimeInputValue).toMatchInlineSnapshot(
`"Oct 3 04:00:00 - Oct 3 08:00:00"`,
);
});
it('handles bad input in `from` and `to` url params', async () => {
const timeQueryRef = React.createRef<UseTimeQueryReturnType>();
testRouter.replace('/search?from=abc&to=def');
render(
<TestWrapper>
<TestComponent
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
</TestWrapper>,
);
jest.runAllTimers();
// Should initialize to the initial time range 11:45am - 12:00pm
expect(timeQueryRef.current?.displayedTimeInputValue).toMatchInlineSnapshot(
`"Oct 3 11:45:00 - Oct 3 12:00:00"`,
);
});
it('prefers `tq` param over `from` and `to` params', async () => {
const timeQueryRef = React.createRef<UseTimeQueryReturnType>();
// 10/03/23 from 04:00am EDT to 08:00am EDT, tq says last 1 hour
testRouter.replace(
'/search?from=1696320000000&to=1696334400000&tq=Past+1h',
);
const result = render(
<TestWrapper>
<TestComponent
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
</TestWrapper>,
);
jest.runAllTimers();
result.rerender(
<TestWrapper>
<TestComponent
isUTC={false}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
</TestWrapper>,
);
jest.runAllTimers();
// The time range should be the last 1 hour even though the `from` and `to`
// params are passed in.
expect(timeQueryRef.current?.displayedTimeInputValue).toMatchInlineSnapshot(
`"Oct 3 11:00:00 - Oct 3 12:00:00"`,
);
});
it('enables custom display value', async () => {
const timeQueryRef = React.createRef<UseTimeQueryReturnType>();
testRouter.replace('/search');
const initialDisplayValue = 'Live Tail';
render(
<TestWrapper>
<TestComponent
isUTC={false}
initialDisplayValue={initialDisplayValue}
initialTimeRange={getLiveTailTimeRange()}
ref={timeQueryRef}
/>
</TestWrapper>,
);
jest.runAllTimers();
expect(timeQueryRef.current?.displayedTimeInputValue).toBe(
initialDisplayValue,
);
});
});

View file

@ -7,8 +7,16 @@ import {
NumberParam,
useQueryParams,
} from 'use-query-params';
import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
import { format, sub, startOfSecond } from 'date-fns';
import {
useState,
useCallback,
useEffect,
useMemo,
useRef,
Dispatch,
SetStateAction,
} from 'react';
import { format, sub, startOfSecond, isValid } from 'date-fns';
import { formatInTimeZone } from 'date-fns-tz';
import { usePrevious } from './utils';
@ -390,3 +398,135 @@ export function useTimeQuery({
setIsLive,
};
}
export type UseTimeQueryInputType = {
/** Whether the displayed value should be in UTC */
isUTC: boolean;
/**
* Optional initial value to be set as the `displayedTimeInputValue`.
* If no value is provided it will return a date string for the initial
* time range.
*/
initialDisplayValue?: string;
/** The initial time range to get values for */
initialTimeRange: [Date, Date];
};
export type UseTimeQueryReturnType = {
isReady: boolean;
displayedTimeInputValue: string;
setDisplayedTimeInputValue: Dispatch<SetStateAction<string>>;
searchedTimeRange: [Date, Date];
onSearch: (timeQuery: string) => void;
onTimeRangeSelect: (start: Date, end: Date) => void;
};
export function useNewTimeQuery({
isUTC,
initialDisplayValue,
initialTimeRange,
}: UseTimeQueryInputType): UseTimeQueryReturnType {
const router = useRouter();
// We need to return true in SSR to prevent mismatch issues
const isReady = typeof window === 'undefined' ? true : router.isReady;
const [displayedTimeInputValue, setDisplayedTimeInputValue] =
useState<string>(() => {
return initialDisplayValue ?? dateRangeToString(initialTimeRange, isUTC);
});
const [{ from, to }, setTimeRangeQuery] = useQueryParams(
{
from: withDefault(NumberParam, undefined),
to: withDefault(NumberParam, undefined),
},
{
updateType: 'pushIn',
enableBatching: true,
},
);
const [searchedTimeRange, setSearchedTimeRange] =
useState<[Date, Date]>(initialTimeRange);
// Allow browser back/fwd button to modify the displayed time input value
const [inputTimeQuery, setInputTimeQuery] = useQueryParam(
'tq',
withDefault(StringParam, undefined),
{
updateType: 'pushIn',
enableBatching: true,
},
);
const onSearch = useCallback(
(timeQuery: string) => {
const [start, end] = parseTimeQuery(timeQuery, isUTC);
// TODO: Add validation UI
if (start != null && end != null) {
setTimeRangeQuery({ from: start.getTime(), to: end.getTime() });
}
},
[isUTC, setTimeRangeQuery],
);
useEffect(() => {
if (from != null && to != null && inputTimeQuery == null && isReady) {
const start = new Date(from);
const end = new Date(to);
if (isValid(start) && isValid(end)) {
setSearchedTimeRange([start, end]);
const dateRangeStr = dateRangeToString([start, end], isUTC);
setDisplayedTimeInputValue(dateRangeStr);
}
} else if (
from == null &&
to == null &&
inputTimeQuery == null &&
isReady
) {
setSearchedTimeRange(initialTimeRange);
const dateRangeStr = dateRangeToString(initialTimeRange, isUTC);
setDisplayedTimeInputValue(initialDisplayValue ?? dateRangeStr);
}
}, [
isReady,
inputTimeQuery,
isUTC,
from,
to,
initialDisplayValue,
initialTimeRange,
]);
useEffect(() => {
// If there is a `tq` param passed in, use it to set the time range and
// then clear the param.
if (inputTimeQuery) {
onSearch(inputTimeQuery);
setInputTimeQuery(undefined);
}
}, [inputTimeQuery, onSearch, setInputTimeQuery]);
return {
isReady,
displayedTimeInputValue,
setDisplayedTimeInputValue: () => {},
searchedTimeRange,
onSearch,
onTimeRangeSelect: useCallback(
(start: Date, end: Date) => {
setTimeRangeQuery({ from: start.getTime(), to: end.getTime() });
setSearchedTimeRange([start, end]);
const dateRangeStr = dateRangeToString([start, end], isUTC);
setDisplayedTimeInputValue(dateRangeStr);
},
[isUTC, setTimeRangeQuery, setDisplayedTimeInputValue],
),
};
}
export function getLiveTailTimeRange(): [Date, Date] {
const end = startOfSecond(new Date());
return [sub(end, { minutes: 15 }), end];
}

View file

@ -1721,6 +1721,11 @@
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==
"@jedmao/location@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@jedmao/location/-/location-3.0.0.tgz#f2b24e937386f95252f3a1fefbf7ca2e0a4b87e9"
integrity sha512-p7mzNlgJbCioUYLUEKds3cQG4CHONVFJNYqMe6ocEtENCL/jYmMo1Q3ApwsMmU+L0ZkaDJEyv4HokaByLoPwlQ==
"@jest/console@^24.9.0":
version "24.9.0"
resolved "https://registry.yarnpkg.com/@jest/console/-/console-24.9.0.tgz#79b1bc06fb74a8cfb01cbdedf945584b1b9707f0"
@ -3883,17 +3888,17 @@
integrity sha512-jtkwqdP2rY2iCCDVAFuaNBH3fiEi29aTn2RhtIoky8DTTiCdc48plpHHreLwmv1PICJ4AJUUESaq3xa8fZH8+g==
"@testing-library/dom@^8.0.0":
version "8.20.0"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.0.tgz#914aa862cef0f5e89b98cc48e3445c4c921010f6"
integrity sha512-d9ULIT+a4EXLX3UU8FBjauG9NnsZHkHztXoIcTsOKoOw030fyjheN9svkTULjJxtYag9DZz5Jz5qkWZDPxTFwA==
version "8.20.1"
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.20.1.tgz#2e52a32e46fc88369eef7eef634ac2a192decd9f"
integrity sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==
dependencies:
"@babel/code-frame" "^7.10.4"
"@babel/runtime" "^7.12.5"
"@types/aria-query" "^5.0.1"
aria-query "^5.0.0"
aria-query "5.1.3"
chalk "^4.1.0"
dom-accessibility-api "^0.5.9"
lz-string "^1.4.4"
lz-string "^1.5.0"
pretty-format "^27.0.2"
"@testing-library/jest-dom@^5.16.5":
@ -4602,9 +4607,9 @@
react-popper "^2.2.5"
"@types/react-dom@<18.0.0":
version "17.0.19"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.19.tgz#36feef3aa35d045cacd5ed60fe0eef5272f19492"
integrity sha512-PiYG40pnQRdPHnlf7tZnp0aQ6q9tspYr72vD61saO6zFCybLfMqwUCN0va1/P+86DXn18ZWeW30Bk7xlC5eEAQ==
version "17.0.21"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-17.0.21.tgz#85d56965483ce4850f5f03f9234e54a1f47786e5"
integrity sha512-3rQEFUNUUz2MYiRwJJj6UekcW7rFLOtmK7ajQP7qJpjNdggInl3I/xM4I3Hq1yYPdCGVMgax1gZsB7BBTtayXg==
dependencies:
"@types/react" "^17"
@ -5141,7 +5146,7 @@ argparse@^2.0.1:
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
aria-query@^5.0.0, aria-query@^5.1.3:
aria-query@5.1.3, aria-query@^5.0.0, aria-query@^5.1.3:
version "5.1.3"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.1.3.tgz#19db27cd101152773631396f7a95a3b58c22c35e"
integrity sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==
@ -10878,10 +10883,10 @@ luxon@^3.2.1:
resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.4.3.tgz#8ddf0358a9492267ffec6a13675fbaab5551315d"
integrity sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg==
lz-string@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"
integrity sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==
lz-string@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941"
integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==
make-dir@^3.0.0:
version "3.1.0"