Browse Source

ref(perf): Introduce `ModuleLayout` layout primitive (#65365)

This is the tidiest and least silly way I could think of to re-use how
we lay out our current module work.

All the modules have a _lot_ in common, on their landing and details
pages!

1. The space is `2` between all elements
2. The page is divided into simple areas like 2-up and 3-up widgets with
borders

And yet, we're doing a lot of strange `PaddedContainer`-like elements
everywhere _and_ our padding is inconsistent as a result. Here, I'm
adding a real simple `ModuleLayout` which is just a `grid` layout with
some helpers. It helps _my_ two modules a bunch, should also be useful
for others, since it does a _little_ bit of work to make phone layouts
make sense, too.
George Gritsouk 1 year ago
parent
commit
96f4e94cb7

+ 64 - 74
static/app/views/performance/database/databaseLandingPage.tsx

@@ -25,6 +25,7 @@ import {NoDataMessage} from 'sentry/views/performance/database/noDataMessage';
 import {isAValidSort, QueriesTable} from 'sentry/views/performance/database/queriesTable';
 import {isAValidSort, QueriesTable} from 'sentry/views/performance/database/queriesTable';
 import {ThroughputChart} from 'sentry/views/performance/database/throughputChart';
 import {ThroughputChart} from 'sentry/views/performance/database/throughputChart';
 import {useSelectedDurationAggregate} from 'sentry/views/performance/database/useSelectedDurationAggregate';
 import {useSelectedDurationAggregate} from 'sentry/views/performance/database/useSelectedDurationAggregate';
+import * as ModuleLayout from 'sentry/views/performance/moduleLayout';
 import {ModulePageProviders} from 'sentry/views/performance/modulePageProviders';
 import {ModulePageProviders} from 'sentry/views/performance/modulePageProviders';
 import Onboarding from 'sentry/views/performance/onboarding';
 import Onboarding from 'sentry/views/performance/onboarding';
 import {useSynchronizeCharts} from 'sentry/views/starfish/components/chart';
 import {useSynchronizeCharts} from 'sentry/views/starfish/components/chart';
@@ -145,61 +146,69 @@ export function DatabaseLandingPage() {
 
 
       <Layout.Body>
       <Layout.Body>
         <Layout.Main fullWidth>
         <Layout.Main fullWidth>
-          {!onboardingProject && !isCriticalDataLoading && (
-            <NoDataMessage
-              Wrapper={AlertBanner}
-              isDataAvailable={isAnyCriticalDataAvailable}
-            />
-          )}
-
-          <FloatingFeedbackWidget />
-
-          <PaddedContainer>
-            <PageFilterBar condensed>
-              <ProjectPageFilter />
-              <EnvironmentPageFilter />
-              <DatePageFilter />
-            </PageFilterBar>
-          </PaddedContainer>
-
-          {onboardingProject && (
-            <Onboarding organization={organization} project={onboardingProject} />
-          )}
-          {!onboardingProject && (
-            <Fragment>
-              <ChartContainer>
-                <ThroughputChart
-                  series={throughputData['spm()']}
-                  isLoading={isThroughputDataLoading}
-                />
-
-                <DurationChart
-                  series={durationData[`${selectedAggregate}(span.self_time)`]}
-                  isLoading={isDurationDataLoading}
-                />
-              </ChartContainer>
-
-              <FilterOptionsContainer>
-                <SelectorContainer>
-                  <ActionSelector moduleName={moduleName} value={spanAction ?? ''} />
-                </SelectorContainer>
-
-                <SelectorContainer>
-                  <DomainSelector moduleName={moduleName} value={spanDomain ?? ''} />
-                </SelectorContainer>
-              </FilterOptionsContainer>
-
-              <SearchBarContainer>
-                <SearchBar
-                  query={spanDescription}
-                  placeholder={t('Search for more Queries')}
-                  onSearch={handleSearch}
-                />
-              </SearchBarContainer>
-
-              <QueriesTable response={queryListResponse} sort={sort} />
-            </Fragment>
-          )}
+          <ModuleLayout.Layout>
+            {!onboardingProject && !isCriticalDataLoading && (
+              <NoDataMessage
+                Wrapper={AlertBanner}
+                isDataAvailable={isAnyCriticalDataAvailable}
+              />
+            )}
+
+            <FloatingFeedbackWidget />
+
+            <ModuleLayout.Full>
+              <PageFilterBar condensed>
+                <ProjectPageFilter />
+                <EnvironmentPageFilter />
+                <DatePageFilter />
+              </PageFilterBar>
+            </ModuleLayout.Full>
+
+            {onboardingProject && (
+              <Onboarding organization={organization} project={onboardingProject} />
+            )}
+            {!onboardingProject && (
+              <Fragment>
+                <ModuleLayout.Half>
+                  <ThroughputChart
+                    series={throughputData['spm()']}
+                    isLoading={isThroughputDataLoading}
+                  />
+                </ModuleLayout.Half>
+
+                <ModuleLayout.Half>
+                  <DurationChart
+                    series={durationData[`${selectedAggregate}(span.self_time)`]}
+                    isLoading={isDurationDataLoading}
+                  />
+                </ModuleLayout.Half>
+
+                <ModuleLayout.Full>
+                  <FilterOptionsContainer>
+                    <SelectorContainer>
+                      <ActionSelector moduleName={moduleName} value={spanAction ?? ''} />
+                    </SelectorContainer>
+
+                    <SelectorContainer>
+                      <DomainSelector moduleName={moduleName} value={spanDomain ?? ''} />
+                    </SelectorContainer>
+                  </FilterOptionsContainer>
+                </ModuleLayout.Full>
+
+                <ModuleLayout.Full>
+                  <SearchBar
+                    query={spanDescription}
+                    placeholder={t('Search for more Queries')}
+                    onSearch={handleSearch}
+                  />
+                </ModuleLayout.Full>
+
+                <ModuleLayout.Full>
+                  <QueriesTable response={queryListResponse} sort={sort} />
+                </ModuleLayout.Full>
+              </Fragment>
+            )}
+          </ModuleLayout.Layout>
         </Layout.Main>
         </Layout.Main>
       </Layout.Body>
       </Layout.Body>
     </React.Fragment>
     </React.Fragment>
@@ -211,20 +220,6 @@ const DEFAULT_SORT = {
   kind: 'desc' as const,
   kind: 'desc' as const,
 };
 };
 
 
-const PaddedContainer = styled('div')`
-  margin-bottom: ${space(2)};
-`;
-
-const ChartContainer = styled('div')`
-  display: grid;
-  grid-template-columns: 1fr;
-
-  @media (min-width: ${p => p.theme.breakpoints.small}) {
-    grid-template-columns: 1fr 1fr;
-    gap: ${space(2)};
-  }
-`;
-
 function AlertBanner(props) {
 function AlertBanner(props) {
   return <Alert {...props} type="info" showIcon />;
   return <Alert {...props} type="info" showIcon />;
 }
 }
@@ -233,7 +228,6 @@ const FilterOptionsContainer = styled('div')`
   display: flex;
   display: flex;
   flex-wrap: wrap;
   flex-wrap: wrap;
   gap: ${space(2)};
   gap: ${space(2)};
-  margin-bottom: ${space(2)};
 
 
   @media (min-width: ${p => p.theme.breakpoints.small}) {
   @media (min-width: ${p => p.theme.breakpoints.small}) {
     flex-wrap: nowrap;
     flex-wrap: nowrap;
@@ -248,10 +242,6 @@ const SelectorContainer = styled('div')`
   }
   }
 `;
 `;
 
 
