Просмотр исходного кода

feat(starfish): starfish api details and generic drawer component (#47550)

Adds a reusable slider (drawer?) component for starfish api/db/cache/etc
details.
Also some progress on api details
edwardgou-sentry 1 год назад
Родитель
Сommit
7f7e294c0f

+ 65 - 0
static/app/views/starfish/components/detailPanel.tsx

@@ -0,0 +1,65 @@
+import {useEffect, useState} from 'react';
+import styled from '@emotion/styled';
+
+import {Button} from 'sentry/components/button';
+import {IconClose} from 'sentry/icons/iconClose';
+import {t} from 'sentry/locale';
+import space from 'sentry/styles/space';
+import SlideOverPanel from 'sentry/views/starfish/components/slideOverPanel';
+
+type DetailProps = {
+  children: React.ReactNode;
+  detailKey?: string;
+  onClose?: () => void;
+};
+
+type DetailState = {
+  collapsed: boolean;
+};
+
+export default function Detail({children, detailKey, onClose}: DetailProps) {
+  const [state, setState] = useState<DetailState>({collapsed: true});
+
+  // Any time the key prop changes (due to user interaction), we want to open the panel
+  useEffect(() => {
+    if (detailKey) {
+      setState({collapsed: false});
+    }
+  }, [detailKey]);
+
+  return (
+    <SlideOverPanel collapsed={state.collapsed}>
+      <CloseButtonWrapper>
+        <CloseButton
+          priority="link"
+          size="zero"
+          borderless
+          aria-label={t('Close Details')}
+          icon={<IconClose size="sm" />}
+          onClick={() => {
+            setState({collapsed: true});
+            onClose?.();
+          }}
+        />
+      </CloseButtonWrapper>
+      <DetailWrapper>{children}</DetailWrapper>
+    </SlideOverPanel>
+  );
+}
+
+const CloseButton = styled(Button)`
+  color: ${p => p.theme.gray300};
+  &:hover {
+    color: ${p => p.theme.gray400};
+  }
+`;
+
+const CloseButtonWrapper = styled('div')`
+  justify-content: flex-end;
+  display: flex;
+  padding: ${space(2)};
+`;
+
+const DetailWrapper = styled('div')`
+  padding: 0 ${space(4)};
+`;

+ 42 - 0
static/app/views/starfish/components/slideOverPanel.tsx

@@ -0,0 +1,42 @@
+import styled from '@emotion/styled';
+import {motion} from 'framer-motion';
+
+const PANEL_WIDTH = '640px';
+
+type SlideOverPanelProps = {
+  children: React.ReactNode;
+  collapsed: boolean;
+};
+
+export default function SlideOverPanel({collapsed, children}: SlideOverPanelProps) {
+  return (
+    <_SlideOverPanel
+      collapsed={collapsed}
+      initial={{opacity: 0, x: PANEL_WIDTH}}
+      animate={!collapsed ? {opacity: 1, x: 0} : {opacity: 0, x: PANEL_WIDTH}}
+      transition={{
+        type: 'spring',
+        stiffness: 1000,
+        damping: 50,
+      }}
+    >
+      {children}
+    </_SlideOverPanel>
+  );
+}
+
+const _SlideOverPanel = styled(motion.div)<{
+  collapsed: boolean;
+}>`
+  width: ${PANEL_WIDTH};
+  position: fixed;
+  top: 0;
+  bottom: 0;
+  right: 0;
+  background: ${p => p.theme.background};
+  color: ${p => p.theme.textColor};
+  border-left: 1px solid ${p => p.theme.border};
+  text-align: left;
+  z-index: ${p => p.theme.zIndex.sidebar - 1};
+  ${p => (p.collapsed ? 'overflow: hidden;' : '')}
+`;

+ 22 - 11
static/app/views/starfish/modules/APIModule/APIModuleView.tsx

@@ -3,20 +3,23 @@ import {useQuery} from '@tanstack/react-query';
 import {Location} from 'history';
 
 import GridEditable, {GridColumnHeader} from 'sentry/components/gridEditable';
+import Link from 'sentry/components/links/link';
 import {Series} from 'sentry/types/echarts';
 import Chart from 'sentry/views/starfish/components/chart';
 
 import {ENDPOINT_GRAPH_QUERY, ENDPOINT_LIST_QUERY} from './queries';
 
-const HOST = 'http://localhost:8080';
+export const HOST = 'http://localhost:8080';
 
 type Props = {
   location: Location;
+  onSelect: (row: DataRow) => void;
 };
 
-type DataRow = {
+export type DataRow = {
   count: number;
   description: string;
+  domain: string;
 };
 
 const COLUMN_ORDER = [
@@ -31,7 +34,7 @@ const COLUMN_ORDER = [
   },
 ];
 
-export default function APIModuleView({location}: Props) {
+export default function APIModuleView({location, onSelect}: Props) {
   const {isLoading: areEndpointsLoading, data: endpointsData} = useQuery({
     queryKey: ['endpoints'],
     queryFn: () => fetch(`${HOST}/?query=${ENDPOINT_LIST_QUERY}`).then(res => res.json()),
@@ -68,6 +71,22 @@ export default function APIModuleView({location}: Props) {
 
   const data = Object.values(seriesByQuantile);
 
+  // TODO: Moved these into the component for easy acces to onSelect. Clean this up later.
+  function renderHeadCell(column: GridColumnHeader): React.ReactNode {
+    return <span>{column.name}</span>;
+  }
+
+  function renderBodyCell(column: GridColumnHeader, row: DataRow): React.ReactNode {
+    if (column.key === 'description') {
+      return (
+        <Link onClick={() => onSelect(row)} to="">
+          {row[column.key]}
+        </Link>
+      );
+    }
+    return <span>{row[column.key]}</span>;
+  }
+
   return (
     <Fragment>
       <Chart
@@ -103,11 +122,3 @@ export default function APIModuleView({location}: Props) {
     </Fragment>
   );
 }
-
-function renderHeadCell(column: GridColumnHeader): React.ReactNode {
-  return <span>{column.name}</span>;
-}
-
-function renderBodyCell(column: GridColumnHeader, row: DataRow): React.ReactNode {
-  return <span>{row[column.key]}</span>;
-}

+ 13 - 2
static/app/views/starfish/modules/APIModule/index.tsx

@@ -1,3 +1,4 @@
+import {useState} from 'react';
 import {Location} from 'history';
 
 import * as Layout from 'sentry/components/layouts/thirds';
@@ -6,14 +7,23 @@ import {
   PageErrorAlert,
   PageErrorProvider,
 } from 'sentry/utils/performance/contexts/pageError';
+import EndpointDetail from 'sentry/views/starfish/views/endpointDetails';
 
-import APIModuleView from './APIModuleView';
+import APIModuleView, {DataRow} from './APIModuleView';
+
+type APIModuleState = {
+  selectedRow?: DataRow;
+};
 
 type Props = {
   location: Location;
 };
 
 export default function APIModule(props: Props) {
+  const [state, setState] = useState<APIModuleState>({selectedRow: undefined});
+  const unsetSelectedSpanGroup = () => setState({selectedRow: undefined});
+  const {selectedRow} = state;
+  const setSelectedRow = (row: DataRow) => setState({selectedRow: row});
   return (
     <Layout.Page>
       <PageErrorProvider>
@@ -26,7 +36,8 @@ export default function APIModule(props: Props) {
         <Layout.Body>
           <Layout.Main fullWidth>
             <PageErrorAlert />
-            <APIModuleView {...props} />
+            <APIModuleView {...props} onSelect={setSelectedRow} />
+            <EndpointDetail row={selectedRow} onClose={unsetSelectedSpanGroup} />
           </Layout.Main>
         </Layout.Body>
       </PageErrorProvider>

+ 13 - 0
static/app/views/starfish/modules/APIModule/queries.js

@@ -17,3 +17,16 @@ export const ENDPOINT_GRAPH_QUERY = `SELECT
  GROUP BY interval
  ORDER BY interval asc
  `;
+
+export const getEndpointDetailQuery = description => {
+  return `SELECT
+  toStartOfInterval(start_timestamp, INTERVAL 5 MINUTE) as interval,
+  quantile(0.5)(exclusive_time) as p50,
+  count() as count
+  FROM spans_experimental_starfish
+  WHERE module = 'http'
+  AND description = '${description}'
+  GROUP BY interval
+  ORDER BY interval asc
+`;
+};

+ 52 - 0
static/app/views/starfish/views/endpointDetails/index.tsx

@@ -0,0 +1,52 @@
+import styled from '@emotion/styled';
+import {useQuery} from '@tanstack/react-query';
+
+import {t} from 'sentry/locale';
+import Detail from 'sentry/views/starfish/components/detailPanel';
+import {DataRow, HOST} from 'sentry/views/starfish/modules/APIModule/APIModuleView';
+import {getEndpointDetailQuery} from 'sentry/views/starfish/modules/APIModule/queries';
+
+type EndpointDetailBodyProps = {
+  row: DataRow;
+};
+
+export default function EndpointDetail({
+  row,
+  onClose,
+}: Partial<EndpointDetailBodyProps> & {onClose: () => void}) {
+  return (
+    <Detail detailKey={row?.description} onClose={onClose}>
+      {row && <EndpointDetailBody row={row} />}
+    </Detail>
+  );
+}
+
+function EndpointDetailBody({row}: EndpointDetailBodyProps) {
+  const throughputQuery = getEndpointDetailQuery(row.description);
+  useQuery({
+    queryKey: ['endpointThroughput'],
+    queryFn: () => fetch(`${HOST}/?query=${throughputQuery}`).then(res => res.json()),
+    retry: false,
+    initialData: [],
+  });
+
+  return (
+    <div>
+      <h2>{t('Endpoint Detail')}</h2>
+      <p>
+        {t(
+          'Detailed summary of http client spans. Detailed summary of http client spans. Detailed summary of http client spans. Detailed summary of http client spans. Detailed summary of http client spans. Detailed summary of http client spans.'
+        )}
+      </p>
+      <SubHeader>{t('Endpoint URL')}</SubHeader>
+      <pre>{row?.description}</pre>
+      <SubHeader>{t('Domain')}</SubHeader>
+      <pre>{row?.domain}</pre>
+    </div>
+  );
+}
+
+const SubHeader = styled('h3')`
+  color: ${p => p.theme.gray300};
+  font-size: ${p => p.theme.fontSizeLarge};
+`;