mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 17:11:49 +00:00
Add pagination to saved searches pages (#45705)
* [Saved Searches] add pagination * [Search Page] remove old home panels
This commit is contained in:
parent
88ac63889e
commit
202a984759
@ -47,6 +47,7 @@ All notable changes to Sourcegraph are documented in this file.
|
||||
- The extension registry no longer supports browsing, creating, or updating legacy extensions. Existing extensions may still be enabled or disabled in user settings and may be listed via the API. (The extension API was deprecated in 2022-09 but is still available if the `enableLegacyExtensions` site config experimental features flag is enabled.)
|
||||
- User and organization auto-defined search contexts have been permanently removed along with the `autoDefinedSearchContexts` GraphQL query. The only auto-defined context now is the `global` context. [#46083](https://github.com/sourcegraph/sourcegraph/pull/46083)
|
||||
- The settings `experimentalFeatures.showSearchContext`, `experimentalFeatures.showSearchNotebook`, and `experimentalFeatures.codeMonitoring` have been removed and these features are now permanently enabled when available. [#46086](https://github.com/sourcegraph/sourcegraph/pull/46086)
|
||||
- The legacy panels on the homepage (recent searches, etc) which were turned off by default but could still be re-enabled by setting `experimentalFeatures.showEnterpriseHomePanels` to true, are permanently removed now. [#45705](https://github.com/sourcegraph/sourcegraph/pull/45705)
|
||||
|
||||
## 4.3.0
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
// @ts-check
|
||||
const path = require('path')
|
||||
|
||||
const logger = require('signale')
|
||||
const semver = require('semver')
|
||||
const logger = require('signale')
|
||||
|
||||
/** @type {import('@babel/core').ConfigFunction} */
|
||||
module.exports = api => {
|
||||
|
||||
@ -14,9 +14,11 @@ import { HistorySidebarProps } from '../HistorySidebarView'
|
||||
import styles from '../../search/SearchSidebarView.module.scss'
|
||||
|
||||
const savedSearchQuery = gql`
|
||||
query SavedSearches {
|
||||
savedSearches {
|
||||
...SavedSearchFields
|
||||
query SavedSearches($namespace: ID!) {
|
||||
savedSearches(namespace: $namespace, first: 15) {
|
||||
nodes {
|
||||
...SavedSearchFields
|
||||
}
|
||||
}
|
||||
}
|
||||
fragment SavedSearchFields on SavedSearch {
|
||||
@ -35,10 +37,10 @@ const savedSearchQuery = gql`
|
||||
`
|
||||
|
||||
export const SavedSearchesSection: React.FunctionComponent<React.PropsWithChildren<HistorySidebarProps>> = ({
|
||||
authenticatedUser,
|
||||
platformContext,
|
||||
extensionCoreAPI,
|
||||
}) => {
|
||||
const itemsToLoad = 15
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
|
||||
const savedSearchesResult = useObservable(
|
||||
@ -47,7 +49,7 @@ export const SavedSearchesSection: React.FunctionComponent<React.PropsWithChildr
|
||||
platformContext
|
||||
.requestGraphQL<SavedSearchesResult, SavedSearchesVariables>({
|
||||
request: savedSearchQuery,
|
||||
variables: {},
|
||||
variables: { namespace: authenticatedUser.id },
|
||||
mightContainPrivateInfo: true,
|
||||
})
|
||||
.pipe(
|
||||
@ -56,11 +58,11 @@ export const SavedSearchesSection: React.FunctionComponent<React.PropsWithChildr
|
||||
return [null]
|
||||
})
|
||||
),
|
||||
[platformContext]
|
||||
[platformContext, authenticatedUser.id]
|
||||
)
|
||||
)
|
||||
|
||||
const savedSearches = savedSearchesResult?.data?.savedSearches
|
||||
const savedSearches = savedSearchesResult?.data?.savedSearches?.nodes
|
||||
|
||||
if (!savedSearches || savedSearches.length === 0) {
|
||||
return null
|
||||
@ -97,21 +99,19 @@ export const SavedSearchesSection: React.FunctionComponent<React.PropsWithChildr
|
||||
|
||||
{!collapsed && savedSearches && (
|
||||
<div className={classNames('p-1', styles.sidebarSectionList)}>
|
||||
{savedSearches
|
||||
.filter((search, index) => index < itemsToLoad)
|
||||
.map(search => (
|
||||
<div key={search.id}>
|
||||
<small className={styles.sidebarSectionListItem}>
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0 text-left text-decoration-none"
|
||||
onClick={() => onSavedSearchClick(search.query)}
|
||||
>
|
||||
{search.description}
|
||||
</Button>
|
||||
</small>
|
||||
</div>
|
||||
))}
|
||||
{savedSearches.map(search => (
|
||||
<div key={search.id}>
|
||||
<small className={styles.sidebarSectionListItem}>
|
||||
<Button
|
||||
variant="link"
|
||||
className="p-0 text-left text-decoration-none"
|
||||
onClick={() => onSavedSearchClick(search.query)}
|
||||
>
|
||||
{search.description}
|
||||
</Button>
|
||||
</small>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -48,7 +48,7 @@ import type { RepoSettingsAreaRoute } from './repo/settings/RepoSettingsArea'
|
||||
import type { RepoSettingsSideBarGroup } from './repo/settings/RepoSettingsSidebar'
|
||||
import type { LayoutRouteComponentProps, LayoutRouteProps } from './routes'
|
||||
import { EnterprisePageRoutes, PageRoutes } from './routes.constants'
|
||||
import { HomePanelsProps, parseSearchURLQuery, SearchAggregationProps, SearchStreamingProps } from './search'
|
||||
import { parseSearchURLQuery, SearchAggregationProps, SearchStreamingProps } from './search'
|
||||
import { NotepadContainer } from './search/Notepad'
|
||||
import type { SiteAdminAreaRoute } from './site-admin/SiteAdminArea'
|
||||
import type { SiteAdminSideBarGroups } from './site-admin/SiteAdminSidebar'
|
||||
@ -69,7 +69,6 @@ export interface LayoutProps
|
||||
ExtensionsControllerProps,
|
||||
TelemetryProps,
|
||||
SearchContextProps,
|
||||
HomePanelsProps,
|
||||
SearchStreamingProps,
|
||||
CodeIntelligenceProps,
|
||||
BatchChangesProps,
|
||||
|
||||
@ -39,6 +39,7 @@ export interface UsePaginatedConnectionResult<TData> extends PaginationProps {
|
||||
connection?: PaginatedConnection<TData>
|
||||
loading: boolean
|
||||
error?: ApolloError
|
||||
refetch: () => any
|
||||
}
|
||||
|
||||
interface UsePaginatedConnectionConfig<TResult> {
|
||||
@ -55,7 +56,7 @@ interface UsePaginatedConnectionConfig<TResult> {
|
||||
interface UsePaginatedConnectionParameters<TResult, TVariables extends PaginatedConnectionQueryArguments, TData> {
|
||||
query: string
|
||||
variables: Omit<TVariables, 'first' | 'last' | 'before' | 'after'>
|
||||
getConnection: (result: GraphQLResult<TResult>) => PaginatedConnection<TData>
|
||||
getConnection: (result: GraphQLResult<TResult>) => PaginatedConnection<TData> | undefined
|
||||
options?: UsePaginatedConnectionConfig<TResult>
|
||||
}
|
||||
|
||||
@ -135,6 +136,7 @@ export const usePageSwitcherPagination = <TResult, TVariables extends PaginatedC
|
||||
connection,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
hasNextPage: connection?.pageInfo?.hasNextPage ?? null,
|
||||
hasPreviousPage: connection?.pageInfo?.hasPreviousPage ?? null,
|
||||
goToNextPage,
|
||||
|
||||
@ -4,12 +4,6 @@ import { mergeSettings } from '@sourcegraph/shared/src/settings/settings'
|
||||
import { testUserID, sharedGraphQlResults } from '@sourcegraph/shared/src/testing/integration/graphQlResults'
|
||||
|
||||
import { WebGraphQlOperations } from '../graphql-operations'
|
||||
import {
|
||||
collaboratorsPayload,
|
||||
recentFilesPayload,
|
||||
recentSearchesPayload,
|
||||
savedSearchesPayload,
|
||||
} from '../search/panels/utils'
|
||||
|
||||
import { builtinAuthProvider, siteGQLID, siteID } from './jscontext'
|
||||
|
||||
@ -123,8 +117,12 @@ export const commonWebGraphQlResults: Partial<WebGraphQlOperations & SharedGraph
|
||||
},
|
||||
},
|
||||
}),
|
||||
savedSearches: () => ({
|
||||
savedSearches: [],
|
||||
SavedSearches: () => ({
|
||||
savedSearches: {
|
||||
nodes: [],
|
||||
totalCount: 0,
|
||||
pageInfo: { startCursor: null, endCursor: null, hasNextPage: false, hasPreviousPage: false },
|
||||
},
|
||||
}),
|
||||
LogEvents: () => ({
|
||||
logEvents: {
|
||||
@ -157,16 +155,6 @@ export const commonWebGraphQlResults: Partial<WebGraphQlOperations & SharedGraph
|
||||
OrgFeatureFlagOverrides: () => ({
|
||||
organizationFeatureFlagOverrides: [],
|
||||
}),
|
||||
HomePanelsQuery: () => ({
|
||||
node: {
|
||||
__typename: 'User',
|
||||
recentlySearchedRepositoriesLogs: recentSearchesPayload(),
|
||||
recentSearchesLogs: recentSearchesPayload(),
|
||||
recentFilesLogs: recentFilesPayload(),
|
||||
collaborators: collaboratorsPayload(),
|
||||
},
|
||||
savedSearches: savedSearchesPayload(),
|
||||
}),
|
||||
SearchHistoryEventLogsQuery: () => ({
|
||||
currentUser: {
|
||||
__typename: 'User',
|
||||
|
||||
@ -495,18 +495,28 @@ describe('Search', () => {
|
||||
test('is styled correctly, with saved searches', async () => {
|
||||
testContext.overrideGraphQL({
|
||||
...commonSearchGraphQLResults,
|
||||
savedSearches: () => ({
|
||||
savedSearches: [
|
||||
{
|
||||
description: 'Demo',
|
||||
id: 'U2F2ZWRTZWFyY2g6NQ==',
|
||||
namespace: { __typename: 'User', id: 'user123', namespaceName: 'test' },
|
||||
notify: false,
|
||||
notifySlack: false,
|
||||
query: 'context:global Batch Change patternType:literal',
|
||||
slackWebhookURL: null,
|
||||
SavedSearches: () => ({
|
||||
savedSearches: {
|
||||
nodes: [
|
||||
{
|
||||
__typename: 'SavedSearch',
|
||||
description: 'Demo',
|
||||
id: 'U2F2ZWRTZWFyY2g6NQ==',
|
||||
namespace: { __typename: 'User', id: 'user123', namespaceName: 'test' },
|
||||
notify: false,
|
||||
notifySlack: false,
|
||||
query: 'context:global Batch Change patternType:literal',
|
||||
slackWebhookURL: null,
|
||||
},
|
||||
],
|
||||
totalCount: 1,
|
||||
pageInfo: {
|
||||
startCursor: 'U2F2ZWRTZWFyY2g6NQ==',
|
||||
endCursor: 'U2F2ZWRTZWFyY2g6NQ==',
|
||||
hasNextPage: false,
|
||||
hasPreviousPage: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
|
||||
@ -5,18 +5,29 @@ import { VisuallyHidden } from '@reach/visually-hidden'
|
||||
import classNames from 'classnames'
|
||||
import { RouteComponentProps, useLocation } from 'react-router'
|
||||
import { Subject, Subscription } from 'rxjs'
|
||||
import { catchError, map, mapTo, startWith, switchMap } from 'rxjs/operators'
|
||||
import { catchError, mapTo, switchMap } from 'rxjs/operators'
|
||||
import { useCallbackRef } from 'use-callback-ref'
|
||||
|
||||
import { asError, ErrorLike, isErrorLike, logger } from '@sourcegraph/common'
|
||||
import { logger } from '@sourcegraph/common'
|
||||
import { SearchPatternTypeProps } from '@sourcegraph/shared/src/search'
|
||||
import { buildSearchURLQuery } from '@sourcegraph/shared/src/util/url'
|
||||
import { Container, PageHeader, LoadingSpinner, Button, Link, Icon, Tooltip, ErrorAlert } from '@sourcegraph/wildcard'
|
||||
import {
|
||||
Container,
|
||||
PageHeader,
|
||||
LoadingSpinner,
|
||||
Button,
|
||||
Link,
|
||||
Icon,
|
||||
Tooltip,
|
||||
ErrorAlert,
|
||||
PageSwitcher,
|
||||
} from '@sourcegraph/wildcard'
|
||||
|
||||
import { usePageSwitcherPagination } from '../components/FilteredConnection/hooks/usePageSwitcherPagination'
|
||||
import { PageTitle } from '../components/PageTitle'
|
||||
import { SavedSearchFields } from '../graphql-operations'
|
||||
import { SavedSearchFields, SavedSearchesResult, SavedSearchesVariables } from '../graphql-operations'
|
||||
import { NamespaceProps } from '../namespaces'
|
||||
import { deleteSavedSearch, fetchSavedSearches } from '../search/backend'
|
||||
import { deleteSavedSearch, savedSearchesQuery } from '../search/backend'
|
||||
import { useNavbarQueryState } from '../stores'
|
||||
import { eventLogger } from '../tracking/eventLogger'
|
||||
|
||||
@ -121,93 +132,90 @@ class SavedSearchNode extends React.PureComponent<NodeProps, NodeState> {
|
||||
}
|
||||
}
|
||||
|
||||
interface State {
|
||||
savedSearchesOrError?: SavedSearchFields[] | ErrorLike
|
||||
}
|
||||
|
||||
interface Props extends RouteComponentProps, NamespaceProps {}
|
||||
|
||||
export class SavedSearchListPage extends React.Component<Props, State> {
|
||||
public subscriptions = new Subscription()
|
||||
private refreshRequests = new Subject<void>()
|
||||
|
||||
public state: State = {}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this.subscriptions.add(
|
||||
this.refreshRequests
|
||||
.pipe(
|
||||
startWith(undefined),
|
||||
switchMap(() => fetchSavedSearches().pipe(catchError(error => [asError(error)]))),
|
||||
map(savedSearchesOrError => ({ savedSearchesOrError }))
|
||||
)
|
||||
.subscribe(newState => this.setState(newState as State))
|
||||
)
|
||||
export const SavedSearchListPage: React.FunctionComponent<Props> = props => {
|
||||
React.useEffect(() => {
|
||||
eventLogger.logViewEvent('SavedSearchListPage')
|
||||
}
|
||||
}, [])
|
||||
|
||||
public render(): JSX.Element | null {
|
||||
return (
|
||||
<div className={styles.savedSearchListPage} data-testid="saved-searches-list-page">
|
||||
<PageHeader
|
||||
description="Manage notifications and alerts for specific search queries."
|
||||
actions={
|
||||
<Button
|
||||
to={`${this.props.match.path}/add`}
|
||||
className="test-add-saved-search-button"
|
||||
variant="primary"
|
||||
as={Link}
|
||||
>
|
||||
<Icon aria-hidden={true} svgPath={mdiPlus} /> Add saved search
|
||||
</Button>
|
||||
}
|
||||
className="mb-3"
|
||||
>
|
||||
<PageTitle title="Saved searches" />
|
||||
<PageHeader.Heading as="h3" styleAs="h2">
|
||||
<PageHeader.Breadcrumb>Saved searches</PageHeader.Breadcrumb>
|
||||
</PageHeader.Heading>
|
||||
</PageHeader>
|
||||
<SavedSearchListPageContent onDelete={this.onDelete} {...this.props} {...this.state} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const { connection, loading, error, refetch, ...paginationProps } = usePageSwitcherPagination<
|
||||
SavedSearchesResult,
|
||||
SavedSearchesVariables,
|
||||
SavedSearchFields
|
||||
>({
|
||||
query: savedSearchesQuery,
|
||||
variables: { namespace: props.namespace.id },
|
||||
getConnection: ({ data }) => data?.savedSearches || undefined,
|
||||
})
|
||||
|
||||
private onDelete = (): void => {
|
||||
this.refreshRequests.next()
|
||||
}
|
||||
return (
|
||||
<div className={styles.savedSearchListPage} data-testid="saved-searches-list-page">
|
||||
<PageHeader
|
||||
description="Manage notifications and alerts for specific search queries."
|
||||
actions={
|
||||
<Button
|
||||
to={`${props.match.path}/add`}
|
||||
className="test-add-saved-search-button"
|
||||
variant="primary"
|
||||
as={Link}
|
||||
>
|
||||
<Icon aria-hidden={true} svgPath={mdiPlus} /> Add saved search
|
||||
</Button>
|
||||
}
|
||||
className="mb-3"
|
||||
>
|
||||
<PageTitle title="Saved searches" />
|
||||
<PageHeader.Heading as="h3" styleAs="h2">
|
||||
<PageHeader.Breadcrumb>Saved searches</PageHeader.Breadcrumb>
|
||||
</PageHeader.Heading>
|
||||
</PageHeader>
|
||||
<SavedSearchListPageContent
|
||||
{...props}
|
||||
onDelete={() => refetch()}
|
||||
savedSearches={connection?.nodes || []}
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
<PageSwitcher {...paginationProps} className="mt-4" totalCount={connection?.totalCount || 0} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface SavedSearchListPageContentProps extends Props, State {
|
||||
interface SavedSearchListPageContentProps extends Props {
|
||||
onDelete: () => void
|
||||
savedSearches: SavedSearchFields[]
|
||||
error: unknown
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const SavedSearchListPageContent: React.FunctionComponent<React.PropsWithChildren<SavedSearchListPageContentProps>> = ({
|
||||
namespace,
|
||||
savedSearchesOrError,
|
||||
savedSearches,
|
||||
error,
|
||||
loading,
|
||||
...props
|
||||
}) => {
|
||||
const location = useLocation<{ description?: string }>()
|
||||
const searchPatternType = useNavbarQueryState(state => state.searchPatternType)
|
||||
const callbackReference = useCallbackRef<HTMLAnchorElement>(null, ref => ref?.focus())
|
||||
|
||||
if (savedSearchesOrError === undefined) {
|
||||
if (loading) {
|
||||
return <LoadingSpinner />
|
||||
}
|
||||
|
||||
if (isErrorLike(savedSearchesOrError)) {
|
||||
return <ErrorAlert className="mb-3" error={savedSearchesOrError} />
|
||||
if (error) {
|
||||
return <ErrorAlert className="mb-3" error={error} />
|
||||
}
|
||||
|
||||
const namespaceSavedSearches = savedSearchesOrError.filter(search => namespace.id === search.namespace.id)
|
||||
if (namespaceSavedSearches.length === 0) {
|
||||
if (savedSearches.length === 0) {
|
||||
return <Container className="text-center text-muted">You haven't created a saved search yet.</Container>
|
||||
}
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<div className="list-group list-group-flush">
|
||||
{namespaceSavedSearches.map(search => (
|
||||
{savedSearches.map(search => (
|
||||
<SavedSearchNode
|
||||
key={search.id}
|
||||
linkRef={location.state?.description === search.description ? callbackReference : null}
|
||||
|
||||
@ -17,7 +17,6 @@ import {
|
||||
Scalars,
|
||||
SavedSearchFields,
|
||||
ReposByQueryResult,
|
||||
savedSearchesResult,
|
||||
SavedSearchResult,
|
||||
} from '../graphql-operations'
|
||||
|
||||
@ -62,23 +61,23 @@ const savedSearchFragment = gql`
|
||||
}
|
||||
`
|
||||
|
||||
export function fetchSavedSearches(): Observable<SavedSearchFields[]> {
|
||||
return queryGraphQL<savedSearchesResult>(gql`
|
||||
query savedSearches {
|
||||
savedSearches {
|
||||
export const savedSearchesQuery = gql`
|
||||
query SavedSearches($namespace: ID!, $first: Int, $last: Int, $after: String, $before: String) {
|
||||
savedSearches(namespace: $namespace, first: $first, last: $last, after: $after, before: $before) {
|
||||
nodes {
|
||||
...SavedSearchFields
|
||||
}
|
||||
}
|
||||
${savedSearchFragment}
|
||||
`).pipe(
|
||||
map(({ data, errors }) => {
|
||||
if (!data || !data.savedSearches) {
|
||||
throw createAggregateError(errors)
|
||||
totalCount
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
hasPreviousPage
|
||||
endCursor
|
||||
startCursor
|
||||
}
|
||||
return data.savedSearches
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
${savedSearchFragment}
|
||||
`
|
||||
|
||||
export function fetchSavedSearch(id: Scalars['ID']): Observable<SavedSearchFields> {
|
||||
return queryGraphQL<SavedSearchResult>(
|
||||
|
||||
@ -1,10 +1,7 @@
|
||||
import { DecoratorFn, Meta, Story } from '@storybook/react'
|
||||
import { parseISO } from 'date-fns'
|
||||
import { createMemoryHistory } from 'history'
|
||||
|
||||
import { getDocumentNode } from '@sourcegraph/http-client'
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo'
|
||||
import {
|
||||
mockFetchSearchContexts,
|
||||
mockGetUserSearchContextNamespaces,
|
||||
@ -16,19 +13,6 @@ import { WebStory } from '../../components/WebStory'
|
||||
import { MockedFeatureFlagsProvider } from '../../featureFlags/MockedFeatureFlagsProvider'
|
||||
import { useExperimentalFeatures } from '../../stores'
|
||||
import { ThemePreference } from '../../theme'
|
||||
import {
|
||||
HOME_PANELS_QUERY,
|
||||
RECENTLY_SEARCHED_REPOSITORIES_TO_LOAD,
|
||||
RECENT_FILES_TO_LOAD,
|
||||
RECENT_SEARCHES_TO_LOAD,
|
||||
} from '../panels/HomePanels'
|
||||
import {
|
||||
authUser,
|
||||
collaboratorsPayload,
|
||||
recentFilesPayload,
|
||||
recentSearchesPayload,
|
||||
savedSearchesPayload,
|
||||
} from '../panels/utils'
|
||||
|
||||
import { SearchPage, SearchPageProps } from './SearchPage'
|
||||
|
||||
@ -45,14 +29,13 @@ const defaultProps = (props: ThemeProps): SearchPageProps => ({
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
themePreference: ThemePreference.Light,
|
||||
onThemePreferenceChange: () => undefined,
|
||||
authenticatedUser: authUser,
|
||||
authenticatedUser: null,
|
||||
globbing: false,
|
||||
platformContext: {} as any,
|
||||
searchContextsEnabled: true,
|
||||
selectedSearchContextSpec: '',
|
||||
setSelectedSearchContextSpec: () => {},
|
||||
isLightTheme: props.isLightTheme,
|
||||
now: () => parseISO('2020-09-16T23:15:01Z'),
|
||||
fetchSearchContexts: mockFetchSearchContexts,
|
||||
getUserSearchContextNamespaces: mockGetUserSearchContextNamespaces,
|
||||
})
|
||||
@ -60,7 +43,7 @@ const defaultProps = (props: ThemeProps): SearchPageProps => ({
|
||||
window.context.allowSignup = true
|
||||
|
||||
const decorator: DecoratorFn = Story => {
|
||||
useExperimentalFeatures.setState({ showSearchContext: false, showEnterpriseHomePanels: false })
|
||||
useExperimentalFeatures.setState({ showSearchContext: false })
|
||||
return <Story />
|
||||
}
|
||||
|
||||
@ -77,60 +60,16 @@ const config: Meta = {
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
function getMocks({
|
||||
enableSavedSearches,
|
||||
enableCollaborators,
|
||||
}: {
|
||||
enableSavedSearches: boolean
|
||||
enableCollaborators: boolean
|
||||
}) {
|
||||
return [
|
||||
{
|
||||
request: {
|
||||
query: getDocumentNode(HOME_PANELS_QUERY),
|
||||
variables: {
|
||||
userId: '0',
|
||||
firstRecentlySearchedRepositories: RECENTLY_SEARCHED_REPOSITORIES_TO_LOAD,
|
||||
firstRecentSearches: RECENT_SEARCHES_TO_LOAD,
|
||||
firstRecentFiles: RECENT_FILES_TO_LOAD,
|
||||
enableSavedSearches,
|
||||
enableCollaborators,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
node: {
|
||||
__typename: 'User',
|
||||
recentlySearchedRepositoriesLogs: recentSearchesPayload(),
|
||||
recentSearchesLogs: recentSearchesPayload(),
|
||||
recentFilesLogs: recentFilesPayload(),
|
||||
collaborators: enableCollaborators ? collaboratorsPayload() : undefined,
|
||||
},
|
||||
savedSearches: enableSavedSearches ? savedSearchesPayload() : undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
export const CloudAuthedHome: Story = () => (
|
||||
<WebStory>
|
||||
{webProps => (
|
||||
<MockedTestProvider
|
||||
mocks={getMocks({
|
||||
enableSavedSearches: false,
|
||||
enableCollaborators: false,
|
||||
})}
|
||||
>
|
||||
<SearchPage {...defaultProps(webProps)} isSourcegraphDotCom={true} />
|
||||
</MockedTestProvider>
|
||||
)}
|
||||
</WebStory>
|
||||
<WebStory>{webProps => <SearchPage {...defaultProps(webProps)} isSourcegraphDotCom={true} />}</WebStory>
|
||||
)
|
||||
|
||||
CloudAuthedHome.storyName = 'Cloud authenticated home'
|
||||
|
||||
export const ServerHome: Story = () => <WebStory>{webProps => <SearchPage {...defaultProps(webProps)} />}</WebStory>
|
||||
|
||||
ServerHome.storyName = 'Server home'
|
||||
|
||||
export const CloudMarketingHome: Story = () => (
|
||||
<WebStory>
|
||||
{webProps => (
|
||||
@ -142,20 +81,3 @@ export const CloudMarketingHome: Story = () => (
|
||||
)
|
||||
|
||||
CloudMarketingHome.storyName = 'Cloud marketing home'
|
||||
|
||||
export const ServerHome: Story = () => (
|
||||
<WebStory>
|
||||
{webProps => (
|
||||
<MockedTestProvider
|
||||
mocks={getMocks({
|
||||
enableSavedSearches: true,
|
||||
enableCollaborators: false,
|
||||
})}
|
||||
>
|
||||
<SearchPage {...defaultProps(webProps)} />
|
||||
</MockedTestProvider>
|
||||
)}
|
||||
</WebStory>
|
||||
)
|
||||
|
||||
ServerHome.storyName = 'Server home'
|
||||
|
||||
@ -1,191 +0,0 @@
|
||||
import { cleanup } from '@testing-library/react'
|
||||
import { createMemoryHistory } from 'history'
|
||||
|
||||
import { getDocumentNode } from '@sourcegraph/http-client'
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo'
|
||||
import {
|
||||
mockFetchSearchContexts,
|
||||
mockGetUserSearchContextNamespaces,
|
||||
} from '@sourcegraph/shared/src/testing/searchContexts/testHelpers'
|
||||
import { extensionsController } from '@sourcegraph/shared/src/testing/searchTestHelpers'
|
||||
import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing'
|
||||
|
||||
import { SourcegraphContext } from '../../jscontext'
|
||||
import { useExperimentalFeatures } from '../../stores'
|
||||
import { ThemePreference } from '../../theme'
|
||||
import {
|
||||
HOME_PANELS_QUERY,
|
||||
RECENTLY_SEARCHED_REPOSITORIES_TO_LOAD,
|
||||
RECENT_FILES_TO_LOAD,
|
||||
RECENT_SEARCHES_TO_LOAD,
|
||||
} from '../panels/HomePanels'
|
||||
import {
|
||||
authUser,
|
||||
collaboratorsPayload,
|
||||
recentFilesPayload,
|
||||
recentSearchesPayload,
|
||||
savedSearchesPayload,
|
||||
} from '../panels/utils'
|
||||
|
||||
import { SearchPage, SearchPageProps } from './SearchPage'
|
||||
|
||||
// Mock the Monaco input box to make this a shallow test
|
||||
jest.mock('./SearchPageInput', () => ({
|
||||
SearchPageInput: () => null,
|
||||
}))
|
||||
|
||||
// Uses import.meta.url, which is a SyntaxError when used outside of ES Modules (Jest runs tests as
|
||||
// CommonJS).
|
||||
|
||||
function getMocks({
|
||||
enableSavedSearches,
|
||||
enableCollaborators,
|
||||
}: {
|
||||
enableSavedSearches: boolean
|
||||
enableCollaborators: boolean
|
||||
}) {
|
||||
return [
|
||||
{
|
||||
request: {
|
||||
query: getDocumentNode(HOME_PANELS_QUERY),
|
||||
variables: {
|
||||
userId: '0',
|
||||
firstRecentlySearchedRepositories: RECENTLY_SEARCHED_REPOSITORIES_TO_LOAD,
|
||||
firstRecentSearches: RECENT_SEARCHES_TO_LOAD,
|
||||
firstRecentFiles: RECENT_FILES_TO_LOAD,
|
||||
enableSavedSearches,
|
||||
enableCollaborators,
|
||||
},
|
||||
},
|
||||
result: {
|
||||
data: {
|
||||
node: {
|
||||
__typename: 'User',
|
||||
recentlySearchedRepositoriesLogs: recentSearchesPayload(),
|
||||
recentSearchesLogs: recentSearchesPayload(),
|
||||
recentFilesLogs: recentFilesPayload(),
|
||||
collaborators: enableCollaborators ? collaboratorsPayload() : undefined,
|
||||
},
|
||||
savedSearches: enableSavedSearches ? savedSearchesPayload() : undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
describe('SearchPage', () => {
|
||||
afterAll(cleanup)
|
||||
|
||||
beforeEach(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
window.context = {} as SourcegraphContext & Mocha.SuiteFunction
|
||||
})
|
||||
|
||||
let container: HTMLElement
|
||||
|
||||
const history = createMemoryHistory()
|
||||
const defaultProps: SearchPageProps = {
|
||||
isSourcegraphDotCom: false,
|
||||
settingsCascade: {
|
||||
final: null,
|
||||
subjects: null,
|
||||
},
|
||||
location: history.location,
|
||||
history,
|
||||
extensionsController,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
themePreference: ThemePreference.Light,
|
||||
onThemePreferenceChange: () => undefined,
|
||||
authenticatedUser: authUser,
|
||||
globbing: false,
|
||||
platformContext: {} as any,
|
||||
searchContextsEnabled: true,
|
||||
selectedSearchContextSpec: '',
|
||||
setSelectedSearchContextSpec: () => {},
|
||||
isLightTheme: true,
|
||||
fetchSearchContexts: mockFetchSearchContexts,
|
||||
getUserSearchContextNamespaces: mockGetUserSearchContextNamespaces,
|
||||
}
|
||||
|
||||
it('should show home panels if not on Sourcegraph.com, logged in and showEnterpriseHomePanels enabled', () => {
|
||||
useExperimentalFeatures.setState({ showEnterpriseHomePanels: true })
|
||||
|
||||
container = renderWithBrandedContext(
|
||||
<MockedTestProvider
|
||||
mocks={getMocks({
|
||||
enableSavedSearches: false,
|
||||
enableCollaborators: false,
|
||||
})}
|
||||
>
|
||||
<SearchPage {...defaultProps} />
|
||||
</MockedTestProvider>
|
||||
).container
|
||||
const homePanels = container.querySelector('[data-testid="home-panels"]')
|
||||
expect(homePanels).toBeVisible()
|
||||
})
|
||||
|
||||
it('should not show home panels if on Sourcegraph.com and showEnterpriseHomePanels disabled', () => {
|
||||
container = renderWithBrandedContext(
|
||||
<MockedTestProvider
|
||||
mocks={getMocks({
|
||||
enableSavedSearches: false,
|
||||
enableCollaborators: false,
|
||||
})}
|
||||
>
|
||||
<SearchPage {...defaultProps} isSourcegraphDotCom={true} />
|
||||
</MockedTestProvider>
|
||||
).container
|
||||
const homePanels = container.querySelector('[data-testid="home-panels"]')
|
||||
expect(homePanels).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show home panels if on Sourcegraph.com and showEnterpriseHomePanels enabled with user logged out', () => {
|
||||
useExperimentalFeatures.setState({ showEnterpriseHomePanels: true })
|
||||
|
||||
container = renderWithBrandedContext(
|
||||
<MockedTestProvider
|
||||
mocks={getMocks({
|
||||
enableSavedSearches: false,
|
||||
enableCollaborators: false,
|
||||
})}
|
||||
>
|
||||
<SearchPage {...defaultProps} isSourcegraphDotCom={true} authenticatedUser={null} />
|
||||
</MockedTestProvider>
|
||||
).container
|
||||
const homePanels = container.querySelector('[data-testid="home-panels"]')
|
||||
expect(homePanels).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show home panels if showEnterpriseHomePanels disabled', () => {
|
||||
container = renderWithBrandedContext(
|
||||
<MockedTestProvider
|
||||
mocks={getMocks({
|
||||
enableSavedSearches: false,
|
||||
enableCollaborators: false,
|
||||
})}
|
||||
>
|
||||
<SearchPage {...defaultProps} />
|
||||
</MockedTestProvider>
|
||||
).container
|
||||
const homePanels = container.querySelector('[data-testid="home-panels"]')
|
||||
expect(homePanels).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show home panels if showEnterpriseHomePanels enabled and not on Sourcegraph.com', () => {
|
||||
useExperimentalFeatures.setState({ showEnterpriseHomePanels: true })
|
||||
|
||||
container = renderWithBrandedContext(
|
||||
<MockedTestProvider
|
||||
mocks={getMocks({
|
||||
enableSavedSearches: false,
|
||||
enableCollaborators: false,
|
||||
})}
|
||||
>
|
||||
<SearchPage {...defaultProps} />
|
||||
</MockedTestProvider>
|
||||
).container
|
||||
const homePanels = container.querySelector('[data-testid="home-panels"]')
|
||||
expect(homePanels).toBeVisible()
|
||||
})
|
||||
})
|
||||
@ -13,7 +13,6 @@ import { ThemeProps } from '@sourcegraph/shared/src/theme'
|
||||
import { buildCloudTrialURL } from '@sourcegraph/shared/src/util/url'
|
||||
import { Link, Tooltip, useWindowSize, VIEWPORT_SM } from '@sourcegraph/wildcard'
|
||||
|
||||
import { HomePanelsProps } from '..'
|
||||
import { AuthenticatedUser } from '../../auth'
|
||||
import { BrandLogo } from '../../components/branding/BrandLogo'
|
||||
import { CodeInsightsProps } from '../../insights/types'
|
||||
@ -21,7 +20,6 @@ import { AddCodeHostWidget, useShouldShowAddCodeHostWidget } from '../../onboard
|
||||
import { useExperimentalFeatures } from '../../stores'
|
||||
import { ThemePreferenceProps } from '../../theme'
|
||||
import { eventLogger } from '../../tracking/eventLogger'
|
||||
import { HomePanels } from '../panels/HomePanels'
|
||||
|
||||
import { QueryExamplesHomepage } from './QueryExamplesHomepage'
|
||||
import { SearchPageFooter } from './SearchPageFooter'
|
||||
@ -37,7 +35,6 @@ export interface SearchPageProps
|
||||
ExtensionsControllerProps<'extHostAPI' | 'executeCommand'>,
|
||||
PlatformContextProps<'settings' | 'sourcegraphURL' | 'updateSettings' | 'requestGraphQL'>,
|
||||
SearchContextInputProps,
|
||||
HomePanelsProps,
|
||||
CodeInsightsProps {
|
||||
authenticatedUser: AuthenticatedUser | null
|
||||
location: H.Location
|
||||
@ -53,7 +50,6 @@ export interface SearchPageProps
|
||||
* The search page
|
||||
*/
|
||||
export const SearchPage: React.FunctionComponent<React.PropsWithChildren<SearchPageProps>> = props => {
|
||||
const showEnterpriseHomePanels = useExperimentalFeatures(features => features.showEnterpriseHomePanels ?? false)
|
||||
const homepageUserInvitation = useExperimentalFeatures(features => features.homepageUserInvitation) ?? false
|
||||
const showCollaborators = window.context.allowSignup && homepageUserInvitation && props.isSourcegraphDotCom
|
||||
const { width } = useWindowSize()
|
||||
@ -112,22 +108,16 @@ export const SearchPage: React.FunctionComponent<React.PropsWithChildren<SearchP
|
||||
[styles.panelsContainerWithCollaborators]: showCollaborators,
|
||||
})}
|
||||
>
|
||||
<>
|
||||
{showEnterpriseHomePanels && !!props.authenticatedUser && !props.isSourcegraphDotCom && (
|
||||
<HomePanels showCollaborators={showCollaborators} {...props} />
|
||||
)}
|
||||
|
||||
{((!showEnterpriseHomePanels && !!props.authenticatedUser) || props.isSourcegraphDotCom) && (
|
||||
<QueryExamplesHomepage
|
||||
selectedSearchContextSpec={props.selectedSearchContextSpec}
|
||||
telemetryService={props.telemetryService}
|
||||
queryState={queryState}
|
||||
setQueryState={setQueryState}
|
||||
isSourcegraphDotCom={props.isSourcegraphDotCom}
|
||||
authenticatedUser={props.authenticatedUser}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
{(!!props.authenticatedUser || props.isSourcegraphDotCom) && (
|
||||
<QueryExamplesHomepage
|
||||
selectedSearchContextSpec={props.selectedSearchContextSpec}
|
||||
telemetryService={props.telemetryService}
|
||||
queryState={queryState}
|
||||
setQueryState={setQueryState}
|
||||
isSourcegraphDotCom={props.isSourcegraphDotCom}
|
||||
authenticatedUser={props.authenticatedUser}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<SearchPageFooter {...props} />
|
||||
|
||||
@ -205,11 +205,6 @@ export function literalSearchCompatibility({ queryInput, patternTypeInput }: Que
|
||||
}
|
||||
}
|
||||
|
||||
export interface HomePanelsProps {
|
||||
/** Function that returns current time (for stability in visual tests). */
|
||||
now?: () => Date
|
||||
}
|
||||
|
||||
export interface SearchStreamingProps {
|
||||
streamSearch: (
|
||||
queryObservable: Observable<string>,
|
||||
|
||||
@ -1,78 +0,0 @@
|
||||
.avatar {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
min-width: 2.5rem;
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
.invite-button {
|
||||
height: 1.875rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.button {
|
||||
text-align: left;
|
||||
margin-left: 0.125rem;
|
||||
margin-right: 0.125rem;
|
||||
padding-top: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
color: var(--body-color) !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.invite-button-overlay {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
background-color: var(--body-bg);
|
||||
opacity: 0;
|
||||
transition: opacity 60ms ease-in-out;
|
||||
}
|
||||
|
||||
.button:focus-within .invite-button-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.invite-button-link {
|
||||
padding: 0;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: calc(100% - 3.5rem);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.clip-text {
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.invitebox:hover {
|
||||
.invite-button-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.info-header {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.panel {
|
||||
height: calc(100% - 1rem);
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
import { Story, Meta } from '@storybook/react'
|
||||
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { H2 } from '@sourcegraph/wildcard'
|
||||
|
||||
import { WebStory } from '../../components/WebStory'
|
||||
|
||||
import { CollaboratorsPanel } from './CollaboratorsPanel'
|
||||
import { collaboratorsPayload, authUser } from './utils'
|
||||
|
||||
const config: Meta = {
|
||||
title: 'web/search/panels/CollaboratorsPanel',
|
||||
parameters: {
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/file/Og1zVk7BbZ7SWTXM5WsWA5/Account-Setups-OKR-explorations?node-id=188%3A17448',
|
||||
},
|
||||
chromatic: { disableSnapshot: false },
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
const props = {
|
||||
authenticatedUser: authUser,
|
||||
collaboratorsFragment: { collaborators: collaboratorsPayload() },
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
}
|
||||
|
||||
export const CollaboratorsPanelStory: Story = () => (
|
||||
<WebStory>
|
||||
{() => (
|
||||
<div style={{ maxWidth: '32rem' }}>
|
||||
<H2>Populated</H2>
|
||||
<CollaboratorsPanel {...props} />
|
||||
|
||||
<H2>Loading</H2>
|
||||
<CollaboratorsPanel {...props} collaboratorsFragment={null} />
|
||||
|
||||
<H2>Empty</H2>
|
||||
<CollaboratorsPanel {...props} collaboratorsFragment={{ collaborators: [] }} />
|
||||
</div>
|
||||
)}
|
||||
</WebStory>
|
||||
)
|
||||
CollaboratorsPanelStory.storyName = 'CollaboratorsPanel'
|
||||
@ -1,271 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { mdiEmailCheck, mdiEmail, mdiInformationOutline } from '@mdi/js'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { ErrorLike, isErrorLike } from '@sourcegraph/common'
|
||||
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { Button, Card, CardBody, Link, LoadingSpinner, Icon, H2, Text, ErrorAlert } from '@sourcegraph/wildcard'
|
||||
|
||||
import { AuthenticatedUser } from '../../auth'
|
||||
import { CopyableText } from '../../components/CopyableText'
|
||||
import { CollaboratorsFragment, Maybe } from '../../graphql-operations'
|
||||
import { eventLogger } from '../../tracking/eventLogger'
|
||||
import { UserAvatar } from '../../user/UserAvatar'
|
||||
|
||||
import { LoadingPanelView } from './LoadingPanelView'
|
||||
import { PanelContainer } from './PanelContainer'
|
||||
import { useInviteEmailToSourcegraph } from './useInviteEmailToSourcegraph'
|
||||
|
||||
import styles from './CollaboratorsPanel.module.scss'
|
||||
|
||||
interface Props extends TelemetryProps {
|
||||
className?: string
|
||||
authenticatedUser: AuthenticatedUser | null
|
||||
collaboratorsFragment: CollaboratorsFragment | null
|
||||
}
|
||||
|
||||
const emailEnabled = window.context?.emailEnabled ?? false
|
||||
|
||||
export interface InvitableCollaborator {
|
||||
email: string
|
||||
displayName: string
|
||||
name: string
|
||||
avatarURL: Maybe<string>
|
||||
}
|
||||
|
||||
export const CollaboratorsPanel: React.FunctionComponent<React.PropsWithChildren<Props>> = ({
|
||||
className,
|
||||
authenticatedUser,
|
||||
collaboratorsFragment,
|
||||
}) => {
|
||||
const inviteEmailToSourcegraph = useInviteEmailToSourcegraph()
|
||||
const collaborators = collaboratorsFragment?.collaborators ?? null
|
||||
const filteredCollaborators = useMemo(() => collaborators?.slice(0, 6), [collaborators])
|
||||
|
||||
const [inviteError, setInviteError] = useState<ErrorLike | null>(null)
|
||||
const [loadingInvites, setLoadingInvites] = useState<Set<string>>(new Set<string>())
|
||||
const [successfulInvites, setSuccessfulInvites] = useState<Set<string>>(new Set<string>())
|
||||
|
||||
const isSiteAdmin = authenticatedUser?.siteAdmin ?? false
|
||||
|
||||
useEffect(() => {
|
||||
if (!Array.isArray(collaborators)) {
|
||||
return
|
||||
}
|
||||
// When Email is not set up we might find some people to invite but won't show that to the user.
|
||||
if (!emailEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const loggerPayload = {
|
||||
discovered: collaborators.length,
|
||||
}
|
||||
eventLogger.log('HomepageInvitationsDiscoveredCollaborators', loggerPayload, loggerPayload)
|
||||
}, [collaborators])
|
||||
|
||||
const invitePerson = useCallback(
|
||||
async (person: InvitableCollaborator): Promise<void> => {
|
||||
if (loadingInvites.has(person.email) || successfulInvites.has(person.email)) {
|
||||
return
|
||||
}
|
||||
setLoadingInvites(set => new Set(set).add(person.email))
|
||||
|
||||
try {
|
||||
await inviteEmailToSourcegraph({ variables: { email: person.email } })
|
||||
|
||||
setLoadingInvites(set => {
|
||||
const removed = new Set(set)
|
||||
removed.delete(person.email)
|
||||
return removed
|
||||
})
|
||||
setSuccessfulInvites(set => new Set(set).add(person.email))
|
||||
|
||||
eventLogger.log('HomepageInvitationsSentEmailInvite')
|
||||
} catch (error) {
|
||||
setInviteError(error)
|
||||
}
|
||||
},
|
||||
[inviteEmailToSourcegraph, loadingInvites, successfulInvites]
|
||||
)
|
||||
|
||||
const loadingDisplay = <LoadingPanelView text="Loading colleagues" />
|
||||
|
||||
const contentDisplay =
|
||||
filteredCollaborators?.length === 0 || !emailEnabled ? (
|
||||
<CollaboratorsPanelNullState username={authenticatedUser?.username || ''} isSiteAdmin={isSiteAdmin} />
|
||||
) : (
|
||||
<div className={classNames('row', 'pt-1')}>
|
||||
{isErrorLike(inviteError) && <ErrorAlert error={inviteError} />}
|
||||
|
||||
<CollaboratorsPanelInfo isSiteAdmin={isSiteAdmin} />
|
||||
|
||||
{filteredCollaborators?.map((person: InvitableCollaborator) => (
|
||||
<div
|
||||
className={classNames('d-flex', 'align-items-center', 'col-lg-6', 'mt-1', styles.invitebox)}
|
||||
key={person.email}
|
||||
>
|
||||
<Button
|
||||
variant="icon"
|
||||
key={person.email}
|
||||
disabled={loadingInvites.has(person.email) || successfulInvites.has(person.email)}
|
||||
className={classNames('w-100', styles.button)}
|
||||
onClick={() => invitePerson(person)}
|
||||
>
|
||||
<UserAvatar size={40} className={classNames(styles.avatar, 'mr-3')} user={person} />
|
||||
<div className={styles.content}>
|
||||
<strong className={styles.clipText}>{person.displayName}</strong>
|
||||
<div className={styles.inviteButton}>
|
||||
{loadingInvites.has(person.email) ? (
|
||||
<span className=" ml-auto mr-3">
|
||||
<LoadingSpinner className="mr-1" />
|
||||
Inviting...
|
||||
</span>
|
||||
) : successfulInvites.has(person.email) ? (
|
||||
<span className="text-success ml-auto mr-3">
|
||||
<Icon aria-hidden={true} className="mr-1" svgPath={mdiEmailCheck} />
|
||||
Invited
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<div className={classNames('text-muted', styles.clipText)}>
|
||||
{person.email}
|
||||
</div>
|
||||
<div className={classNames('text-primary', styles.inviteButtonOverlay)}>
|
||||
<Icon aria-hidden={true} className="mr-1" svgPath={mdiEmail} />
|
||||
Invite to Sourcegraph
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<PanelContainer
|
||||
className={classNames(className, styles.panel)}
|
||||
title="Invite your colleagues"
|
||||
insideTabPanel={true}
|
||||
state={collaborators === null ? 'loading' : 'populated'}
|
||||
loadingContent={loadingDisplay}
|
||||
populatedContent={contentDisplay}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const CollaboratorsPanelNullState: React.FunctionComponent<
|
||||
React.PropsWithChildren<{ username: string; isSiteAdmin: boolean }>
|
||||
> = ({ username, isSiteAdmin }) => {
|
||||
const inviteURL = `${window.context.externalURL}/sign-up?invitedBy=${username}`
|
||||
|
||||
useEffect(() => {
|
||||
const loggerPayload = {
|
||||
// The third type, `config-disabled`, is emitted in <HomePanels />
|
||||
type: emailEnabled ? 'email-not-configured' : 'no-collaborators',
|
||||
}
|
||||
eventLogger.log('HomepageInvitationsViewEmpty', loggerPayload, loggerPayload)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'd-flex',
|
||||
'align-items-center',
|
||||
'flex-column',
|
||||
'justify-content-center',
|
||||
'col-lg-12',
|
||||
'h-100'
|
||||
)}
|
||||
>
|
||||
{emailEnabled ? (
|
||||
<div className="text-muted text-center">No collaborators found in sampled repositories.</div>
|
||||
) : isSiteAdmin ? (
|
||||
<div className="text-muted text-center">
|
||||
This server is not configured to send emails. <Link to="/help/admin/config/email">Learn more</Link>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="text-muted mt-3 text-center">
|
||||
You can invite people to Sourcegraph with this direct link:
|
||||
</div>
|
||||
<CopyableText
|
||||
className="mt-3"
|
||||
text={inviteURL}
|
||||
flex={true}
|
||||
size={inviteURL.length}
|
||||
onCopy={() => eventLogger.log('HomepageInvitationsCopiedInviteLink')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const CollaboratorsPanelInfo: React.FunctionComponent<React.PropsWithChildren<{ isSiteAdmin: boolean }>> = ({
|
||||
isSiteAdmin,
|
||||
}) => {
|
||||
const [infoShown, setInfoShown] = useState<boolean>(false)
|
||||
|
||||
if (infoShown) {
|
||||
return (
|
||||
<div className="col-12 mt-2 mb-2 position-relative">
|
||||
<Card>
|
||||
<CardBody>
|
||||
<div className={classNames('d-flex', 'align-content-start', 'mb-2')}>
|
||||
<H2 className={classNames(styles.infoBox, 'mb-0')}>
|
||||
<Icon aria-hidden={true} className="mr-2 text-muted" svgPath={mdiInformationOutline} />
|
||||
What is this?
|
||||
</H2>
|
||||
<div className="flex-grow-1" />
|
||||
<Button
|
||||
variant="icon"
|
||||
onClick={() => setInfoShown(false)}
|
||||
aria-label="Close info box"
|
||||
aria-expanded="true"
|
||||
>
|
||||
<span aria-hidden="true">×</span>
|
||||
</Button>
|
||||
</div>
|
||||
{isSiteAdmin ? (
|
||||
<>
|
||||
<Text className={styles.infoBox}>
|
||||
This feature enables Sourcegraph users to invite collaborators we discover through
|
||||
your Git repository commit history. The invitee will receive a link to Sourcegraph,
|
||||
but no special permissions are granted.
|
||||
</Text>
|
||||
<Text className={classNames(styles.infoBox, 'mb-0')}>
|
||||
If you wish to disable this feature, see{' '}
|
||||
<Link to="/help/admin/config/user_invitations">this documentation</Link>.
|
||||
</Text>
|
||||
</>
|
||||
) : (
|
||||
<Text className={classNames(styles.infoBox, 'mb-0')}>
|
||||
These collaborators were found via your repositories Git commit history. The invitee
|
||||
will receive a link to Sourcegraph, but no special permissions are granted.
|
||||
</Text>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className={classNames('col-12', 'd-flex', 'mt-2', 'mb-1')}>
|
||||
<div className={classNames('text-muted', styles.info)}>Collaborators from your repositories</div>
|
||||
<div className="flex-grow-1" />
|
||||
<div>
|
||||
<Icon aria-hidden={true} className="mr-1 text-muted" svgPath={mdiInformationOutline} />
|
||||
<Button
|
||||
variant="link"
|
||||
className={classNames(styles.info, 'p-0')}
|
||||
onClick={() => setInfoShown(true)}
|
||||
aria-haspopup="true"
|
||||
aria-expanded="false"
|
||||
>
|
||||
What is this?
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,5 +0,0 @@
|
||||
.icon {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
import { DecoratorFn, Meta, Story } from '@storybook/react'
|
||||
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
|
||||
import { WebStory } from '../../components/WebStory'
|
||||
|
||||
import { CommunitySearchContextsPanel } from './CommunitySearchContextPanel'
|
||||
|
||||
const decorator: DecoratorFn = story => <div style={{ width: '800px' }}>{story()}</div>
|
||||
|
||||
const config: Meta = {
|
||||
title: 'web/search/panels/CommunitySearchContextPanel',
|
||||
parameters: {
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/file/zCGglxWBFm8Fv5DwdcHdAQ/Repository-group-home-page-panel-14393?node-id=1%3A159',
|
||||
},
|
||||
chromatic: { viewports: [800] },
|
||||
},
|
||||
decorators: [decorator],
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
const props = {
|
||||
authenticatedUser: null,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
}
|
||||
|
||||
export const CommunitySearchContextPanelStory: Story = () => (
|
||||
<WebStory>
|
||||
{() => (
|
||||
<div style={{ maxWidth: '32rem' }}>
|
||||
<CommunitySearchContextsPanel {...props} />
|
||||
</div>
|
||||
)}
|
||||
</WebStory>
|
||||
)
|
||||
|
||||
CommunitySearchContextPanelStory.storyName = 'CommunitySearchContextsPanel'
|
||||
@ -1,15 +0,0 @@
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing'
|
||||
|
||||
import { CommunitySearchContextsPanel } from './CommunitySearchContextPanel'
|
||||
|
||||
describe('CommunitySearchContextPanel', () => {
|
||||
test('renders correctly', () => {
|
||||
const props = {
|
||||
authenticatedUser: null,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
}
|
||||
|
||||
expect(renderWithBrandedContext(<CommunitySearchContextsPanel {...props} />).asFragment()).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
@ -1,61 +0,0 @@
|
||||
import React, { useCallback, useEffect } from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { Link } from '@sourcegraph/wildcard'
|
||||
|
||||
import { AuthenticatedUser } from '../../auth'
|
||||
import { communitySearchContextsList } from '../../communitySearchContexts/HomepageConfig'
|
||||
|
||||
import { PanelContainer } from './PanelContainer'
|
||||
|
||||
import styles from './CommunitySearchContextPanel.module.scss'
|
||||
|
||||
interface Props extends TelemetryProps {
|
||||
className?: string
|
||||
authenticatedUser: AuthenticatedUser | null
|
||||
insideTabPanel?: boolean
|
||||
}
|
||||
|
||||
export const CommunitySearchContextsPanel: React.FunctionComponent<React.PropsWithChildren<Props>> = ({
|
||||
className,
|
||||
telemetryService,
|
||||
insideTabPanel,
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
telemetryService.log('HomepageContextsPanelViewed')
|
||||
}, [telemetryService])
|
||||
const logContextClicked = useCallback(
|
||||
() => telemetryService.log('CommunitySearchContextsPanelCommunitySearchContextClicked'),
|
||||
[telemetryService]
|
||||
)
|
||||
|
||||
const populatedContent = (
|
||||
<div className="mt-2 row">
|
||||
{communitySearchContextsList.map(communitySearchContext => (
|
||||
<div
|
||||
className="d-flex align-items-center mb-4 col-xl-6 col-lg-12 col-sm-6"
|
||||
key={communitySearchContext.spec}
|
||||
>
|
||||
<img className={classNames('mr-4', styles.icon)} src={communitySearchContext.homepageIcon} alt="" />
|
||||
<div className="d-flex flex-column">
|
||||
<Link to={communitySearchContext.url} onClick={logContextClicked} className="mb-1">
|
||||
{communitySearchContext.title}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
|
||||
return (
|
||||
<PanelContainer
|
||||
insideTabPanel={insideTabPanel}
|
||||
className={classNames(className, 'community-search-context-panel')}
|
||||
title="Community search contexts"
|
||||
state="populated"
|
||||
populatedContent={populatedContent}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
.empty-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
justify-content: center;
|
||||
background: var(--color-bg-2);
|
||||
height: 100%;
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
|
||||
import styles from './EmptyPanelContainer.module.scss'
|
||||
|
||||
interface EmptyPanelContainerProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const EmptyPanelContainer: React.FunctionComponent<React.PropsWithChildren<EmptyPanelContainerProps>> = ({
|
||||
children,
|
||||
className,
|
||||
}) => <div className={classNames(className, styles.emptyContainer)}>{children}</div>
|
||||
@ -1,3 +0,0 @@
|
||||
.footer {
|
||||
background: var(--color-bg-2);
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
|
||||
import styles from './FooterPanel.module.scss'
|
||||
|
||||
interface FooterPanelProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const FooterPanel: React.FunctionComponent<React.PropsWithChildren<FooterPanelProps>> = ({
|
||||
children,
|
||||
className,
|
||||
}) => <div className={classNames(className, styles.footer)}>{children}</div>
|
||||
@ -1,20 +0,0 @@
|
||||
.home-panels {
|
||||
margin-top: 1rem;
|
||||
max-width: 90vw;
|
||||
}
|
||||
|
||||
.panel {
|
||||
height: 15rem;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tab-panels {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@ -1,215 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import { useEffect, useCallback } from 'react'
|
||||
|
||||
import { ApolloQueryResult, gql } from '@apollo/client'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { useQuery } from '@sourcegraph/http-client'
|
||||
import { useTemporarySetting } from '@sourcegraph/shared/src/settings/temporary/useTemporarySetting'
|
||||
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { Tabs, Tab, TabList, TabPanel, TabPanels } from '@sourcegraph/wildcard'
|
||||
|
||||
import { HomePanelsProps } from '..'
|
||||
import { AuthenticatedUser } from '../../auth'
|
||||
import { CollaboratorsFragment, HomePanelsQueryResult, HomePanelsQueryVariables } from '../../graphql-operations'
|
||||
import { GettingStartedTour } from '../../tour/GettingStartedTour'
|
||||
import { eventLogger } from '../../tracking/eventLogger'
|
||||
|
||||
import { CollaboratorsPanel } from './CollaboratorsPanel'
|
||||
import { CommunitySearchContextsPanel } from './CommunitySearchContextPanel'
|
||||
import {
|
||||
collaboratorsFragment,
|
||||
recentlySearchedRepositoriesFragment,
|
||||
recentFilesFragment,
|
||||
recentSearchesPanelFragment,
|
||||
savedSearchesPanelFragment,
|
||||
} from './PanelFragments'
|
||||
import { RecentFilesPanel } from './RecentFilesPanel'
|
||||
import { RecentSearchesPanel } from './RecentSearchesPanel'
|
||||
import { RepositoriesPanel } from './RepositoriesPanel'
|
||||
import { SavedSearchesPanel } from './SavedSearchesPanel'
|
||||
|
||||
import styles from './HomePanels.module.scss'
|
||||
|
||||
interface Props extends TelemetryProps, HomePanelsProps {
|
||||
authenticatedUser: AuthenticatedUser | null
|
||||
isSourcegraphDotCom: boolean
|
||||
showCollaborators: boolean
|
||||
}
|
||||
|
||||
const INVITES_TAB_KEY = 'homepage.userInvites.tab'
|
||||
|
||||
// Use a larger page size because not every search may have a `repo:` filter, and `repo:` filters could often
|
||||
// be duplicated. Therefore, we fetch more searches to populate this panel.
|
||||
export const RECENTLY_SEARCHED_REPOSITORIES_TO_LOAD = 50
|
||||
export const RECENT_SEARCHES_TO_LOAD = 20
|
||||
export const RECENT_FILES_TO_LOAD = 20
|
||||
|
||||
export type HomePanelsFetchMore = (
|
||||
fetchMoreOptions: Partial<HomePanelsQueryVariables>
|
||||
) => Promise<ApolloQueryResult<HomePanelsQueryResult>>
|
||||
|
||||
export const HOME_PANELS_QUERY = gql`
|
||||
query HomePanelsQuery(
|
||||
$userId: ID!
|
||||
$firstRecentlySearchedRepositories: Int!
|
||||
$firstRecentSearches: Int!
|
||||
$firstRecentFiles: Int!
|
||||
$enableSavedSearches: Boolean!
|
||||
$enableCollaborators: Boolean!
|
||||
) {
|
||||
node(id: $userId) {
|
||||
__typename
|
||||
...RecentlySearchedRepositoriesFragment
|
||||
...RecentSearchesPanelFragment
|
||||
...RecentFilesFragment
|
||||
...CollaboratorsFragment
|
||||
}
|
||||
...SavedSearchesPanelFragment
|
||||
}
|
||||
${recentlySearchedRepositoriesFragment}
|
||||
${recentSearchesPanelFragment}
|
||||
${savedSearchesPanelFragment}
|
||||
${recentFilesFragment}
|
||||
${collaboratorsFragment}
|
||||
`
|
||||
|
||||
export const HomePanels: React.FunctionComponent<React.PropsWithChildren<Props>> = (props: Props) => {
|
||||
const userId = props.authenticatedUser?.id || ''
|
||||
const showCollaborators = props.showCollaborators
|
||||
const showSavedSearches = !props.isSourcegraphDotCom
|
||||
|
||||
const { data, fetchMore: rawFetchMore } = useQuery<HomePanelsQueryResult, HomePanelsQueryVariables>(
|
||||
HOME_PANELS_QUERY,
|
||||
{
|
||||
variables: {
|
||||
userId,
|
||||
firstRecentlySearchedRepositories: RECENTLY_SEARCHED_REPOSITORIES_TO_LOAD,
|
||||
firstRecentSearches: RECENT_SEARCHES_TO_LOAD,
|
||||
firstRecentFiles: RECENT_FILES_TO_LOAD,
|
||||
enableSavedSearches: showSavedSearches,
|
||||
enableCollaborators: showCollaborators,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const fetchMore: HomePanelsFetchMore = useCallback(
|
||||
(variables: Partial<HomePanelsQueryVariables>) =>
|
||||
rawFetchMore({
|
||||
variables: {
|
||||
userId,
|
||||
firstRecentlySearchedRepositories: 0,
|
||||
firstRecentSearches: 0,
|
||||
firstRecentFiles: 0,
|
||||
enableSavedSearches: false,
|
||||
enableCollaborators: false,
|
||||
...variables,
|
||||
},
|
||||
}),
|
||||
[rawFetchMore, userId]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (props.showCollaborators === true) {
|
||||
return
|
||||
}
|
||||
const loggerPayload = {
|
||||
// The other types are emitted in <CollaboratorsPanel />
|
||||
type: 'config-disabled',
|
||||
}
|
||||
eventLogger.log('HomepageInvitationsViewEmpty', loggerPayload, loggerPayload)
|
||||
}, [props.showCollaborators])
|
||||
|
||||
const node = data?.node ?? null
|
||||
if (node !== null && node.__typename !== 'User') {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('container', styles.homePanels)} data-testid="home-panels">
|
||||
<div className="row">
|
||||
<GettingStartedTour
|
||||
isSourcegraphDotCom={props.isSourcegraphDotCom}
|
||||
telemetryService={props.telemetryService}
|
||||
isAuthenticated={true}
|
||||
className="w-100"
|
||||
variant="horizontal"
|
||||
/>
|
||||
</div>
|
||||
<div className="row">
|
||||
<RepositoriesPanel
|
||||
{...props}
|
||||
className={classNames('col-lg-4', styles.panel)}
|
||||
recentlySearchedRepositories={node}
|
||||
fetchMore={fetchMore}
|
||||
/>
|
||||
<RecentSearchesPanel
|
||||
{...props}
|
||||
recentSearches={node}
|
||||
className={classNames('col-lg-8', styles.panel)}
|
||||
fetchMore={fetchMore}
|
||||
/>
|
||||
</div>
|
||||
<div className="row">
|
||||
<RecentFilesPanel
|
||||
{...props}
|
||||
className={classNames('col-lg-7', styles.panel)}
|
||||
recentFilesFragment={node}
|
||||
fetchMore={fetchMore}
|
||||
/>
|
||||
|
||||
{showCollaborators ? (
|
||||
<CollaboratorsTabPanel {...props} data={data} collaboratorsFragment={node} />
|
||||
) : showSavedSearches ? (
|
||||
<SavedSearchesPanel
|
||||
{...props}
|
||||
className={classNames('col-lg-5', styles.panel)}
|
||||
savedSearchesFragment={data ?? null}
|
||||
/>
|
||||
) : (
|
||||
<CommunitySearchContextsPanel {...props} className={classNames('col-lg-5', styles.panel)} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface CollaboratorsTabPanelProps extends Props {
|
||||
data: undefined | HomePanelsQueryResult
|
||||
collaboratorsFragment: null | CollaboratorsFragment
|
||||
}
|
||||
|
||||
const CollaboratorsTabPanel: React.FunctionComponent<React.PropsWithChildren<CollaboratorsTabPanelProps>> = ({
|
||||
data,
|
||||
collaboratorsFragment,
|
||||
...props
|
||||
}) => {
|
||||
const [persistedTabIndex, setPersistedTabIndex] = useTemporarySetting(INVITES_TAB_KEY, 1)
|
||||
|
||||
if (persistedTabIndex === undefined) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('col-lg-5', styles.panel)}>
|
||||
<Tabs defaultIndex={persistedTabIndex} onChange={setPersistedTabIndex} className={styles.tabs}>
|
||||
<TabList>
|
||||
<Tab>{props.isSourcegraphDotCom ? 'Community search contexts' : 'Saved searches'}</Tab>
|
||||
<Tab>Invite colleagues</Tab>
|
||||
</TabList>
|
||||
<TabPanels className={classNames('h-100', styles.tabPanels)}>
|
||||
<TabPanel className="h-100">
|
||||
{props.isSourcegraphDotCom ? (
|
||||
<CommunitySearchContextsPanel {...props} insideTabPanel={true} />
|
||||
) : (
|
||||
<SavedSearchesPanel {...props} insideTabPanel={true} savedSearchesFragment={data ?? null} />
|
||||
)}
|
||||
</TabPanel>
|
||||
<TabPanel className="h-100">
|
||||
<CollaboratorsPanel {...props} collaboratorsFragment={collaboratorsFragment} />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
.loading-container {
|
||||
background: none !important;
|
||||
}
|
||||
@ -1,18 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { LoadingSpinner } from '@sourcegraph/wildcard'
|
||||
|
||||
import { EmptyPanelContainer } from './EmptyPanelContainer'
|
||||
|
||||
import styles from './LoadingPanelView.module.scss'
|
||||
|
||||
export const LoadingPanelView: React.FunctionComponent<React.PropsWithChildren<{ text: string }>> = ({ text }) => (
|
||||
<EmptyPanelContainer
|
||||
className={classNames('d-flex justify-content-center align-items-center', styles.loadingContainer)}
|
||||
>
|
||||
<LoadingSpinner />
|
||||
<span className="text-muted">{text}</span>
|
||||
</EmptyPanelContainer>
|
||||
)
|
||||
@ -1,25 +0,0 @@
|
||||
.panel-container {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
min-height: 2rem;
|
||||
|
||||
&-text {
|
||||
margin-bottom: 0.125rem;
|
||||
flex-grow: 1;
|
||||
align-self: flex-end;
|
||||
font-weight: 400;
|
||||
}
|
||||
}
|
||||
|
||||
.header-inside-tab-panel {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
import { render } from '@testing-library/react'
|
||||
|
||||
import { PanelContainer } from './PanelContainer'
|
||||
|
||||
describe('PanelContainer', () => {
|
||||
const defaultProps = {
|
||||
title: 'Test Panel',
|
||||
state: 'loading',
|
||||
loadingContent: <div>Loading</div>,
|
||||
populatedContent: <div>Content</div>,
|
||||
emptyContent: <div>Empty</div>,
|
||||
}
|
||||
|
||||
test('loading state', () => {
|
||||
expect(render(<PanelContainer {...defaultProps} state="loading" />).asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('empty state', () => {
|
||||
expect(render(<PanelContainer {...defaultProps} state="empty" />).asFragment).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('content state', () => {
|
||||
expect(render(<PanelContainer {...defaultProps} state="populated" />).asFragment).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('with action buttons', () => {
|
||||
const actionButtons = <button type="button">Button</button>
|
||||
expect(
|
||||
render(<PanelContainer {...defaultProps} state="populated" actionButtons={actionButtons} />).asFragment()
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
@ -1,51 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { H2, H4 } from '@sourcegraph/wildcard'
|
||||
|
||||
import styles from './PanelContainer.module.scss'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
state: 'loading' | 'populated' | 'empty'
|
||||
// the content displayed when state is 'loading'
|
||||
loadingContent?: JSX.Element
|
||||
// the content displayed when state is 'empty'
|
||||
emptyContent?: JSX.Element
|
||||
// the content displayed when state is 'populated'
|
||||
populatedContent: JSX.Element
|
||||
actionButtons?: JSX.Element
|
||||
className?: string
|
||||
insideTabPanel?: boolean
|
||||
}
|
||||
|
||||
export const PanelContainer: React.FunctionComponent<React.PropsWithChildren<Props>> = ({
|
||||
title,
|
||||
state,
|
||||
loadingContent = <></>,
|
||||
emptyContent = <></>,
|
||||
populatedContent,
|
||||
actionButtons,
|
||||
className,
|
||||
insideTabPanel,
|
||||
}) => (
|
||||
<div className={classNames(className, styles.panelContainer, 'd-flex', 'flex-column')}>
|
||||
{insideTabPanel !== true ? (
|
||||
<div className={classNames('d-flex border-bottom', styles.header)}>
|
||||
<H4 as={H2} className={styles.headerText}>
|
||||
{title}
|
||||
</H4>
|
||||
{actionButtons}
|
||||
</div>
|
||||
) : (
|
||||
<div className={classNames(styles.header, styles.headerInsideTabPanel)}>{actionButtons}</div>
|
||||
)}
|
||||
|
||||
<div className={classNames('h-100', styles.content)}>
|
||||
{state === 'loading' && loadingContent}
|
||||
{state === 'populated' && populatedContent}
|
||||
{state === 'empty' && emptyContent}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@ -1,87 +0,0 @@
|
||||
import { gql } from '@sourcegraph/http-client'
|
||||
|
||||
// We define all fragments for the different homepage panels in this file to
|
||||
// avoid creating circular imports in Jest, see:
|
||||
//
|
||||
// - https://blackdeerdev.com/graphqlerror-syntax-error-unexpected-name-undefined/
|
||||
// - https://spectrum.chat/apollo/general/fragments-not-working-cross-files-in-mutation~c4e90568-f89a-458f-9810-0730846fc5f0
|
||||
|
||||
export const collaboratorsFragment = gql`
|
||||
fragment CollaboratorsFragment on User {
|
||||
collaborators: invitableCollaborators @include(if: $enableCollaborators) {
|
||||
name
|
||||
email
|
||||
displayName
|
||||
avatarURL
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const recentFilesFragment = gql`
|
||||
fragment RecentFilesFragment on User {
|
||||
recentFilesLogs: eventLogs(first: $firstRecentFiles, eventName: "ViewBlob") {
|
||||
nodes {
|
||||
argument
|
||||
timestamp
|
||||
url
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const recentSearchesPanelFragment = gql`
|
||||
fragment RecentSearchesPanelFragment on User {
|
||||
recentSearchesLogs: eventLogs(first: $firstRecentSearches, eventName: "SearchResultsQueried") {
|
||||
nodes {
|
||||
argument
|
||||
timestamp
|
||||
url
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const recentlySearchedRepositoriesFragment = gql`
|
||||
fragment RecentlySearchedRepositoriesFragment on User {
|
||||
recentlySearchedRepositoriesLogs: eventLogs(
|
||||
first: $firstRecentlySearchedRepositories
|
||||
eventName: "SearchResultsQueried"
|
||||
) {
|
||||
nodes {
|
||||
argument
|
||||
timestamp
|
||||
url
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const savedSearchesPanelFragment = gql`
|
||||
fragment SavedSearchesPanelFragment on Query {
|
||||
savedSearches @include(if: $enableSavedSearches) {
|
||||
id
|
||||
description
|
||||
notify
|
||||
notifySlack
|
||||
query
|
||||
namespace {
|
||||
__typename
|
||||
id
|
||||
namespaceName
|
||||
}
|
||||
slackWebhookURL
|
||||
}
|
||||
}
|
||||
`
|
||||
@ -1,62 +0,0 @@
|
||||
import { Story, DecoratorFn, Meta } from '@storybook/react'
|
||||
import { noop } from 'lodash'
|
||||
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { H2 } from '@sourcegraph/wildcard'
|
||||
|
||||
import { WebStory } from '../../components/WebStory'
|
||||
|
||||
import { RecentFilesPanel } from './RecentFilesPanel'
|
||||
import { recentFilesPayload } from './utils'
|
||||
|
||||
const decorator: DecoratorFn = story => <div style={{ width: '800px' }}>{story()}</div>
|
||||
|
||||
const config: Meta = {
|
||||
title: 'web/search/panels/RecentFilesPanel',
|
||||
parameters: {
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/file/sPRyyv3nt5h0284nqEuAXE/12192-Sourcegraph-server-page-v1?node-id=255%3A3',
|
||||
},
|
||||
chromatic: { viewports: [800], disableSnapshot: false },
|
||||
},
|
||||
decorators: [decorator],
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
const emptyRecentFiles = {
|
||||
totalCount: 0,
|
||||
nodes: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
},
|
||||
}
|
||||
|
||||
const props = {
|
||||
authenticatedUser: null,
|
||||
recentFilesFragment: { recentFilesLogs: recentFilesPayload() },
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any
|
||||
fetchMore: noop as any,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
}
|
||||
|
||||
export const RecentFilesPanelStory: Story = () => (
|
||||
<WebStory>
|
||||
{() => (
|
||||
<div style={{ maxWidth: '32rem' }}>
|
||||
<H2>Populated</H2>
|
||||
<RecentFilesPanel {...props} />
|
||||
|
||||
<H2>Loading</H2>
|
||||
<RecentFilesPanel {...props} recentFilesFragment={null} />
|
||||
|
||||
<H2>Empty</H2>
|
||||
<RecentFilesPanel {...props} recentFilesFragment={{ recentFilesLogs: emptyRecentFiles }} />
|
||||
</div>
|
||||
)}
|
||||
</WebStory>
|
||||
)
|
||||
|
||||
RecentFilesPanelStory.storyName = 'RecentFilesPanel'
|
||||
@ -1,157 +0,0 @@
|
||||
import { screen } from '@testing-library/react'
|
||||
import { noop } from 'rxjs'
|
||||
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing'
|
||||
|
||||
import { RecentFilesPanel } from './RecentFilesPanel'
|
||||
|
||||
describe('RecentFilesPanel', () => {
|
||||
test('duplicate files are only shown once', () => {
|
||||
const recentFiles = {
|
||||
nodes: [
|
||||
{
|
||||
argument: '{"filePath": "go.mod", "repoName": "ghe.sgdev.org/sourcegraph/gorilla-mux"}',
|
||||
timestamp: '2020-09-10T22:55:30Z',
|
||||
url: 'https://sourcegraph.test:3443/ghe.sgdev.org/sourcegraph/gorilla-mux/-/blob/go.mod',
|
||||
},
|
||||
{
|
||||
argument: '{"filePath": ".eslintrc.js", "repoName": "github.com/sourcegraph/sourcegraph"}',
|
||||
timestamp: '2020-09-10T22:55:18Z',
|
||||
url: 'https://sourcegraph.test:3443/github.com/sourcegraph/sourcegraph/-/blob/.eslintrc.js',
|
||||
},
|
||||
{
|
||||
argument: '{"filePath": "go.mod", "repoName": "ghe.sgdev.org/sourcegraph/gorilla-mux"}',
|
||||
timestamp: '2020-09-10T22:55:06Z',
|
||||
url: 'https://sourcegraph.test:3443/ghe.sgdev.org/sourcegraph/gorilla-mux/-/blob/go.mod',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
},
|
||||
totalCount: 3,
|
||||
}
|
||||
|
||||
const props = {
|
||||
authenticatedUser: null,
|
||||
recentFilesFragment: { recentFilesLogs: recentFiles },
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any
|
||||
fetchMore: noop as any,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
}
|
||||
|
||||
renderWithBrandedContext(<RecentFilesPanel {...props} />)
|
||||
const listItems = screen.getAllByTestId('recent-files-item')
|
||||
expect(listItems).toHaveLength(2)
|
||||
expect(listItems[0]).toHaveTextContent('ghe.sgdev.org/sourcegraph/gorilla-mux › go.mod')
|
||||
expect(listItems[1]).toHaveTextContent('github.com/sourcegraph/sourcegraph › .eslintrc.js')
|
||||
})
|
||||
|
||||
test('files with missing data can extract it from the URL if available', () => {
|
||||
const recentFiles = {
|
||||
nodes: [
|
||||
{
|
||||
argument: '{"filePath": ".eslintrc.js", "repoName": "github.com/sourcegraph/sourcegraph"}',
|
||||
timestamp: '2020-09-10T22:55:18Z',
|
||||
url: 'https://sourcegraph.test:3443/github.com/sourcegraph/sourcegraph/-/blob/.eslintrc.js',
|
||||
},
|
||||
{
|
||||
argument: '{}',
|
||||
timestamp: '2020-09-10T22:55:06Z',
|
||||
url: 'https://sourcegraph.test:3443/ghe.sgdev.org/sourcegraph/gorilla-mux/-/blob/go.mod',
|
||||
},
|
||||
{
|
||||
argument: '{}',
|
||||
timestamp: '2020-09-10T22:55:06Z',
|
||||
url: 'https://sourcegraph.test:3443/bitbucket.sgdev.org/SOURCEGRAPH/jsonrpc2',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
},
|
||||
totalCount: 2,
|
||||
}
|
||||
|
||||
const props = {
|
||||
authenticatedUser: null,
|
||||
recentFilesFragment: { recentFilesLogs: recentFiles },
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any
|
||||
fetchMore: noop as any,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
}
|
||||
|
||||
renderWithBrandedContext(<RecentFilesPanel {...props} />)
|
||||
const listItems = screen.getAllByTestId('recent-files-item')
|
||||
expect(listItems).toHaveLength(2)
|
||||
expect(listItems[0]).toHaveTextContent('github.com/sourcegraph/sourcegraph › .eslintrc.js')
|
||||
expect(listItems[1]).toHaveTextContent('ghe.sgdev.org/sourcegraph/gorilla-mux › go.mod')
|
||||
})
|
||||
|
||||
test('Show More button shown when more items can be loaded', () => {
|
||||
const recentFiles = {
|
||||
nodes: [
|
||||
{
|
||||
argument: '{"filePath": ".eslintrc.js", "repoName": "github.com/sourcegraph/sourcegraph"}',
|
||||
timestamp: '2020-09-10T22:55:18Z',
|
||||
url: 'https://sourcegraph.test:3443/github.com/sourcegraph/sourcegraph/-/blob/.eslintrc.js',
|
||||
},
|
||||
{
|
||||
argument: '{}',
|
||||
timestamp: '2020-09-10T22:55:06Z',
|
||||
url: 'https://sourcegraph.test:3443/ghe.sgdev.org/sourcegraph/gorilla-mux/-/blob/go.mod',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: true,
|
||||
},
|
||||
totalCount: 2,
|
||||
}
|
||||
|
||||
const props = {
|
||||
authenticatedUser: null,
|
||||
recentFilesFragment: { recentFilesLogs: recentFiles },
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any
|
||||
fetchMore: noop as any,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
}
|
||||
|
||||
renderWithBrandedContext(<RecentFilesPanel {...props} />)
|
||||
expect(screen.getByTestId('recent-files-panel-show-more')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
test('Show More button not shown when more items cannot be loaded', () => {
|
||||
const recentFiles = {
|
||||
nodes: [
|
||||
{
|
||||
argument: '{"filePath": ".eslintrc.js", "repoName": "github.com/sourcegraph/sourcegraph"}',
|
||||
timestamp: '2020-09-10T22:55:18Z',
|
||||
url: 'https://sourcegraph.test:3443/github.com/sourcegraph/sourcegraph/-/blob/.eslintrc.js',
|
||||
},
|
||||
{
|
||||
argument: '{}',
|
||||
timestamp: '2020-09-10T22:55:06Z',
|
||||
url: 'https://sourcegraph.test:3443/ghe.sgdev.org/sourcegraph/gorilla-mux/-/blob/go.mod',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
},
|
||||
totalCount: 2,
|
||||
}
|
||||
|
||||
const props = {
|
||||
authenticatedUser: null,
|
||||
recentFilesFragment: { recentFilesLogs: recentFiles },
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any
|
||||
fetchMore: noop as any,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
}
|
||||
|
||||
renderWithBrandedContext(<RecentFilesPanel {...props} />)
|
||||
expect(screen.queryByTestId('recent-files-panel-show-more')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -1,248 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { mdiFileCode } from '@mdi/js'
|
||||
import { VisuallyHidden } from '@reach/visually-hidden'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { gql } from '@sourcegraph/http-client'
|
||||
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { Link, Icon, useFocusOnLoadedMore } from '@sourcegraph/wildcard'
|
||||
|
||||
import { AuthenticatedUser } from '../../auth'
|
||||
import { RecentFilesFragment } from '../../graphql-operations'
|
||||
import { EventLogResult } from '../backend'
|
||||
|
||||
import { EmptyPanelContainer } from './EmptyPanelContainer'
|
||||
import { HomePanelsFetchMore, RECENT_FILES_TO_LOAD } from './HomePanels'
|
||||
import { LoadingPanelView } from './LoadingPanelView'
|
||||
import { PanelContainer } from './PanelContainer'
|
||||
import { ShowMoreButton } from './ShowMoreButton'
|
||||
import { useComputeResults } from './useComputeResults'
|
||||
|
||||
import styles from './RecentSearchesPanel.module.scss'
|
||||
interface Props extends TelemetryProps {
|
||||
className?: string
|
||||
authenticatedUser: AuthenticatedUser | null
|
||||
recentFilesFragment: RecentFilesFragment | null
|
||||
fetchMore: HomePanelsFetchMore
|
||||
}
|
||||
|
||||
export const recentFilesFragment = gql`
|
||||
fragment RecentFilesFragment on User {
|
||||
recentFilesLogs: eventLogs(first: $firstRecentFiles, eventName: "ViewBlob") {
|
||||
nodes {
|
||||
argument
|
||||
timestamp
|
||||
url
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`
|
||||
export const RecentFilesPanel: React.FunctionComponent<React.PropsWithChildren<Props>> = ({
|
||||
className,
|
||||
recentFilesFragment,
|
||||
telemetryService,
|
||||
fetchMore,
|
||||
authenticatedUser,
|
||||
}) => {
|
||||
const [recentFiles, setRecentFiles] = useState<null | RecentFilesFragment['recentFilesLogs']>(
|
||||
recentFilesFragment?.recentFilesLogs ?? null
|
||||
)
|
||||
useEffect(
|
||||
() => setRecentFiles(recentFilesFragment?.recentFilesLogs ?? null),
|
||||
[recentFilesFragment?.recentFilesLogs]
|
||||
)
|
||||
|
||||
const [itemsToLoad, setItemsToLoad] = useState(RECENT_FILES_TO_LOAD)
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
||||
const [processedResults, setProcessedResults] = useState<RecentFile[] | null>(null)
|
||||
const getItemRef = useFocusOnLoadedMore(processedResults?.length ?? 0)
|
||||
// Only update processed results when results are valid to prevent
|
||||
// flashing loading screen when "Show more" button is clicked
|
||||
useEffect(() => {
|
||||
if (recentFiles) {
|
||||
setProcessedResults(processRecentFiles(recentFiles))
|
||||
}
|
||||
}, [recentFiles])
|
||||
|
||||
useEffect(() => {
|
||||
// Only log the first load (when items to load is equal to the page size)
|
||||
if (processedResults && itemsToLoad === RECENT_FILES_TO_LOAD) {
|
||||
telemetryService.log(
|
||||
'RecentFilesPanelLoaded',
|
||||
{ empty: processedResults.length === 0 },
|
||||
{ empty: processedResults.length === 0 }
|
||||
)
|
||||
}
|
||||
}, [processedResults, telemetryService, itemsToLoad])
|
||||
|
||||
const logFileClicked = useCallback(() => telemetryService.log('RecentFilesPanelFileClicked'), [telemetryService])
|
||||
|
||||
const loadingDisplay = <LoadingPanelView text="Loading recent files" />
|
||||
|
||||
const emptyDisplay = (
|
||||
<EmptyPanelContainer className="align-items-center text-muted">
|
||||
<Icon className="mb-2" svgPath={mdiFileCode} inline={false} aria-hidden={true} height="2rem" width="2rem" />
|
||||
<small className="mb-2">This panel will display your most recently viewed files.</small>
|
||||
</EmptyPanelContainer>
|
||||
)
|
||||
|
||||
async function loadMoreItems(): Promise<void> {
|
||||
telemetryService.log('RecentFilesPanelShowMoreClicked')
|
||||
const newItemsToLoad = itemsToLoad + RECENT_FILES_TO_LOAD
|
||||
setItemsToLoad(newItemsToLoad)
|
||||
|
||||
setIsLoadingMore(true)
|
||||
const { data } = await fetchMore({
|
||||
firstRecentFiles: newItemsToLoad,
|
||||
})
|
||||
setIsLoadingMore(false)
|
||||
if (data === undefined) {
|
||||
return
|
||||
}
|
||||
const node = data.node
|
||||
if (node === null || node.__typename !== 'User') {
|
||||
return
|
||||
}
|
||||
|
||||
setRecentFiles(node.recentFilesLogs)
|
||||
}
|
||||
|
||||
const { isLoading: computeLoading, results: computeResults } = useComputeResults(authenticatedUser, '$repo › $path')
|
||||
|
||||
const renderComputeResults = computeResults.size > 0
|
||||
|
||||
const contentDisplay = (
|
||||
<>
|
||||
<table className={classNames('mt-2', styles.resultsTable)}>
|
||||
<thead>
|
||||
<tr className={styles.resultsTableRow}>
|
||||
<th>
|
||||
<small>File</small>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{renderComputeResults
|
||||
? [...computeResults].map((file, index) => (
|
||||
<tr key={index} className={classNames('text-monospace d-block', styles.resultsTableRow)}>
|
||||
<td>
|
||||
<small>
|
||||
<Link
|
||||
to={`/${file.split(' › ')[0]}/-/blob/${file.split(' › ')[1].trim()}`}
|
||||
onClick={logFileClicked}
|
||||
data-testid="recent-files-item"
|
||||
>
|
||||
{file}
|
||||
</Link>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
: processedResults?.map((recentFile, index) => (
|
||||
<tr key={index} className={styles.resultsTableRow}>
|
||||
<td>
|
||||
<small>
|
||||
<Link
|
||||
to={recentFile.url}
|
||||
ref={getItemRef(index)}
|
||||
onClick={logFileClicked}
|
||||
data-testid="recent-files-item"
|
||||
>
|
||||
{recentFile.repoName} › {recentFile.filePath}
|
||||
</Link>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{!renderComputeResults && (
|
||||
<>
|
||||
{isLoadingMore && <VisuallyHidden aria-live="polite">Loading more recent files</VisuallyHidden>}
|
||||
{recentFiles?.pageInfo.hasNextPage && (
|
||||
<div>
|
||||
<ShowMoreButton onClick={loadMoreItems} dataTestid="recent-files-panel-show-more" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
// Wait for both the search event logs and the git history to be loaded
|
||||
const isLoading = computeLoading || !processedResults
|
||||
// If neither search event logs or git history have items, then display the empty display
|
||||
const isEmpty = processedResults?.length === 0 && computeResults.size === 0
|
||||
|
||||
return (
|
||||
<PanelContainer
|
||||
className={classNames(className, 'recent-files-panel')}
|
||||
title="Recent files"
|
||||
state={isLoading ? 'loading' : isEmpty ? 'empty' : 'populated'}
|
||||
loadingContent={loadingDisplay}
|
||||
populatedContent={contentDisplay}
|
||||
emptyContent={emptyDisplay}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
interface RecentFile {
|
||||
repoName: string
|
||||
filePath: string
|
||||
timestamp: string
|
||||
url: string
|
||||
}
|
||||
|
||||
function processRecentFiles(eventLogResult?: EventLogResult): RecentFile[] | null {
|
||||
if (!eventLogResult) {
|
||||
return null
|
||||
}
|
||||
|
||||
const recentFiles: RecentFile[] = []
|
||||
|
||||
for (const node of eventLogResult.nodes) {
|
||||
if (node.argument && node.url) {
|
||||
const parsedArguments = JSON.parse(node.argument)
|
||||
let repoName = parsedArguments?.repoName as string
|
||||
let filePath = parsedArguments?.filePath as string
|
||||
|
||||
if (!repoName || !filePath) {
|
||||
;({ repoName, filePath } = extractFileInfoFromUrl(node.url))
|
||||
}
|
||||
|
||||
if (
|
||||
filePath &&
|
||||
repoName &&
|
||||
!recentFiles.some(file => file.repoName === repoName && file.filePath === filePath) // Don't show the same file twice
|
||||
) {
|
||||
const parsedUrl = new URL(node.url)
|
||||
recentFiles.push({
|
||||
url: parsedUrl.pathname + parsedUrl.search, // Strip domain from URL so clicking on it doesn't reload page
|
||||
repoName,
|
||||
filePath,
|
||||
timestamp: node.timestamp,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return recentFiles
|
||||
}
|
||||
|
||||
function extractFileInfoFromUrl(url: string): { repoName: string; filePath: string } {
|
||||
const parsedUrl = new URL(url)
|
||||
|
||||
// Remove first character as it's a '/'
|
||||
const [repoName, filePath] = parsedUrl.pathname.slice(1).split('/-/blob/')
|
||||
if (!repoName || !filePath) {
|
||||
return { repoName: '', filePath: '' }
|
||||
}
|
||||
|
||||
return { repoName, filePath }
|
||||
}
|
||||
@ -1,27 +0,0 @@
|
||||
.results-table {
|
||||
width: 100%;
|
||||
|
||||
&-row {
|
||||
border-bottom: solid 4px transparent;
|
||||
}
|
||||
|
||||
&-date-col {
|
||||
width: 7rem;
|
||||
}
|
||||
}
|
||||
|
||||
.examples-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
align-self: center;
|
||||
text-align: left;
|
||||
|
||||
&-item {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.recent-query {
|
||||
word-break: break-word;
|
||||
}
|
||||
@ -1,64 +0,0 @@
|
||||
import { Meta, DecoratorFn, Story } from '@storybook/react'
|
||||
import { parseISO } from 'date-fns'
|
||||
import { noop } from 'lodash'
|
||||
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { H2 } from '@sourcegraph/wildcard'
|
||||
|
||||
import { WebStory } from '../../components/WebStory'
|
||||
|
||||
import { RecentSearchesPanel } from './RecentSearchesPanel'
|
||||
import { recentSearchesPayload } from './utils'
|
||||
|
||||
const decorator: DecoratorFn = story => <div style={{ width: '800px' }}>{story()}</div>
|
||||
|
||||
const config: Meta = {
|
||||
title: 'web/search/panels/RecentSearchesPanel',
|
||||
parameters: {
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/file/sPRyyv3nt5h0284nqEuAXE/12192-Sourcegraph-server-page-v1?node-id=255%3A3',
|
||||
},
|
||||
chromatic: { viewports: [800], disableSnapshot: false },
|
||||
},
|
||||
decorators: [decorator],
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
const emptyRecentSearches = {
|
||||
totalCount: 0,
|
||||
nodes: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
},
|
||||
}
|
||||
|
||||
const props = {
|
||||
authenticatedUser: null,
|
||||
recentSearches: { recentSearchesLogs: recentSearchesPayload() },
|
||||
now: () => parseISO('2020-09-16T23:15:01Z'),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any
|
||||
fetchMore: noop as any,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
}
|
||||
|
||||
export const RecentSearchesPanelStory: Story = () => (
|
||||
<WebStory>
|
||||
{() => (
|
||||
<div style={{ maxWidth: '32rem' }}>
|
||||
<H2>Populated</H2>
|
||||
<RecentSearchesPanel {...props} />
|
||||
|
||||
<H2>Loading</H2>
|
||||
<RecentSearchesPanel {...props} recentSearches={null} />
|
||||
|
||||
<H2>Empty</H2>
|
||||
<RecentSearchesPanel {...props} recentSearches={{ recentSearchesLogs: emptyRecentSearches }} />
|
||||
</div>
|
||||
)}
|
||||
</WebStory>
|
||||
)
|
||||
|
||||
RecentSearchesPanelStory.storyName = 'RecentSearchesPanel'
|
||||
@ -1,225 +0,0 @@
|
||||
import { screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { noop } from 'rxjs'
|
||||
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { MockedTestProvider } from '@sourcegraph/shared/src/testing/apollo/mockedTestProvider'
|
||||
import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing'
|
||||
|
||||
import { RecentSearchesPanel } from './RecentSearchesPanel'
|
||||
|
||||
describe('RecentSearchesPanel', () => {
|
||||
test('consecutive identical searches are correctly merged when rendered', () => {
|
||||
const recentSearches = {
|
||||
nodes: [
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 4, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 0}, "field_default": {"count": 1, "count_regexp": 0, "count_literal": 1, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "test"}}}',
|
||||
timestamp: '2020-09-08T17:36:52Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=test&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:39Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:30Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
},
|
||||
totalCount: 3,
|
||||
}
|
||||
|
||||
const props = {
|
||||
authenticatedUser: null,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
recentSearches: { recentSearchesLogs: recentSearches },
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any
|
||||
fetchMore: noop as any,
|
||||
}
|
||||
|
||||
expect(
|
||||
renderWithBrandedContext(
|
||||
<MockedTestProvider>
|
||||
<RecentSearchesPanel {...props} />
|
||||
</MockedTestProvider>
|
||||
).asFragment()
|
||||
).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('searches with no argument are skipped', () => {
|
||||
const recentSearches = {
|
||||
nodes: [
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 4, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 0}, "field_default": {"count": 1, "count_regexp": 0, "count_literal": 1, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "test"}}}',
|
||||
timestamp: '2020-09-08T17:36:52Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=test&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument: '{}',
|
||||
timestamp: '2020-09-04T18:44:39Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:30Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
},
|
||||
totalCount: 3,
|
||||
}
|
||||
|
||||
const props = {
|
||||
authenticatedUser: null,
|
||||
recentSearches: { recentSearchesLogs: recentSearches },
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any
|
||||
fetchMore: noop as any,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
}
|
||||
|
||||
expect(renderWithBrandedContext(<RecentSearchesPanel {...props} />).asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Show More button is shown if more pages are available', () => {
|
||||
const recentSearches = {
|
||||
nodes: [
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 4, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 0}, "field_default": {"count": 1, "count_regexp": 0, "count_literal": 1, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "test"}}}',
|
||||
timestamp: '2020-09-08T17:36:52Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=test&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:39Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:30Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: true,
|
||||
},
|
||||
totalCount: 6,
|
||||
}
|
||||
|
||||
const props = {
|
||||
authenticatedUser: null,
|
||||
recentSearches: { recentSearchesLogs: recentSearches },
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any
|
||||
fetchMore: noop as any,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
}
|
||||
|
||||
expect(renderWithBrandedContext(<RecentSearchesPanel {...props} />).asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Show More button loads more items', () => {
|
||||
const recentSearches1 = {
|
||||
nodes: [
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 4, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 0}, "field_default": {"count": 1, "count_regexp": 0, "count_literal": 1, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "test"}}}',
|
||||
timestamp: '2020-09-08T17:36:52Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=test&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:39Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:30Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: true,
|
||||
},
|
||||
totalCount: 6,
|
||||
}
|
||||
|
||||
const recentSearches2 = {
|
||||
nodes: [
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 4, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 0}, "field_default": {"count": 1, "count_regexp": 0, "count_literal": 1, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "test"}}}',
|
||||
timestamp: '2020-09-08T17:36:52Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=test&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:39Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:30Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 4, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 0}, "field_default": {"count": 1, "count_regexp": 0, "count_literal": 1, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "test"}}}',
|
||||
timestamp: '2020-09-08T17:36:52Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=test&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:39Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:30Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
},
|
||||
totalCount: 6,
|
||||
}
|
||||
|
||||
const props = {
|
||||
className: '',
|
||||
authenticatedUser: null,
|
||||
recentSearches: { recentSearchesLogs: recentSearches1 },
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any
|
||||
fetchMore: (() => ({ recentSearchesLogs: recentSearches2 })) as any,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
}
|
||||
|
||||
const { asFragment } = renderWithBrandedContext(<RecentSearchesPanel {...props} />)
|
||||
userEvent.click(screen.getByRole('button', { name: /Show more/ }))
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
@ -1,252 +0,0 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { gql } from '@apollo/client'
|
||||
import { VisuallyHidden } from '@reach/visually-hidden'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { Timestamp } from '@sourcegraph/branded/src/components/Timestamp'
|
||||
import { SyntaxHighlightedSearchQuery } from '@sourcegraph/search-ui'
|
||||
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { buildSearchURLQuery } from '@sourcegraph/shared/src/util/url'
|
||||
import { Link, useFocusOnLoadedMore } from '@sourcegraph/wildcard'
|
||||
|
||||
import { AuthenticatedUser } from '../../auth'
|
||||
import { RecentSearchesPanelFragment, SearchPatternType } from '../../graphql-operations'
|
||||
import { EventLogResult } from '../backend'
|
||||
|
||||
import { EmptyPanelContainer } from './EmptyPanelContainer'
|
||||
import { HomePanelsFetchMore, RECENT_SEARCHES_TO_LOAD } from './HomePanels'
|
||||
import { LoadingPanelView } from './LoadingPanelView'
|
||||
import { PanelContainer } from './PanelContainer'
|
||||
import { ShowMoreButton } from './ShowMoreButton'
|
||||
|
||||
import styles from './RecentSearchesPanel.module.scss'
|
||||
|
||||
interface RecentSearch {
|
||||
count: number
|
||||
searchText: string
|
||||
timestamp: string
|
||||
url: string
|
||||
}
|
||||
|
||||
interface Props extends TelemetryProps {
|
||||
className?: string
|
||||
authenticatedUser: AuthenticatedUser | null
|
||||
recentSearches: RecentSearchesPanelFragment | null
|
||||
/** Function that returns current time (for stability in visual tests). */
|
||||
now?: () => Date
|
||||
fetchMore: HomePanelsFetchMore
|
||||
}
|
||||
|
||||
export const recentSearchesPanelFragment = gql`
|
||||
fragment RecentSearchesPanelFragment on User {
|
||||
recentSearchesLogs: eventLogs(first: $firstRecentSearches, eventName: "SearchResultsQueried") {
|
||||
nodes {
|
||||
argument
|
||||
timestamp
|
||||
url
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const RecentSearchesPanel: React.FunctionComponent<React.PropsWithChildren<Props>> = ({
|
||||
className,
|
||||
now,
|
||||
telemetryService,
|
||||
recentSearches,
|
||||
fetchMore,
|
||||
}) => {
|
||||
const [searchEventLogs, setSearchEventLogs] = useState<null | RecentSearchesPanelFragment['recentSearchesLogs']>(
|
||||
recentSearches?.recentSearchesLogs ?? null
|
||||
)
|
||||
useEffect(
|
||||
() => setSearchEventLogs(recentSearches?.recentSearchesLogs ?? null),
|
||||
[recentSearches?.recentSearchesLogs]
|
||||
)
|
||||
|
||||
const [itemsToLoad, setItemsToLoad] = useState(RECENT_SEARCHES_TO_LOAD)
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
||||
|
||||
const processedResults = useMemo(
|
||||
() => (searchEventLogs === null ? null : processRecentSearches(searchEventLogs)),
|
||||
[searchEventLogs]
|
||||
)
|
||||
const getItemRef = useFocusOnLoadedMore(processedResults?.length ?? 0)
|
||||
|
||||
useEffect(() => {
|
||||
// Only log the first load (when items to load is equal to the page size)
|
||||
if (processedResults && itemsToLoad === RECENT_SEARCHES_TO_LOAD) {
|
||||
telemetryService.log(
|
||||
'RecentSearchesPanelLoaded',
|
||||
{ empty: processedResults.length === 0 },
|
||||
{ empty: processedResults.length === 0 }
|
||||
)
|
||||
}
|
||||
}, [processedResults, telemetryService, itemsToLoad])
|
||||
|
||||
const logSearchClicked = useCallback(
|
||||
() => telemetryService.log('RecentSearchesPanelSearchClicked'),
|
||||
[telemetryService]
|
||||
)
|
||||
|
||||
const loadingDisplay = <LoadingPanelView text="Loading recent searches" />
|
||||
const emptyDisplay = (
|
||||
<EmptyPanelContainer className="text-muted">
|
||||
<small className="mb-2">
|
||||
Your recent searches will be displayed here. Here are a few searches to get you started:
|
||||
</small>
|
||||
|
||||
<ul className={styles.examplesList}>
|
||||
<li className={styles.examplesListItem}>
|
||||
<small>
|
||||
<Link
|
||||
to={
|
||||
'/search?' +
|
||||
buildSearchURLQuery(
|
||||
'lang:c if(:[eval_match]) { :[statement_match] }',
|
||||
SearchPatternType.structural,
|
||||
false
|
||||
)
|
||||
}
|
||||
className="text-monospace"
|
||||
>
|
||||
<SyntaxHighlightedSearchQuery query="lang:c if(:[eval_match]) { :[statement_match] }" />
|
||||
</Link>
|
||||
</small>
|
||||
</li>
|
||||
<li className={styles.examplesListItem}>
|
||||
<small>
|
||||
<Link
|
||||
to={
|
||||
'/search?' +
|
||||
buildSearchURLQuery(
|
||||
'lang:java type:diff after:"1 week ago"',
|
||||
SearchPatternType.standard,
|
||||
false
|
||||
)
|
||||
}
|
||||
className="text-monospace"
|
||||
>
|
||||
<SyntaxHighlightedSearchQuery query='lang:java type:diff after:"1 week ago"' />
|
||||
</Link>
|
||||
</small>
|
||||
</li>
|
||||
<li className={styles.examplesListItem}>
|
||||
<small>
|
||||
<Link
|
||||
to={'/search?' + buildSearchURLQuery('lang:java', SearchPatternType.standard, false)}
|
||||
className="text-monospace"
|
||||
>
|
||||
<SyntaxHighlightedSearchQuery query="lang:java" />
|
||||
</Link>
|
||||
</small>
|
||||
</li>
|
||||
</ul>
|
||||
</EmptyPanelContainer>
|
||||
)
|
||||
|
||||
async function loadMoreItems(): Promise<void> {
|
||||
telemetryService.log('RecentSearchesPanelShowMoreClicked')
|
||||
const newItemsToLoad = itemsToLoad + RECENT_SEARCHES_TO_LOAD
|
||||
setItemsToLoad(newItemsToLoad)
|
||||
|
||||
setIsLoadingMore(true)
|
||||
const { data } = await fetchMore({
|
||||
firstRecentSearches: newItemsToLoad,
|
||||
})
|
||||
|
||||
setIsLoadingMore(false)
|
||||
if (data === undefined) {
|
||||
return
|
||||
}
|
||||
const node = data.node
|
||||
if (node === null || node.__typename !== 'User') {
|
||||
return
|
||||
}
|
||||
setSearchEventLogs(node.recentSearchesLogs)
|
||||
}
|
||||
|
||||
const contentDisplay = (
|
||||
<>
|
||||
<table className={classNames('mt-2', styles.resultsTable)}>
|
||||
<thead>
|
||||
<tr className={styles.resultsTableRow}>
|
||||
<th>
|
||||
<small>Search</small>
|
||||
</th>
|
||||
<th>
|
||||
<small>Date</small>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{processedResults?.map((recentSearch, index) => (
|
||||
<tr key={index} className={styles.resultsTableRow}>
|
||||
<td>
|
||||
<small className={styles.recentQuery}>
|
||||
<Link to={recentSearch.url} onClick={logSearchClicked} ref={getItemRef(index)}>
|
||||
<SyntaxHighlightedSearchQuery query={recentSearch.searchText} />
|
||||
</Link>
|
||||
</small>
|
||||
</td>
|
||||
<td className={styles.resultsTableDateCol}>
|
||||
<Timestamp noAbout={true} date={recentSearch.timestamp} now={now} strict={true} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{isLoadingMore && <VisuallyHidden aria-live="polite">Loading more recent searches</VisuallyHidden>}
|
||||
{searchEventLogs?.pageInfo.hasNextPage && (
|
||||
<ShowMoreButton className="test-repositories-panel-show-more" onClick={loadMoreItems} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<PanelContainer
|
||||
className={classNames(className, 'recent-searches-panel')}
|
||||
title="Recent searches"
|
||||
state={processedResults ? (processedResults.length > 0 ? 'populated' : 'empty') : 'loading'}
|
||||
loadingContent={loadingDisplay}
|
||||
populatedContent={contentDisplay}
|
||||
emptyContent={emptyDisplay}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function processRecentSearches(eventLogResult?: EventLogResult): RecentSearch[] | null {
|
||||
if (!eventLogResult) {
|
||||
return null
|
||||
}
|
||||
|
||||
const recentSearches: RecentSearch[] = []
|
||||
|
||||
for (const node of eventLogResult.nodes) {
|
||||
if (node.argument && node.url) {
|
||||
const parsedArguments = JSON.parse(node.argument)
|
||||
const searchText: string | undefined = parsedArguments?.code_search?.query_data?.combined
|
||||
|
||||
if (searchText) {
|
||||
if (recentSearches.length > 0 && recentSearches[recentSearches.length - 1].searchText === searchText) {
|
||||
recentSearches[recentSearches.length - 1].count += 1
|
||||
} else {
|
||||
const parsedUrl = new URL(node.url)
|
||||
recentSearches.push({
|
||||
count: 1,
|
||||
url: parsedUrl.pathname + parsedUrl.search, // Strip domain from URL so clicking on it doesn't reload page
|
||||
searchText,
|
||||
timestamp: node.timestamp,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return recentSearches
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
import { Meta, DecoratorFn, Story } from '@storybook/react'
|
||||
import { noop } from 'lodash'
|
||||
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { H2 } from '@sourcegraph/wildcard'
|
||||
|
||||
import { WebStory } from '../../components/WebStory'
|
||||
|
||||
import { RepositoriesPanel } from './RepositoriesPanel'
|
||||
import { recentSearchesPayload } from './utils'
|
||||
|
||||
const decorator: DecoratorFn = story => <div style={{ width: '800px' }}>{story()}</div>
|
||||
|
||||
const config: Meta = {
|
||||
title: 'web/search/panels/RepositoriesPanel',
|
||||
parameters: {
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/file/sPRyyv3nt5h0284nqEuAXE/12192-Sourcegraph-server-page-v1?node-id=255%3A3',
|
||||
},
|
||||
chromatic: { viewports: [800], disableSnapshot: false },
|
||||
},
|
||||
decorators: [decorator],
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
const emptyRecentSearches = {
|
||||
totalCount: 0,
|
||||
nodes: [],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
},
|
||||
}
|
||||
|
||||
const props = {
|
||||
authenticatedUser: null,
|
||||
recentlySearchedRepositories: { recentlySearchedRepositoriesLogs: recentSearchesPayload() },
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any
|
||||
fetchMore: noop as any,
|
||||
}
|
||||
|
||||
export const RepositoriesPanelStory: Story = () => (
|
||||
<WebStory>
|
||||
{() => (
|
||||
<div style={{ maxWidth: '32rem' }}>
|
||||
<H2>Populated</H2>
|
||||
<RepositoriesPanel {...props} />
|
||||
|
||||
<H2>Loading</H2>
|
||||
<RepositoriesPanel {...props} recentlySearchedRepositories={null} />
|
||||
|
||||
<H2>Empty</H2>
|
||||
<RepositoriesPanel
|
||||
{...props}
|
||||
recentlySearchedRepositories={{ recentlySearchedRepositoriesLogs: emptyRecentSearches }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</WebStory>
|
||||
)
|
||||
|
||||
RepositoriesPanelStory.storyName = 'RepositoriesPanel'
|
||||
@ -1,213 +0,0 @@
|
||||
import { screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { noop } from 'rxjs'
|
||||
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing'
|
||||
|
||||
import { RepositoriesPanel } from './RepositoriesPanel'
|
||||
|
||||
describe('RepositoriesPanel', () => {
|
||||
test('Both r: and repo: filters are tracked', () => {
|
||||
const recentSearches = {
|
||||
nodes: [
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:39Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "repo:test"}}}',
|
||||
timestamp: '2020-09-04T18:44:30Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=repo:test&patternType=literal',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
},
|
||||
totalCount: 3,
|
||||
}
|
||||
|
||||
const props = {
|
||||
authenticatedUser: null,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
recentlySearchedRepositories: { recentlySearchedRepositoriesLogs: recentSearches },
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any
|
||||
fetchMore: noop as any,
|
||||
}
|
||||
|
||||
expect(renderWithBrandedContext(<RepositoriesPanel {...props} />).asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('consecutive searches with identical repo filters are correctly merged when rendered', () => {
|
||||
const recentSearches = {
|
||||
nodes: [
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 4, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 0}, "field_default": {"count": 1, "count_regexp": 0, "count_literal": 1, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "test"}}}',
|
||||
timestamp: '2020-09-08T17:36:52Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:test&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:39Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph+test&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:30Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
},
|
||||
totalCount: 3,
|
||||
}
|
||||
|
||||
const props = {
|
||||
authenticatedUser: null,
|
||||
recentlySearchedRepositories: { recentlySearchedRepositoriesLogs: recentSearches },
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any
|
||||
fetchMore: noop as any,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
}
|
||||
|
||||
expect(renderWithBrandedContext(<RepositoriesPanel {...props} />).asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Show More button is shown if more pages are available', () => {
|
||||
const recentSearches = {
|
||||
nodes: [
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 4, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 0}, "field_default": {"count": 1, "count_regexp": 0, "count_literal": 1, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "test"}}}',
|
||||
timestamp: '2020-09-08T17:36:52Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:test&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:39Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:30Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:test-two&patternType=literal',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: true,
|
||||
},
|
||||
totalCount: 6,
|
||||
}
|
||||
|
||||
const props = {
|
||||
authenticatedUser: null,
|
||||
recentlySearchedRepositories: { recentlySearchedRepositoriesLogs: recentSearches },
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any
|
||||
fetchMore: noop as any,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
}
|
||||
|
||||
expect(renderWithBrandedContext(<RepositoriesPanel {...props} />).asFragment()).toMatchSnapshot()
|
||||
})
|
||||
|
||||
test('Show More button loads more items', () => {
|
||||
const recentSearches1 = {
|
||||
nodes: [
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 4, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 0}, "field_default": {"count": 1, "count_regexp": 0, "count_literal": 1, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "test"}}}',
|
||||
timestamp: '2020-09-08T17:36:52Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:test&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:39Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:30Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:test-two&patternType=literal',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: true,
|
||||
},
|
||||
totalCount: 6,
|
||||
}
|
||||
|
||||
const recentSearches2 = {
|
||||
nodes: [
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 4, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 0}, "field_default": {"count": 1, "count_regexp": 0, "count_literal": 1, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "test"}}}',
|
||||
timestamp: '2020-09-08T17:36:52Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:test&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:39Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:30Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:test-two&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 4, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 0}, "field_default": {"count": 1, "count_regexp": 0, "count_literal": 1, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "test"}}}',
|
||||
timestamp: '2020-09-08T17:36:52Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:test-three&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:39Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:r:test-four&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:30Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:test-five&patternType=literal',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
endCursor: null,
|
||||
hasNextPage: false,
|
||||
},
|
||||
totalCount: 6,
|
||||
}
|
||||
|
||||
const props = {
|
||||
className: '',
|
||||
authenticatedUser: null,
|
||||
recentlySearchedRepositories: { recentlySearchedRepositoriesLogs: recentSearches1 },
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-explicit-any
|
||||
fetchMore: (() => ({ recentSearchesLogs: recentSearches2 })) as any,
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
}
|
||||
|
||||
const { asFragment } = renderWithBrandedContext(<RepositoriesPanel {...props} />)
|
||||
userEvent.click(screen.getByRole('button', { name: /Show more/ }))
|
||||
expect(asFragment()).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
@ -1,239 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { gql } from '@apollo/client'
|
||||
import VisuallyHidden from '@reach/visually-hidden'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { SyntaxHighlightedSearchQuery } from '@sourcegraph/search-ui'
|
||||
import { scanSearchQuery } from '@sourcegraph/shared/src/search/query/scanner'
|
||||
import { isRepoFilter } from '@sourcegraph/shared/src/search/query/validate'
|
||||
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { Link, Text, useFocusOnLoadedMore } from '@sourcegraph/wildcard'
|
||||
|
||||
import { parseSearchURLQuery } from '..'
|
||||
import { AuthenticatedUser } from '../../auth'
|
||||
import { RecentlySearchedRepositoriesFragment } from '../../graphql-operations'
|
||||
import { EventLogResult } from '../backend'
|
||||
|
||||
import { EmptyPanelContainer } from './EmptyPanelContainer'
|
||||
import { HomePanelsFetchMore, RECENTLY_SEARCHED_REPOSITORIES_TO_LOAD } from './HomePanels'
|
||||
import { LoadingPanelView } from './LoadingPanelView'
|
||||
import { PanelContainer } from './PanelContainer'
|
||||
import { ShowMoreButton } from './ShowMoreButton'
|
||||
import { useComputeResults } from './useComputeResults'
|
||||
|
||||
import styles from './RecentSearchesPanel.module.scss'
|
||||
|
||||
interface Props extends TelemetryProps {
|
||||
className?: string
|
||||
authenticatedUser: AuthenticatedUser | null
|
||||
recentlySearchedRepositories: RecentlySearchedRepositoriesFragment | null
|
||||
fetchMore: HomePanelsFetchMore
|
||||
}
|
||||
|
||||
export const recentlySearchedRepositoriesFragment = gql`
|
||||
fragment RecentlySearchedRepositoriesFragment on User {
|
||||
recentlySearchedRepositoriesLogs: eventLogs(
|
||||
first: $firstRecentlySearchedRepositories
|
||||
eventName: "SearchResultsQueried"
|
||||
) {
|
||||
nodes {
|
||||
argument
|
||||
timestamp
|
||||
url
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
}
|
||||
totalCount
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const RepositoriesPanel: React.FunctionComponent<React.PropsWithChildren<Props>> = ({
|
||||
className,
|
||||
telemetryService,
|
||||
recentlySearchedRepositories,
|
||||
fetchMore,
|
||||
authenticatedUser,
|
||||
}) => {
|
||||
const [recentlySearchedRepos, setRecentlySearchedRepos] = useState<
|
||||
null | RecentlySearchedRepositoriesFragment['recentlySearchedRepositoriesLogs']
|
||||
>(recentlySearchedRepositories?.recentlySearchedRepositoriesLogs ?? null)
|
||||
useEffect(
|
||||
() => setRecentlySearchedRepos(recentlySearchedRepositories?.recentlySearchedRepositoriesLogs ?? null),
|
||||
[recentlySearchedRepositories?.recentlySearchedRepositoriesLogs]
|
||||
)
|
||||
|
||||
const [itemsToLoad, setItemsToLoad] = useState(RECENTLY_SEARCHED_REPOSITORIES_TO_LOAD)
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false)
|
||||
const [repoFilterValues, setRepoFilterValues] = useState<string[] | null>(null)
|
||||
const getItemRef = useFocusOnLoadedMore(repoFilterValues?.length ?? 0)
|
||||
useEffect(() => {
|
||||
if (recentlySearchedRepos) {
|
||||
setRepoFilterValues(processRepositories(recentlySearchedRepos))
|
||||
}
|
||||
}, [recentlySearchedRepos])
|
||||
|
||||
useEffect(() => {
|
||||
// Only log the first load (when items to load is equal to the page size)
|
||||
if (repoFilterValues && itemsToLoad === RECENTLY_SEARCHED_REPOSITORIES_TO_LOAD) {
|
||||
telemetryService.log(
|
||||
'RepositoriesPanelLoaded',
|
||||
{ empty: repoFilterValues.length === 0 },
|
||||
{ empty: repoFilterValues.length === 0 }
|
||||
)
|
||||
}
|
||||
}, [repoFilterValues, telemetryService, itemsToLoad])
|
||||
|
||||
const logRepoClicked = useCallback(
|
||||
() => telemetryService.log('RepositoriesPanelRepoFilterClicked'),
|
||||
[telemetryService]
|
||||
)
|
||||
|
||||
const loadingDisplay = <LoadingPanelView text="Loading recently searched repositories" />
|
||||
|
||||
const emptyDisplay = (
|
||||
<EmptyPanelContainer className="text-muted">
|
||||
<small className="mb-2">
|
||||
<Text className="mb-1">Recently searched repositories will be displayed here.</Text>
|
||||
<Text className="mb-1">
|
||||
Search in repositories with the <strong>repo:</strong> filter:
|
||||
</Text>
|
||||
<Text className="mb-1">
|
||||
<SyntaxHighlightedSearchQuery query="repo:sourcegraph/sourcegraph" />
|
||||
</Text>
|
||||
<Text className="mb-1">Add the code host to scope to a single repository:</Text>
|
||||
<Text className="mb-1">
|
||||
<SyntaxHighlightedSearchQuery query="repo:^git\.local/my/repo$" />
|
||||
</Text>
|
||||
</small>
|
||||
</EmptyPanelContainer>
|
||||
)
|
||||
|
||||
async function loadMoreItems(): Promise<void> {
|
||||
telemetryService.log('RepositoriesPanelShowMoreClicked')
|
||||
const newItemsToLoad = itemsToLoad + RECENTLY_SEARCHED_REPOSITORIES_TO_LOAD
|
||||
setItemsToLoad(newItemsToLoad)
|
||||
|
||||
setIsLoadingMore(true)
|
||||
const { data } = await fetchMore({
|
||||
firstRecentlySearchedRepositories: newItemsToLoad,
|
||||
})
|
||||
setIsLoadingMore(false)
|
||||
if (data === undefined) {
|
||||
return
|
||||
}
|
||||
const node = data.node
|
||||
if (node === null || node.__typename !== 'User') {
|
||||
return
|
||||
}
|
||||
setRecentlySearchedRepos(node.recentlySearchedRepositoriesLogs)
|
||||
}
|
||||
|
||||
const { isLoading: computeLoading, results: computeResults } = useComputeResults(authenticatedUser, '$repo')
|
||||
|
||||
const renderComputeResults = computeResults.size > 0
|
||||
|
||||
const contentDisplay = (
|
||||
<>
|
||||
<table className={classNames('mt-2', styles.resultsTable)}>
|
||||
<thead>
|
||||
<tr className={styles.resultsTableRow}>
|
||||
<th>
|
||||
<small>Search</small>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{renderComputeResults
|
||||
? [...computeResults].map((repoFilterValue, index) => (
|
||||
<tr
|
||||
key={index}
|
||||
className={classNames('text-monospace text-break', styles.resultsTableRow)}
|
||||
>
|
||||
<td>
|
||||
<small>
|
||||
<Link
|
||||
to={`/search?q=repo:${repoFilterValue}`}
|
||||
ref={getItemRef(index)}
|
||||
onClick={logRepoClicked}
|
||||
>
|
||||
<SyntaxHighlightedSearchQuery query={`repo:${repoFilterValue}`} />
|
||||
</Link>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
: repoFilterValues?.map((repoFilterValue, index) => (
|
||||
<tr
|
||||
key="index"
|
||||
className={classNames('text-monospace text-break', styles.resultsTableRow)}
|
||||
>
|
||||
<td>
|
||||
<small>
|
||||
<Link
|
||||
to={`/search?q=repo:${repoFilterValue}`}
|
||||
ref={getItemRef(index)}
|
||||
onClick={logRepoClicked}
|
||||
>
|
||||
<SyntaxHighlightedSearchQuery query={`repo:${repoFilterValue}`} />
|
||||
</Link>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{!renderComputeResults && (
|
||||
<>
|
||||
{isLoadingMore && <VisuallyHidden aria-live="polite">Loading more repositories</VisuallyHidden>}
|
||||
{recentlySearchedRepos?.pageInfo.hasNextPage && (
|
||||
<ShowMoreButton className="test-repositories-panel-show-more" onClick={loadMoreItems} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
// Wait for both the search event logs and the git history to be loaded
|
||||
const isLoading = computeLoading || !repoFilterValues
|
||||
// If neither search event logs or git history have items, then display the empty display
|
||||
const isEmpty = repoFilterValues?.length === 0 && computeResults.size === 0
|
||||
|
||||
return (
|
||||
<PanelContainer
|
||||
className={classNames(className, 'repositories-panel')}
|
||||
title="Repositories"
|
||||
state={isLoading ? 'loading' : isEmpty ? 'empty' : 'populated'}
|
||||
loadingContent={loadingDisplay}
|
||||
populatedContent={contentDisplay}
|
||||
emptyContent={emptyDisplay}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function processRepositories(eventLogResult: EventLogResult): string[] | null {
|
||||
if (!eventLogResult) {
|
||||
return null
|
||||
}
|
||||
|
||||
const recentlySearchedRepos: string[] = []
|
||||
|
||||
for (const node of eventLogResult.nodes) {
|
||||
if (node.url) {
|
||||
const url = new URL(node.url)
|
||||
const queryFromURL = parseSearchURLQuery(url.search)
|
||||
const scannedQuery = scanSearchQuery(queryFromURL || '')
|
||||
if (scannedQuery.type === 'success') {
|
||||
for (const token of scannedQuery.term) {
|
||||
if (isRepoFilter(token) && token.value && !recentlySearchedRepos.includes(token.value.value)) {
|
||||
recentlySearchedRepos.push(token.value.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return recentlySearchedRepos
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
import { Meta, Story } from '@storybook/react'
|
||||
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { H2 } from '@sourcegraph/wildcard'
|
||||
|
||||
import { WebStory } from '../../components/WebStory'
|
||||
import { SearchPatternType } from '../../graphql-operations'
|
||||
|
||||
import { SavedSearchesPanel } from './SavedSearchesPanel'
|
||||
import { savedSearchesPayload, authUser } from './utils'
|
||||
|
||||
const config: Meta = {
|
||||
title: 'web/search/panels/SavedSearchesPanel',
|
||||
parameters: {
|
||||
design: {
|
||||
type: 'figma',
|
||||
url: 'https://www.figma.com/file/sPRyyv3nt5h0284nqEuAXE/12192-Sourcegraph-server-page-v1?node-id=255%3A3',
|
||||
},
|
||||
chromatic: { disableSnapshot: false },
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
||||
const props = {
|
||||
authenticatedUser: authUser,
|
||||
patternType: SearchPatternType.standard,
|
||||
savedSearchesFragment: { savedSearches: savedSearchesPayload() },
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
}
|
||||
|
||||
export const SavedSearchesPanelStory: Story = () => (
|
||||
<WebStory>
|
||||
{() => (
|
||||
<div style={{ maxWidth: '32rem' }}>
|
||||
<H2>Populated</H2>
|
||||
<SavedSearchesPanel {...props} />
|
||||
|
||||
<H2>Loading</H2>
|
||||
<SavedSearchesPanel {...props} savedSearchesFragment={null} />
|
||||
|
||||
<H2>Empty</H2>
|
||||
<SavedSearchesPanel {...props} savedSearchesFragment={{ savedSearches: [] }} />
|
||||
</div>
|
||||
)}
|
||||
</WebStory>
|
||||
)
|
||||
|
||||
SavedSearchesPanelStory.storyName = 'SavedSearchesPanel'
|
||||
@ -1,36 +0,0 @@
|
||||
import { cleanup, fireEvent } from '@testing-library/react'
|
||||
|
||||
import { NOOP_TELEMETRY_SERVICE } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { renderWithBrandedContext } from '@sourcegraph/wildcard/src/testing'
|
||||
|
||||
import { SearchPatternType } from '../../graphql-operations'
|
||||
|
||||
import { SavedSearchesPanel } from './SavedSearchesPanel'
|
||||
import { savedSearchesPayload, authUser } from './utils'
|
||||
|
||||
describe('SavedSearchesPanel', () => {
|
||||
afterAll(cleanup)
|
||||
|
||||
let container: HTMLElement
|
||||
|
||||
const defaultProps = {
|
||||
patternType: SearchPatternType.standard,
|
||||
authenticatedUser: authUser,
|
||||
savedSearchesFragment: { savedSearches: savedSearchesPayload() },
|
||||
telemetryService: NOOP_TELEMETRY_SERVICE,
|
||||
}
|
||||
|
||||
it('should show correct mode and number of entries when clicking on "my searches" and "all searches" buttons', () => {
|
||||
container = renderWithBrandedContext(<SavedSearchesPanel {...defaultProps} />).container
|
||||
let savedSearchEntries = container.querySelectorAll('.test-saved-search-entry')
|
||||
expect(savedSearchEntries.length).toBe(2)
|
||||
const mySearchesButton = container.querySelector('.test-saved-search-panel-my-searches')!
|
||||
fireEvent.click(mySearchesButton)
|
||||
savedSearchEntries = container.querySelectorAll('.test-saved-search-entry')
|
||||
expect(savedSearchEntries.length).toBe(1)
|
||||
const allSearchesButton = container.querySelector('.test-saved-search-panel-all-searches')!
|
||||
fireEvent.click(allSearchesButton)
|
||||
savedSearchEntries = container.querySelectorAll('.test-saved-search-entry')
|
||||
expect(savedSearchEntries.length).toBe(2)
|
||||
})
|
||||
})
|
||||
@ -1,231 +0,0 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
import { gql } from '@apollo/client'
|
||||
import { mdiPlus, mdiPencilOutline } from '@mdi/js'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
ButtonLink,
|
||||
Link,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuList,
|
||||
MenuItem,
|
||||
Icon,
|
||||
} from '@sourcegraph/wildcard'
|
||||
|
||||
import { AuthenticatedUser } from '../../auth'
|
||||
import { SavedSearchesPanelFragment } from '../../graphql-operations'
|
||||
import { buildSearchURLQueryFromQueryState } from '../../stores'
|
||||
|
||||
import { EmptyPanelContainer } from './EmptyPanelContainer'
|
||||
import { FooterPanel } from './FooterPanel'
|
||||
import { LoadingPanelView } from './LoadingPanelView'
|
||||
import { PanelContainer } from './PanelContainer'
|
||||
|
||||
interface Props extends TelemetryProps {
|
||||
className?: string
|
||||
authenticatedUser: AuthenticatedUser | null
|
||||
savedSearchesFragment: SavedSearchesPanelFragment | null
|
||||
insideTabPanel?: boolean
|
||||
}
|
||||
|
||||
export const savedSearchesPanelFragment = gql`
|
||||
fragment SavedSearchesPanelFragment on Query {
|
||||
savedSearches @include(if: $enableSavedSearches) {
|
||||
id
|
||||
description
|
||||
notify
|
||||
notifySlack
|
||||
query
|
||||
namespace {
|
||||
__typename
|
||||
id
|
||||
namespaceName
|
||||
}
|
||||
slackWebhookURL
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const SavedSearchesPanel: React.FunctionComponent<React.PropsWithChildren<Props>> = ({
|
||||
authenticatedUser,
|
||||
className,
|
||||
telemetryService,
|
||||
insideTabPanel,
|
||||
savedSearchesFragment,
|
||||
}) => {
|
||||
const savedSearches = savedSearchesFragment?.savedSearches ?? null
|
||||
|
||||
const [showAllSearches, setShowAllSearches] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
// Only log the first load (when items to load is equal to the page size)
|
||||
if (savedSearches) {
|
||||
telemetryService.log(
|
||||
'SavedSearchesPanelLoaded',
|
||||
{ empty: savedSearches.length === 0, showAllSearches },
|
||||
{ empty: savedSearches.length === 0, showAllSearches }
|
||||
)
|
||||
}
|
||||
}, [savedSearches, telemetryService, showAllSearches])
|
||||
|
||||
const logEvent = useCallback(
|
||||
(event: string, props?: any) => (): void => telemetryService.log(event, props),
|
||||
[telemetryService]
|
||||
)
|
||||
|
||||
const emptyDisplay = (
|
||||
<EmptyPanelContainer className="text-muted">
|
||||
<small>
|
||||
Use saved searches to alert you to uses of a favorite API, or changes to code you need to monitor.
|
||||
</small>
|
||||
{authenticatedUser && (
|
||||
<ButtonLink
|
||||
to={`/users/${authenticatedUser.username}/searches/add`}
|
||||
onClick={logEvent('SavedSearchesPanelCreateButtonClicked', { source: 'empty view' })}
|
||||
className="mt-2 align-self-center"
|
||||
variant="secondary"
|
||||
as={Link}
|
||||
>
|
||||
<Icon aria-hidden={true} svgPath={mdiPlus} />
|
||||
Create a saved search
|
||||
</ButtonLink>
|
||||
)}
|
||||
</EmptyPanelContainer>
|
||||
)
|
||||
const loadingDisplay = <LoadingPanelView text="Loading saved searches" />
|
||||
|
||||
const contentDisplay = (
|
||||
<div className="d-flex flex-column h-100 justify-content-between">
|
||||
<table className="w-100 mt-2">
|
||||
<thead className="pb-1">
|
||||
<tr>
|
||||
<th>
|
||||
<small>Search</small>
|
||||
</th>
|
||||
<th className="text-right">
|
||||
<small>Edit</small>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{savedSearches
|
||||
?.filter(search => (showAllSearches ? true : search.namespace.id === authenticatedUser?.id))
|
||||
.map(search => (
|
||||
<tr key={search.id} className="text-monospace test-saved-search-entry">
|
||||
<td className="pb-2">
|
||||
<small>
|
||||
<Link
|
||||
to={'/search?' + buildSearchURLQueryFromQueryState({ query: search.query })}
|
||||
className="p-0"
|
||||
onClick={logEvent('SavedSearchesPanelSearchClicked')}
|
||||
>
|
||||
{search.description}
|
||||
</Link>
|
||||
</small>
|
||||
</td>
|
||||
<td className="text-right align-top pb-2">
|
||||
{authenticatedUser &&
|
||||
(search.namespace.__typename === 'User' ? (
|
||||
<Link
|
||||
to={`/users/${search.namespace.namespaceName}/searches/${search.id}`}
|
||||
onClick={logEvent('SavedSearchesPanelEditClicked')}
|
||||
aria-label={`Edit saved search ${search.description}`}
|
||||
>
|
||||
<Icon role="img" aria-hidden={true} svgPath={mdiPencilOutline} />
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
to={`/organizations/${search.namespace.namespaceName}/searches/${search.id}`}
|
||||
onClick={logEvent('SavedSearchesPanelEditClicked')}
|
||||
aria-label={`Edit saved search ${search.description}`}
|
||||
>
|
||||
<Icon role="img" aria-hidden={true} svgPath={mdiPencilOutline} />
|
||||
</Link>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{authenticatedUser && (
|
||||
<FooterPanel className="p-1 mt-3">
|
||||
<small>
|
||||
{/*
|
||||
a11y-ignore
|
||||
Rule: "color-contrast" (Elements must have sufficient color contrast)
|
||||
*/}
|
||||
<Link
|
||||
to={`/users/${authenticatedUser.username}/searches`}
|
||||
className="text-left a11y-ignore"
|
||||
onClick={logEvent('SavedSearchesPanelViewAllClicked')}
|
||||
>
|
||||
View saved searches
|
||||
</Link>
|
||||
</small>
|
||||
</FooterPanel>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
const actionButtons = (
|
||||
<>
|
||||
<ButtonGroup className="d-none d-sm-block d-lg-none d-xl-block" role="tablist">
|
||||
<Button
|
||||
onClick={() => setShowAllSearches(false)}
|
||||
className="test-saved-search-panel-my-searches"
|
||||
outline={showAllSearches}
|
||||
aria-selected={showAllSearches}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
role="tab"
|
||||
>
|
||||
My searches
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShowAllSearches(true)}
|
||||
className="test-saved-search-panel-all-searches"
|
||||
outline={!showAllSearches}
|
||||
aria-selected={!showAllSearches}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
role="tab"
|
||||
>
|
||||
All searches
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
<Menu>
|
||||
<MenuButton
|
||||
variant="icon"
|
||||
outline={true}
|
||||
className="d-block d-sm-none d-lg-block d-xl-none p-0"
|
||||
size="lg"
|
||||
aria-label="Filter saved searches"
|
||||
>
|
||||
...
|
||||
</MenuButton>
|
||||
|
||||
<MenuList>
|
||||
<MenuItem onSelect={() => setShowAllSearches(false)}>My searches</MenuItem>
|
||||
<MenuItem onSelect={() => setShowAllSearches(true)}>All searches</MenuItem>
|
||||
</MenuList>
|
||||
</Menu>
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<PanelContainer
|
||||
insideTabPanel={insideTabPanel}
|
||||
title="Saved searches"
|
||||
className={classNames(className, { 'h-100': insideTabPanel })}
|
||||
state={savedSearches ? (savedSearches.length > 0 ? 'populated' : 'empty') : 'loading'}
|
||||
loadingContent={loadingDisplay}
|
||||
populatedContent={contentDisplay}
|
||||
emptyContent={emptyDisplay}
|
||||
actionButtons={actionButtons}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
import { Button } from '@sourcegraph/wildcard'
|
||||
|
||||
export const ShowMoreButton: React.FunctionComponent<
|
||||
React.PropsWithChildren<{
|
||||
onClick: () => void
|
||||
className?: string
|
||||
dataTestid?: string
|
||||
}>
|
||||
> = ({ onClick, className, dataTestid }) => (
|
||||
<div className="text-center py-3">
|
||||
<Button className={className} onClick={onClick} data-testid={dataTestid} variant="link">
|
||||
Show more
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
@ -1,160 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CommunitySearchContextPanel renders correctly 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="community-search-context-panel panelContainer d-flex flex-column"
|
||||
>
|
||||
<div
|
||||
class="d-flex border-bottom header"
|
||||
>
|
||||
<h2
|
||||
class="h2 h4 headerText"
|
||||
>
|
||||
Community search contexts
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="h-100 content"
|
||||
>
|
||||
<div
|
||||
class="mt-2 row"
|
||||
>
|
||||
<div
|
||||
class="d-flex align-items-center mb-4 col-xl-6 col-lg-12 col-sm-6"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="mr-4 icon"
|
||||
src="https://raw.githubusercontent.com/cncf/artwork/master/other/cncf/icon/color/cncf-icon-color.png"
|
||||
/>
|
||||
<div
|
||||
class="d-flex flex-column"
|
||||
>
|
||||
<a
|
||||
class="anchorLink mb-1"
|
||||
href="/cncf"
|
||||
>
|
||||
Cloud Native Computing Foundation (CNCF)
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center mb-4 col-xl-6 col-lg-12 col-sm-6"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="mr-4 icon"
|
||||
src="https://avatars.githubusercontent.com/u/56493103?s=200&v=4"
|
||||
/>
|
||||
<div
|
||||
class="d-flex flex-column"
|
||||
>
|
||||
<a
|
||||
class="anchorLink mb-1"
|
||||
href="/temporal"
|
||||
>
|
||||
Temporal
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center mb-4 col-xl-6 col-lg-12 col-sm-6"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="mr-4 icon"
|
||||
src="https://raw.githubusercontent.com/o3de/artwork/19b89e72e15824f20204a8977a007f53d5fcd5b8/o3de/03_O3DE%20Application%20Icon/SVG/O3DE-Circle-Icon.svg"
|
||||
/>
|
||||
<div
|
||||
class="d-flex flex-column"
|
||||
>
|
||||
<a
|
||||
class="anchorLink mb-1"
|
||||
href="/o3de"
|
||||
>
|
||||
O3DE
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center mb-4 col-xl-6 col-lg-12 col-sm-6"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="mr-4 icon"
|
||||
src="https://avatars.githubusercontent.com/u/4969009?s=200&v=4"
|
||||
/>
|
||||
<div
|
||||
class="d-flex flex-column"
|
||||
>
|
||||
<a
|
||||
class="anchorLink mb-1"
|
||||
href="/stackstorm"
|
||||
>
|
||||
StackStorm
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center mb-4 col-xl-6 col-lg-12 col-sm-6"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="mr-4 icon"
|
||||
src="https://code.benco.io/icon-collection/logos/kubernetes.svg"
|
||||
/>
|
||||
<div
|
||||
class="d-flex flex-column"
|
||||
>
|
||||
<a
|
||||
class="anchorLink mb-1"
|
||||
href="/kubernetes"
|
||||
>
|
||||
Kubernetes
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center mb-4 col-xl-6 col-lg-12 col-sm-6"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="mr-4 icon"
|
||||
src="https://upload.wikimedia.org/wikipedia/commons/thumb/a/aa/Icons8_flat_graduation_cap.svg/120px-Icons8_flat_graduation_cap.svg.png"
|
||||
/>
|
||||
<div
|
||||
class="d-flex flex-column"
|
||||
>
|
||||
<a
|
||||
class="anchorLink mb-1"
|
||||
href="/stanford"
|
||||
>
|
||||
Stanford University
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="d-flex align-items-center mb-4 col-xl-6 col-lg-12 col-sm-6"
|
||||
>
|
||||
<img
|
||||
alt=""
|
||||
class="mr-4 icon"
|
||||
src="https://raw.githubusercontent.com/sourcegraph-community/julia-context/main/julia.svg"
|
||||
/>
|
||||
<div
|
||||
class="d-flex flex-column"
|
||||
>
|
||||
<a
|
||||
class="anchorLink mb-1"
|
||||
href="/julia"
|
||||
>
|
||||
Julia
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@ -1,60 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PanelContainer content state 1`] = `[Function]`;
|
||||
|
||||
exports[`PanelContainer empty state 1`] = `[Function]`;
|
||||
|
||||
exports[`PanelContainer loading state 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="panelContainer d-flex flex-column"
|
||||
>
|
||||
<div
|
||||
class="d-flex border-bottom header"
|
||||
>
|
||||
<h2
|
||||
class="h2 h4 headerText"
|
||||
>
|
||||
Test Panel
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="h-100 content"
|
||||
>
|
||||
<div>
|
||||
Loading
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`PanelContainer with action buttons 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="panelContainer d-flex flex-column"
|
||||
>
|
||||
<div
|
||||
class="d-flex border-bottom header"
|
||||
>
|
||||
<h2
|
||||
class="h2 h4 headerText"
|
||||
>
|
||||
Test Panel
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
>
|
||||
Button
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="h-100 content"
|
||||
>
|
||||
<div>
|
||||
Content
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@ -1,507 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RecentSearchesPanel Show More button is shown if more pages are available 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="recent-searches-panel panelContainer d-flex flex-column"
|
||||
>
|
||||
<div
|
||||
class="d-flex border-bottom header"
|
||||
>
|
||||
<h2
|
||||
class="h2 h4 headerText"
|
||||
>
|
||||
Recent searches
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="h-100 content"
|
||||
>
|
||||
<table
|
||||
class="mt-2 resultsTable"
|
||||
>
|
||||
<thead>
|
||||
<tr
|
||||
class="resultsTableRow"
|
||||
>
|
||||
<th>
|
||||
<small>
|
||||
Search
|
||||
</small>
|
||||
</th>
|
||||
<th>
|
||||
<small>
|
||||
Date
|
||||
</small>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
class="resultsTableRow"
|
||||
>
|
||||
<td>
|
||||
<small
|
||||
class="recentQuery"
|
||||
>
|
||||
<a
|
||||
class="anchorLink"
|
||||
href="/search?q=test&patternType=literal"
|
||||
>
|
||||
<span
|
||||
class="text-monospace search-query-link"
|
||||
>
|
||||
<span
|
||||
class="search-query-text"
|
||||
>
|
||||
test
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</small>
|
||||
</td>
|
||||
<td
|
||||
class="resultsTableDateCol"
|
||||
>
|
||||
<span
|
||||
class="timestamp"
|
||||
>
|
||||
in 15 years
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="resultsTableRow"
|
||||
>
|
||||
<td>
|
||||
<small
|
||||
class="recentQuery"
|
||||
>
|
||||
<a
|
||||
class="anchorLink"
|
||||
href="/search?q=r:sourcegraph&patternType=literal"
|
||||
>
|
||||
<span
|
||||
class="text-monospace search-query-link"
|
||||
>
|
||||
<span
|
||||
class="search-filter-keyword"
|
||||
>
|
||||
r
|
||||
</span>
|
||||
<span
|
||||
class="search-filter-separator"
|
||||
>
|
||||
:
|
||||
</span>
|
||||
<span
|
||||
class="search-query-text"
|
||||
>
|
||||
sourcegraph
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</small>
|
||||
</td>
|
||||
<td
|
||||
class="resultsTableDateCol"
|
||||
>
|
||||
<span
|
||||
class="timestamp"
|
||||
>
|
||||
in 15 years
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div
|
||||
class="text-center py-3"
|
||||
>
|
||||
<button
|
||||
class="btn btnLink test-repositories-panel-show-more"
|
||||
type="button"
|
||||
>
|
||||
Show more
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`RecentSearchesPanel Show More button loads more items 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="recent-searches-panel panelContainer d-flex flex-column"
|
||||
>
|
||||
<div
|
||||
class="d-flex border-bottom header"
|
||||
>
|
||||
<h2
|
||||
class="h2 h4 headerText"
|
||||
>
|
||||
Recent searches
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="h-100 content"
|
||||
>
|
||||
<table
|
||||
class="mt-2 resultsTable"
|
||||
>
|
||||
<thead>
|
||||
<tr
|
||||
class="resultsTableRow"
|
||||
>
|
||||
<th>
|
||||
<small>
|
||||
Search
|
||||
</small>
|
||||
</th>
|
||||
<th>
|
||||
<small>
|
||||
Date
|
||||
</small>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
class="resultsTableRow"
|
||||
>
|
||||
<td>
|
||||
<small
|
||||
class="recentQuery"
|
||||
>
|
||||
<a
|
||||
class="anchorLink"
|
||||
href="/search?q=test&patternType=literal"
|
||||
>
|
||||
<span
|
||||
class="text-monospace search-query-link"
|
||||
>
|
||||
<span
|
||||
class="search-query-text"
|
||||
>
|
||||
test
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</small>
|
||||
</td>
|
||||
<td
|
||||
class="resultsTableDateCol"
|
||||
>
|
||||
<span
|
||||
class="timestamp"
|
||||
>
|
||||
in 15 years
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="resultsTableRow"
|
||||
>
|
||||
<td>
|
||||
<small
|
||||
class="recentQuery"
|
||||
>
|
||||
<a
|
||||
class="anchorLink"
|
||||
href="/search?q=r:sourcegraph&patternType=literal"
|
||||
>
|
||||
<span
|
||||
class="text-monospace search-query-link"
|
||||
>
|
||||
<span
|
||||
class="search-filter-keyword"
|
||||
>
|
||||
r
|
||||
</span>
|
||||
<span
|
||||
class="search-filter-separator"
|
||||
>
|
||||
:
|
||||
</span>
|
||||
<span
|
||||
class="search-query-text"
|
||||
>
|
||||
sourcegraph
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</small>
|
||||
</td>
|
||||
<td
|
||||
class="resultsTableDateCol"
|
||||
>
|
||||
<span
|
||||
class="timestamp"
|
||||
>
|
||||
in 15 years
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<span
|
||||
aria-live="polite"
|
||||
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap; word-wrap: normal;"
|
||||
>
|
||||
Loading more recent searches
|
||||
</span>
|
||||
<div
|
||||
class="text-center py-3"
|
||||
>
|
||||
<button
|
||||
class="btn btnLink test-repositories-panel-show-more"
|
||||
type="button"
|
||||
>
|
||||
Show more
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`RecentSearchesPanel consecutive identical searches are correctly merged when rendered 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="recent-searches-panel panelContainer d-flex flex-column"
|
||||
>
|
||||
<div
|
||||
class="d-flex border-bottom header"
|
||||
>
|
||||
<h2
|
||||
class="h2 h4 headerText"
|
||||
>
|
||||
Recent searches
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="h-100 content"
|
||||
>
|
||||
<table
|
||||
class="mt-2 resultsTable"
|
||||
>
|
||||
<thead>
|
||||
<tr
|
||||
class="resultsTableRow"
|
||||
>
|
||||
<th>
|
||||
<small>
|
||||
Search
|
||||
</small>
|
||||
</th>
|
||||
<th>
|
||||
<small>
|
||||
Date
|
||||
</small>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
class="resultsTableRow"
|
||||
>
|
||||
<td>
|
||||
<small
|
||||
class="recentQuery"
|
||||
>
|
||||
<a
|
||||
class="anchorLink"
|
||||
href="/search?q=test&patternType=literal"
|
||||
>
|
||||
<span
|
||||
class="text-monospace search-query-link"
|
||||
>
|
||||
<span
|
||||
class="search-query-text"
|
||||
>
|
||||
test
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</small>
|
||||
</td>
|
||||
<td
|
||||
class="resultsTableDateCol"
|
||||
>
|
||||
<span
|
||||
class="timestamp"
|
||||
>
|
||||
in 15 years
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="resultsTableRow"
|
||||
>
|
||||
<td>
|
||||
<small
|
||||
class="recentQuery"
|
||||
>
|
||||
<a
|
||||
class="anchorLink"
|
||||
href="/search?q=r:sourcegraph&patternType=literal"
|
||||
>
|
||||
<span
|
||||
class="text-monospace search-query-link"
|
||||
>
|
||||
<span
|
||||
class="search-filter-keyword"
|
||||
>
|
||||
r
|
||||
</span>
|
||||
<span
|
||||
class="search-filter-separator"
|
||||
>
|
||||
:
|
||||
</span>
|
||||
<span
|
||||
class="search-query-text"
|
||||
>
|
||||
sourcegraph
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</small>
|
||||
</td>
|
||||
<td
|
||||
class="resultsTableDateCol"
|
||||
>
|
||||
<span
|
||||
class="timestamp"
|
||||
>
|
||||
in 15 years
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`RecentSearchesPanel searches with no argument are skipped 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="recent-searches-panel panelContainer d-flex flex-column"
|
||||
>
|
||||
<div
|
||||
class="d-flex border-bottom header"
|
||||
>
|
||||
<h2
|
||||
class="h2 h4 headerText"
|
||||
>
|
||||
Recent searches
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="h-100 content"
|
||||
>
|
||||
<table
|
||||
class="mt-2 resultsTable"
|
||||
>
|
||||
<thead>
|
||||
<tr
|
||||
class="resultsTableRow"
|
||||
>
|
||||
<th>
|
||||
<small>
|
||||
Search
|
||||
</small>
|
||||
</th>
|
||||
<th>
|
||||
<small>
|
||||
Date
|
||||
</small>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
class="resultsTableRow"
|
||||
>
|
||||
<td>
|
||||
<small
|
||||
class="recentQuery"
|
||||
>
|
||||
<a
|
||||
class="anchorLink"
|
||||
href="/search?q=test&patternType=literal"
|
||||
>
|
||||
<span
|
||||
class="text-monospace search-query-link"
|
||||
>
|
||||
<span
|
||||
class="search-query-text"
|
||||
>
|
||||
test
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</small>
|
||||
</td>
|
||||
<td
|
||||
class="resultsTableDateCol"
|
||||
>
|
||||
<span
|
||||
class="timestamp"
|
||||
>
|
||||
in 15 years
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="resultsTableRow"
|
||||
>
|
||||
<td>
|
||||
<small
|
||||
class="recentQuery"
|
||||
>
|
||||
<a
|
||||
class="anchorLink"
|
||||
href="/search?q=r:sourcegraph&patternType=literal"
|
||||
>
|
||||
<span
|
||||
class="text-monospace search-query-link"
|
||||
>
|
||||
<span
|
||||
class="search-filter-keyword"
|
||||
>
|
||||
r
|
||||
</span>
|
||||
<span
|
||||
class="search-filter-separator"
|
||||
>
|
||||
:
|
||||
</span>
|
||||
<span
|
||||
class="search-query-text"
|
||||
>
|
||||
sourcegraph
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</small>
|
||||
</td>
|
||||
<td
|
||||
class="resultsTableDateCol"
|
||||
>
|
||||
<span
|
||||
class="timestamp"
|
||||
>
|
||||
in 15 years
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@ -1,503 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`RepositoriesPanel Both r: and repo: filters are tracked 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="repositories-panel panelContainer d-flex flex-column"
|
||||
>
|
||||
<div
|
||||
class="d-flex border-bottom header"
|
||||
>
|
||||
<h2
|
||||
class="h2 h4 headerText"
|
||||
>
|
||||
Repositories
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="h-100 content"
|
||||
>
|
||||
<table
|
||||
class="mt-2 resultsTable"
|
||||
>
|
||||
<thead>
|
||||
<tr
|
||||
class="resultsTableRow"
|
||||
>
|
||||
<th>
|
||||
<small>
|
||||
Search
|
||||
</small>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
class="text-monospace text-break resultsTableRow"
|
||||
>
|
||||
<td>
|
||||
<small>
|
||||
<a
|
||||
class="anchorLink"
|
||||
href="/search?q=repo:sourcegraph"
|
||||
>
|
||||
<span
|
||||
class="text-monospace search-query-link"
|
||||
>
|
||||
<span
|
||||
class="search-filter-keyword"
|
||||
>
|
||||
repo
|
||||
</span>
|
||||
<span
|
||||
class="search-filter-separator"
|
||||
>
|
||||
:
|
||||
</span>
|
||||
<span
|
||||
class="search-query-text"
|
||||
>
|
||||
sourcegraph
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="text-monospace text-break resultsTableRow"
|
||||
>
|
||||
<td>
|
||||
<small>
|
||||
<a
|
||||
class="anchorLink"
|
||||
href="/search?q=repo:test"
|
||||
>
|
||||
<span
|
||||
class="text-monospace search-query-link"
|
||||
>
|
||||
<span
|
||||
class="search-filter-keyword"
|
||||
>
|
||||
repo
|
||||
</span>
|
||||
<span
|
||||
class="search-filter-separator"
|
||||
>
|
||||
:
|
||||
</span>
|
||||
<span
|
||||
class="search-query-text"
|
||||
>
|
||||
test
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`RepositoriesPanel Show More button is shown if more pages are available 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="repositories-panel panelContainer d-flex flex-column"
|
||||
>
|
||||
<div
|
||||
class="d-flex border-bottom header"
|
||||
>
|
||||
<h2
|
||||
class="h2 h4 headerText"
|
||||
>
|
||||
Repositories
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="h-100 content"
|
||||
>
|
||||
<table
|
||||
class="mt-2 resultsTable"
|
||||
>
|
||||
<thead>
|
||||
<tr
|
||||
class="resultsTableRow"
|
||||
>
|
||||
<th>
|
||||
<small>
|
||||
Search
|
||||
</small>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
class="text-monospace text-break resultsTableRow"
|
||||
>
|
||||
<td>
|
||||
<small>
|
||||
<a
|
||||
class="anchorLink"
|
||||
href="/search?q=repo:test"
|
||||
>
|
||||
<span
|
||||
class="text-monospace search-query-link"
|
||||
>
|
||||
<span
|
||||
class="search-filter-keyword"
|
||||
>
|
||||
repo
|
||||
</span>
|
||||
<span
|
||||
class="search-filter-separator"
|
||||
>
|
||||
:
|
||||
</span>
|
||||
<span
|
||||
class="search-query-text"
|
||||
>
|
||||
test
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="text-monospace text-break resultsTableRow"
|
||||
>
|
||||
<td>
|
||||
<small>
|
||||
<a
|
||||
class="anchorLink"
|
||||
href="/search?q=repo:sourcegraph"
|
||||
>
|
||||
<span
|
||||
class="text-monospace search-query-link"
|
||||
>
|
||||
<span
|
||||
class="search-filter-keyword"
|
||||
>
|
||||
repo
|
||||
</span>
|
||||
<span
|
||||
class="search-filter-separator"
|
||||
>
|
||||
:
|
||||
</span>
|
||||
<span
|
||||
class="search-query-text"
|
||||
>
|
||||
sourcegraph
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="text-monospace text-break resultsTableRow"
|
||||
>
|
||||
<td>
|
||||
<small>
|
||||
<a
|
||||
class="anchorLink"
|
||||
href="/search?q=repo:test-two"
|
||||
>
|
||||
<span
|
||||
class="text-monospace search-query-link"
|
||||
>
|
||||
<span
|
||||
class="search-filter-keyword"
|
||||
>
|
||||
repo
|
||||
</span>
|
||||
<span
|
||||
class="search-filter-separator"
|
||||
>
|
||||
:
|
||||
</span>
|
||||
<span
|
||||
class="search-query-text"
|
||||
>
|
||||
test-two
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div
|
||||
class="text-center py-3"
|
||||
>
|
||||
<button
|
||||
class="btn btnLink test-repositories-panel-show-more"
|
||||
type="button"
|
||||
>
|
||||
Show more
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`RepositoriesPanel Show More button loads more items 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="repositories-panel panelContainer d-flex flex-column"
|
||||
>
|
||||
<div
|
||||
class="d-flex border-bottom header"
|
||||
>
|
||||
<h2
|
||||
class="h2 h4 headerText"
|
||||
>
|
||||
Repositories
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="h-100 content"
|
||||
>
|
||||
<table
|
||||
class="mt-2 resultsTable"
|
||||
>
|
||||
<thead>
|
||||
<tr
|
||||
class="resultsTableRow"
|
||||
>
|
||||
<th>
|
||||
<small>
|
||||
Search
|
||||
</small>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
class="text-monospace text-break resultsTableRow"
|
||||
>
|
||||
<td>
|
||||
<small>
|
||||
<a
|
||||
class="anchorLink"
|
||||
href="/search?q=repo:test"
|
||||
>
|
||||
<span
|
||||
class="text-monospace search-query-link"
|
||||
>
|
||||
<span
|
||||
class="search-filter-keyword"
|
||||
>
|
||||
repo
|
||||
</span>
|
||||
<span
|
||||
class="search-filter-separator"
|
||||
>
|
||||
:
|
||||
</span>
|
||||
<span
|
||||
class="search-query-text"
|
||||
>
|
||||
test
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="text-monospace text-break resultsTableRow"
|
||||
>
|
||||
<td>
|
||||
<small>
|
||||
<a
|
||||
class="anchorLink"
|
||||
href="/search?q=repo:sourcegraph"
|
||||
>
|
||||
<span
|
||||
class="text-monospace search-query-link"
|
||||
>
|
||||
<span
|
||||
class="search-filter-keyword"
|
||||
>
|
||||
repo
|
||||
</span>
|
||||
<span
|
||||
class="search-filter-separator"
|
||||
>
|
||||
:
|
||||
</span>
|
||||
<span
|
||||
class="search-query-text"
|
||||
>
|
||||
sourcegraph
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="text-monospace text-break resultsTableRow"
|
||||
>
|
||||
<td>
|
||||
<small>
|
||||
<a
|
||||
class="anchorLink"
|
||||
href="/search?q=repo:test-two"
|
||||
>
|
||||
<span
|
||||
class="text-monospace search-query-link"
|
||||
>
|
||||
<span
|
||||
class="search-filter-keyword"
|
||||
>
|
||||
repo
|
||||
</span>
|
||||
<span
|
||||
class="search-filter-separator"
|
||||
>
|
||||
:
|
||||
</span>
|
||||
<span
|
||||
class="search-query-text"
|
||||
>
|
||||
test-two
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<span
|
||||
aria-live="polite"
|
||||
style="border: 0px; height: 1px; margin: -1px; overflow: hidden; padding: 0px; position: absolute; width: 1px; white-space: nowrap; word-wrap: normal;"
|
||||
>
|
||||
Loading more repositories
|
||||
</span>
|
||||
<div
|
||||
class="text-center py-3"
|
||||
>
|
||||
<button
|
||||
class="btn btnLink test-repositories-panel-show-more"
|
||||
type="button"
|
||||
>
|
||||
Show more
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
|
||||
exports[`RepositoriesPanel consecutive searches with identical repo filters are correctly merged when rendered 1`] = `
|
||||
<DocumentFragment>
|
||||
<div
|
||||
class="repositories-panel panelContainer d-flex flex-column"
|
||||
>
|
||||
<div
|
||||
class="d-flex border-bottom header"
|
||||
>
|
||||
<h2
|
||||
class="h2 h4 headerText"
|
||||
>
|
||||
Repositories
|
||||
</h2>
|
||||
</div>
|
||||
<div
|
||||
class="h-100 content"
|
||||
>
|
||||
<table
|
||||
class="mt-2 resultsTable"
|
||||
>
|
||||
<thead>
|
||||
<tr
|
||||
class="resultsTableRow"
|
||||
>
|
||||
<th>
|
||||
<small>
|
||||
Search
|
||||
</small>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
class="text-monospace text-break resultsTableRow"
|
||||
>
|
||||
<td>
|
||||
<small>
|
||||
<a
|
||||
class="anchorLink"
|
||||
href="/search?q=repo:test"
|
||||
>
|
||||
<span
|
||||
class="text-monospace search-query-link"
|
||||
>
|
||||
<span
|
||||
class="search-filter-keyword"
|
||||
>
|
||||
repo
|
||||
</span>
|
||||
<span
|
||||
class="search-filter-separator"
|
||||
>
|
||||
:
|
||||
</span>
|
||||
<span
|
||||
class="search-query-text"
|
||||
>
|
||||
test
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr
|
||||
class="text-monospace text-break resultsTableRow"
|
||||
>
|
||||
<td>
|
||||
<small>
|
||||
<a
|
||||
class="anchorLink"
|
||||
href="/search?q=repo:sourcegraph"
|
||||
>
|
||||
<span
|
||||
class="text-monospace search-query-link"
|
||||
>
|
||||
<span
|
||||
class="search-filter-keyword"
|
||||
>
|
||||
repo
|
||||
</span>
|
||||
<span
|
||||
class="search-filter-separator"
|
||||
>
|
||||
:
|
||||
</span>
|
||||
<span
|
||||
class="search-query-text"
|
||||
>
|
||||
sourcegraph
|
||||
</span>
|
||||
</span>
|
||||
</a>
|
||||
</small>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
@ -1,49 +0,0 @@
|
||||
import { useMemo } from 'react'
|
||||
|
||||
import { of } from 'rxjs'
|
||||
|
||||
import { streamComputeQuery } from '@sourcegraph/shared/src/search/stream'
|
||||
import { useObservable } from '@sourcegraph/wildcard'
|
||||
|
||||
import { AuthenticatedUser } from '../../auth'
|
||||
import { useExperimentalFeatures } from '../../stores'
|
||||
|
||||
export type ComputeParseResult = [{ kind: string; value: string }]
|
||||
|
||||
export function useComputeResults(
|
||||
authenticatedUser: AuthenticatedUser | null,
|
||||
computeOutput: string
|
||||
): { isLoading: boolean; results: Set<string> } {
|
||||
const checkHomePanelsFeatureFlag = useExperimentalFeatures(features => features.homePanelsComputeSuggestions)
|
||||
const gitRecentFiles = useObservable(
|
||||
useMemo(
|
||||
() =>
|
||||
checkHomePanelsFeatureFlag && authenticatedUser
|
||||
? streamComputeQuery(
|
||||
`content:output((.|\n)* -> ${computeOutput}) author:${authenticatedUser.email} type:diff after:"1 year ago" count:all`
|
||||
)
|
||||
: of([]),
|
||||
[authenticatedUser, checkHomePanelsFeatureFlag, computeOutput]
|
||||
)
|
||||
)
|
||||
|
||||
const gitSet = useMemo(() => {
|
||||
let gitRepositoryParsedString: ComputeParseResult[] = []
|
||||
if (gitRecentFiles) {
|
||||
gitRepositoryParsedString = gitRecentFiles.map(value => JSON.parse(value) as ComputeParseResult)
|
||||
}
|
||||
const gitReposList = gitRepositoryParsedString?.flat()
|
||||
|
||||
const gitSet = new Set<string>()
|
||||
if (gitReposList) {
|
||||
for (const git of gitReposList) {
|
||||
if (git.value) {
|
||||
gitSet.add(git.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
return gitSet
|
||||
}, [gitRecentFiles])
|
||||
|
||||
return { isLoading: gitRecentFiles === undefined, results: gitSet }
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
import { gql, MutationFunctionOptions, FetchResult } from '@apollo/client'
|
||||
|
||||
import { useMutation } from '@sourcegraph/http-client'
|
||||
|
||||
import { Exact, InviteEmailToSourcegraphResult, InviteEmailToSourcegraphVariables } from '../../graphql-operations'
|
||||
|
||||
const INVITE_EMAIL_TO_SOURCEGRAPH = gql`
|
||||
mutation InviteEmailToSourcegraph($email: String!) {
|
||||
inviteEmailToSourcegraph(email: $email) {
|
||||
alwaysNil
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
type UseInviteEmailToSourcegraphResult = (
|
||||
options?:
|
||||
| MutationFunctionOptions<
|
||||
InviteEmailToSourcegraphResult,
|
||||
Exact<{
|
||||
email: string
|
||||
}>
|
||||
>
|
||||
| undefined
|
||||
) => Promise<FetchResult<InviteEmailToSourcegraphResult>>
|
||||
|
||||
export const useInviteEmailToSourcegraph = (): UseInviteEmailToSourcegraphResult => {
|
||||
const [inviteEmailToSourcegraph] = useMutation<InviteEmailToSourcegraphResult, InviteEmailToSourcegraphVariables>(
|
||||
INVITE_EMAIL_TO_SOURCEGRAPH
|
||||
)
|
||||
return inviteEmailToSourcegraph
|
||||
}
|
||||
@ -1,345 +0,0 @@
|
||||
import { AuthenticatedUser } from '../../auth'
|
||||
import { SavedSearchesPanelFragment } from '../../graphql-operations'
|
||||
import { EventLogResult } from '../backend'
|
||||
|
||||
import { InvitableCollaborator } from './CollaboratorsPanel'
|
||||
|
||||
type SavedSearchPanelFields = NonNullable<SavedSearchesPanelFragment['savedSearches']>[number]
|
||||
type NameSpaceOrg = Extract<SavedSearchPanelFields['namespace'], { __typename: 'Org' }>
|
||||
type NameSpaceUser = Extract<SavedSearchPanelFields['namespace'], { __typename: 'User' }>
|
||||
|
||||
export const authUser: AuthenticatedUser & NameSpaceUser = {
|
||||
__typename: 'User',
|
||||
id: '0',
|
||||
email: 'alice@sourcegraph.com',
|
||||
username: 'alice',
|
||||
avatarURL: null,
|
||||
session: { canSignOut: true },
|
||||
displayName: null,
|
||||
url: '',
|
||||
settingsURL: '#',
|
||||
siteAdmin: true,
|
||||
organizations: {
|
||||
nodes: [
|
||||
{ id: '0', settingsURL: '#', displayName: 'Acme Corp' },
|
||||
{ id: '1', settingsURL: '#', displayName: 'Beta Inc' },
|
||||
] as AuthenticatedUser['organizations']['nodes'],
|
||||
},
|
||||
tags: [],
|
||||
viewerCanAdminister: true,
|
||||
databaseID: 0,
|
||||
tosAccepted: true,
|
||||
searchable: true,
|
||||
namespaceName: 'alice',
|
||||
emails: [],
|
||||
latestSettings: null,
|
||||
}
|
||||
|
||||
const org: NameSpaceOrg = {
|
||||
__typename: 'Org',
|
||||
id: '1',
|
||||
namespaceName: 'test-org',
|
||||
}
|
||||
|
||||
export const savedSearchesPayload = (): SavedSearchPanelFields[] => [
|
||||
{
|
||||
__typename: 'SavedSearch',
|
||||
id: 'test',
|
||||
description: 'test',
|
||||
query: 'test',
|
||||
notify: false,
|
||||
notifySlack: false,
|
||||
namespace: authUser,
|
||||
slackWebhookURL: null,
|
||||
},
|
||||
{
|
||||
__typename: 'SavedSearch',
|
||||
id: 'test-org',
|
||||
description: 'org test',
|
||||
query: 'org test',
|
||||
notify: false,
|
||||
notifySlack: false,
|
||||
namespace: org,
|
||||
slackWebhookURL: null,
|
||||
},
|
||||
]
|
||||
|
||||
export const recentSearchesPayload = (): EventLogResult => ({
|
||||
nodes: [
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 4, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 0}, "field_default": {"count": 1, "count_regexp": 0, "count_literal": 1, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "test and spec"}}}',
|
||||
timestamp: '2020-09-08T17:36:52Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=test%20and%20spec&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 5, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 0}, "field_default": {"count": 1, "count_regexp": 1, "count_literal": 1, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "^test"}}}',
|
||||
timestamp: '2020-09-08T17:26:05Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=%5Etest&patternType=regexp',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 5, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 0}, "field_default": {"count": 1, "count_regexp": 1, "count_literal": 1, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "^test"}}}',
|
||||
timestamp: '2020-09-08T17:20:11Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=%5Etest&patternType=regexp',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 5, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 0}, "field_default": {"count": 1, "count_regexp": 1, "count_literal": 1, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "^test"}}}',
|
||||
timestamp: '2020-09-08T17:20:05Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=%5Etest&patternType=regexp',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 26, "space": 2, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 3, "count_non_default": 1}, "field_lang": {"count": 1, "count_alias": 0, "count_negated": 0}, "field_default": {"count": 2, "count_regexp": 0, "count_literal": 2, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "lang:cpp try {:[my_match]}"}}}',
|
||||
timestamp: '2020-09-08T17:12:53Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=lang:cpp+try+%7B:%5Bmy_match%5D%7D&patternType=structural&onboardingTour=true',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 26, "space": 2, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 3, "count_non_default": 1}, "field_lang": {"count": 1, "count_alias": 0, "count_negated": 0}, "field_default": {"count": 2, "count_regexp": 0, "count_literal": 2, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "lang:cpp try {:[my_match]}"}}}',
|
||||
timestamp: '2020-09-08T17:11:46Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=lang:cpp+try+%7B:%5Bmy_match%5D%7D&patternType=structural&onboardingTour=true',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 86, "space": 4, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 4, "count_non_default": 3}, "field_lang": {"count": 1, "count_alias": 0, "count_negated": 0}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 0, "value_regexp": 1, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_type": {"count": 1, "value_diff": 0, "value_file": 0, "value_commit": 1, "value_symbol": 0}, "field_default": {"count": 2, "count_regexp": 0, "count_literal": 1, "count_pattern": 1, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "repo:^github\\\\.com/sourcegraph/sourcegraph$ PanelContainer lang:typescript type:commit"}}}',
|
||||
timestamp: '2020-09-04T20:31:57Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=repo:%5Egithub%5C.com/sourcegraph/sourcegraph%24+PanelContainer+lang:typescript++type:commit&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 86, "space": 4, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 4, "count_non_default": 3}, "field_lang": {"count": 1, "count_alias": 0, "count_negated": 0}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 0, "value_regexp": 1, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_type": {"count": 1, "value_diff": 0, "value_file": 0, "value_commit": 1, "value_symbol": 0}, "field_default": {"count": 2, "count_regexp": 0, "count_literal": 1, "count_pattern": 1, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "repo:^github\\\\.com/sourcegraph/sourcegraph$ PanelContainer lang:typescript type:commit"}}}',
|
||||
timestamp: '2020-09-04T20:27:02Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=repo:%5Egithub%5C.com/sourcegraph/sourcegraph%24+PanelContainer+lang:typescript++type:commit&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 86, "space": 4, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 4, "count_non_default": 3}, "field_lang": {"count": 1, "count_alias": 0, "count_negated": 0}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 0, "value_regexp": 1, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_type": {"count": 1, "value_diff": 0, "value_file": 0, "value_commit": 1, "value_symbol": 0}, "field_default": {"count": 2, "count_regexp": 0, "count_literal": 1, "count_pattern": 1, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "repo:^github\\\\.com/sourcegraph/sourcegraph$ PanelContainer lang:typescript type:commit"}}}',
|
||||
timestamp: '2020-09-04T20:24:56Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=repo:%5Egithub%5C.com/sourcegraph/sourcegraph%24+PanelContainer+lang:typescript++type:commit&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 74, "space": 3, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 3, "count_non_default": 2}, "field_lang": {"count": 1, "count_alias": 0, "count_negated": 0}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 0, "value_regexp": 1, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 2, "count_regexp": 0, "count_literal": 1, "count_pattern": 1, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "repo:^github\\\\.com/sourcegraph/sourcegraph$ PanelContainer lang:typescript "}}}',
|
||||
timestamp: '2020-09-04T20:23:44Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=repo:%5Egithub%5C.com/sourcegraph/sourcegraph%24+PanelContainer+lang:typescript+&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 57, "space": 1, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 2, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 0, "value_regexp": 1, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 2, "count_regexp": 0, "count_literal": 1, "count_pattern": 1, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "repo:^github\\\\.com/sourcegraph/sourcegraph$ PanelContainer"}}}',
|
||||
timestamp: '2020-09-04T20:23:38Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=repo:%5Egithub%5C.com/sourcegraph/sourcegraph%24+PanelContainer&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 43, "space": 1, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 0, "value_regexp": 1, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 1, "count_regexp": 0, "count_literal": 0, "count_pattern": 1, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "repo:^github\\\\.com/sourcegraph/sourcegraph$ "}}}',
|
||||
timestamp: '2020-09-04T20:23:30Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=repo:%5Egithub%5C.com/sourcegraph/sourcegraph%24+&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 28, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 0, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "repo:sourcegraph/sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T20:23:23Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=repo:sourcegraph/sourcegraph&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 4, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 0}, "field_default": {"count": 1, "count_regexp": 0, "count_literal": 1, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "test"}}}',
|
||||
timestamp: '2020-09-04T20:23:09Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=test&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T20:23:08Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 4, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 0}, "field_default": {"count": 1, "count_regexp": 0, "count_literal": 1, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "test"}}}',
|
||||
timestamp: '2020-09-04T20:23:07Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=test&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T20:23:06Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 4, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 0}, "field_default": {"count": 1, "count_regexp": 0, "count_literal": 1, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "test"}}}',
|
||||
timestamp: '2020-09-04T20:23:06Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=test&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:39Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"mode": "plain", "code_search": {"query_data": {"empty": false, "query": {"chars": {"count": 13, "space": 0, "non_ascii": 0, "double_quote": 0, "single_quote": 0}, "fields": {"count": 1, "count_non_default": 1}, "field_repo": {"count": 1, "value_glob": 0, "value_pipe": 0, "count_alias": 1, "value_regexp": 0, "count_negated": 0, "value_at_sign": 0, "value_rev_star": 0, "value_rev_caret": 0, "value_rev_colon": 0}, "field_default": {"count": 0, "count_regexp": 0, "count_literal": 0, "count_pattern": 0, "count_double_quote": 0, "count_single_quote": 0}}, "combined": "r:sourcegraph"}}}',
|
||||
timestamp: '2020-09-04T18:44:30Z',
|
||||
url: 'https://sourcegraph.test:3443/search?q=r:sourcegraph&patternType=literal',
|
||||
},
|
||||
],
|
||||
pageInfo: {
|
||||
hasNextPage: true,
|
||||
},
|
||||
totalCount: 436,
|
||||
})
|
||||
|
||||
export const recentFilesPayload = (): EventLogResult => ({
|
||||
nodes: [
|
||||
{
|
||||
argument: '{"filePath": "web/src/tree/Tree.tsx", "repoName": "github.com/sourcegraph/sourcegraph"}',
|
||||
timestamp: '2020-09-10T23:07:55Z',
|
||||
url: 'https://sourcegraph.test:3443/github.com/sourcegraph/sourcegraph/-/blob/web/src/tree/Tree.tsx',
|
||||
},
|
||||
{
|
||||
argument: '{"filePath": "web/src/tree/TreeRoot.tsx", "repoName": "github.com/sourcegraph/sourcegraph"}',
|
||||
timestamp: '2020-09-10T23:07:55Z',
|
||||
url: 'https://sourcegraph.test:3443/github.com/sourcegraph/sourcegraph/-/blob/web/src/tree/TreeRoot.tsx',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"filePath": "web/src/tree/SingleChildTreeLayer.tsx", "repoName": "github.com/sourcegraph/sourcegraph"}',
|
||||
timestamp: '2020-09-10T23:07:54Z',
|
||||
url: 'https://sourcegraph.test:3443/github.com/sourcegraph/sourcegraph/-/blob/web/src/tree/SingleChildTreeLayer.tsx',
|
||||
},
|
||||
{
|
||||
argument: '{"filePath": "web/src/tree/Directory.tsx", "repoName": "github.com/sourcegraph/sourcegraph"}',
|
||||
timestamp: '2020-09-10T23:07:54Z',
|
||||
url: 'https://sourcegraph.test:3443/github.com/sourcegraph/sourcegraph/-/blob/web/src/tree/Directory.tsx',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"filePath": "web/src/site/FreeUsersExceededAlert.tsx", "repoName": "github.com/sourcegraph/sourcegraph"}',
|
||||
timestamp: '2020-09-10T23:07:51Z',
|
||||
url: 'https://sourcegraph.test:3443/github.com/sourcegraph/sourcegraph/-/blob/web/src/site/FreeUsersExceededAlert.tsx',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"filePath": "web/src/site/DockerForMacAlert.scss", "repoName": "github.com/sourcegraph/sourcegraph"}',
|
||||
timestamp: '2020-09-10T23:07:50Z',
|
||||
url: 'https://sourcegraph.test:3443/github.com/sourcegraph/sourcegraph/-/blob/web/src/site/DockerForMacAlert.scss',
|
||||
},
|
||||
{
|
||||
argument: '{"filePath": "web/jest.config.js", "repoName": "github.com/sourcegraph/sourcegraph"}',
|
||||
timestamp: '2020-09-10T23:07:45Z',
|
||||
url: 'https://sourcegraph.test:3443/github.com/sourcegraph/sourcegraph/-/blob/web/jest.config.js',
|
||||
},
|
||||
{
|
||||
argument: '{"filePath": "go.mod", "repoName": "ghe.sgdev.org/sourcegraph/gorilla-mux"}',
|
||||
timestamp: '2020-09-10T22:55:30Z',
|
||||
url: 'https://sourcegraph.test:3443/ghe.sgdev.org/sourcegraph/gorilla-mux/-/blob/go.mod',
|
||||
},
|
||||
{
|
||||
argument: '{"filePath": ".eslintrc.js", "repoName": "github.com/sourcegraph/sourcegraph"}',
|
||||
timestamp: '2020-09-10T22:55:18Z',
|
||||
url: 'https://sourcegraph.test:3443/github.com/sourcegraph/sourcegraph/-/blob/.eslintrc.js',
|
||||
},
|
||||
{
|
||||
argument: '{"filePath": "go.mod", "repoName": "ghe.sgdev.org/sourcegraph/gorilla-mux"}',
|
||||
timestamp: '2020-09-10T22:55:06Z',
|
||||
url: 'https://sourcegraph.test:3443/ghe.sgdev.org/sourcegraph/gorilla-mux/-/blob/go.mod',
|
||||
},
|
||||
{
|
||||
argument: '{"filePath": "go.mod", "repoName": "ghe.sgdev.org/sourcegraph/gorilla-mux"}',
|
||||
timestamp: '2020-09-10T22:54:54Z',
|
||||
url: 'https://sourcegraph.test:3443/ghe.sgdev.org/sourcegraph/gorilla-mux/-/blob/go.mod',
|
||||
},
|
||||
{
|
||||
argument: '{"filePath": "go.mod", "repoName": "ghe.sgdev.org/sourcegraph/gorilla-mux"}',
|
||||
timestamp: '2020-09-10T22:54:50Z',
|
||||
url: 'https://sourcegraph.test:3443/ghe.sgdev.org/sourcegraph/gorilla-mux/-/blob/go.mod',
|
||||
},
|
||||
{
|
||||
argument: '{"filePath": "AUTHORS", "repoName": "ghe.sgdev.org/sourcegraph/gorilla-mux"}',
|
||||
timestamp: '2020-09-10T21:21:23Z',
|
||||
url: 'https://sourcegraph.test:3443/ghe.sgdev.org/sourcegraph/gorilla-mux/-/blob/AUTHORS',
|
||||
},
|
||||
{
|
||||
argument: '{"filePath": "LICENSE", "repoName": "ghe.sgdev.org/sourcegraph/gorilla-mux"}',
|
||||
timestamp: '2020-09-10T21:21:23Z',
|
||||
url: 'https://sourcegraph.test:3443/ghe.sgdev.org/sourcegraph/gorilla-mux/-/blob/LICENSE',
|
||||
},
|
||||
{
|
||||
argument: '{"filePath": "README.md", "repoName": "ghe.sgdev.org/sourcegraph/gorilla-mux"}',
|
||||
timestamp: '2020-09-10T21:21:22Z',
|
||||
url: 'https://sourcegraph.test:3443/ghe.sgdev.org/sourcegraph/gorilla-mux/-/blob/README.md',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"filePath": "example_authentication_middleware_test.go", "repoName": "ghe.sgdev.org/sourcegraph/gorilla-mux"}',
|
||||
timestamp: '2020-09-10T21:21:21Z',
|
||||
url: 'https://sourcegraph.test:3443/ghe.sgdev.org/sourcegraph/gorilla-mux/-/blob/example_authentication_middleware_test.go',
|
||||
},
|
||||
{
|
||||
argument:
|
||||
'{"filePath": "example_cors_method_middleware_test.go", "repoName": "ghe.sgdev.org/sourcegraph/gorilla-mux"}',
|
||||
timestamp: '2020-09-10T21:21:20Z',
|
||||
url: 'https://sourcegraph.test:3443/ghe.sgdev.org/sourcegraph/gorilla-mux/-/blob/example_cors_method_middleware_test.go',
|
||||
},
|
||||
{
|
||||
argument: '{"filePath": "example_route_test.go", "repoName": "ghe.sgdev.org/sourcegraph/gorilla-mux"}',
|
||||
timestamp: '2020-09-10T21:21:19Z',
|
||||
url: 'https://sourcegraph.test:3443/ghe.sgdev.org/sourcegraph/gorilla-mux/-/blob/example_route_test.go',
|
||||
},
|
||||
{
|
||||
argument: '{"filePath": "go.mod", "repoName": "ghe.sgdev.org/sourcegraph/gorilla-mux"}',
|
||||
timestamp: '2020-09-10T21:21:16Z',
|
||||
url: 'https://sourcegraph.test:3443/ghe.sgdev.org/sourcegraph/gorilla-mux/-/blob/go.mod',
|
||||
},
|
||||
{
|
||||
argument: '{"filePath": "mux_test.go", "repoName": "ghe.sgdev.org/sourcegraph/gorilla-mux"}',
|
||||
timestamp: '2020-09-10T21:21:03Z',
|
||||
url: 'https://sourcegraph.test:3443/ghe.sgdev.org/sourcegraph/gorilla-mux/-/blob/mux_test.go',
|
||||
},
|
||||
],
|
||||
totalCount: 500,
|
||||
pageInfo: { hasNextPage: true },
|
||||
})
|
||||
|
||||
export const collaboratorsPayload: () => InvitableCollaborator[] = () => [
|
||||
{
|
||||
email: 'hello@philippspiess.com',
|
||||
displayName: 'Philipp Spiess',
|
||||
name: 'Philipp Spiess',
|
||||
avatarURL: 'https://avatars.githubusercontent.com/u/458591?v=4',
|
||||
},
|
||||
{
|
||||
email: 'hello@philippspiess.com',
|
||||
displayName: 'Philipp Spiess',
|
||||
name: 'Philipp Spiess',
|
||||
avatarURL: 'https://avatars.githubusercontent.com/u/458591?v=4',
|
||||
},
|
||||
{
|
||||
email: 'hello@philippspiess.com',
|
||||
displayName: 'Philipp Spiess',
|
||||
name: 'Philipp Spiess',
|
||||
avatarURL: 'https://avatars.githubusercontent.com/u/458591?v=4',
|
||||
},
|
||||
{
|
||||
email: 'hello@nicolasdular.com',
|
||||
displayName: 'Nicolas Dular',
|
||||
name: 'Nicolas Dular',
|
||||
avatarURL: 'https://avatars.githubusercontent.com/u/890544?v=4',
|
||||
},
|
||||
{
|
||||
email: 'mario.telesklav@gmx.at',
|
||||
displayName: 'Mario Telesklav',
|
||||
name: 'Mario Telesklav',
|
||||
avatarURL: 'https://avatars.githubusercontent.com/u/3846403?v=4',
|
||||
},
|
||||
{
|
||||
email: 'gluastoned@gmail.com',
|
||||
displayName: 'Gregor Steiner',
|
||||
name: 'Gregor Steiner',
|
||||
avatarURL: 'https://avatars.githubusercontent.com/u/173158?v=4',
|
||||
},
|
||||
]
|
||||
@ -6,7 +6,6 @@ import { SettingsCascadeOrError } from '@sourcegraph/shared/src/settings/setting
|
||||
|
||||
const defaultSettings: SettingsExperimentalFeatures = {
|
||||
codeMonitoring: true,
|
||||
showEnterpriseHomePanels: false,
|
||||
/**
|
||||
* Whether we show the multiline editor at /search/console
|
||||
*/
|
||||
|
||||
@ -6,6 +6,7 @@ import (
|
||||
"github.com/graph-gophers/graphql-go"
|
||||
"github.com/graph-gophers/graphql-go/relay"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/actor"
|
||||
"github.com/sourcegraph/sourcegraph/internal/auth"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database"
|
||||
@ -111,20 +112,80 @@ func (r *schemaResolver) toSavedSearchResolver(entry types.SavedSearch) *savedSe
|
||||
return &savedSearchResolver{db: r.db, s: entry}
|
||||
}
|
||||
|
||||
func (r *schemaResolver) SavedSearches(ctx context.Context) ([]*savedSearchResolver, error) {
|
||||
a := actor.FromContext(ctx)
|
||||
if !a.IsAuthenticated() {
|
||||
return nil, errors.New("no currently authenticated user")
|
||||
type savedSearchesArgs struct {
|
||||
graphqlutil.ConnectionResolverArgs
|
||||
Namespace graphql.ID
|
||||
}
|
||||
|
||||
func (r *schemaResolver) SavedSearches(ctx context.Context, args savedSearchesArgs) (*graphqlutil.ConnectionResolver[savedSearchResolver], error) {
|
||||
var userID, orgID int32
|
||||
if err := UnmarshalNamespaceID(args.Namespace, &userID, &orgID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allSavedSearches, err := r.db.SavedSearches().ListSavedSearchesByUserID(ctx, a.UID)
|
||||
if userID != 0 {
|
||||
if err := auth.CheckSiteAdminOrSameUser(ctx, r.db, userID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else if orgID != 0 {
|
||||
if err := auth.CheckOrgAccessOrSiteAdmin(ctx, r.db, orgID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
return nil, errors.New("User or Organisation namespace must be provided.")
|
||||
}
|
||||
|
||||
connectionStore := &savedSearchesConnectionStore{
|
||||
db: r.db,
|
||||
userID: &userID,
|
||||
orgID: &orgID,
|
||||
}
|
||||
|
||||
return graphqlutil.NewConnectionResolver[savedSearchResolver](connectionStore, &args.ConnectionResolverArgs, nil)
|
||||
}
|
||||
|
||||
type savedSearchesConnectionStore struct {
|
||||
db database.DB
|
||||
userID *int32
|
||||
orgID *int32
|
||||
}
|
||||
|
||||
func (s *savedSearchesConnectionStore) MarshalCursor(node *savedSearchResolver) (*string, error) {
|
||||
cursor := string(node.ID())
|
||||
|
||||
return &cursor, nil
|
||||
}
|
||||
|
||||
func (s *savedSearchesConnectionStore) UnmarshalCursor(cursor string) (*int, error) {
|
||||
nodeID, err := unmarshalSavedSearchID(graphql.ID(cursor))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
id := int(nodeID)
|
||||
|
||||
return &id, nil
|
||||
}
|
||||
|
||||
func (s *savedSearchesConnectionStore) ComputeTotal(ctx context.Context) (*int32, error) {
|
||||
count, err := s.db.SavedSearches().CountSavedSearchesByOrgOrUser(ctx, s.userID, s.orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
total := int32(count)
|
||||
return &total, nil
|
||||
}
|
||||
|
||||
func (s *savedSearchesConnectionStore) ComputeNodes(ctx context.Context, args *database.PaginationArgs) ([]*savedSearchResolver, error) {
|
||||
allSavedSearches, err := s.db.SavedSearches().ListSavedSearchesByOrgOrUser(ctx, s.userID, s.orgID, args)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var savedSearches []*savedSearchResolver
|
||||
for _, savedSearch := range allSavedSearches {
|
||||
savedSearches = append(savedSearches, r.toSavedSearchResolver(*savedSearch))
|
||||
savedSearches = append(savedSearches, &savedSearchResolver{db: s.db, s: *savedSearch})
|
||||
}
|
||||
|
||||
return savedSearches, nil
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
"github.com/graph-gophers/graphql-go"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/cmd/frontend/graphqlbackend/graphqlutil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/actor"
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
"github.com/sourcegraph/sourcegraph/internal/auth"
|
||||
@ -21,30 +22,156 @@ func TestSavedSearches(t *testing.T) {
|
||||
key := int32(1)
|
||||
|
||||
users := database.NewMockUserStore()
|
||||
users.GetByCurrentAuthUserFunc.SetDefaultReturn(&types.User{SiteAdmin: true, ID: key}, nil)
|
||||
users.GetByIDFunc.SetDefaultReturn(&types.User{SiteAdmin: true, ID: key}, nil)
|
||||
|
||||
ss := database.NewMockSavedSearchStore()
|
||||
ss.ListSavedSearchesByUserIDFunc.SetDefaultHook(func(_ context.Context, userID int32) ([]*types.SavedSearch, error) {
|
||||
return []*types.SavedSearch{{ID: key, Description: "test query", Query: "test type:diff patternType:regexp", UserID: &userID, OrgID: nil}}, nil
|
||||
ss.ListSavedSearchesByOrgOrUserFunc.SetDefaultHook(func(_ context.Context, userID, orgId *int32, paginationArgs *database.PaginationArgs) ([]*types.SavedSearch, error) {
|
||||
return []*types.SavedSearch{{ID: key, Description: "test query", Query: "test type:diff patternType:regexp", UserID: userID, OrgID: nil}}, nil
|
||||
})
|
||||
ss.CountSavedSearchesByOrgOrUserFunc.SetDefaultHook(func(_ context.Context, userID, orgId *int32) (int, error) {
|
||||
return 1, nil
|
||||
})
|
||||
|
||||
db := database.NewMockDB()
|
||||
db.UsersFunc.SetDefaultReturn(users)
|
||||
db.SavedSearchesFunc.SetDefaultReturn(ss)
|
||||
|
||||
savedSearches, err := newSchemaResolver(db, gitserver.NewClient(db)).SavedSearches(actor.WithActor(context.Background(), actor.FromUser(key)))
|
||||
args := savedSearchesArgs{
|
||||
ConnectionResolverArgs: graphqlutil.ConnectionResolverArgs{First: &key},
|
||||
Namespace: MarshalUserID(key),
|
||||
}
|
||||
|
||||
resolver, err := newSchemaResolver(db, gitserver.NewClient(db)).SavedSearches(actor.WithActor(context.Background(), actor.FromUser(key)), args)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []*savedSearchResolver{{db, types.SavedSearch{
|
||||
ID: key,
|
||||
Description: "test query",
|
||||
Query: "test type:diff patternType:regexp",
|
||||
UserID: &key,
|
||||
OrgID: nil,
|
||||
|
||||
nodes, err := resolver.Nodes(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wantNodes := []*savedSearchResolver{{db, types.SavedSearch{
|
||||
ID: key,
|
||||
Description: "test query",
|
||||
Query: "test type:diff patternType:regexp",
|
||||
UserID: &key,
|
||||
OrgID: nil,
|
||||
SlackWebhookURL: nil,
|
||||
}}}
|
||||
if !reflect.DeepEqual(savedSearches, want) {
|
||||
t.Errorf("got %v+, want %v+", savedSearches[0], want[0])
|
||||
if !reflect.DeepEqual(nodes, wantNodes) {
|
||||
t.Errorf("got %v+, want %v+", nodes[0], wantNodes[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSavedSearchesForSameUser(t *testing.T) {
|
||||
key := int32(1)
|
||||
|
||||
users := database.NewMockUserStore()
|
||||
users.GetByIDFunc.SetDefaultReturn(&types.User{SiteAdmin: false, ID: key}, nil)
|
||||
|
||||
ss := database.NewMockSavedSearchStore()
|
||||
ss.ListSavedSearchesByOrgOrUserFunc.SetDefaultHook(func(_ context.Context, userID, orgId *int32, paginationArgs *database.PaginationArgs) ([]*types.SavedSearch, error) {
|
||||
return []*types.SavedSearch{{ID: key, Description: "test query", Query: "test type:diff patternType:regexp", UserID: userID, OrgID: nil}}, nil
|
||||
})
|
||||
ss.CountSavedSearchesByOrgOrUserFunc.SetDefaultHook(func(_ context.Context, userID, orgId *int32) (int, error) {
|
||||
return 1, nil
|
||||
})
|
||||
|
||||
db := database.NewMockDB()
|
||||
db.UsersFunc.SetDefaultReturn(users)
|
||||
db.SavedSearchesFunc.SetDefaultReturn(ss)
|
||||
|
||||
args := savedSearchesArgs{
|
||||
ConnectionResolverArgs: graphqlutil.ConnectionResolverArgs{First: &key},
|
||||
Namespace: MarshalUserID(key),
|
||||
}
|
||||
|
||||
resolver, err := newSchemaResolver(db, gitserver.NewClient(db)).SavedSearches(actor.WithActor(context.Background(), actor.FromUser(key)), args)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
nodes, err := resolver.Nodes(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wantNodes := []*savedSearchResolver{{db, types.SavedSearch{
|
||||
ID: key,
|
||||
Description: "test query",
|
||||
Query: "test type:diff patternType:regexp",
|
||||
UserID: &key,
|
||||
OrgID: nil,
|
||||
SlackWebhookURL: nil,
|
||||
}}}
|
||||
if !reflect.DeepEqual(nodes, wantNodes) {
|
||||
t.Errorf("got %v+, want %v+", nodes[0], wantNodes[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSavedSearchesForDifferentUser(t *testing.T) {
|
||||
key := int32(1)
|
||||
userID := int32(2)
|
||||
|
||||
users := database.NewMockUserStore()
|
||||
users.GetByIDFunc.SetDefaultReturn(&types.User{SiteAdmin: false, ID: userID}, nil)
|
||||
|
||||
ss := database.NewMockSavedSearchStore()
|
||||
ss.ListSavedSearchesByOrgOrUserFunc.SetDefaultHook(func(_ context.Context, userID, orgId *int32, paginationArgs *database.PaginationArgs) ([]*types.SavedSearch, error) {
|
||||
return []*types.SavedSearch{{ID: key, Description: "test query", Query: "test type:diff patternType:regexp", UserID: userID, OrgID: nil}}, nil
|
||||
})
|
||||
ss.CountSavedSearchesByOrgOrUserFunc.SetDefaultHook(func(_ context.Context, userID, orgId *int32) (int, error) {
|
||||
return 1, nil
|
||||
})
|
||||
|
||||
db := database.NewMockDB()
|
||||
db.UsersFunc.SetDefaultReturn(users)
|
||||
db.SavedSearchesFunc.SetDefaultReturn(ss)
|
||||
|
||||
args := savedSearchesArgs{
|
||||
ConnectionResolverArgs: graphqlutil.ConnectionResolverArgs{First: &key},
|
||||
Namespace: MarshalUserID(key),
|
||||
}
|
||||
|
||||
_, err := newSchemaResolver(db, gitserver.NewClient(db)).SavedSearches(actor.WithActor(context.Background(), actor.FromUser(userID)), args)
|
||||
if err == nil {
|
||||
t.Error("got nil, want error to be returned for accessing saved searches of different user by non site admin.")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSavedSearchesForDifferentOrg(t *testing.T) {
|
||||
key := int32(1)
|
||||
|
||||
users := database.NewMockUserStore()
|
||||
users.GetByIDFunc.SetDefaultReturn(&types.User{SiteAdmin: false, ID: key}, nil)
|
||||
users.GetByCurrentAuthUserFunc.SetDefaultReturn(&types.User{SiteAdmin: false, ID: key}, nil)
|
||||
|
||||
om := database.NewMockOrgMemberStore()
|
||||
om.GetByOrgIDAndUserIDFunc.SetDefaultHook(func(ctx context.Context, oid, uid int32) (*types.OrgMembership, error) {
|
||||
return nil, nil
|
||||
})
|
||||
|
||||
ss := database.NewMockSavedSearchStore()
|
||||
ss.ListSavedSearchesByOrgOrUserFunc.SetDefaultHook(func(_ context.Context, userID, orgId *int32, paginationArgs *database.PaginationArgs) ([]*types.SavedSearch, error) {
|
||||
return []*types.SavedSearch{{ID: key, Description: "test query", Query: "test type:diff patternType:regexp", UserID: nil, OrgID: &key}}, nil
|
||||
})
|
||||
ss.CountSavedSearchesByOrgOrUserFunc.SetDefaultHook(func(_ context.Context, userID, orgId *int32) (int, error) {
|
||||
return 1, nil
|
||||
})
|
||||
|
||||
db := database.NewMockDB()
|
||||
db.UsersFunc.SetDefaultReturn(users)
|
||||
db.OrgMembersFunc.SetDefaultReturn(om)
|
||||
db.SavedSearchesFunc.SetDefaultReturn(ss)
|
||||
|
||||
args := savedSearchesArgs{
|
||||
ConnectionResolverArgs: graphqlutil.ConnectionResolverArgs{First: &key},
|
||||
Namespace: MarshalOrgID(key),
|
||||
}
|
||||
|
||||
if _, err := newSchemaResolver(db, gitserver.NewClient(db)).SavedSearches(actor.WithActor(context.Background(), actor.FromUser(key)), args); err != auth.ErrNotAnOrgMember {
|
||||
t.Errorf("got %v+, want %v+", err, auth.ErrNotAnOrgMember)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1465,9 +1465,30 @@ type Query {
|
||||
query: String = ""
|
||||
): Search
|
||||
"""
|
||||
All saved searches configured for the current user, merged from all configurations.
|
||||
List of saved searches based on namespace
|
||||
"""
|
||||
savedSearches: [SavedSearch!]!
|
||||
savedSearches(
|
||||
"""
|
||||
The namespace to list the saved searches for.
|
||||
"""
|
||||
namespace: ID!
|
||||
"""
|
||||
The limit argument for forward pagination.
|
||||
"""
|
||||
first: Int
|
||||
"""
|
||||
The limit argument for backward pagination.
|
||||
"""
|
||||
last: Int
|
||||
"""
|
||||
The cursor argument for forward pagination.
|
||||
"""
|
||||
after: String
|
||||
"""
|
||||
The cursor argument for backward pagination.
|
||||
"""
|
||||
before: String
|
||||
): SavedSearchesConnection!
|
||||
"""
|
||||
(experimental) Return the parse tree of a search query.
|
||||
"""
|
||||
@ -2516,6 +2537,26 @@ type SearchAlert {
|
||||
proposedQueries: [SearchQueryDescription!]
|
||||
}
|
||||
|
||||
"""
|
||||
A paginated connection for saved search queries, defined in settings.
|
||||
"""
|
||||
type SavedSearchesConnection implements Connection {
|
||||
"""
|
||||
A list of saved searches.
|
||||
"""
|
||||
nodes: [SavedSearch!]!
|
||||
|
||||
"""
|
||||
The total number of saved searches in the connection.
|
||||
"""
|
||||
totalCount: Int!
|
||||
|
||||
"""
|
||||
Pagination information.
|
||||
"""
|
||||
pageInfo: ConnectionPageInfo!
|
||||
}
|
||||
|
||||
"""
|
||||
A saved search query, defined in settings.
|
||||
"""
|
||||
@ -8317,3 +8358,38 @@ type SlowRequest {
|
||||
"""
|
||||
filepath: String
|
||||
}
|
||||
"""
|
||||
An object with totalCount and PageInfo.
|
||||
"""
|
||||
interface Connection {
|
||||
"""
|
||||
The total count of items in the connection.
|
||||
"""
|
||||
totalCount: Int!
|
||||
"""
|
||||
The pagination info for the connection.
|
||||
"""
|
||||
pageInfo: ConnectionPageInfo!
|
||||
}
|
||||
|
||||
"""
|
||||
Pagination information.
|
||||
"""
|
||||
type ConnectionPageInfo {
|
||||
"""
|
||||
When paginating forwards, the cursor to continue.
|
||||
"""
|
||||
endCursor: String
|
||||
"""
|
||||
When paginating forwards, are there more items?
|
||||
"""
|
||||
hasNextPage: Boolean!
|
||||
"""
|
||||
When paginating backward, the cursor to continue.
|
||||
"""
|
||||
startCursor: String
|
||||
"""
|
||||
When paginating backward, are there more items?
|
||||
"""
|
||||
hasPreviousPage: Boolean!
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
* [`code-intel`](code-intel/index.md)
|
||||
* [`config`](config/index.md)
|
||||
* [`debug`](debug.md)
|
||||
* [`extensions`](extensions/index.md)
|
||||
* [`extsvc`](extsvc/index.md)
|
||||
* [`login`](login.md)
|
||||
* [`lsif`](lsif.md)
|
||||
@ -19,4 +20,4 @@
|
||||
* [`users`](users/index.md)
|
||||
* [`validate`](validate.md)
|
||||
* [`version`](version.md)
|
||||
|
||||
|
||||
@ -3,7 +3,10 @@
|
||||
## Subcommands
|
||||
|
||||
|
||||
* [`add-kvp`](add-kvp.md)
|
||||
* [`delete`](delete.md)
|
||||
* [`delete-kvp`](delete-kvp.md)
|
||||
* [`get`](get.md)
|
||||
* [`list`](list.md)
|
||||
* [`update-kvp`](update-kvp.md)
|
||||
|
||||
@ -34770,6 +34770,10 @@ func (c RepoStoreWithFuncCall) Results() []interface{} {
|
||||
// github.com/sourcegraph/sourcegraph/internal/database) used for unit
|
||||
// testing.
|
||||
type MockSavedSearchStore struct {
|
||||
// CountSavedSearchesByOrgOrUserFunc is an instance of a mock function
|
||||
// object controlling the behavior of the method
|
||||
// CountSavedSearchesByOrgOrUser.
|
||||
CountSavedSearchesByOrgOrUserFunc *SavedSearchStoreCountSavedSearchesByOrgOrUserFunc
|
||||
// CreateFunc is an instance of a mock function object controlling the
|
||||
// behavior of the method Create.
|
||||
CreateFunc *SavedSearchStoreCreateFunc
|
||||
@ -34791,6 +34795,10 @@ type MockSavedSearchStore struct {
|
||||
// ListSavedSearchesByOrgIDFunc is an instance of a mock function object
|
||||
// controlling the behavior of the method ListSavedSearchesByOrgID.
|
||||
ListSavedSearchesByOrgIDFunc *SavedSearchStoreListSavedSearchesByOrgIDFunc
|
||||
// ListSavedSearchesByOrgOrUserFunc is an instance of a mock function
|
||||
// object controlling the behavior of the method
|
||||
// ListSavedSearchesByOrgOrUser.
|
||||
ListSavedSearchesByOrgOrUserFunc *SavedSearchStoreListSavedSearchesByOrgOrUserFunc
|
||||
// ListSavedSearchesByUserIDFunc is an instance of a mock function
|
||||
// object controlling the behavior of the method
|
||||
// ListSavedSearchesByUserID.
|
||||
@ -34811,6 +34819,11 @@ type MockSavedSearchStore struct {
|
||||
// overwritten.
|
||||
func NewMockSavedSearchStore() *MockSavedSearchStore {
|
||||
return &MockSavedSearchStore{
|
||||
CountSavedSearchesByOrgOrUserFunc: &SavedSearchStoreCountSavedSearchesByOrgOrUserFunc{
|
||||
defaultHook: func(context.Context, *int32, *int32) (r0 int, r1 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
CreateFunc: &SavedSearchStoreCreateFunc{
|
||||
defaultHook: func(context.Context, *types.SavedSearch) (r0 *types.SavedSearch, r1 error) {
|
||||
return
|
||||
@ -34846,6 +34859,11 @@ func NewMockSavedSearchStore() *MockSavedSearchStore {
|
||||
return
|
||||
},
|
||||
},
|
||||
ListSavedSearchesByOrgOrUserFunc: &SavedSearchStoreListSavedSearchesByOrgOrUserFunc{
|
||||
defaultHook: func(context.Context, *int32, *int32, *PaginationArgs) (r0 []*types.SavedSearch, r1 error) {
|
||||
return
|
||||
},
|
||||
},
|
||||
ListSavedSearchesByUserIDFunc: &SavedSearchStoreListSavedSearchesByUserIDFunc{
|
||||
defaultHook: func(context.Context, int32) (r0 []*types.SavedSearch, r1 error) {
|
||||
return
|
||||
@ -34873,6 +34891,11 @@ func NewMockSavedSearchStore() *MockSavedSearchStore {
|
||||
// interface. All methods panic on invocation, unless overwritten.
|
||||
func NewStrictMockSavedSearchStore() *MockSavedSearchStore {
|
||||
return &MockSavedSearchStore{
|
||||
CountSavedSearchesByOrgOrUserFunc: &SavedSearchStoreCountSavedSearchesByOrgOrUserFunc{
|
||||
defaultHook: func(context.Context, *int32, *int32) (int, error) {
|
||||
panic("unexpected invocation of MockSavedSearchStore.CountSavedSearchesByOrgOrUser")
|
||||
},
|
||||
},
|
||||
CreateFunc: &SavedSearchStoreCreateFunc{
|
||||
defaultHook: func(context.Context, *types.SavedSearch) (*types.SavedSearch, error) {
|
||||
panic("unexpected invocation of MockSavedSearchStore.Create")
|
||||
@ -34908,6 +34931,11 @@ func NewStrictMockSavedSearchStore() *MockSavedSearchStore {
|
||||
panic("unexpected invocation of MockSavedSearchStore.ListSavedSearchesByOrgID")
|
||||
},
|
||||
},
|
||||
ListSavedSearchesByOrgOrUserFunc: &SavedSearchStoreListSavedSearchesByOrgOrUserFunc{
|
||||
defaultHook: func(context.Context, *int32, *int32, *PaginationArgs) ([]*types.SavedSearch, error) {
|
||||
panic("unexpected invocation of MockSavedSearchStore.ListSavedSearchesByOrgOrUser")
|
||||
},
|
||||
},
|
||||
ListSavedSearchesByUserIDFunc: &SavedSearchStoreListSavedSearchesByUserIDFunc{
|
||||
defaultHook: func(context.Context, int32) ([]*types.SavedSearch, error) {
|
||||
panic("unexpected invocation of MockSavedSearchStore.ListSavedSearchesByUserID")
|
||||
@ -34936,6 +34964,9 @@ func NewStrictMockSavedSearchStore() *MockSavedSearchStore {
|
||||
// implementation, unless overwritten.
|
||||
func NewMockSavedSearchStoreFrom(i SavedSearchStore) *MockSavedSearchStore {
|
||||
return &MockSavedSearchStore{
|
||||
CountSavedSearchesByOrgOrUserFunc: &SavedSearchStoreCountSavedSearchesByOrgOrUserFunc{
|
||||
defaultHook: i.CountSavedSearchesByOrgOrUser,
|
||||
},
|
||||
CreateFunc: &SavedSearchStoreCreateFunc{
|
||||
defaultHook: i.Create,
|
||||
},
|
||||
@ -34957,6 +34988,9 @@ func NewMockSavedSearchStoreFrom(i SavedSearchStore) *MockSavedSearchStore {
|
||||
ListSavedSearchesByOrgIDFunc: &SavedSearchStoreListSavedSearchesByOrgIDFunc{
|
||||
defaultHook: i.ListSavedSearchesByOrgID,
|
||||
},
|
||||
ListSavedSearchesByOrgOrUserFunc: &SavedSearchStoreListSavedSearchesByOrgOrUserFunc{
|
||||
defaultHook: i.ListSavedSearchesByOrgOrUser,
|
||||
},
|
||||
ListSavedSearchesByUserIDFunc: &SavedSearchStoreListSavedSearchesByUserIDFunc{
|
||||
defaultHook: i.ListSavedSearchesByUserID,
|
||||
},
|
||||
@ -34972,6 +35006,121 @@ func NewMockSavedSearchStoreFrom(i SavedSearchStore) *MockSavedSearchStore {
|
||||
}
|
||||
}
|
||||
|
||||
// SavedSearchStoreCountSavedSearchesByOrgOrUserFunc describes the behavior
|
||||
// when the CountSavedSearchesByOrgOrUser method of the parent
|
||||
// MockSavedSearchStore instance is invoked.
|
||||
type SavedSearchStoreCountSavedSearchesByOrgOrUserFunc struct {
|
||||
defaultHook func(context.Context, *int32, *int32) (int, error)
|
||||
hooks []func(context.Context, *int32, *int32) (int, error)
|
||||
history []SavedSearchStoreCountSavedSearchesByOrgOrUserFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// CountSavedSearchesByOrgOrUser delegates to the next hook function in the
|
||||
// queue and stores the parameter and result values of this invocation.
|
||||
func (m *MockSavedSearchStore) CountSavedSearchesByOrgOrUser(v0 context.Context, v1 *int32, v2 *int32) (int, error) {
|
||||
r0, r1 := m.CountSavedSearchesByOrgOrUserFunc.nextHook()(v0, v1, v2)
|
||||
m.CountSavedSearchesByOrgOrUserFunc.appendCall(SavedSearchStoreCountSavedSearchesByOrgOrUserFuncCall{v0, v1, v2, r0, r1})
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the
|
||||
// CountSavedSearchesByOrgOrUser method of the parent MockSavedSearchStore
|
||||
// instance is invoked and the hook queue is empty.
|
||||
func (f *SavedSearchStoreCountSavedSearchesByOrgOrUserFunc) SetDefaultHook(hook func(context.Context, *int32, *int32) (int, error)) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// CountSavedSearchesByOrgOrUser method of the parent MockSavedSearchStore
|
||||
// instance invokes the hook at the front of the queue and discards it.
|
||||
// After the queue is empty, the default hook function is invoked for any
|
||||
// future action.
|
||||
func (f *SavedSearchStoreCountSavedSearchesByOrgOrUserFunc) PushHook(hook func(context.Context, *int32, *int32) (int, error)) {
|
||||
f.mutex.Lock()
|
||||
f.hooks = append(f.hooks, hook)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetDefaultReturn calls SetDefaultHook with a function that returns the
|
||||
// given values.
|
||||
func (f *SavedSearchStoreCountSavedSearchesByOrgOrUserFunc) SetDefaultReturn(r0 int, r1 error) {
|
||||
f.SetDefaultHook(func(context.Context, *int32, *int32) (int, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *SavedSearchStoreCountSavedSearchesByOrgOrUserFunc) PushReturn(r0 int, r1 error) {
|
||||
f.PushHook(func(context.Context, *int32, *int32) (int, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
func (f *SavedSearchStoreCountSavedSearchesByOrgOrUserFunc) nextHook() func(context.Context, *int32, *int32) (int, error) {
|
||||
f.mutex.Lock()
|
||||
defer f.mutex.Unlock()
|
||||
|
||||
if len(f.hooks) == 0 {
|
||||
return f.defaultHook
|
||||
}
|
||||
|
||||
hook := f.hooks[0]
|
||||
f.hooks = f.hooks[1:]
|
||||
return hook
|
||||
}
|
||||
|
||||
func (f *SavedSearchStoreCountSavedSearchesByOrgOrUserFunc) appendCall(r0 SavedSearchStoreCountSavedSearchesByOrgOrUserFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of
|
||||
// SavedSearchStoreCountSavedSearchesByOrgOrUserFuncCall objects describing
|
||||
// the invocations of this function.
|
||||
func (f *SavedSearchStoreCountSavedSearchesByOrgOrUserFunc) History() []SavedSearchStoreCountSavedSearchesByOrgOrUserFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]SavedSearchStoreCountSavedSearchesByOrgOrUserFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// SavedSearchStoreCountSavedSearchesByOrgOrUserFuncCall is an object that
|
||||
// describes an invocation of method CountSavedSearchesByOrgOrUser on an
|
||||
// instance of MockSavedSearchStore.
|
||||
type SavedSearchStoreCountSavedSearchesByOrgOrUserFuncCall struct {
|
||||
// Arg0 is the value of the 1st argument passed to this method
|
||||
// invocation.
|
||||
Arg0 context.Context
|
||||
// Arg1 is the value of the 2nd argument passed to this method
|
||||
// invocation.
|
||||
Arg1 *int32
|
||||
// Arg2 is the value of the 3rd argument passed to this method
|
||||
// invocation.
|
||||
Arg2 *int32
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 int
|
||||
// Result1 is the value of the 2nd result returned from this method
|
||||
// invocation.
|
||||
Result1 error
|
||||
}
|
||||
|
||||
// Args returns an interface slice containing the arguments of this
|
||||
// invocation.
|
||||
func (c SavedSearchStoreCountSavedSearchesByOrgOrUserFuncCall) Args() []interface{} {
|
||||
return []interface{}{c.Arg0, c.Arg1, c.Arg2}
|
||||
}
|
||||
|
||||
// Results returns an interface slice containing the results of this
|
||||
// invocation.
|
||||
func (c SavedSearchStoreCountSavedSearchesByOrgOrUserFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// SavedSearchStoreCreateFunc describes the behavior when the Create method
|
||||
// of the parent MockSavedSearchStore instance is invoked.
|
||||
type SavedSearchStoreCreateFunc struct {
|
||||
@ -35714,6 +35863,124 @@ func (c SavedSearchStoreListSavedSearchesByOrgIDFuncCall) Results() []interface{
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// SavedSearchStoreListSavedSearchesByOrgOrUserFunc describes the behavior
|
||||
// when the ListSavedSearchesByOrgOrUser method of the parent
|
||||
// MockSavedSearchStore instance is invoked.
|
||||
type SavedSearchStoreListSavedSearchesByOrgOrUserFunc struct {
|
||||
defaultHook func(context.Context, *int32, *int32, *PaginationArgs) ([]*types.SavedSearch, error)
|
||||
hooks []func(context.Context, *int32, *int32, *PaginationArgs) ([]*types.SavedSearch, error)
|
||||
history []SavedSearchStoreListSavedSearchesByOrgOrUserFuncCall
|
||||
mutex sync.Mutex
|
||||
}
|
||||
|
||||
// ListSavedSearchesByOrgOrUser delegates to the next hook function in the
|
||||
// queue and stores the parameter and result values of this invocation.
|
||||
func (m *MockSavedSearchStore) ListSavedSearchesByOrgOrUser(v0 context.Context, v1 *int32, v2 *int32, v3 *PaginationArgs) ([]*types.SavedSearch, error) {
|
||||
r0, r1 := m.ListSavedSearchesByOrgOrUserFunc.nextHook()(v0, v1, v2, v3)
|
||||
m.ListSavedSearchesByOrgOrUserFunc.appendCall(SavedSearchStoreListSavedSearchesByOrgOrUserFuncCall{v0, v1, v2, v3, r0, r1})
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetDefaultHook sets function that is called when the
|
||||
// ListSavedSearchesByOrgOrUser method of the parent MockSavedSearchStore
|
||||
// instance is invoked and the hook queue is empty.
|
||||
func (f *SavedSearchStoreListSavedSearchesByOrgOrUserFunc) SetDefaultHook(hook func(context.Context, *int32, *int32, *PaginationArgs) ([]*types.SavedSearch, error)) {
|
||||
f.defaultHook = hook
|
||||
}
|
||||
|
||||
// PushHook adds a function to the end of hook queue. Each invocation of the
|
||||
// ListSavedSearchesByOrgOrUser method of the parent MockSavedSearchStore
|
||||
// instance invokes the hook at the front of the queue and discards it.
|
||||
// After the queue is empty, the default hook function is invoked for any
|
||||
// future action.
|
||||
func (f *SavedSearchStoreListSavedSearchesByOrgOrUserFunc) PushHook(hook func(context.Context, *int32, *int32, *PaginationArgs) ([]*types.SavedSearch, error)) {
|
||||
f.mutex.Lock()
|
||||
f.hooks = append(f.hooks, hook)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// SetDefaultReturn calls SetDefaultHook with a function that returns the
|
||||
// given values.
|
||||
func (f *SavedSearchStoreListSavedSearchesByOrgOrUserFunc) SetDefaultReturn(r0 []*types.SavedSearch, r1 error) {
|
||||
f.SetDefaultHook(func(context.Context, *int32, *int32, *PaginationArgs) ([]*types.SavedSearch, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
// PushReturn calls PushHook with a function that returns the given values.
|
||||
func (f *SavedSearchStoreListSavedSearchesByOrgOrUserFunc) PushReturn(r0 []*types.SavedSearch, r1 error) {
|
||||
f.PushHook(func(context.Context, *int32, *int32, *PaginationArgs) ([]*types.SavedSearch, error) {
|
||||
return r0, r1
|
||||
})
|
||||
}
|
||||
|
||||
func (f *SavedSearchStoreListSavedSearchesByOrgOrUserFunc) nextHook() func(context.Context, *int32, *int32, *PaginationArgs) ([]*types.SavedSearch, error) {
|
||||
f.mutex.Lock()
|
||||
defer f.mutex.Unlock()
|
||||
|
||||
if len(f.hooks) == 0 {
|
||||
return f.defaultHook
|
||||
}
|
||||
|
||||
hook := f.hooks[0]
|
||||
f.hooks = f.hooks[1:]
|
||||
return hook
|
||||
}
|
||||
|
||||
func (f *SavedSearchStoreListSavedSearchesByOrgOrUserFunc) appendCall(r0 SavedSearchStoreListSavedSearchesByOrgOrUserFuncCall) {
|
||||
f.mutex.Lock()
|
||||
f.history = append(f.history, r0)
|
||||
f.mutex.Unlock()
|
||||
}
|
||||
|
||||
// History returns a sequence of
|
||||
// SavedSearchStoreListSavedSearchesByOrgOrUserFuncCall objects describing
|
||||
// the invocations of this function.
|
||||
func (f *SavedSearchStoreListSavedSearchesByOrgOrUserFunc) History() []SavedSearchStoreListSavedSearchesByOrgOrUserFuncCall {
|
||||
f.mutex.Lock()
|
||||
history := make([]SavedSearchStoreListSavedSearchesByOrgOrUserFuncCall, len(f.history))
|
||||
copy(history, f.history)
|
||||
f.mutex.Unlock()
|
||||
|
||||
return history
|
||||
}
|
||||
|
||||
// SavedSearchStoreListSavedSearchesByOrgOrUserFuncCall is an object that
|
||||
// describes an invocation of method ListSavedSearchesByOrgOrUser on an
|
||||
// instance of MockSavedSearchStore.
|
||||
type SavedSearchStoreListSavedSearchesByOrgOrUserFuncCall struct {
|
||||
// Arg0 is the value of the 1st argument passed to this method
|
||||
// invocation.
|
||||
Arg0 context.Context
|
||||
// Arg1 is the value of the 2nd argument passed to this method
|
||||
// invocation.
|
||||
Arg1 *int32
|
||||
// Arg2 is the value of the 3rd argument passed to this method
|
||||
// invocation.
|
||||
Arg2 *int32
|
||||
// Arg3 is the value of the 4th argument passed to this method
|
||||
// invocation.
|
||||
Arg3 *PaginationArgs
|
||||
// Result0 is the value of the 1st result returned from this method
|
||||
// invocation.
|
||||
Result0 []*types.SavedSearch
|
||||
// Result1 is the value of the 2nd result returned from this method
|
||||
// invocation.
|
||||
Result1 error
|
||||
}
|
||||
|
||||
// Args returns an interface slice containing the arguments of this
|
||||
// invocation.
|
||||
func (c SavedSearchStoreListSavedSearchesByOrgOrUserFuncCall) Args() []interface{} {
|
||||
return []interface{}{c.Arg0, c.Arg1, c.Arg2, c.Arg3}
|
||||
}
|
||||
|
||||
// Results returns an interface slice containing the results of this
|
||||
// invocation.
|
||||
func (c SavedSearchStoreListSavedSearchesByOrgOrUserFuncCall) Results() []interface{} {
|
||||
return []interface{}{c.Result0, c.Result1}
|
||||
}
|
||||
|
||||
// SavedSearchStoreListSavedSearchesByUserIDFunc describes the behavior when
|
||||
// the ListSavedSearchesByUserID method of the parent MockSavedSearchStore
|
||||
// instance is invoked.
|
||||
|
||||
@ -9,6 +9,7 @@ import (
|
||||
|
||||
"github.com/sourcegraph/sourcegraph/internal/api"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database/basestore"
|
||||
"github.com/sourcegraph/sourcegraph/internal/database/dbutil"
|
||||
"github.com/sourcegraph/sourcegraph/internal/trace"
|
||||
"github.com/sourcegraph/sourcegraph/internal/types"
|
||||
"github.com/sourcegraph/sourcegraph/lib/errors"
|
||||
@ -22,6 +23,8 @@ type SavedSearchStore interface {
|
||||
ListAll(context.Context) ([]api.SavedQuerySpecAndConfig, error)
|
||||
ListSavedSearchesByOrgID(ctx context.Context, orgID int32) ([]*types.SavedSearch, error)
|
||||
ListSavedSearchesByUserID(ctx context.Context, userID int32) ([]*types.SavedSearch, error)
|
||||
ListSavedSearchesByOrgOrUser(ctx context.Context, userID, orgID *int32, paginationArgs *PaginationArgs) ([]*types.SavedSearch, error)
|
||||
CountSavedSearchesByOrgOrUser(ctx context.Context, userID, orgID *int32) (int, error)
|
||||
Transact(context.Context) (SavedSearchStore, error)
|
||||
Update(context.Context, *types.SavedSearch) (*types.SavedSearch, error)
|
||||
With(basestore.ShareableStore) SavedSearchStore
|
||||
@ -239,6 +242,76 @@ func (s *savedSearchStore) ListSavedSearchesByOrgID(ctx context.Context, orgID i
|
||||
return savedSearches, nil
|
||||
}
|
||||
|
||||
// ListSavedSearchesByOrgOrUser lists all the saved searches associated with an
|
||||
// organization for the user.
|
||||
//
|
||||
// 🚨 SECURITY: This method does NOT verify the user's identity or that the
|
||||
// user is an admin. It is the callers responsibility to ensure only admins or
|
||||
// members of the specified organization can access the returned saved
|
||||
// searches.
|
||||
func (s *savedSearchStore) ListSavedSearchesByOrgOrUser(ctx context.Context, userID, orgID *int32, paginationArgs *PaginationArgs) ([]*types.SavedSearch, error) {
|
||||
p, err := paginationArgs.SQL()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var where []*sqlf.Query
|
||||
|
||||
if userID != nil && *userID != 0 {
|
||||
where = append(where, sqlf.Sprintf("user_id = %v", *userID))
|
||||
} else if orgID != nil && *orgID != 0 {
|
||||
where = append(where, sqlf.Sprintf("org_id = %v", *orgID))
|
||||
} else {
|
||||
return nil, errors.New("userID or orgID must be provided.")
|
||||
}
|
||||
|
||||
if p.Where != nil {
|
||||
where = append(where, p.Where)
|
||||
}
|
||||
|
||||
query := sqlf.Sprintf(listSavedSearchesQueryFmtStr, sqlf.Sprintf("WHERE %v", sqlf.Join(where, " AND ")))
|
||||
query = p.AppendOrderToQuery(query)
|
||||
query = p.AppendLimitToQuery(query)
|
||||
|
||||
return scanSavedSearches(s.Query(ctx, query))
|
||||
}
|
||||
|
||||
const listSavedSearchesQueryFmtStr = `
|
||||
SELECT
|
||||
id,
|
||||
description,
|
||||
query,
|
||||
notify_owner,
|
||||
notify_slack,
|
||||
user_id,
|
||||
org_id,
|
||||
slack_webhook_url
|
||||
FROM saved_searches %v
|
||||
`
|
||||
|
||||
var scanSavedSearches = basestore.NewSliceScanner(scanSavedSearch)
|
||||
|
||||
func scanSavedSearch(s dbutil.Scanner) (*types.SavedSearch, error) {
|
||||
var ss types.SavedSearch
|
||||
if err := s.Scan(&ss.ID, &ss.Description, &ss.Query, &ss.Notify, &ss.NotifySlack, &ss.UserID, &ss.OrgID, &ss.SlackWebhookURL); err != nil {
|
||||
return nil, errors.Wrap(err, "Scan")
|
||||
}
|
||||
return &ss, nil
|
||||
}
|
||||
|
||||
// CountSavedSearchesByOrgOrUser counts all the saved searches associated with an
|
||||
// organization for the user.
|
||||
//
|
||||
// 🚨 SECURITY: This method does NOT verify the user's identity or that the
|
||||
// user is an admin. It is the callers responsibility to ensure only admins or
|
||||
// members of the specified organization can access the returned saved
|
||||
// searches.
|
||||
func (s *savedSearchStore) CountSavedSearchesByOrgOrUser(ctx context.Context, userID, orgID *int32) (int, error) {
|
||||
query := sqlf.Sprintf(`SELECT COUNT(*) FROM saved_searches WHERE user_id=%v OR org_id=%v`, userID, orgID)
|
||||
count, _, err := basestore.ScanFirstInt(s.Query(ctx, query))
|
||||
return count, err
|
||||
}
|
||||
|
||||
// Create creates a new saved search with the specified parameters. The ID
|
||||
// field must be zero, or an error will be returned.
|
||||
//
|
||||
|
||||
@ -73,7 +73,7 @@ const config = {
|
||||
],
|
||||
globalSetup: path.join(__dirname, 'client/shared/dev/jestGlobalSetup.js'),
|
||||
globals: {
|
||||
Uint8Array: Uint8Array,
|
||||
Uint8Array,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@ -2022,8 +2022,16 @@ type SettingsExperimentalFeatures struct {
|
||||
BatchChangesExecution *bool `json:"batchChangesExecution,omitempty"`
|
||||
// ClientSearchResultRanking description: How to rank search results in the client
|
||||
ClientSearchResultRanking *string `json:"clientSearchResultRanking,omitempty"`
|
||||
// CodeInsights description: Enables code insights on directory pages.
|
||||
CodeInsights *bool `json:"codeInsights,omitempty"`
|
||||
// CodeInsightsAllRepos description: DEPRECATED: Enables the experimental ability to run an insight over all repositories on the instance.
|
||||
CodeInsightsAllRepos *bool `json:"codeInsightsAllRepos,omitempty"`
|
||||
// CodeInsightsCompute description: Enables Compute powered Code Insights
|
||||
CodeInsightsCompute *bool `json:"codeInsightsCompute,omitempty"`
|
||||
// CodeInsightsGqlApi description: DEPRECATED: Enables gql api instead of using setting cascade as a main storage fro code insights entities
|
||||
CodeInsightsGqlApi *bool `json:"codeInsightsGqlApi,omitempty"`
|
||||
// CodeInsightsLandingPage description: DEPRECATED: Enables code insights landing page layout.
|
||||
CodeInsightsLandingPage *bool `json:"codeInsightsLandingPage,omitempty"`
|
||||
// CodeInsightsRepoUI description: Specifies which (code insight repo) editor to use for repo query UI
|
||||
CodeInsightsRepoUI *string `json:"codeInsightsRepoUI,omitempty"`
|
||||
CodeIntelRepositoryBadge *CodeIntelRepositoryBadge `json:"codeIntelRepositoryBadge,omitempty"`
|
||||
@ -2031,6 +2039,8 @@ type SettingsExperimentalFeatures struct {
|
||||
CodeMonitoringWebHooks *bool `json:"codeMonitoringWebHooks,omitempty"`
|
||||
// CodeNavigation description: What kind of experimental code navigation UX to enable. The most recommended option is 'selection-driven'.
|
||||
CodeNavigation *string `json:"codeNavigation,omitempty"`
|
||||
// CopyQueryButton description: DEPRECATED: This feature is now permanently enabled. Enables displaying the copy query button in the search bar when hovering over the global navigation bar.
|
||||
CopyQueryButton *bool `json:"copyQueryButton,omitempty"`
|
||||
// Editor description: Specifies which (code) editor to use for query and text input
|
||||
Editor *string `json:"editor,omitempty"`
|
||||
// EnableCodeMirrorFileView description: Uses CodeMirror to display files. In this first iteration not all features of the current file view are available.
|
||||
@ -2063,8 +2073,6 @@ type SettingsExperimentalFeatures struct {
|
||||
FuzzyFinderSymbols *bool `json:"fuzzyFinderSymbols,omitempty"`
|
||||
// GoCodeCheckerTemplates description: Shows a panel with code insights templates for go code checker results.
|
||||
GoCodeCheckerTemplates *bool `json:"goCodeCheckerTemplates,omitempty"`
|
||||
// HomePanelsComputeSuggestions description: Enable the home panels compute suggestions feature.
|
||||
HomePanelsComputeSuggestions bool `json:"homePanelsComputeSuggestions,omitempty"`
|
||||
// HomepageUserInvitation description: Shows a panel to invite collaborators to Sourcegraph on home page.
|
||||
HomepageUserInvitation *bool `json:"homepageUserInvitation,omitempty"`
|
||||
// PreloadGoToDefinition description: Preload definitions for available tokens in the visible viewport.
|
||||
@ -2077,12 +2085,22 @@ type SettingsExperimentalFeatures struct {
|
||||
SearchQueryInput *string `json:"searchQueryInput,omitempty"`
|
||||
// SearchResultsAggregations description: Display aggregations for your search results on the search screen.
|
||||
SearchResultsAggregations *bool `json:"searchResultsAggregations,omitempty"`
|
||||
// SearchStats description: Enables a button on the search results page that shows language statistics about the results for a search query.
|
||||
SearchStats *bool `json:"searchStats,omitempty"`
|
||||
// SearchStreaming description: DEPRECATED: This feature is now permanently enabled. Enables streaming search support.
|
||||
SearchStreaming *bool `json:"searchStreaming,omitempty"`
|
||||
// ShowCodeMonitoringLogs description: Shows code monitoring logs tab.
|
||||
ShowCodeMonitoringLogs *bool `json:"showCodeMonitoringLogs,omitempty"`
|
||||
// ShowEnterpriseHomePanels description: Enabled the homepage panels in the Enterprise homepage
|
||||
ShowEnterpriseHomePanels *bool `json:"showEnterpriseHomePanels,omitempty"`
|
||||
// ShowMultilineSearchConsole description: Enables the multiline search console at search/console
|
||||
ShowMultilineSearchConsole *bool `json:"showMultilineSearchConsole,omitempty"`
|
||||
// ShowOnboardingTour description: REMOVED.
|
||||
ShowOnboardingTour *bool `json:"showOnboardingTour,omitempty"`
|
||||
// ShowRepogroupHomepage description: Enables the repository group homepage
|
||||
ShowRepogroupHomepage *bool `json:"showRepogroupHomepage,omitempty"`
|
||||
// ShowSearchContext description: Enables the search context dropdown.
|
||||
ShowSearchContext *bool `json:"showSearchContext,omitempty"`
|
||||
// ShowSearchContextManagement description: REMOVED.
|
||||
ShowSearchContextManagement *bool `json:"showSearchContextManagement,omitempty"`
|
||||
// SymbolKindTags description: Show the initial letter of the symbol kind instead of icons.
|
||||
SymbolKindTags bool `json:"symbolKindTags,omitempty"`
|
||||
Additional map[string]any `json:"-"` // additionalProperties not explicitly defined in the schema
|
||||
@ -2121,11 +2139,16 @@ func (v *SettingsExperimentalFeatures) UnmarshalJSON(data []byte) error {
|
||||
delete(m, "applySearchQuerySuggestionOnEnter")
|
||||
delete(m, "batchChangesExecution")
|
||||
delete(m, "clientSearchResultRanking")
|
||||
delete(m, "codeInsights")
|
||||
delete(m, "codeInsightsAllRepos")
|
||||
delete(m, "codeInsightsCompute")
|
||||
delete(m, "codeInsightsGqlApi")
|
||||
delete(m, "codeInsightsLandingPage")
|
||||
delete(m, "codeInsightsRepoUI")
|
||||
delete(m, "codeIntelRepositoryBadge")
|
||||
delete(m, "codeMonitoringWebHooks")
|
||||
delete(m, "codeNavigation")
|
||||
delete(m, "copyQueryButton")
|
||||
delete(m, "editor")
|
||||
delete(m, "enableCodeMirrorFileView")
|
||||
delete(m, "enableGoImportsSearchQueryTransform")
|
||||
@ -2142,16 +2165,20 @@ func (v *SettingsExperimentalFeatures) UnmarshalJSON(data []byte) error {
|
||||
delete(m, "fuzzyFinderRepositories")
|
||||
delete(m, "fuzzyFinderSymbols")
|
||||
delete(m, "goCodeCheckerTemplates")
|
||||
delete(m, "homePanelsComputeSuggestions")
|
||||
delete(m, "homepageUserInvitation")
|
||||
delete(m, "preloadGoToDefinition")
|
||||
delete(m, "proactiveSearchResultsAggregations")
|
||||
delete(m, "searchContextsQuery")
|
||||
delete(m, "searchQueryInput")
|
||||
delete(m, "searchResultsAggregations")
|
||||
delete(m, "searchStats")
|
||||
delete(m, "searchStreaming")
|
||||
delete(m, "showCodeMonitoringLogs")
|
||||
delete(m, "showEnterpriseHomePanels")
|
||||
delete(m, "showMultilineSearchConsole")
|
||||
delete(m, "showOnboardingTour")
|
||||
delete(m, "showRepogroupHomepage")
|
||||
delete(m, "showSearchContext")
|
||||
delete(m, "showSearchContextManagement")
|
||||
delete(m, "symbolKindTags")
|
||||
if len(m) > 0 {
|
||||
v.Additional = make(map[string]any, len(m))
|
||||
|
||||
@ -13,11 +13,40 @@
|
||||
"type": "object",
|
||||
"additionalProperties": true,
|
||||
"properties": {
|
||||
"homePanelsComputeSuggestions": {
|
||||
"codeInsights": {
|
||||
"description": "Enables code insights on directory pages.",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"deprecated": false,
|
||||
"description": "Enable the home panels compute suggestions feature."
|
||||
"!go": {
|
||||
"pointer": true
|
||||
}
|
||||
},
|
||||
"codeInsightsGqlApi": {
|
||||
"deprecated": true,
|
||||
"description": "DEPRECATED: Enables gql api instead of using setting cascade as a main storage fro code insights entities",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"!go": {
|
||||
"pointer": true
|
||||
}
|
||||
},
|
||||
"codeInsightsAllRepos": {
|
||||
"deprecated": true,
|
||||
"description": "DEPRECATED: Enables the experimental ability to run an insight over all repositories on the instance.",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"!go": {
|
||||
"pointer": true
|
||||
}
|
||||
},
|
||||
"codeInsightsLandingPage": {
|
||||
"deprecated": true,
|
||||
"description": "DEPRECATED: Enables code insights landing page layout.",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"!go": {
|
||||
"pointer": true
|
||||
}
|
||||
},
|
||||
"codeInsightsRepoUI": {
|
||||
"description": "Specifies which (code insight repo) editor to use for repo query UI",
|
||||
@ -36,8 +65,56 @@
|
||||
"pointer": true
|
||||
}
|
||||
},
|
||||
"showEnterpriseHomePanels": {
|
||||
"description": "Enabled the homepage panels in the Enterprise homepage",
|
||||
"searchStats": {
|
||||
"description": "Enables a button on the search results page that shows language statistics about the results for a search query.",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"!go": {
|
||||
"pointer": true
|
||||
}
|
||||
},
|
||||
"searchStreaming": {
|
||||
"description": "DEPRECATED: This feature is now permanently enabled. Enables streaming search support.",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"!go": {
|
||||
"pointer": true
|
||||
}
|
||||
},
|
||||
"copyQueryButton": {
|
||||
"description": "DEPRECATED: This feature is now permanently enabled. Enables displaying the copy query button in the search bar when hovering over the global navigation bar.",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"!go": {
|
||||
"pointer": true
|
||||
}
|
||||
},
|
||||
"showRepogroupHomepage": {
|
||||
"description": "Enables the repository group homepage ",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"!go": {
|
||||
"pointer": true
|
||||
}
|
||||
},
|
||||
"showOnboardingTour": {
|
||||
"description": "REMOVED.",
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"!go": {
|
||||
"pointer": true
|
||||
}
|
||||
},
|
||||
"showSearchContext": {
|
||||
"description": "Enables the search context dropdown.",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"!go": {
|
||||
"pointer": true
|
||||
}
|
||||
},
|
||||
"showSearchContextManagement": {
|
||||
"description": "REMOVED.",
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"!go": {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user