-const SearchBarContainer = styled('div')`
-  margin-bottom: ${space(2)};
-`;
-
 const LIMIT: number = 25;
 const LIMIT: number = 25;
 
 
 function LandingPageWithProviders() {
 function LandingPageWithProviders() {

+ 36 - 44
static/app/views/performance/http/httpLandingPage.tsx

@@ -1,5 +1,4 @@
 import React, {Fragment} from 'react';
 import React, {Fragment} from 'react';
-import styled from '@emotion/styled';
 import pickBy from 'lodash/pickBy';
 import pickBy from 'lodash/pickBy';
 
 
 import {Breadcrumbs} from 'sentry/components/breadcrumbs';
 import {Breadcrumbs} from 'sentry/components/breadcrumbs';
@@ -10,7 +9,6 @@ import {EnvironmentPageFilter} from 'sentry/components/organizations/environment
 import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
 import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
 import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
 import {ProjectPageFilter} from 'sentry/components/organizations/projectPageFilter';
 import {t} from 'sentry/locale';
 import {t} from 'sentry/locale';
-import {space} from 'sentry/styles/space';
 import {fromSorts} from 'sentry/utils/discover/eventView';
 import {fromSorts} from 'sentry/utils/discover/eventView';
 import {decodeScalar} from 'sentry/utils/queryString';
 import {decodeScalar} from 'sentry/utils/queryString';
 import {useLocation} from 'sentry/utils/useLocation';
 import {useLocation} from 'sentry/utils/useLocation';
@@ -20,6 +18,7 @@ import {useOnboardingProject} from 'sentry/views/performance/browser/webVitals/u
 import {DurationChart} from 'sentry/views/performance/database/durationChart';
 import {DurationChart} from 'sentry/views/performance/database/durationChart';
 import {DomainsTable, isAValidSort} from 'sentry/views/performance/http/domainsTable';
 import {DomainsTable, isAValidSort} from 'sentry/views/performance/http/domainsTable';
 import {ThroughputChart} from 'sentry/views/performance/http/throughputChart';
 import {ThroughputChart} from 'sentry/views/performance/http/throughputChart';
+import * as ModuleLayout from 'sentry/views/performance/moduleLayout';
 import {ModulePageProviders} from 'sentry/views/performance/modulePageProviders';
 import {ModulePageProviders} from 'sentry/views/performance/modulePageProviders';
 import Onboarding from 'sentry/views/performance/onboarding';
 import Onboarding from 'sentry/views/performance/onboarding';
 import {useSynchronizeCharts} from 'sentry/views/starfish/components/chart';
 import {useSynchronizeCharts} from 'sentry/views/starfish/components/chart';
@@ -106,34 +105,41 @@ export function HTTPLandingPage() {
         <Layout.Main fullWidth>
         <Layout.Main fullWidth>
           <FloatingFeedbackWidget />
           <FloatingFeedbackWidget />
 
 
-          <PaddedContainer>
-            <PageFilterBar condensed>
-              <ProjectPageFilter />
-              <EnvironmentPageFilter />
-              <DatePageFilter />
-            </PageFilterBar>
-          </PaddedContainer>
-
-          {onboardingProject && (
-            <Onboarding organization={organization} project={onboardingProject} />
-          )}
-          {!onboardingProject && (
-            <Fragment>
-              <ChartContainer>
-                <ThroughputChart
-                  series={throughputData['spm()']}
-                  isLoading={isThroughputDataLoading}
-                />
-
-                <DurationChart
-                  series={durationData[`avg(span.self_time)`]}
-                  isLoading={isDurationDataLoading}
-                />
-              </ChartContainer>
-
-              <DomainsTable response={domainsListResponse} sort={sort} />
-            </Fragment>
-          )}
+          <ModuleLayout.Layout>
+            <ModuleLayout.Full>
+              <PageFilterBar condensed>
+                <ProjectPageFilter />
+                <EnvironmentPageFilter />
+                <DatePageFilter />
+              </PageFilterBar>
+            </ModuleLayout.Full>
+
+            {onboardingProject && (
+              <Onboarding organization={organization} project={onboardingProject} />
+            )}
+
+            {!onboardingProject && (
+              <Fragment>
+                <ModuleLayout.Half>
+                  <ThroughputChart
+                    series={throughputData['spm()']}
+                    isLoading={isThroughputDataLoading}
+                  />
+                </ModuleLayout.Half>
+
+                <ModuleLayout.Half>
+                  <DurationChart
+                    series={durationData[`avg(span.self_time)`]}
+                    isLoading={isDurationDataLoading}
+                  />
+                </ModuleLayout.Half>
+
+                <ModuleLayout.Full>
+                  <DomainsTable response={domainsListResponse} sort={sort} />
+                </ModuleLayout.Full>
+              </Fragment>
+            )}
+          </ModuleLayout.Layout>
         </Layout.Main>
         </Layout.Main>
       </Layout.Body>
       </Layout.Body>
     </React.Fragment>
     </React.Fragment>
@@ -145,20 +151,6 @@ const DEFAULT_SORT = {
   kind: 'desc' as const,
   kind: 'desc' as const,
 };
 };
 
 
-const PaddedContainer = styled('div')`
-  margin-bottom: ${space(2)};
-`;
-
-const ChartContainer = styled('div')`
-  display: grid;
-  grid-template-columns: 1fr;
-
-  @media (min-width: ${p => p.theme.breakpoints.small}) {
-    grid-template-columns: 1fr 1fr;
-    gap: ${space(2)};
-  }
-`;
-
 const DOMAIN_TABLE_ROW_COUNT = 10;
 const DOMAIN_TABLE_ROW_COUNT = 10;
 
 
 function LandingPageWithProviders() {
 function LandingPageWithProviders() {

+ 33 - 0
static/app/views/performance/moduleLayout.tsx

@@ -0,0 +1,33 @@
+import styled from '@emotion/styled';
+
+import {space} from 'sentry/styles/space';
+
+export const Layout = styled('div')`
+  display: grid;
+  grid-template-columns: repeat(12, 1fr);
+  gap: ${space(2)};
+`;
+
+export const Quarter = styled('div')`
+  grid-column: span 3;
+`;
+
+export const Third = styled('div')`
+  grid-column: span 4;
+`;
+
+export const Half = styled('div')`
+  grid-column: span 12;
+
+  @media (min-width: ${p => p.theme.breakpoints.small}) {
+    grid-column: span 6;
+  }
+`;
+
+export const ThreeQuarters = styled('div')`
+  grid-column: span 8;
+`;
+
+export const Full = styled('div')`
+  grid-column: span 12;
+`;

+ 19 - 3
static/app/views/starfish/components/chartPanel.tsx

@@ -13,11 +13,19 @@ type Props = {
 
 
 export default function ChartPanel({title, children, button, subtitle}: Props) {
 export default function ChartPanel({title, children, button, subtitle}: Props) {
   return (
   return (
-    <Panel>
+    <PanelWithNoPadding>
       <PanelBody>
       <PanelBody>
         {title && (
         {title && (
           <Header>
           <Header>
-            {title && <ChartLabel>{title}</ChartLabel>}
+            {title && (
+              <ChartLabel>
+                {typeof title === 'string' ? (
+                  <TextTitleContainer>{title}</TextTitleContainer>
+                ) : (
+                  title
+                )}
+              </ChartLabel>
+            )}
             {button}
             {button}
           </Header>
           </Header>
         )}
         )}
@@ -28,10 +36,18 @@ export default function ChartPanel({title, children, button, subtitle}: Props) {
         )}
         )}
         {children}
         {children}
       </PanelBody>
       </PanelBody>
-    </Panel>
+    </PanelWithNoPadding>
   );
   );
 }
 }
 
 
+const PanelWithNoPadding = styled(Panel)`
+  margin-bottom: 0;
+`;
+
+const TextTitleContainer = styled('div')`
+  padding: 1px 0;
+`;
+
 const SubtitleContainer = styled('div')`
 const SubtitleContainer = styled('div')`
   padding-top: ${space(0.5)};
   padding-top: ${space(0.5)};
 `;
 `;