Browse Source

feat(explore): Add hooks to get the explore query options (#76180)

These hooks will fetch the current state of the user specified options.
Tony Xiao 7 months ago
parent
commit
a3db9c1299

+ 42 - 0
static/app/views/explore/hooks/useResultsMode.spec.tsx

@@ -0,0 +1,42 @@
+import {createMemoryHistory, Route, Router, RouterContext} from 'react-router';
+
+import {act, render} from 'sentry-test/reactTestingLibrary';
+
+import {useResultMode} from 'sentry/views/explore/hooks/useResultsMode';
+import {RouteContext} from 'sentry/views/routeContext';
+
+describe('useResultMode', function () {
+  it('allows changing results mode', function () {
+    let resultMode, setResultMode;
+
+    function TestPage() {
+      [resultMode, setResultMode] = useResultMode();
+      return null;
+    }
+
+    const memoryHistory = createMemoryHistory();
+
+    render(
+      <Router
+        history={memoryHistory}
+        render={props => {
+          return (
+            <RouteContext.Provider value={props}>
+              <RouterContext {...props} />
+            </RouteContext.Provider>
+          );
+        }}
+      >
+        <Route path="/" component={TestPage} />
+      </Router>
+    );
+
+    expect(resultMode).toEqual('samples'); // default
+
+    act(() => setResultMode('aggregate'));
+    expect(resultMode).toEqual('aggregate');
+
+    act(() => setResultMode('samples'));
+    expect(resultMode).toEqual('samples');
+  });
+});

+ 49 - 0
static/app/views/explore/hooks/useResultsMode.tsx

@@ -0,0 +1,49 @@
+import {useCallback, useMemo} from 'react';
+import type {Location} from 'history';
+
+import {decodeScalar} from 'sentry/utils/queryString';
+import {useLocation} from 'sentry/utils/useLocation';
+import {useNavigate} from 'sentry/utils/useNavigate';
+
+interface Options {
+  location: Location;
+  navigate: ReturnType<typeof useNavigate>;
+}
+
+export type ResultMode = 'samples' | 'aggregate';
+
+export function useResultMode(): [ResultMode, (newMode: ResultMode) => void] {
+  const location = useLocation();
+  const navigate = useNavigate();
+  const options = {location, navigate};
+
+  return useResultModeImpl(options);
+}
+
+function useResultModeImpl({
+  location,
+  navigate,
+}: Options): [ResultMode, (newMode: ResultMode) => void] {
+  const resultMode: ResultMode = useMemo(() => {
+    const rawMode = decodeScalar(location.query.mode);
+    if (rawMode === 'aggregate') {
+      return 'aggregate' as const;
+    }
+    return 'samples' as const;
+  }, [location.query.mode]);
+
+  const setResultMode = useCallback(
+    (newMode: ResultMode) => {
+      navigate({
+        ...location,
+        query: {
+          ...location.query,
+          mode: newMode,
+        },
+      });
+    },
+    [location, navigate]
+  );
+
+  return [resultMode, setResultMode];
+}

+ 56 - 0
static/app/views/explore/hooks/useSampleFields.spec.tsx

@@ -0,0 +1,56 @@
+import {createMemoryHistory, Route, Router, RouterContext} from 'react-router';
+
+import {act, render} from 'sentry-test/reactTestingLibrary';
+
+import {useSampleFields} from 'sentry/views/explore/hooks/useSampleFields';
+import {RouteContext} from 'sentry/views/routeContext';
+
+describe('useSampleFields', function () {
+  it('allows changing sample fields', function () {
+    let sampleFields, setSampleFields;
+
+    function TestPage() {
+      [sampleFields, setSampleFields] = useSampleFields();
+      return null;
+    }
+
+    const memoryHistory = createMemoryHistory();
+
+    render(
+      <Router
+        history={memoryHistory}
+        render={props => {
+          return (
+            <RouteContext.Provider value={props}>
+              <RouterContext {...props} />
+            </RouteContext.Provider>
+          );
+        }}
+      >
+        <Route path="/" component={TestPage} />
+      </Router>
+    );
+
+    expect(sampleFields).toEqual([
+      'project',
+      'id',
+      'span.op',
+      'span.description',
+      'span.duration',
+      'timestamp',
+    ]); // default
+
+    act(() => setSampleFields(['foo', 'bar']));
+    expect(sampleFields).toEqual(['foo', 'bar']);
+
+    act(() => setSampleFields([]));
+    expect(sampleFields).toEqual([
+      'project',
+      'id',
+      'span.op',
+      'span.description',
+      'span.duration',
+      'timestamp',
+    ]); // default
+  });
+});

+ 51 - 0
static/app/views/explore/hooks/useSampleFields.tsx

@@ -0,0 +1,51 @@
+import {useCallback, useMemo} from 'react';
+import type {Location} from 'history';
+
+import {decodeList} from 'sentry/utils/queryString';
+import {useLocation} from 'sentry/utils/useLocation';
+import {useNavigate} from 'sentry/utils/useNavigate';
+
+interface Options {
+  location: Location;
+  navigate: ReturnType<typeof useNavigate>;
+}
+
+export type Field = string;
+
+export function useSampleFields(): [Field[], (fields: Field[]) => void] {
+  const location = useLocation();
+  const navigate = useNavigate();
+  const options = {location, navigate};
+
+  return useSampleFieldsImpl(options);
+}
+
+function useSampleFieldsImpl({
+  location,
+  navigate,
+}: Options): [Field[], (fields: Field[]) => void] {
+  const sampleFields = useMemo(() => {
+    const fields = decodeList(location.query.field);
+
+    if (fields.length) {
+      return fields;
+    }
+
+    return ['project', 'id', 'span.op', 'span.description', 'span.duration', 'timestamp'];
+  }, [location.query.field]);
+
+  const setSampleFields = useCallback(
+    (fields: Field[]) => {
+      navigate({
+        ...location,
+        query: {
+          ...location.query,
+          field: fields,
+        },
+      });
+    },
+    [location, navigate]
+  );
+
+  return [sampleFields, setSampleFields];
+}

+ 74 - 0
static/app/views/explore/hooks/useSort.spec.tsx

@@ -0,0 +1,74 @@
+import {createMemoryHistory, Route, Router, RouterContext} from 'react-router';
+
+import {act, render} from 'sentry-test/reactTestingLibrary';
+
+import {useSort} from 'sentry/views/explore/hooks/useSort';
+import {RouteContext} from 'sentry/views/routeContext';
+
+describe('useSort', function () {
+  it('allows changing sort', function () {
+    let sort, setSort;
+
+    const fields = ['id', 'timestamp'];
+
+    function TestPage() {
+      [sort, setSort] = useSort({fields});
+      return null;
+    }
+
+    const memoryHistory = createMemoryHistory();
+
+    render(
+      <Router
+        history={memoryHistory}
+        render={props => {
+          return (
+            <RouteContext.Provider value={props}>
+              <RouterContext {...props} />
+            </RouteContext.Provider>
+          );
+        }}
+      >
+        <Route path="/" component={TestPage} />
+      </Router>
+    );
+
+    expect(sort).toEqual({
+      direction: 'desc',
+      field: 'timestamp',
+    }); // default
+
+    act(() =>
+      setSort({
+        direction: 'asc',
+        field: 'timestamp',
+      })
+    );
+    expect(sort).toEqual({
+      direction: 'asc',
+      field: 'timestamp',
+    });
+
+    act(() =>
+      setSort({
+        direction: 'desc',
+        field: 'id',
+      })
+    );
+    expect(sort).toEqual({
+      direction: 'desc',
+      field: 'id',
+    });
+
+    act(() =>
+      setSort({
+        direction: 'asc',
+        field: 'foo',
+      })
+    );
+    expect(sort).toEqual({
+      direction: 'asc',
+      field: 'id',
+    });
+  });
+});

+ 77 - 0
static/app/views/explore/hooks/useSort.tsx

@@ -0,0 +1,77 @@
+import {useCallback, useMemo} from 'react';
+import type {Location} from 'history';
+
+import {decodeScalar} from 'sentry/utils/queryString';
+import {useLocation} from 'sentry/utils/useLocation';
+import {useNavigate} from 'sentry/utils/useNavigate';
+
+import type {Field} from './useSampleFields';
+
+interface Options {
+  fields: Field[];
+}
+
+export type Direction = 'asc' | 'desc';
+export type Sort = {
+  direction: Direction;
+  field: Field;
+};
+
+export function useSort(props): [Sort, (newSort: Sort) => void] {
+  const location = useLocation();
+  const navigate = useNavigate();
+  const options = {location, navigate, ...props};
+
+  return useSortImpl(options);
+}
+
+interface ImplOptions extends Options {
+  location: Location;
+  navigate: ReturnType<typeof useNavigate>;
+}
+
+function useSortImpl({
+  fields,
+  location,
+  navigate,
+}: ImplOptions): [Sort, (newSort: Sort) => void] {
+  const rawSort = decodeScalar(location.query.sort);
+
+  const direction: Direction = !rawSort || rawSort.startsWith('-') ? 'desc' : 'asc';
+
+  const field: Field = useMemo(() => {
+    let f = rawSort;
+    if (direction === 'desc') {
+      f = f?.substring(1);
+    }
+    f = f || 'timestamp';
+    if (fields.length && !fields.includes(f)) {
+      f = fields[0];
+    }
+    return f;
+  }, [rawSort, direction, fields]);
+
+  const sort: Sort = useMemo(() => {
+    return {
+      direction,
+      field,
+    };
+  }, [direction, field]);
+
+  const setSort = useCallback(
+    (newSort: Sort) => {
+      const formatted =
+        newSort.direction === 'desc' ? `-${newSort.field}` : newSort.field;
+      navigate({
+        ...location,
+        query: {
+          ...location.query,
+          sort: formatted,
+        },
+      });
+    },
+    [location, navigate]
+  );
+
+  return [sort, setSort];
+}

+ 39 - 0
static/app/views/explore/hooks/useUserQuery.spec.tsx

@@ -0,0 +1,39 @@
+import {createMemoryHistory, Route, Router, RouterContext} from 'react-router';
+
+import {act, render} from 'sentry-test/reactTestingLibrary';
+
+import {useUserQuery} from 'sentry/views/explore/hooks/useUserQuery';
+import {RouteContext} from 'sentry/views/routeContext';
+
+describe('useUserQuery', function () {
+  it('allows changing user query', function () {
+    let userQuery, setUserQuery;
+
+    function TestPage() {
+      [userQuery, setUserQuery] = useUserQuery();
+      return null;
+    }
+
+    const memoryHistory = createMemoryHistory();
+
+    render(
+      <Router
+        history={memoryHistory}
+        render={props => {
+          return (
+            <RouteContext.Provider value={props}>
+              <RouterContext {...props} />
+            </RouteContext.Provider>
+          );
+        }}
+      >
+        <Route path="/" component={TestPage} />
+      </Router>
+    );
+
+    expect(userQuery).toEqual(''); // default
+
+    act(() => setUserQuery('foo:bar'));
+    expect(userQuery).toEqual('foo:bar');
+  });
+});

+ 41 - 0
static/app/views/explore/hooks/useUserQuery.tsx

@@ -0,0 +1,41 @@
+import {useCallback} from 'react';
+import type {Location} from 'history';
+
+import {decodeScalar} from 'sentry/utils/queryString';
+import {useLocation} from 'sentry/utils/useLocation';
+import {useNavigate} from 'sentry/utils/useNavigate';
+
+interface Options {
+  location: Location;
+  navigate: ReturnType<typeof useNavigate>;
+}
+
+export function useUserQuery(): [string, (newQuery: string) => void] {
+  const location = useLocation();
+  const navigate = useNavigate();
+  const options = {location, navigate};
+
+  return useUserQueryImpl(options);
+}
+
+function useUserQueryImpl({
+  location,
+  navigate,
+}: Options): [string, (newQuery: string) => void] {
+  const userQuery = decodeScalar(location.query.query, '');
+
+  const setUserQuery = useCallback(
+    (newQuery: string) => {
+      navigate({
+        ...location,
+        query: {
+          ...location.query,
+          query: newQuery,
+        },
+      });
+    },
+    [location, navigate]
+  );
+
+  return [userQuery, setUserQuery];
+}