Browse Source

docs(seenBy): Create a story for the SeenByList component (#67664)

This is a demo of a component that fetches data from the server to
render. It's kind of inefficient because we make multiple fetches at the
same time. MemberListStore has some issues.

Also, i'm failry sure that the `iconPosition` values are backwards, but
that's another issue.

This also includes some tweaks to allow `<Matrix>` to accept one item in
the `selectedProps` array, makes it easier to splat out a list of
possible prop values.

<img width="1151" alt="SCR-20240325-opnu"
src="https://github.com/getsentry/sentry/assets/187460/2563d40a-a9f3-402e-a8c3-2c11fe8cba37">

---------

Co-authored-by: Michelle Zhang <56095982+michellewzhang@users.noreply.github.com>
Ryan Albrecht 11 months ago
parent
commit
e048f9e185
2 changed files with 169 additions and 11 deletions
  1. 151 0
      static/app/components/seenByList.stories.tsx
  2. 18 11
      static/app/components/stories/matrix.tsx

+ 151 - 0
static/app/components/seenByList.stories.tsx

@@ -0,0 +1,151 @@
+import {Fragment, useEffect} from 'react';
+
+import Placeholder from 'sentry/components/placeholder';
+import SeenByList from 'sentry/components/seenByList';
+import JSXProperty from 'sentry/components/stories/jsxProperty';
+import Matrix from 'sentry/components/stories/matrix';
+import SizingWindow from 'sentry/components/stories/sizingWindow';
+import storyBook from 'sentry/stories/storyBook';
+import {useMembers} from 'sentry/utils/useMembers';
+import {useUser} from 'sentry/utils/useUser';
+
+function useLoadedMembers() {
+  const {members, loadMore, ...rest} = useMembers();
+
+  useEffect(() => {
+    // `loadMore` is not referentially stable, so we cannot include it in the dependencies array
+    loadMore();
+  }, []); // eslint-disable-line react-hooks/exhaustive-deps
+
+  return {members, loadMore, ...rest};
+}
+
+export default storyBook(SeenByList, story => {
+  story('Default', () => {
+    const {members, fetching} = useLoadedMembers();
+
+    return (
+      <SizingWindow display="block" style={{width: '50%'}}>
+        {fetching ? <Placeholder /> : <SeenByList seenBy={members} />}
+      </SizingWindow>
+    );
+  });
+
+  story('iconTooltip', () => {
+    const {members, fetching} = useLoadedMembers();
+
+    return (
+      <Fragment>
+        <p>
+          Default is{' '}
+          <JSXProperty name="iconTooltip" value="People who have viewed this" />
+        </p>
+        <SizingWindow display="block" style={{width: '50%'}}>
+          {fetching ? (
+            <Placeholder />
+          ) : (
+            <SeenByList
+              seenBy={members}
+              iconTooltip="These folks have all seen this record"
+            />
+          )}
+        </SizingWindow>
+      </Fragment>
+    );
+  });
+
+  story('Always shows users except yourself', () => {
+    const user = useUser();
+    const {members, fetching} = useLoadedMembers();
+
+    return (
+      <Fragment>
+        <p>
+          In this example we've explicitly put `user` at the start of the list, that's
+          you! But it'll be filtered out. The idea is that that viewer is more interested
+          in who else has seen a resource. On the issue stream, for example, we indicate
+          if the viewer (you) has seen an issue by changing the font-weight to normal
+          after viewed.
+        </p>
+        <SizingWindow display="block" style={{width: '50%'}}>
+          {fetching ? <Placeholder /> : <SeenByList seenBy={[user, ...members]} />}
+        </SizingWindow>
+      </Fragment>
+    );
+  });
+
+  story('avatarSize', () => {
+    const {members, fetching} = useLoadedMembers();
+
+    return (
+      <Fragment>
+        <p>
+          Default is <JSXProperty name="avatarSize" value={28} />
+        </p>
+        {fetching ? (
+          <Placeholder />
+        ) : (
+          <Matrix
+            sizingWindowProps={{display: 'block'}}
+            render={SeenByList}
+            selectedProps={['avatarSize']}
+            propMatrix={{
+              avatarSize: [12, 16, 20, 24, 28, 30],
+              seenBy: [members],
+            }}
+          />
+        )}
+      </Fragment>
+    );
+  });
+
+  story('maxVisibleAvatars', () => {
+    const {members, fetching} = useLoadedMembers();
+
+    return (
+      <Fragment>
+        <p>
+          Default is <JSXProperty name="maxVisibleAvatars" value={10} />
+        </p>
+        {fetching ? (
+          <Placeholder />
+        ) : (
+          <Matrix
+            sizingWindowProps={{display: 'block'}}
+            render={SeenByList}
+            selectedProps={['maxVisibleAvatars']}
+            propMatrix={{
+              maxVisibleAvatars: [10, 5, 3, 1],
+              seenBy: [members],
+            }}
+          />
+        )}
+      </Fragment>
+    );
+  });
+
+  story('iconPosition', () => {
+    const {members, fetching} = useLoadedMembers();
+
+    return (
+      <Fragment>
+        <p>
+          Default is <JSXProperty name="iconPosition" value="left" />
+        </p>
+        {fetching ? (
+          <Placeholder />
+        ) : (
+          <Matrix
+            sizingWindowProps={{display: 'block'}}
+            render={SeenByList}
+            selectedProps={['iconPosition']}
+            propMatrix={{
+              iconPosition: ['left', 'right'],
+              seenBy: [members],
+            }}
+          />
+        )}
+      </Fragment>
+    );
+  });
+});

+ 18 - 11
static/app/components/stories/matrix.tsx

@@ -15,7 +15,7 @@ export type PropMatrix<P extends RenderProps> = Partial<{
 interface Props<P extends RenderProps> {
   propMatrix: PropMatrix<P>;
   render: ElementType<P>;
-  selectedProps: [keyof P, keyof P];
+  selectedProps: [keyof P] | [keyof P, keyof P];
   sizingWindowProps?: SizingWindowProps;
 }
 
@@ -32,7 +32,7 @@ export default function Matrix<P extends RenderProps>({
   );
 
   const values1 = propMatrix[selectedProps[0]] ?? [];
-  const values2 = propMatrix[selectedProps[1]] ?? [];
+  const values2 = selectedProps.length === 2 ? propMatrix[selectedProps[1]] : undefined;
 
   const items = values1.flatMap(value1 => {
     const label = (
@@ -40,13 +40,14 @@ export default function Matrix<P extends RenderProps>({
         <JSXProperty name={String(selectedProps[0])} value={value1} />
       </div>
     );
-    const content = values2.map(value2 => {
+
+    const content = (values2 ?? ['']).map(value2 => {
       return item(
         render,
         {
           ...defaultValues,
           [selectedProps[0]]: value1,
-          [selectedProps[1]]: value2,
+          ...(selectedProps.length === 2 ? {[selectedProps[1]]: value2} : {}),
         },
         sizingWindowProps
       );
@@ -56,17 +57,23 @@ export default function Matrix<P extends RenderProps>({
 
   return (
     <div>
-      <h4 style={{margin: 0}}>
-        <samp>{selectedProps[0] as string | number}</samp> vs{' '}
-        <samp>{selectedProps[1] as string | number}</samp>
-      </h4>
+      {selectedProps.length === 2 ? (
+        <h4 style={{margin: 0}}>
+          <samp>{selectedProps[0] as string | number}</samp> vs{' '}
+          <samp>{selectedProps[1] as string | number}</samp>
+        </h4>
+      ) : (
+        <h4 style={{margin: 0}}>
+          <samp>{selectedProps[0] as string | number}</samp>
+        </h4>
+      )}
       <Grid
         style={{
-          gridTemplateColumns: `max-content repeat(${values2.length}, max-content)`,
+          gridTemplateColumns: `max-content repeat(${values2?.length ?? 1}, max-content)`,
         }}
       >
-        <div key="space-head" />
-        {values2.map(value2 => (
+        {values2 ? <div key="space-head" /> : null}
+        {values2?.map(value2 => (
           <div key={`title-2-${value2}`}>
             <JSXProperty name={String(selectedProps[1])} value={value2} />
           </div>