Browse Source

feat(feedback): Mock Feedback data fetching for list and details pages (#55831)

The important things in here are the types, which follow the blueprint:
https://github.com/getsentry/sentry/pull/55566/files

The page with the data looks like this (behind our ui feature flag):

![SCR-20230907-kclf](https://github.com/getsentry/sentry/assets/187460/79e3b0bd-3ab6-47fd-86cc-a9341ca904a3)


Relates to https://github.com/getsentry/sentry/issues/55809
Ryan Albrecht 1 year ago
parent
commit
302b80c9b1

+ 10 - 0
static/app/components/feedback/hydrateFeedbackRecord.tsx

@@ -0,0 +1,10 @@
+import {FeedbackItemResponse, HydratedFeedbackItem} from 'sentry/utils/feedback/types';
+
+export default function hydrateFeedbackRecord(
+  data: FeedbackItemResponse
+): HydratedFeedbackItem {
+  return {
+    ...data,
+    timestamp: new Date(data.timestamp),
+  };
+}

+ 40 - 0
static/app/components/feedback/useFetchFeedbackItem.tsx

@@ -0,0 +1,40 @@
+import {useEffect, useState} from 'react';
+
+import hydrateFeedbackRecord from 'sentry/components/feedback/hydrateFeedbackRecord';
+import {exampleItemResponse} from 'sentry/utils/feedback/example';
+import {FeedbackItemResponse, HydratedFeedbackItem} from 'sentry/utils/feedback/types';
+import type {UseApiQueryOptions} from 'sentry/utils/queryClient';
+
+type MockState = {
+  data: undefined | FeedbackItemResponse;
+  isError: false;
+  isLoading: boolean;
+};
+
+export default function useFetchFeedbackItem(
+  _params: {},
+  _options: Partial<UseApiQueryOptions<HydratedFeedbackItem>> = {}
+) {
+  // Mock some state to simulate `useApiQuery` while the backend is being constructed
+  const [state, setState] = useState<MockState>({
+    isLoading: true,
+    isError: false,
+    data: undefined,
+  });
+
+  useEffect(() => {
+    const timeout = setTimeout(() => {
+      setState({
+        isLoading: false,
+        isError: false,
+        data: exampleItemResponse,
+      });
+    }, Math.random() * 1000);
+    return () => clearTimeout(timeout);
+  }, []);
+
+  return {
+    ...state,
+    data: state.data ? hydrateFeedbackRecord(state.data) : undefined,
+  };
+}

+ 44 - 0
static/app/components/feedback/useFetchFeedbackList.tsx

@@ -0,0 +1,44 @@
+import {useEffect, useState} from 'react';
+
+import hydrateFeedbackRecord from 'sentry/components/feedback/hydrateFeedbackRecord';
+import {exampleListResponse} from 'sentry/utils/feedback/example';
+import {
+  FeedbackListQueryParams,
+  FeedbackListResponse,
+  HydratedFeedbackList,
+} from 'sentry/utils/feedback/types';
+import type {UseApiQueryOptions} from 'sentry/utils/queryClient';
+
+type MockState = {
+  data: undefined | FeedbackListResponse;
+  isError: false;
+  isLoading: boolean;
+};
+
+export default function useFetchFeedbackList(
+  _params: FeedbackListQueryParams,
+  _options: Partial<UseApiQueryOptions<HydratedFeedbackList>> = {}
+) {
+  // Mock some state to simulate `useApiQuery` while the backend is being constructed
+  const [state, setState] = useState<MockState>({
+    isLoading: true,
+    isError: false,
+    data: undefined,
+  });
+
+  useEffect(() => {
+    const timeout = setTimeout(() => {
+      setState({
+        isLoading: false,
+        isError: false,
+        data: exampleListResponse,
+      });
+    }, Math.random() * 1000);
+    return () => clearTimeout(timeout);
+  }, []);
+
+  return {
+    ...state,
+    data: state.data?.map(hydrateFeedbackRecord),
+  };
+}

+ 50 - 0
static/app/utils/feedback/example.tsx

@@ -0,0 +1,50 @@
+import {FeedbackItemResponse, FeedbackListResponse} from 'sentry/utils/feedback/types';
+
+export const exampleItemResponse: FeedbackItemResponse = {
+  browser: {
+    name: 'Chome',
+    version: '103.0.38',
+  },
+  contact_email: 'colton.allen@sentry.io',
+  device: {
+    brand: 'Apple',
+    family: 'iPhone',
+    model: '11',
+    name: 'iPhone 11',
+  },
+  dist: 'abc123',
+  environment: 'production',
+  id: '1ffe0775ac0f4417aed9de36d9f6f8dc',
+  locale: {
+    lang: 'en',
+    timezone: 'UTC+1',
+  },
+  message: 'I really like this user-feedback feature!',
+  os: {
+    name: 'iOS',
+    version: '16.2',
+  },
+  platform: 'javascript',
+  release: 'version@1.3',
+  replay_id: 'ec3b4dc8b79f417596f7a1aa4fcca5d2',
+  sdk: {
+    name: 'sentry.javascript.react',
+    version: '6.18.1',
+  },
+  status: 'unresolved',
+  tags: {
+    hello: 'is',
+    it: ['me', "you're", 'looking', 'for'],
+  },
+  timestamp: '2023-08-31T14:10:34.954048',
+  url: 'https://docs.sentry.io/platforms/javascript/',
+  user: {
+    display_name: 'John Doe',
+    email: 'john.doe@example.com',
+    id: '30246326',
+    ip: '213.164.1.114',
+    username: 'John Doe',
+  },
+};
+
+export const exampleListResponse: FeedbackListResponse = [exampleItemResponse];

+ 67 - 0
static/app/utils/feedback/types.tsx

@@ -0,0 +1,67 @@
+type Overwrite<T, U> = Pick<T, Exclude<keyof T, keyof U>> & U;
+
+export interface FeedbackItemResponse {
+  browser: {
+    name: null | string;
+    version: null | string;
+  };
+  contact_email: string;
+  device: {
+    brand: null | string;
+    family: null | string;
+    model: null | string;
+    name: null | string;
+  };
+  dist: string;
+  environment: string;
+  id: string;
+  locale: {
+    lang: string;
+    timezone: string;
+  };
+  message: string;
+  os: {
+    name: null | string;
+    version: null | string;
+  };
+  platform: string;
+  release: string;
+  replay_id: null | string;
+  sdk: {
+    name: string;
+    version: string;
+  };
+  status: 'unresolved' | 'resolved';
+  tags: Record<string, unknown>;
+  timestamp: string;
+  url: string;
+  user: {
+    display_name: null | string;
+    email: null | string;
+    id: null | string;
+    ip: null | string;
+    username: null | string;
+  };
+}
+
+export type FeedbackListResponse = FeedbackItemResponse[];
+
+export type HydratedFeedbackItem = Overwrite<FeedbackItemResponse, {timestamp: Date}>;
+
+export type HydratedFeedbackList = HydratedFeedbackItem[];
+
+export interface FeedbackListQueryParams {
+  cursor?: string;
+  end?: string;
+  environment?: string[];
+  field?: string[];
+  offset?: string;
+  per_page?: string;
+  project?: string[];
+  query?: string;
+  queryReferrer?: string;
+  sort?: 'timestamp' | '-timestamp' | 'projectId' | '-projectId';
+  start?: string;
+  statsPeriod?: string;
+  utc?: 'true' | 'false';
+}

+ 16 - 1
static/app/views/feedback/details.tsx

@@ -1,6 +1,9 @@
+import {Alert} from 'sentry/components/alert';
+import useFetchFeedbackItem from 'sentry/components/feedback/useFetchFeedbackItem';
 import * as Layout from 'sentry/components/layouts/thirds';
 import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
 import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
+import Placeholder from 'sentry/components/placeholder';
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
 import {t} from 'sentry/locale';
 import useOrganization from 'sentry/utils/useOrganization';
@@ -8,6 +11,8 @@ import useOrganization from 'sentry/utils/useOrganization';
 export default function Details() {
   const organization = useOrganization();
 
+  const {isLoading, isError, data} = useFetchFeedbackItem({}, {});
+
   return (
     <SentryDocumentTitle title={`Feedback v2 — ${organization.slug}`}>
       <Layout.Header>
@@ -25,7 +30,17 @@ export default function Details() {
       </Layout.Header>
       <PageFiltersContainer>
         <Layout.Body>
-          <Layout.Main fullWidth>TODO details page</Layout.Main>
+          <Layout.Main fullWidth>
+            {isLoading ? (
+              <Placeholder />
+            ) : isError ? (
+              <Alert type="error" showIcon>
+                {t('An error occurred')}
+              </Alert>
+            ) : (
+              <pre>{JSON.stringify(data, null, '\t')}</pre>
+            )}
+          </Layout.Main>
         </Layout.Body>
       </PageFiltersContainer>
     </SentryDocumentTitle>

+ 16 - 1
static/app/views/feedback/list.tsx

@@ -1,6 +1,9 @@
+import Alert from 'sentry/components/alert';
+import useFetchFeedbackList from 'sentry/components/feedback/useFetchFeedbackList';
 import * as Layout from 'sentry/components/layouts/thirds';
 import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
 import {PageHeadingQuestionTooltip} from 'sentry/components/pageHeadingQuestionTooltip';
+import Placeholder from 'sentry/components/placeholder';
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
 import {t} from 'sentry/locale';
 import useOrganization from 'sentry/utils/useOrganization';
@@ -8,6 +11,8 @@ import useOrganization from 'sentry/utils/useOrganization';
 export default function List() {
   const organization = useOrganization();
 
+  const {isLoading, isError, data} = useFetchFeedbackList({}, {});
+
   return (
     <SentryDocumentTitle title={`Feedback v2 — ${organization.slug}`}>
       <Layout.Header>
@@ -25,7 +30,17 @@ export default function List() {
       </Layout.Header>
       <PageFiltersContainer>
         <Layout.Body>
-          <Layout.Main fullWidth>TODO List page</Layout.Main>
+          <Layout.Main fullWidth>
+            {isLoading ? (
+              <Placeholder />
+            ) : isError ? (
+              <Alert type="error" showIcon>
+                {t('An error occurred')}
+              </Alert>
+            ) : (
+              <pre>{JSON.stringify(data, null, '\t')}</pre>
+            )}
+          </Layout.Main>
         </Layout.Body>
       </PageFiltersContainer>
     </SentryDocumentTitle>