Browse Source

feat(stats): Add Project Selector to Stats page (#39773)

See [ER-1203](https://getsentry.atlassian.net/browse/ER-1203)

This PR refactors the organization statistics page to support project
selection!

Demo: 


https://user-images.githubusercontent.com/35509934/196281200-55ac6658-0fa6-4155-bc9b-bca82a256b1e.mov



With and without flag:


https://user-images.githubusercontent.com/35509934/196281152-0c347827-35bf-42b2-b2ef-36932e6520e8.mov



TODO: 
- [x] Merge https://github.com/getsentry/sentry/pull/39955
- [x] Add video and screenshots
- [x] Implement a feature flag

Co-authored-by: Danny Lee <dlee@sentry.io>
Leander Rodrigues 2 years ago
parent
commit
648026e40f

+ 157 - 15
static/app/views/organizationStats/index.spec.tsx

@@ -1,5 +1,5 @@
 import {initializeOrg} from 'sentry-test/initializeOrg';
-import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
+import {act, cleanup, render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
 
 import {DEFAULT_STATS_PERIOD} from 'sentry/constants';
 import OrganizationStore from 'sentry/stores/organizationStore';
@@ -17,7 +17,7 @@ describe('OrganizationStats', function () {
     datetime: {
       start: null,
       end: null,
-      period: '24h',
+      period: DEFAULT_STATS_PERIOD,
       utc: false,
     },
   };
@@ -33,6 +33,7 @@ describe('OrganizationStats', function () {
     router,
     organization,
     ...router,
+    selection: defaultSelection,
     route: {},
     params: {orgId: organization.slug as string},
     routeParams: {},
@@ -60,14 +61,11 @@ describe('OrganizationStats', function () {
   /**
    * Features and Alerts
    */
-
-  it('renders header state wihout tabs', () => {
+  it('renders header state without tabs', () => {
     const newOrg = initializeOrg();
-    const newProps = {
-      ...defaultProps,
-      organization: newOrg.organization,
-    };
-    render(<OrganizationStats {...newProps} />, {context: newOrg.routerContext});
+    render(<OrganizationStats {...defaultProps} organization={newOrg.organization} />, {
+      context: newOrg.routerContext,
+    });
     expect(screen.getByText('Organization Usage Stats')).toBeInTheDocument();
   });
 
@@ -112,6 +110,7 @@ describe('OrganizationStats', function () {
         statsPeriod: DEFAULT_STATS_PERIOD,
         interval: '1h',
         groupBy: ['category', 'outcome'],
+        project: [],
         field: ['sum(quantity)'],
       },
       UsageStatsPerMin: {
@@ -124,7 +123,7 @@ describe('OrganizationStats', function () {
         statsPeriod: DEFAULT_STATS_PERIOD,
         interval: '1h',
         groupBy: ['outcome', 'project'],
-        project: '-1',
+        project: [],
         field: ['sum(quantity)'],
         category: 'error',
       },
@@ -206,11 +205,9 @@ describe('OrganizationStats', function () {
       },
       {query: {}}
     );
-    const newProps = {
-      ...defaultProps,
-      location: dummyLocation as any,
-    };
-    render(<OrganizationStats {...newProps} />, {context: routerContext});
+    render(<OrganizationStats {...defaultProps} location={dummyLocation as any} />, {
+      context: routerContext,
+    });
 
     const projectLinks = screen.getAllByTestId('badge-display-name');
     expect(projectLinks.length).toBeGreaterThan(0);
@@ -222,6 +219,151 @@ describe('OrganizationStats', function () {
       );
     }
   });
+
+  /**
+   * Project Selection
+   */
+  it('renders with no projects selected', () => {
+    const newOrg = initializeOrg();
+    newOrg.organization.features = [
+      'global-views',
+      'team-insights',
+      // TODO(Leander): Remove the following check once the project-stats flag is GA
+      'project-stats',
+    ];
+    render(<OrganizationStats {...defaultProps} organization={newOrg.organization} />, {
+      context: newOrg.routerContext,
+    });
+
+    expect(screen.getByText('My Projects')).toBeInTheDocument();
+    expect(screen.getByTestId('usage-stats-chart')).toBeInTheDocument();
+    expect(screen.getByTestId('usage-stats-table')).toBeInTheDocument();
+
+    mockRequest.mock.calls.forEach(([_path, {query}]) => {
+      // Ignore UsageStatsPerMin's query
+      if (query?.statsPeriod === '5m') {
+        return;
+      }
+      expect(query.project).toEqual(defaultSelection.projects);
+    });
+  });
+
+  it('renders with multiple projects selected', () => {
+    const newOrg = initializeOrg();
+    newOrg.organization.features = [
+      'global-views',
+      'team-insights',
+      // TODO(Leander): Remove the following check once the project-stats flag is GA
+      'project-stats',
+    ];
+
+    const selectedProjects = [1, 2];
+    const newSelection = {
+      ...defaultSelection,
+      projects: selectedProjects,
+    };
+
+    render(
+      <OrganizationStats
+        {...defaultProps}
+        organization={newOrg.organization}
+        selection={newSelection}
+      />,
+      {context: newOrg.routerContext}
+    );
+    act(() => PageFiltersStore.updateProjects(selectedProjects, []));
+
+    expect(screen.queryByText('My Projects')).not.toBeInTheDocument();
+    expect(screen.getByTestId('usage-stats-chart')).toBeInTheDocument();
+    expect(screen.getByTestId('usage-stats-table')).toBeInTheDocument();
+
+    expect(mockRequest).toHaveBeenCalledWith(
+      endpoint,
+      expect.objectContaining({
+        query: {
+          statsPeriod: DEFAULT_STATS_PERIOD,
+          interval: '1h',
+          groupBy: ['category', 'outcome'],
+          project: selectedProjects,
+          field: ['sum(quantity)'],
+        },
+      })
+    );
+  });
+
+  it('renders with a single project selected', () => {
+    const newOrg = initializeOrg();
+    newOrg.organization.features = [
+      'global-views',
+      'team-insights',
+      // TODO(Leander): Remove the following check once the project-stats flag is GA
+      'project-stats',
+    ];
+    const selectedProject = [1];
+    const newSelection = {
+      ...defaultSelection,
+      projects: selectedProject,
+    };
+
+    render(
+      <OrganizationStats
+        {...defaultProps}
+        organization={newOrg.organization}
+        selection={newSelection}
+      />,
+      {context: newOrg.routerContext}
+    );
+    act(() => PageFiltersStore.updateProjects(selectedProject, []));
+
+    expect(screen.queryByText('My Projects')).not.toBeInTheDocument();
+    expect(screen.getByTestId('usage-stats-chart')).toBeInTheDocument();
+    // Doesn't render for single project view
+    expect(screen.queryByTestId('usage-stats-table')).not.toBeInTheDocument();
+
+    expect(mockRequest).toHaveBeenCalledWith(
+      endpoint,
+      expect.objectContaining({
+        query: {
+          statsPeriod: DEFAULT_STATS_PERIOD,
+          interval: '1h',
+          groupBy: ['category', 'outcome'],
+          project: selectedProject,
+          field: ['sum(quantity)'],
+        },
+      })
+    );
+  });
+
+  /**
+   * Feature Flagging
+   */
+  it('renders legacy organization stats without appropriate flags', () => {
+    const selectedProject = [1];
+    const newSelection = {
+      ...defaultSelection,
+      projects: selectedProject,
+    };
+    for (const features of [
+      ['team-insights'],
+      ['team-insights', 'project-stats'],
+      ['team-insights', 'global-views'],
+    ]) {
+      const newOrg = initializeOrg();
+      newOrg.organization.features = features;
+      render(
+        <OrganizationStats
+          {...defaultProps}
+          organization={newOrg.organization}
+          selection={newSelection}
+        />,
+        {context: newOrg.routerContext}
+      );
+      act(() => PageFiltersStore.updateProjects(selectedProject, []));
+      expect(screen.queryByText('My Projects')).not.toBeInTheDocument();
+      expect(screen.getByTestId('usage-stats-table')).toBeInTheDocument();
+      cleanup();
+    }
+  });
 });
 
 const mockStatsResponse = {

+ 128 - 50
static/app/views/organizationStats/index.tsx

@@ -8,13 +8,17 @@ import moment from 'moment';
 
 import {DateTimeObject} from 'sentry/components/charts/utils';
 import CompactSelect from 'sentry/components/compactSelect';
+import DatePageFilter from 'sentry/components/datePageFilter';
 import ErrorBoundary from 'sentry/components/errorBoundary';
 import HookOrDefault from 'sentry/components/hookOrDefault';
 import * as Layout from 'sentry/components/layouts/thirds';
+import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
+import PageFiltersContainer from 'sentry/components/organizations/pageFilters/container';
 import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
 import {ChangeData} from 'sentry/components/organizations/timeRangeSelector';
 import PageHeading from 'sentry/components/pageHeading';
 import PageTimeRangeSelector from 'sentry/components/pageTimeRangeSelector';
+import ProjectPageFilter from 'sentry/components/projectPageFilter';
 import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
 import {
   DATA_CATEGORY_NAMES,
@@ -24,8 +28,9 @@ import {
 import {t} from 'sentry/locale';
 import {PageHeader} from 'sentry/styles/organization';
 import space from 'sentry/styles/space';
-import {DataCategory, DateString, Organization, Project} from 'sentry/types';
+import {DataCategory, DateString, Organization, PageFilters, Project} from 'sentry/types';
 import withOrganization from 'sentry/utils/withOrganization';
+import withPageFilters from 'sentry/utils/withPageFilters';
 import HeaderTabs from 'sentry/views/organizationStats/header';
 
 import {CHART_OPTIONS_DATACATEGORY, ChartDataTransform} from './usageChart';
@@ -35,12 +40,21 @@ import UsageStatsProjects from './usageStatsProjects';
 const HookHeader = HookOrDefault({hookName: 'component:org-stats-banner'});
 
 export const PAGE_QUERY_PARAMS = [
-  'pageStatsPeriod',
-  'pageStart',
+  // From DatePageFilter
+  'statsPeriod',
+  'start',
+  'end',
+  'utc',
+  // TODO(Leander): Remove date selector props once project-stats flag is GA
   'pageEnd',
-  'pageUtc',
+  'pageStart',
+  'pageStatsPeriod',
+  'pageStatsUtc',
+  // From data category selector
   'dataCategory',
+  // From UsageOrganizationStats
   'transform',
+  // From UsageProjectStats
   'sort',
   'query',
   'cursor',
@@ -48,6 +62,7 @@ export const PAGE_QUERY_PARAMS = [
 
 type Props = {
   organization: Organization;
+  selection: PageFilters;
 } & RouteComponentProps<{orgId: string}, {}>;
 
 export class OrganizationStats extends Component<Props> {
@@ -71,14 +86,16 @@ export class OrganizationStats extends Component<Props> {
   }
 
   get dataDatetime(): DateTimeObject {
-    const query = this.props.location?.query ?? {};
+    const params = this.hasProjectStats
+      ? this.props.selection.datetime
+      : this.props.location?.query ?? {};
 
     const {
       start,
       end,
       statsPeriod,
       utc: utcString,
-    } = normalizeDateTimeParams(query, {
+    } = normalizeDateTimeParams(params, {
       allowEmptyPeriod: true,
       allowAbsoluteDatetime: true,
       allowAbsolutePageDatetime: true,
@@ -129,6 +146,23 @@ export class OrganizationStats extends Component<Props> {
     return this.props.location?.query?.cursor;
   }
 
+  // Project selection from GlobalSelectionHeader
+  get projectIds(): number[] {
+    return this.hasProjectStats ? this.props.selection.projects : [];
+  }
+
+  /**
+   * Note: For now, we're checking for both project-stats and global-views to enable this new UI
+   * This may change once we GA the project-stats feature flag. These are the planned scenarios:
+   *  - w/o global-views: Project Selector defaults to first project, hence no more 'Org Stats' w/o global-views
+   *  - w/ global-views: Project Selector defaults to 'My Projects', behaviour for 'Org Stats' is preserved
+   */
+  get hasProjectStats(): boolean {
+    return ['project-stats', 'global-views'].every(flag =>
+      this.props.organization.features.includes(flag)
+    );
+  }
+
   getNextLocations = (project: Project): Record<string, LocationDescriptorObject> => {
     const {location, organization} = this.props;
     const nextLocation: LocationDescriptorObject = {
@@ -161,40 +195,18 @@ export class OrganizationStats extends Component<Props> {
     };
   };
 
-  handleUpdateDatetime = (datetime: ChangeData): LocationDescriptorObject => {
-    const {start, end, relative, utc} = datetime;
-
-    if (start && end) {
-      const parser = utc ? moment.utc : moment;
-
-      return this.setStateOnUrl({
-        pageStatsPeriod: undefined,
-        pageStart: parser(start).format(),
-        pageEnd: parser(end).format(),
-        pageUtc: utc ?? undefined,
-      });
-    }
-
-    return this.setStateOnUrl({
-      pageStatsPeriod: relative || undefined,
-      pageStart: undefined,
-      pageEnd: undefined,
-      pageUtc: undefined,
-    });
-  };
-
   /**
-   * TODO: Enable user to set dateStart/dateEnd
-   *
    * See PAGE_QUERY_PARAMS for list of accepted keys on nextState
    */
   setStateOnUrl = (
     nextState: {
       cursor?: string;
       dataCategory?: DataCategory;
+      // TODO(Leander): Remove date selector props once project-stats flag is GA
       pageEnd?: DateString;
       pageStart?: DateString;
       pageStatsPeriod?: string | null;
+      pageStatsUtc?: string | null;
       pageUtc?: boolean | null;
       query?: string;
       sort?: string;
@@ -224,8 +236,57 @@ export class OrganizationStats extends Component<Props> {
     return nextLocation;
   };
 
+  renderProjectPageControl = () => {
+    if (!this.hasProjectStats) {
+      return null;
+    }
+    return (
+      <PageControl>
+        <PageFilterBar>
+          <ProjectPageFilter />
+          <DropdownDataCategory
+            triggerProps={{prefix: t('Category')}}
+            value={this.dataCategory}
+            options={CHART_OPTIONS_DATACATEGORY}
+            onChange={opt =>
+              this.setStateOnUrl({dataCategory: opt.value as DataCategory})
+            }
+          />
+          <DatePageFilter alignDropdown="left" />
+        </PageFilterBar>
+      </PageControl>
+    );
+  };
+
+  // TODO(Leander): Remove the following method once the project-stats flag is GA
+  handleUpdateDatetime = (datetime: ChangeData): LocationDescriptorObject => {
+    const {start, end, relative, utc} = datetime;
+
+    if (start && end) {
+      const parser = utc ? moment.utc : moment;
+
+      return this.setStateOnUrl({
+        pageStatsPeriod: undefined,
+        pageStart: parser(start).format(),
+        pageEnd: parser(end).format(),
+        pageUtc: utc ?? undefined,
+      });
+    }
+
+    return this.setStateOnUrl({
+      pageStatsPeriod: relative || undefined,
+      pageStart: undefined,
+      pageEnd: undefined,
+      pageUtc: undefined,
+    });
+  };
+
+  // TODO(Leander): Remove the following method once the project-stats flag is GA
   renderPageControl = () => {
     const {organization} = this.props;
+    if (this.hasProjectStats) {
+      return null;
+    }
 
     const {start, end, period, utc} = this.dataDatetime;
 
@@ -255,9 +316,15 @@ export class OrganizationStats extends Component<Props> {
     const {organization} = this.props;
     const hasTeamInsights = organization.features.includes('team-insights');
 
+    // We only show UsageProjectStats if multiple projects are selected
+    const shouldRenderProjectStats = this.hasProjectStats
+      ? this.projectIds.includes(-1) || this.projectIds.length !== 1
+      : // Always render if they don't have the proper flags
+        true;
+
     return (
       <SentryDocumentTitle title="Usage Stats">
-        <Fragment>
+        <PageFiltersContainer>
           {hasTeamInsights && (
             <HeaderTabs organization={organization} activeTab="stats" />
           )}
@@ -276,10 +343,9 @@ export class OrganizationStats extends Component<Props> {
                 </Fragment>
               )}
               <HookHeader organization={organization} />
-
+              {this.renderProjectPageControl()}
               <PageGrid>
                 {this.renderPageControl()}
-
                 <ErrorBoundary mini>
                   <UsageStatsOrg
                     organization={organization}
@@ -288,32 +354,35 @@ export class OrganizationStats extends Component<Props> {
                     dataDatetime={this.dataDatetime}
                     chartTransform={this.chartTransform}
                     handleChangeState={this.setStateOnUrl}
+                    projectIds={this.projectIds}
                   />
                 </ErrorBoundary>
               </PageGrid>
-
-              <ErrorBoundary mini>
-                <UsageStatsProjects
-                  organization={organization}
-                  dataCategory={this.dataCategory}
-                  dataCategoryName={this.dataCategoryName}
-                  dataDatetime={this.dataDatetime}
-                  tableSort={this.tableSort}
-                  tableQuery={this.tableQuery}
-                  tableCursor={this.tableCursor}
-                  handleChangeState={this.setStateOnUrl}
-                  getNextLocations={this.getNextLocations}
-                />
-              </ErrorBoundary>
+              {shouldRenderProjectStats && (
+                <ErrorBoundary mini>
+                  <UsageStatsProjects
+                    organization={organization}
+                    dataCategory={this.dataCategory}
+                    dataCategoryName={this.dataCategoryName}
+                    projectIds={this.projectIds}
+                    dataDatetime={this.dataDatetime}
+                    tableSort={this.tableSort}
+                    tableQuery={this.tableQuery}
+                    tableCursor={this.tableCursor}
+                    handleChangeState={this.setStateOnUrl}
+                    getNextLocations={this.getNextLocations}
+                  />
+                </ErrorBoundary>
+              )}
             </Layout.Main>
           </Body>
-        </Fragment>
+        </PageFiltersContainer>
       </SentryDocumentTitle>
     );
   }
 }
 
-export default withOrganization(OrganizationStats);
+export default withPageFilters(withOrganization(OrganizationStats));
 
 const PageGrid = styled('div')`
   display: grid;
@@ -346,7 +415,6 @@ const DropdownDataCategory = styled(CompactSelect)`
 
 const StyledPageTimeRangeSelector = styled(PageTimeRangeSelector)`
   grid-column: auto / span 1;
-
   @media (min-width: ${p => p.theme.breakpoints.small}) {
     grid-column: auto / span 2;
   }
@@ -360,3 +428,13 @@ const Body = styled(Layout.Body)`
     display: block;
   }
 `;
+
+const PageControl = styled('div')`
+  display: grid;
+  width: 100%;
+  margin-bottom: ${space(2)};
+  grid-template-columns: minmax(0, max-content);
+  @media (max-width: ${p => p.theme.breakpoints.small}) {
+    grid-template-columns: minmax(0, 1fr);
+  }
+`;

+ 8 - 5
static/app/views/organizationStats/usageStatsOrg.tsx

@@ -1,6 +1,7 @@
 import {Fragment} from 'react';
 import styled from '@emotion/styled';
 import * as Sentry from '@sentry/react';
+import isEqual from 'lodash/isEqual';
 import moment from 'moment';
 
 import AsyncComponent from 'sentry/components/asyncComponent';
@@ -39,6 +40,7 @@ type Props = {
     transform?: ChartDataTransform;
   }) => void;
   organization: Organization;
+  projectIds: number[];
   chartTransform?: string;
 } & AsyncComponent['props'];
 
@@ -48,14 +50,15 @@ type State = {
 
 class UsageStatsOrganization extends AsyncComponent<Props, State> {
   componentDidUpdate(prevProps: Props) {
-    const {dataDatetime: prevDateTime} = prevProps;
-    const {dataDatetime: currDateTime} = this.props;
+    const {dataDatetime: prevDateTime, projectIds: prevProjectIds} = prevProps;
+    const {dataDatetime: currDateTime, projectIds: currProjectIds} = this.props;
 
     if (
       prevDateTime.start !== currDateTime.start ||
       prevDateTime.end !== currDateTime.end ||
       prevDateTime.period !== currDateTime.period ||
-      prevDateTime.utc !== currDateTime.utc
+      prevDateTime.utc !== currDateTime.utc ||
+      !isEqual(prevProjectIds, currProjectIds)
     ) {
       this.reloadData();
     }
@@ -71,7 +74,7 @@ class UsageStatsOrganization extends AsyncComponent<Props, State> {
   }
 
   get endpointQuery() {
-    const {dataDatetime} = this.props;
+    const {dataDatetime, projectIds} = this.props;
 
     const queryDatetime =
       dataDatetime.start && dataDatetime.end
@@ -88,6 +91,7 @@ class UsageStatsOrganization extends AsyncComponent<Props, State> {
       ...queryDatetime,
       interval: getSeriesApiInterval(dataDatetime),
       groupBy: ['category', 'outcome'],
+      project: projectIds,
       field: ['sum(quantity)'],
     };
   }
@@ -399,7 +403,6 @@ class UsageStatsOrganization extends AsyncComponent<Props, State> {
 
     const hasError = error || !!dataError;
     const chartErrors: any = dataError ? {...errors, data: dataError} : errors; // TODO(ts): AsyncComponent
-
     return (
       <UsageChart
         isLoading={loading}

+ 31 - 7
static/app/views/organizationStats/usageStatsProjects.tsx

@@ -2,6 +2,7 @@ import {Fragment} from 'react';
 import styled from '@emotion/styled';
 import * as Sentry from '@sentry/react';
 import {LocationDescriptorObject} from 'history';
+import isEqual from 'lodash/isEqual';
 
 import AsyncComponent from 'sentry/components/asyncComponent';
 import {DateTimeObject, getSeriesApiInterval} from 'sentry/components/charts/utils';
@@ -33,6 +34,7 @@ type Props = {
   ) => LocationDescriptorObject;
   loadingProjects: boolean;
   organization: Organization;
+  projectIds: number[];
   projects: Project[];
   tableCursor?: string;
   tableQuery?: string;
@@ -57,15 +59,24 @@ class UsageStatsProjects extends AsyncComponent<Props, State> {
   static MAX_ROWS_USAGE_TABLE = 25;
 
   componentDidUpdate(prevProps: Props) {
-    const {dataDatetime: prevDateTime, dataCategory: prevDataCategory} = prevProps;
-    const {dataDatetime: currDateTime, dataCategory: currDataCategory} = this.props;
+    const {
+      dataDatetime: prevDateTime,
+      dataCategory: prevDataCategory,
+      projectIds: prevProjectIds,
+    } = prevProps;
+    const {
+      dataDatetime: currDateTime,
+      dataCategory: currDataCategory,
+      projectIds: currProjectIds,
+    } = this.props;
 
     if (
       prevDateTime.start !== currDateTime.start ||
       prevDateTime.end !== currDateTime.end ||
       prevDateTime.period !== currDateTime.period ||
       prevDateTime.utc !== currDateTime.utc ||
-      currDataCategory !== prevDataCategory
+      prevDataCategory !== currDataCategory ||
+      !isEqual(prevProjectIds, currProjectIds)
     ) {
       this.reloadData();
     }
@@ -81,7 +92,7 @@ class UsageStatsProjects extends AsyncComponent<Props, State> {
   }
 
   get endpointQuery() {
-    const {dataDatetime, dataCategory} = this.props;
+    const {dataDatetime, dataCategory, projectIds} = this.props;
 
     const queryDatetime =
       dataDatetime.start && dataDatetime.end
@@ -100,7 +111,7 @@ class UsageStatsProjects extends AsyncComponent<Props, State> {
       interval: getSeriesApiInterval(dataDatetime),
       groupBy: ['outcome', 'project'],
       field: ['sum(quantity)'],
-      project: '-1', // get all project user has access to
+      project: projectIds,
       category: dataCategory.slice(0, -1), // backend is singular
     };
   }
@@ -172,14 +183,27 @@ class UsageStatsProjects extends AsyncComponent<Props, State> {
     }"; cursor="0:${nextOffset}:0"`;
   }
 
+  get projectSelectionFilter(): (p: Project) => boolean {
+    const {projectIds} = this.props;
+    const selectedProjects = new Set(projectIds.map(id => `${id}`));
+
+    // If 'My Projects' or 'All Projects' are selected
+    return selectedProjects.size === 0 || selectedProjects.has('-1')
+      ? _p => true
+      : p => selectedProjects.has(p.id);
+  }
+
   /**
    * Filter projects if there's a query
    */
   get filteredProjects() {
     const {projects, tableQuery} = this.props;
     return tableQuery
-      ? projects.filter(p => p.slug.includes(tableQuery) && p.hasAccess)
-      : projects.filter(p => p.hasAccess);
+      ? projects.filter(
+          p =>
+            p.slug.includes(tableQuery) && p.hasAccess && this.projectSelectionFilter(p)
+        )
+      : projects.filter(p => p.hasAccess && this.projectSelectionFilter(p));
   }
 
   get tableHeader() {