search: remove all GraphQL dependencies from SearchResultsInfoBar (#15991)

This is part of the work to refactor search results UI components for streaming search. SearchResultsInfoBar will now receive its stats/progress as a JSX element property. Created a new component for the GQL search stats.
This commit is contained in:
Juliana Peña 2020-11-19 11:45:28 -08:00 committed by GitHub
parent 214788e2b7
commit 8f09fe258b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 170 additions and 170 deletions

View File

@ -119,7 +119,7 @@ export const MULTIPLE_MATCH_RESULT = {
export const SEARCH_RESULT = {
__typename: 'SearchResults' as const,
limitHit: false,
resultCount: 1,
matchCount: 1,
approximateResultCount: '1',
missing: [] as IRepository[],
cloning: [] as IRepository[],
@ -144,7 +144,7 @@ export const SEARCH_RESULT = {
export const MULTIPLE_SEARCH_RESULT = {
...SEARCH_RESULT,
limitHit: false,
resultCount: 136,
matchCount: 136,
approximateResultCount: '136',
results: [
RESULT,

View File

@ -1,25 +1,17 @@
import * as GQL from '../../../../shared/src/graphql/schema'
import * as H from 'history'
import * as React from 'react'
import AlertCircleIcon from 'mdi-react/AlertCircleIcon'
import ArrowCollapseVerticalIcon from 'mdi-react/ArrowCollapseVerticalIcon'
import ArrowExpandVerticalIcon from 'mdi-react/ArrowExpandVerticalIcon'
import CalculatorIcon from 'mdi-react/CalculatorIcon'
import CheckIcon from 'mdi-react/CheckIcon'
import classNames from 'classnames'
import CloudDownloadIcon from 'mdi-react/CloudDownloadIcon'
import DownloadIcon from 'mdi-react/DownloadIcon'
import FormatQuoteOpenIcon from 'mdi-react/FormatQuoteOpenIcon'
import MapSearchIcon from 'mdi-react/MapSearchIcon'
import TimerSandIcon from 'mdi-react/TimerSandIcon'
import { AuthenticatedUser } from '../../auth'
import { ContributableMenu } from '../../../../shared/src/api/protocol'
import { ExtensionsControllerProps } from '../../../../shared/src/extensions/controller'
import { PatternTypeProps, SearchStreamingProps } from '..'
import { PatternTypeProps } from '..'
import { PlatformContextProps } from '../../../../shared/src/platform/context'
import { pluralize } from '../../../../shared/src/util/strings'
import { SearchPatternType } from '../../graphql-operations'
import { StreamingProgress } from './streaming-progress/StreamingProgress'
import { TelemetryProps } from '../../../../shared/src/telemetry/telemetryService'
import { WebActionsNavItems as ActionsNavItems } from '../../components/shared'
@ -27,21 +19,18 @@ interface SearchResultsInfoBarProps
extends ExtensionsControllerProps<'executeCommand' | 'services'>,
PlatformContextProps<'forceUpdateTooltip' | 'settings'>,
TelemetryProps,
PatternTypeProps,
SearchStreamingProps {
PatternTypeProps {
/** The currently authenticated user or null */
authenticatedUser: AuthenticatedUser | null
/** The loaded search results and metadata */
/** The search query and if any results were found */
query?: string
results: GQL.ISearchResults
onShowMoreResultsClick?: () => void
resultsFound: boolean
// Expand all feature
allExpanded: boolean
onExpandAllResultsToggle: () => void
showDotComMarketing: boolean
// Saved queries
showSavedQueryButton?: boolean
onDidCreateSavedQuery: () => void
@ -51,6 +40,8 @@ interface SearchResultsInfoBarProps
location: H.Location
className?: string
stats: JSX.Element
}
/**
@ -76,158 +67,67 @@ const QuotesInterpretedLiterallyNotice: React.FunctionComponent<SearchResultsInf
* The info bar shown over the search results list that displays metadata
* and a few actions like expand all and save query
*/
export const SearchResultsInfoBar: React.FunctionComponent<SearchResultsInfoBarProps> = props => {
const excludeForksFilter = props.results.dynamicFilters.find(filter => filter.value === 'fork:yes')
const excludedForksCount = excludeForksFilter?.count || 0
const excludeArchivedFilter = props.results.dynamicFilters.find(filter => filter.value === 'archived:yes')
const excludedArchivedCount = excludeArchivedFilter?.count || 0
return (
<div className={classNames(props.className, 'search-results-info-bar')} data-testid="results-info-bar">
<small className="search-results-info-bar__row">
{!props.searchStreaming && (
<div className="search-results-info-bar__row-left">
<div className="search-results-info-bar__notice test-search-results-stats">
<span>
<CalculatorIcon className="icon-inline" /> {props.results.approximateResultCount}{' '}
{pluralize('result', props.results.matchCount)} in{' '}
{(props.results.elapsedMilliseconds / 1000).toFixed(2)} seconds
{props.results.indexUnavailable && ' (index unavailable)'}
{props.results.limitHit && String.fromCharCode(160)}
</span>
export const SearchResultsInfoBar: React.FunctionComponent<SearchResultsInfoBarProps> = props => (
<div className={classNames(props.className, 'search-results-info-bar')} data-testid="results-info-bar">
<small className="search-results-info-bar__row">
<div className="search-results-info-bar__row-left">
{props.stats}
<QuotesInterpretedLiterallyNotice {...props} />
</div>
{props.results.limitHit && props.onShowMoreResultsClick && (
<button
type="button"
className="btn btn-link btn-sm p-0"
onClick={props.onShowMoreResultsClick}
>
(show more)
</button>
<ul className="search-results-info-bar__row-right nav align-items-center justify-content-end">
<ActionsNavItems
{...props}
extraContext={{ searchQuery: props.query || null }}
menu={ContributableMenu.SearchResultsToolbar}
wrapInList={false}
showLoadingSpinnerDuringExecution={true}
actionItemClass="btn btn-link nav-link text-decoration-none"
/>
{props.resultsFound && (
<li className="nav-item">
<button
type="button"
onClick={props.onExpandAllResultsToggle}
className="btn btn-link nav-link text-decoration-none"
data-tooltip={`${props.allExpanded ? 'Hide' : 'Show'} more matches on all results`}
>
{props.allExpanded ? (
<>
<ArrowCollapseVerticalIcon className="icon-inline" /> Collapse all
</>
) : (
<>
<ArrowExpandVerticalIcon className="icon-inline" /> Expand all
</>
)}
</div>
{excludedForksCount > 0 && (
<div
className="search-results-info-bar__notice"
data-tooltip="add fork:yes to include forks"
>
<span>
<AlertCircleIcon className="icon-inline" /> {excludedForksCount} forked{' '}
{pluralize('repository', excludedForksCount, 'repositories')} excluded
</span>
</div>
)}
{excludedArchivedCount > 0 && (
<div
className="search-results-info-bar__notice"
data-tooltip="add archived:yes to include archives"
>
<span>
<AlertCircleIcon className="icon-inline" /> {excludedArchivedCount} archived{' '}
{pluralize('repository', excludedArchivedCount, 'repositories')} excluded
</span>
</div>
)}
{props.results.missing.length > 0 && (
<div
className="search-results-info-bar__notice"
data-tooltip={props.results.missing.map(repo => repo.name).join('\n')}
>
<span>
<MapSearchIcon className="icon-inline" /> {props.results.missing.length}{' '}
{pluralize('repository', props.results.missing.length, 'repositories')} not found
</span>
</div>
)}
{props.results.timedout.length > 0 && (
<div
className="search-results-info-bar__notice"
data-tooltip={props.results.timedout.map(repo => repo.name).join('\n')}
>
<span>
<TimerSandIcon className="icon-inline" /> {props.results.timedout.length}{' '}
{pluralize('repository', props.results.timedout.length, 'repositories')} timed out
(reload to try again, or specify a longer "timeout:" in your query)
</span>
</div>
)}
{props.results.cloning.length > 0 && (
<div
className="search-results-info-bar__notice"
data-tooltip={props.results.cloning.map(repo => repo.name).join('\n')}
>
<span>
<CloudDownloadIcon className="icon-inline" /> {props.results.cloning.length}{' '}
{pluralize('repository', props.results.cloning.length, 'repositories')} cloning
(reload to try again)
</span>
</div>
)}
<QuotesInterpretedLiterallyNotice {...props} />
</div>
</button>
</li>
)}
{props.searchStreaming && <StreamingProgress />}
<ul className="search-results-info-bar__row-right nav align-items-center justify-content-end">
<ActionsNavItems
{...props}
extraContext={{ searchQuery: props.query || null }}
menu={ContributableMenu.SearchResultsToolbar}
wrapInList={false}
showLoadingSpinnerDuringExecution={true}
actionItemClass="btn btn-link nav-link text-decoration-none"
/>
{props.results.results.length > 0 && (
<li className="nav-item">
<button
type="button"
onClick={props.onExpandAllResultsToggle}
className="btn btn-link nav-link text-decoration-none"
data-tooltip={`${props.allExpanded ? 'Hide' : 'Show'} more matches on all results`}
>
{props.allExpanded ? (
<>
<ArrowCollapseVerticalIcon className="icon-inline" /> Collapse all
</>
) : (
<>
<ArrowExpandVerticalIcon className="icon-inline" /> Expand all
</>
)}
</button>
</li>
)}
{props.showSavedQueryButton !== false && props.authenticatedUser && (
<li className="nav-item">
<button
type="button"
onClick={props.onSaveQueryClick}
className="btn btn-link nav-link text-decoration-none"
disabled={props.didSave}
>
{props.didSave ? (
<>
<CheckIcon className="icon-inline" /> Query saved
</>
) : (
<>
<DownloadIcon className="icon-inline test-save-search-link" /> Save this search
query
</>
)}
</button>
</li>
)}
</ul>
</small>
</div>
)
}
{props.showSavedQueryButton !== false && props.authenticatedUser && (
<li className="nav-item">
<button
type="button"
onClick={props.onSaveQueryClick}
className="btn btn-link nav-link text-decoration-none"
disabled={props.didSave}
>
{props.didSave ? (
<>
<CheckIcon className="icon-inline" /> Query saved
</>
) : (
<>
<DownloadIcon className="icon-inline test-save-search-link" /> Save this search
query
</>
)}
</button>
</li>
)}
</ul>
</small>
</div>
)

View File

@ -40,6 +40,7 @@ import { AuthenticatedUser } from '../../auth'
import { SearchResultTypeTabs } from './SearchResultTypeTabs'
import { QueryState } from '../helpers'
import { PerformanceWarningAlert } from '../../site/PerformanceWarningAlert'
import { SearchResultsStats } from './SearchResultsStats'
const isSearchResults = (value: unknown): value is GQL.ISearchResults =>
typeof value === 'object' &&
@ -377,9 +378,14 @@ export class SearchResultsList extends React.PureComponent<SearchResultsListProp
<SearchResultsInfoBar
{...this.props}
query={parsedQuery}
results={results}
showDotComMarketing={this.props.isSourcegraphDotCom}
resultsFound={results.results.length > 0}
className="border-bottom flex-grow-1"
stats={
<SearchResultsStats
results={results}
onShowMoreResultsClick={this.props.onShowMoreResultsClick}
/>
}
/>
</div>

View File

@ -0,0 +1,94 @@
import AlertCircleIcon from 'mdi-react/AlertCircleIcon'
import CalculatorIcon from 'mdi-react/CalculatorIcon'
import CloudDownloadIcon from 'mdi-react/CloudDownloadIcon'
import MapSearchIcon from 'mdi-react/MapSearchIcon'
import TimerSandIcon from 'mdi-react/TimerSandIcon'
import React from 'react'
import * as GQL from '../../../../shared/src/graphql/schema'
import { pluralize } from '../../../../shared/src/util/strings'
/** Search result statistics for GraphQL searches */
export const SearchResultsStats: React.FunctionComponent<{
results: GQL.ISearchResults
onShowMoreResultsClick?: () => void
}> = ({ results, onShowMoreResultsClick }) => {
const excludeForksFilter = results.dynamicFilters.find(filter => filter.value === 'fork:yes')
const excludedForksCount = excludeForksFilter?.count || 0
const excludeArchivedFilter = results.dynamicFilters.find(filter => filter.value === 'archived:yes')
const excludedArchivedCount = excludeArchivedFilter?.count || 0
return (
<>
<div className="search-results-info-bar__notice test-search-results-stats">
<span>
<CalculatorIcon className="icon-inline" /> {results.approximateResultCount}{' '}
{pluralize('result', results.matchCount)} in {(results.elapsedMilliseconds / 1000).toFixed(2)}{' '}
seconds
{results.indexUnavailable && ' (index unavailable)'}
{results.limitHit && String.fromCharCode(160)}
</span>
{results.limitHit && onShowMoreResultsClick && (
<button type="button" className="btn btn-link btn-sm p-0" onClick={onShowMoreResultsClick}>
(show more)
</button>
)}
</div>
{excludedForksCount > 0 && (
<div className="search-results-info-bar__notice" data-tooltip="add fork:yes to include forks">
<span>
<AlertCircleIcon className="icon-inline" /> {excludedForksCount} forked{' '}
{pluralize('repository', excludedForksCount, 'repositories')} excluded
</span>
</div>
)}
{excludedArchivedCount > 0 && (
<div className="search-results-info-bar__notice" data-tooltip="add archived:yes to include archives">
<span>
<AlertCircleIcon className="icon-inline" /> {excludedArchivedCount} archived{' '}
{pluralize('repository', excludedArchivedCount, 'repositories')} excluded
</span>
</div>
)}
{results.missing.length > 0 && (
<div
className="search-results-info-bar__notice"
data-tooltip={results.missing.map(repo => repo.name).join('\n')}
>
<span>
<MapSearchIcon className="icon-inline" /> {results.missing.length}{' '}
{pluralize('repository', results.missing.length, 'repositories')} not found
</span>
</div>
)}
{results.timedout.length > 0 && (
<div
className="search-results-info-bar__notice"
data-tooltip={results.timedout.map(repo => repo.name).join('\n')}
>
<span>
<TimerSandIcon className="icon-inline" /> {results.timedout.length}{' '}
{pluralize('repository', results.timedout.length, 'repositories')} timed out (reload to try
again, or specify a longer "timeout:" in your query)
</span>
</div>
)}
{results.cloning.length > 0 && (
<div
className="search-results-info-bar__notice"
data-tooltip={results.cloning.map(repo => repo.name).join('\n')}
>
<span>
<CloudDownloadIcon className="icon-inline" /> {results.cloning.length}{' '}
{pluralize('repository', results.cloning.length, 'repositories')} cloning (reload to try again)
</span>
</div>
)}
</>
)
}