codeintel: Add UI for data retention and index scheduling configuration (#24095)

This commit is contained in:
Eric Fritz 2021-08-31 11:03:18 -05:00 committed by GitHub
parent 6798b59c69
commit 895ffa20d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
81 changed files with 3539 additions and 1560 deletions

View File

@ -7,5 +7,4 @@
@import './enterprise/productSubscription/ProductCertificate';
@import './enterprise/productSubscription/TrueUpStatusSummary';
@import './enterprise/user/productSubscriptions/PaymentTokenFormControl';
@import './enterprise/codeintel/index';
@import './enterprise/code-monitoring/index';

View File

@ -0,0 +1,99 @@
import { debounce } from 'lodash'
import React, { FunctionComponent, useState } from 'react'
import { CodeIntelligenceConfigurationPolicyFields, GitObjectType } from '../../../graphql-operations'
import {
repoName as defaultRepoName,
searchGitBranches as defaultSearchGitBranches,
searchGitTags as defaultSearchGitTags,
} from './backend'
import { GitObjectPreview } from './GitObjectPreview'
export interface BranchTargetSettingsProps {
repoId?: string
policy: CodeIntelligenceConfigurationPolicyFields
setPolicy: (policy: CodeIntelligenceConfigurationPolicyFields) => void
repoName: typeof defaultRepoName
searchGitBranches: typeof defaultSearchGitBranches
searchGitTags: typeof defaultSearchGitTags
}
const GIT_OBJECT_PREVIEW_DEBOUNCE_TIMEOUT = 300
export const BranchTargetSettings: FunctionComponent<BranchTargetSettingsProps> = ({
repoId,
policy,
setPolicy,
repoName,
searchGitBranches,
searchGitTags,
}) => {
const [debouncedPattern, setDebouncedPattern] = useState(policy.pattern)
const setPattern = debounce(value => setDebouncedPattern(value), GIT_OBJECT_PREVIEW_DEBOUNCE_TIMEOUT)
return (
<>
<div className="form-group">
<label htmlFor="name">Name</label>
<input
id="name"
type="text"
className="form-control"
value={policy.name}
onChange={event => setPolicy({ ...policy, name: event.target.value })}
/>
</div>
<div className="form-group">
<label htmlFor="type">Type</label>
<select
id="type"
className="form-control"
value={policy.type}
onChange={event =>
setPolicy({
...policy,
type: event.target.value as GitObjectType,
...(event.target.value !== GitObjectType.GIT_TREE
? {
retainIntermediateCommits: false,
indexIntermediateCommits: false,
}
: {}),
})
}
>
<option value="">Select Git object type</option>
{repoId && <option value={GitObjectType.GIT_COMMIT}>Commit</option>}
<option value={GitObjectType.GIT_TAG}>Tag</option>
<option value={GitObjectType.GIT_TREE}>Branch</option>
</select>
</div>
<div className="form-group">
<label htmlFor="pattern">Pattern</label>
<input
id="pattern"
type="text"
className="form-control text-monospace"
value={policy.pattern}
onChange={event => {
setPolicy({ ...policy, pattern: event.target.value })
setPattern(event.target.value)
}}
/>
</div>
{repoId && (
<GitObjectPreview
pattern={debouncedPattern}
repoId={repoId}
type={policy.type}
repoName={repoName}
searchGitTags={searchGitTags}
searchGitBranches={searchGitBranches}
/>
)}
</>
)
}

View File

@ -0,0 +1,214 @@
import { boolean, withKnobs } from '@storybook/addon-knobs'
import { Meta, Story } from '@storybook/react'
import React from 'react'
import { of } from 'rxjs'
import { GitObjectType } from '@sourcegraph/shared/src/graphql-operations'
import { CodeIntelligenceConfigurationPolicyFields } from '../../../graphql-operations'
import { EnterpriseWebStory } from '../../components/EnterpriseWebStory'
import { CodeIntelConfigurationPage, CodeIntelConfigurationPageProps } from './CodeIntelConfigurationPage'
const trim = (value: string) => {
const firstSignificantLine = value
.split('\n')
.map(line => ({ length: line.length, trimmedLength: line.trimStart().length }))
.find(({ trimmedLength }) => trimmedLength !== 0)
if (!firstSignificantLine) {
return value
}
const { length, trimmedLength } = firstSignificantLine
return value
.split('\n')
.map(line => line.slice(length - trimmedLength))
.join('\n')
.trim()
}
const globalPolicies: CodeIntelligenceConfigurationPolicyFields[] = [
{
__typename: 'CodeIntelligenceConfigurationPolicy' as const,
id: 'g1',
name: 'Default major release retention',
type: GitObjectType.GIT_TAG,
pattern: '.0.0',
retentionEnabled: true,
retentionDurationHours: 168,
retainIntermediateCommits: false,
indexingEnabled: false,
indexCommitMaxAgeHours: 672,
indexIntermediateCommits: false,
},
{
__typename: 'CodeIntelligenceConfigurationPolicy' as const,
id: 'g2',
name: 'Default brach retention',
type: GitObjectType.GIT_TREE,
pattern: '',
retentionEnabled: true,
retentionDurationHours: 2016,
retainIntermediateCommits: false,
indexingEnabled: false,
indexCommitMaxAgeHours: 4032,
indexIntermediateCommits: false,
},
]
const policies: CodeIntelligenceConfigurationPolicyFields[] = [
{
__typename: 'CodeIntelligenceConfigurationPolicy' as const,
id: 'id1',
name: 'All branches created by Eric',
type: GitObjectType.GIT_TREE,
pattern: 'ef/',
retentionEnabled: true,
retentionDurationHours: 8064,
retainIntermediateCommits: true,
indexingEnabled: true,
indexCommitMaxAgeHours: 40320,
indexIntermediateCommits: true,
},
{
__typename: 'CodeIntelligenceConfigurationPolicy' as const,
id: 'id2',
name: 'All branches created by Erik',
type: GitObjectType.GIT_TREE,
pattern: 'es/',
retentionEnabled: true,
retentionDurationHours: 8064,
retainIntermediateCommits: true,
indexingEnabled: true,
indexCommitMaxAgeHours: 40320,
indexIntermediateCommits: true,
},
]
const repositoryConfiguration = {
__typename: 'Repository' as const,
indexConfiguration: {
configuration: trim(`
{
"shared_steps": [],
"index_jobs": [
{
"steps": [
{
"root": "",
"image": "sourcegraph/lsif-node:autoindex",
"commands": [
"N_NODE_MIRROR=https://unofficial-builds.nodejs.org/download/release n --arch x64-musl auto",
"yarn --ignore-engines"
]
},
{
"root": "client/web",
"image": "sourcegraph/lsif-node:autoindex",
"commands": [
"N_NODE_MIRROR=https://unofficial-builds.nodejs.org/download/release n --arch x64-musl auto",
"npm install"
]
}
],
"local_steps": [
"N_NODE_MIRROR=https://unofficial-builds.nodejs.org/download/release n --arch x64-musl auto"
],
"root": "client/web",
"indexer": "sourcegraph/lsif-node:autoindex",
"indexer_args": [
"lsif-tsc",
"-p",
"."
],
"outfile": ""
}
]
}
`),
},
}
const inferredRepositoryConfiguration = {
__typename: 'Repository' as const,
indexConfiguration: {
inferredConfiguration: trim(`
{
"shared_steps": [],
"index_jobs": [
{
"steps": [
{
"root": "lib",
"image": "sourcegraph/lsif-go:latest",
"commands": [
"go mod download"
]
}
],
"local_steps": [],
"root": "lib",
"indexer": "sourcegraph/lsif-go:latest",
"indexer_args": [
"lsif-go",
"--no-animation"
],
"outfile": ""
}
]
}
`),
},
}
const story: Meta = {
title: 'web/codeintel/configuration/CodeIntelConfigurationPage',
decorators: [story => <div className="p-3 container">{story()}</div>, withKnobs],
parameters: {
component: CodeIntelConfigurationPage,
chromatic: {
viewports: [320, 576, 978, 1440],
},
},
}
export default story
const Template: Story<CodeIntelConfigurationPageProps> = args => (
<EnterpriseWebStory>
{props => (
<CodeIntelConfigurationPage {...props} indexingEnabled={boolean('indexingEnabled', true)} {...args} />
)}
</EnterpriseWebStory>
)
const defaults: Partial<CodeIntelConfigurationPageProps> = {
getPolicies: () => of([]),
getConfigurationForRepository: () => of(repositoryConfiguration),
getInferredConfigurationForRepository: () => of(inferredRepositoryConfiguration),
updateConfigurationForRepository: () => of(),
deletePolicyById: () => of(),
}
export const EmptyGlobalPage = Template.bind({})
EmptyGlobalPage.args = {
...defaults,
}
export const GlobalPage = Template.bind({})
GlobalPage.args = {
...defaults,
getPolicies: (repositoryId?: string) => of(repositoryId ? policies : globalPolicies),
}
export const EmptyRepositoryPage = Template.bind({})
EmptyRepositoryPage.args = {
...defaults,
repo: { id: 'sourcegraph' },
}
export const RepositoryPage = Template.bind({})
RepositoryPage.args = {
...defaults,
repo: { id: 'sourcegraph' },
getPolicies: (repositoryId?: string) => of(repositoryId ? policies : globalPolicies),
}

View File

@ -0,0 +1,172 @@
import * as H from 'history'
import React, { FunctionComponent, useCallback, useEffect, useState } from 'react'
import { RouteComponentProps } from 'react-router'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { ThemeProps } from '@sourcegraph/shared/src/theme'
import { ErrorAlert } from '@sourcegraph/web/src/components/alerts'
import { PageTitle } from '@sourcegraph/web/src/components/PageTitle'
import { PageHeader } from '@sourcegraph/wildcard'
import { CodeIntelligenceConfigurationPolicyFields } from '../../../graphql-operations'
import {
deletePolicyById as defaultDeletePolicyById,
getConfigurationForRepository as defaultGetConfigurationForRepository,
getInferredConfigurationForRepository as defaultGetInferredConfigurationForRepository,
getPolicies as defaultGetPolicies,
updateConfigurationForRepository as defaultUpdateConfigurationForRepository,
} from './backend'
import { GlobalPolicies } from './GlobalPolicies'
import { RepositoryConfiguration } from './RepositoryConfiguration'
enum State {
Idle,
Deleting,
}
export interface CodeIntelConfigurationPageProps extends RouteComponentProps<{}>, ThemeProps, TelemetryProps {
repo?: { id: string }
indexingEnabled?: boolean
getPolicies?: typeof defaultGetPolicies
updateConfigurationForRepository?: typeof defaultUpdateConfigurationForRepository
deletePolicyById?: typeof defaultDeletePolicyById
getConfigurationForRepository?: typeof defaultGetConfigurationForRepository
getInferredConfigurationForRepository?: typeof defaultGetInferredConfigurationForRepository
history: H.History
}
export const CodeIntelConfigurationPage: FunctionComponent<CodeIntelConfigurationPageProps> = ({
repo,
indexingEnabled = window.context?.codeIntelAutoIndexingEnabled,
getPolicies = defaultGetPolicies,
updateConfigurationForRepository = defaultUpdateConfigurationForRepository,
deletePolicyById = defaultDeletePolicyById,
getConfigurationForRepository = defaultGetConfigurationForRepository,
getInferredConfigurationForRepository = defaultGetInferredConfigurationForRepository,
isLightTheme,
telemetryService,
history,
}) => {
useEffect(() => telemetryService.logViewEvent('CodeIntelConfigurationPage'), [telemetryService])
const [policies, setPolicies] = useState<CodeIntelligenceConfigurationPolicyFields[]>()
const [globalPolicies, setGlobalPolicies] = useState<CodeIntelligenceConfigurationPolicyFields[]>()
const [fetchError, setFetchError] = useState<Error>()
useEffect(() => {
const subscription = getPolicies().subscribe(policies => {
setGlobalPolicies(policies)
}, setFetchError)
return () => subscription.unsubscribe()
}, [getPolicies])
useEffect(() => {
if (!repo) {
return
}
const subscription = getPolicies(repo.id).subscribe(policies => {
setPolicies(policies)
}, setFetchError)
return () => subscription.unsubscribe()
}, [repo, getPolicies])
const [deleteError, setDeleteError] = useState<Error>()
const [state, setState] = useState(() => State.Idle)
const deleteGlobalPolicy = useCallback(
async (id: string, name: string) => {
if (!globalPolicies || !window.confirm(`Delete global policy ${name}?`)) {
return
}
setState(State.Deleting)
setDeleteError(undefined)
try {
await deletePolicyById(id).toPromise()
setGlobalPolicies((globalPolicies || []).filter(policy => policy.id !== id))
} catch (error) {
setDeleteError(error)
} finally {
setState(State.Idle)
}
},
[globalPolicies, deletePolicyById]
)
const deletePolicy = useCallback(
async (id: string, name: string) => {
if (!policies || !window.confirm(`Delete policy ${name}?`)) {
return
}
setState(State.Deleting)
setDeleteError(undefined)
try {
await deletePolicyById(id).toPromise()
setPolicies((policies || []).filter(policy => policy.id !== id))
} catch (error) {
setDeleteError(error)
} finally {
setState(State.Idle)
}
},
[policies, deletePolicyById]
)
return fetchError ? (
<ErrorAlert prefix="Error fetching configuration" error={fetchError} />
) : (
<>
<PageTitle title="Precise code intelligence configuration" />
<PageHeader
headingElement="h2"
path={[
{
text: <>Precise code intelligence configuration</>,
},
]}
description={`Rules that define configuration for precise code intelligence ${
repo ? 'in this repository' : 'over all repositories'
}.`}
className="mb-3"
/>
{repo ? (
<RepositoryConfiguration
repo={repo}
disabled={state !== State.Idle}
deleting={state === State.Deleting}
policies={policies}
deletePolicy={deletePolicy}
globalPolicies={globalPolicies}
deleteGlobalPolicy={deleteGlobalPolicy}
deleteError={deleteError}
updateConfigurationForRepository={updateConfigurationForRepository}
getConfigurationForRepository={getConfigurationForRepository}
getInferredConfigurationForRepository={getInferredConfigurationForRepository}
indexingEnabled={indexingEnabled}
isLightTheme={isLightTheme}
telemetryService={telemetryService}
history={history}
/>
) : (
<GlobalPolicies
repo={repo}
disabled={state !== State.Idle}
deleting={state === State.Deleting}
globalPolicies={globalPolicies}
deleteGlobalPolicy={deleteGlobalPolicy}
deleteError={deleteError}
indexingEnabled={indexingEnabled}
history={history}
/>
)}
</>
)
}

View File

@ -0,0 +1,85 @@
import { boolean, withKnobs } from '@storybook/addon-knobs'
import { Meta, Story } from '@storybook/react'
import React from 'react'
import { of } from 'rxjs'
import { GitObjectType } from '@sourcegraph/shared/src/graphql-operations'
import { CodeIntelligenceConfigurationPolicyFields } from '../../../graphql-operations'
import { EnterpriseWebStory } from '../../components/EnterpriseWebStory'
import {
CodeIntelConfigurationPolicyPage,
CodeIntelConfigurationPolicyPageProps,
} from './CodeIntelConfigurationPolicyPage'
const policy: CodeIntelligenceConfigurationPolicyFields = {
__typename: 'CodeIntelligenceConfigurationPolicy' as const,
id: '1',
name: "Eric's feature branches",
type: GitObjectType.GIT_TREE,
pattern: 'ef/',
retentionEnabled: true,
retentionDurationHours: 168,
retainIntermediateCommits: true,
indexingEnabled: true,
indexCommitMaxAgeHours: 672,
indexIntermediateCommits: true,
}
const repoResult = {
__typename: 'Repository' as const,
name: 'github.com/sourcegraph/sourcegraph',
}
const branchesResult = {
...repoResult,
branches: {
totalCount: 3,
nodes: [{ displayName: 'ef/wip1' }, { displayName: 'ef/wip2' }, { displayName: 'ef/wip3' }],
},
}
const tagsResult = {
...repoResult,
tags: { totalCount: 2, nodes: [{ displayName: 'v3.0-ref' }, { displayName: 'v3-ref.1' }] },
}
const story: Meta = {
title: 'web/codeintel/configuration/CodeIntelConfigurationPolicyPage',
decorators: [story => <div className="p-3 container">{story()}</div>, withKnobs],
parameters: {
component: CodeIntelConfigurationPolicyPage,
chromatic: {
viewports: [320, 576, 978, 1440],
},
},
}
export default story
const Template: Story<CodeIntelConfigurationPolicyPageProps> = args => (
<EnterpriseWebStory>
{props => (
<CodeIntelConfigurationPolicyPage {...props} indexingEnabled={boolean('indexingEnabled', true)} {...args} />
)}
</EnterpriseWebStory>
)
const defaults: Partial<CodeIntelConfigurationPolicyPageProps> = {
getPolicyById: () => of(policy),
updatePolicy: () => of(),
searchGitBranches: () => of(branchesResult),
searchGitTags: () => of(tagsResult),
repoName: () => of(repoResult),
}
export const GlobalPage = Template.bind({})
GlobalPage.args = {
...defaults,
}
export const RepositoryPage = Template.bind({})
RepositoryPage.args = {
...defaults,
repo: { id: '42' },
}

View File

@ -0,0 +1,198 @@
import * as H from 'history'
import React, { FunctionComponent, useEffect, useState } from 'react'
import { RouteComponentProps } from 'react-router'
import { of } from 'rxjs'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { ThemeProps } from '@sourcegraph/shared/src/theme'
import { ErrorAlert } from '@sourcegraph/web/src/components/alerts'
import { PageTitle } from '@sourcegraph/web/src/components/PageTitle'
import { Button, Container, LoadingSpinner, PageHeader } from '@sourcegraph/wildcard'
import { CodeIntelligenceConfigurationPolicyFields, GitObjectType } from '../../../graphql-operations'
import {
getPolicyById as defaultGetPolicyById,
repoName as defaultRepoName,
searchGitBranches as defaultSearchGitBranches,
searchGitTags as defaultSearchGitTags,
updatePolicy as defaultUpdatePolicy,
} from './backend'
import { BranchTargetSettings } from './BranchTargetSettings'
import { IndexingSettings } from './IndexSettings'
import { RetentionSettings } from './RetentionSettings'
export interface CodeIntelConfigurationPolicyPageProps
extends RouteComponentProps<{ id: string }>,
ThemeProps,
TelemetryProps {
repo?: { id: string }
indexingEnabled?: boolean
getPolicyById?: typeof defaultGetPolicyById
repoName?: typeof defaultRepoName
searchGitBranches?: typeof defaultSearchGitBranches
searchGitTags?: typeof defaultSearchGitTags
updatePolicy?: typeof defaultUpdatePolicy
history: H.History
}
enum State {
Idle,
Saving,
}
const emptyPolicy: CodeIntelligenceConfigurationPolicyFields = {
__typename: 'CodeIntelligenceConfigurationPolicy',
id: '',
name: '',
type: GitObjectType.GIT_COMMIT,
pattern: '',
retentionEnabled: false,
retentionDurationHours: null,
retainIntermediateCommits: false,
indexingEnabled: false,
indexCommitMaxAgeHours: null,
indexIntermediateCommits: false,
}
export const CodeIntelConfigurationPolicyPage: FunctionComponent<CodeIntelConfigurationPolicyPageProps> = ({
match: {
params: { id },
},
repo,
indexingEnabled = window.context?.codeIntelAutoIndexingEnabled,
getPolicyById = defaultGetPolicyById,
repoName = defaultRepoName,
searchGitBranches = defaultSearchGitBranches,
searchGitTags = defaultSearchGitTags,
updatePolicy = defaultUpdatePolicy,
history,
telemetryService,
}) => {
useEffect(() => telemetryService.logViewEvent('CodeIntelConfigurationPolicyPageProps'), [telemetryService])
const [saved, setSaved] = useState<CodeIntelligenceConfigurationPolicyFields>()
const [policy, setPolicy] = useState<CodeIntelligenceConfigurationPolicyFields>()
const [fetchError, setFetchError] = useState<Error>()
useEffect(() => {
const subscription = (id === 'new' ? of(emptyPolicy) : getPolicyById(id)).subscribe(policy => {
setSaved(policy)
setPolicy(policy)
}, setFetchError)
return () => subscription.unsubscribe()
}, [id, getPolicyById])
const [saveError, setSaveError] = useState<Error>()
const [state, setState] = useState(() => State.Idle)
const save = async (): Promise<void> => {
if (!policy) {
return
}
let navigatingAway = false
setState(State.Saving)
setSaveError(undefined)
try {
await updatePolicy(policy, repo?.id).toPromise()
history.push('./')
navigatingAway = true
} catch (error) {
setSaveError(error)
} finally {
if (!navigatingAway) {
setState(State.Idle)
}
}
}
return fetchError ? (
<ErrorAlert prefix="Error fetching configuration policy" error={fetchError} />
) : (
<>
<PageTitle title="Precise code intelligence configuration policy" />
<PageHeader
headingElement="h2"
path={[
{
text: <>{policy?.id === '' ? 'Create' : 'Update'} configuration policy</>,
},
]}
description={`${policy?.id === '' ? 'Create' : 'Update'} a new configuration policy that applies to ${
repo ? 'this repository' : 'all repositories'
}.`}
className="mb-3"
/>
{policy === undefined ? (
<LoadingSpinner className="icon-inline" />
) : (
<>
<Container className="container form">
{saveError && <ErrorAlert prefix="Error saving configuration policy" error={saveError} />}
<BranchTargetSettings
repoId={repo?.id}
policy={policy}
setPolicy={setPolicy}
repoName={repoName}
searchGitBranches={searchGitBranches}
searchGitTags={searchGitTags}
/>
</Container>
<RetentionSettings policy={policy} setPolicy={setPolicy} />
{indexingEnabled && <IndexingSettings policy={policy} setPolicy={setPolicy} />}
<Container className="mt-2">
<Button
type="submit"
variant="primary"
onClick={save}
disabled={state !== State.Idle || comparePolicies(policy, saved)}
>
{policy.id === '' ? 'Create' : 'Update'} policy
</Button>
<Button
type="button"
className="ml-3"
variant="secondary"
onClick={() => history.push('./')}
disabled={state !== State.Idle}
>
Cancel
</Button>
{state === State.Saving && (
<span className="ml-2">
<LoadingSpinner className="icon-inline" /> Saving...
</span>
)}
</Container>
</>
)}
</>
)
}
function comparePolicies(
a: CodeIntelligenceConfigurationPolicyFields,
b?: CodeIntelligenceConfigurationPolicyFields
): boolean {
return (
b !== undefined &&
a.id === b.id &&
a.name === b.name &&
a.type === b.type &&
a.pattern === b.pattern &&
a.retentionEnabled === b.retentionEnabled &&
a.retentionDurationHours === b.retentionDurationHours &&
a.retainIntermediateCommits === b.retainIntermediateCommits &&
a.indexingEnabled === b.indexingEnabled &&
a.indexCommitMaxAgeHours === b.indexCommitMaxAgeHours &&
a.indexIntermediateCommits === b.indexIntermediateCommits
)
}

View File

@ -1,75 +0,0 @@
import { storiesOf } from '@storybook/react'
import React from 'react'
import { of } from 'rxjs'
import { EnterpriseWebStory } from '../../components/EnterpriseWebStory'
import { CodeIntelIndexConfigurationPage } from './CodeIntelIndexConfigurationPage'
const { add } = storiesOf('web/codeintel/configuration/CodeIntelIndexConfigurationPage', module)
.addDecorator(story => <div className="p-3 container">{story()}</div>)
.addParameters({
chromatic: {
viewports: [320, 576, 978, 1440],
},
})
add('Empty', () => (
<EnterpriseWebStory>
{props => (
<CodeIntelIndexConfigurationPage
{...props}
repo={{ id: '42' }}
getConfiguration={() =>
of({
__typename: 'Repository',
indexConfiguration: {
configuration: '',
inferredConfiguration: '',
},
})
}
/>
)}
</EnterpriseWebStory>
))
add('SavedConfiguration', () => (
<EnterpriseWebStory>
{props => (
<CodeIntelIndexConfigurationPage
{...props}
repo={{ id: '42' }}
getConfiguration={() =>
of({
__typename: 'Repository',
indexConfiguration: {
configuration: '{"foo": "bar"}',
inferredConfiguration: '',
},
})
}
/>
)}
</EnterpriseWebStory>
))
add('InferredConfiguration', () => (
<EnterpriseWebStory>
{props => (
<CodeIntelIndexConfigurationPage
{...props}
repo={{ id: '42' }}
getConfiguration={() =>
of({
__typename: 'Repository',
indexConfiguration: {
configuration: '{"foo": "bar"}',
inferredConfiguration: '{"baz": "bonk"}',
},
})
}
/>
)}
</EnterpriseWebStory>
))

View File

@ -1,174 +0,0 @@
import * as H from 'history'
import { editor } from 'monaco-editor'
import React, { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
import { RouteComponentProps } from 'react-router'
import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { ThemeProps } from '@sourcegraph/shared/src/theme'
import { Container, PageHeader } from '@sourcegraph/wildcard'
import { ErrorAlert } from '../../../components/alerts'
import { PageTitle } from '../../../components/PageTitle'
import { SaveToolbar, SaveToolbarProps, SaveToolbarPropsGenerator } from '../../../components/SaveToolbar'
import { DynamicallyImportedMonacoSettingsEditor } from '../../../settings/DynamicallyImportedMonacoSettingsEditor'
import { getConfiguration as defaultGetConfiguration, updateConfiguration } from './backend'
import allConfigSchema from './schema.json'
export interface CodeIntelIndexConfigurationPageProps extends RouteComponentProps<{}>, ThemeProps, TelemetryProps {
repo: { id: string }
history: H.History
getConfiguration?: typeof defaultGetConfiguration
}
enum State {
Idle,
Saving,
}
export const CodeIntelIndexConfigurationPage: FunctionComponent<CodeIntelIndexConfigurationPageProps> = ({
repo,
isLightTheme,
telemetryService,
history,
getConfiguration = defaultGetConfiguration,
}) => {
useEffect(() => telemetryService.logViewEvent('CodeIntelIndexConfigurationPage'), [telemetryService])
const [configuration, setConfiguration] = useState<string>()
const [inferredConfiguration, setInferredConfiguration] = useState<string>()
const [fetchError, setFetchError] = useState<Error>()
useEffect(() => {
const subscription = getConfiguration({ id: repo.id }).subscribe(config => {
setConfiguration(config?.indexConfiguration?.configuration || '')
setInferredConfiguration(config?.indexConfiguration?.inferredConfiguration || '')
}, setFetchError)
return () => subscription.unsubscribe()
}, [repo, getConfiguration])
const [saveError, setSaveError] = useState<Error>()
const [state, setState] = useState(() => State.Idle)
const save = useCallback(
async (content: string) => {
setState(State.Saving)
setSaveError(undefined)
try {
await updateConfiguration({ id: repo.id, content }).toPromise()
setDirty(false)
setConfiguration(content)
} catch (error) {
setSaveError(error)
} finally {
setState(State.Idle)
}
},
[repo]
)
const [dirty, setDirty] = useState<boolean>()
const [editor, setEditor] = useState<editor.ICodeEditor>()
const infer = useCallback(() => editor?.setValue(inferredConfiguration || ''), [editor, inferredConfiguration])
const customToolbar = useMemo<{
saveToolbar: React.FunctionComponent<SaveToolbarProps & AutoIndexProps>
propsGenerator: SaveToolbarPropsGenerator<AutoIndexProps>
}>(
() => ({
saveToolbar: CodeIntelAutoIndexSaveToolbar,
propsGenerator: props => {
const mergedProps = {
...props,
onInfer: infer,
inferEnabled: !!inferredConfiguration && configuration !== inferredConfiguration,
}
mergedProps.willShowError = () => !mergedProps.saving
mergedProps.saveDiscardDisabled = () => mergedProps.saving || !dirty
return mergedProps
},
}),
[dirty, configuration, inferredConfiguration, infer]
)
return fetchError ? (
<ErrorAlert prefix="Error fetching index configuration" error={fetchError} />
) : (
<div className="code-intel-index-configuration">
<PageTitle title="Auto-indexing configuration" />
<PageHeader
headingElement="h2"
path={[
{
text: <>Auto-indexing configuration</>,
},
]}
className="mb-3"
/>
<Container>
{saveError && <ErrorAlert prefix="Error saving index configuration" error={saveError} />}
{configuration === undefined ? (
<LoadingSpinner className="icon-inline" />
) : (
<DynamicallyImportedMonacoSettingsEditor
value={configuration}
jsonSchema={allConfigSchema}
canEdit={true}
onSave={save}
saving={state === State.Saving}
height={600}
isLightTheme={isLightTheme}
history={history}
telemetryService={telemetryService}
customSaveToolbar={customToolbar}
onDirtyChange={setDirty}
onEditor={setEditor}
/>
)}
</Container>
</div>
)
}
interface AutoIndexProps {
inferEnabled: boolean
onInfer?: () => void
}
const CodeIntelAutoIndexSaveToolbar: React.FunctionComponent<SaveToolbarProps & AutoIndexProps> = ({
dirty,
saving,
error,
onSave,
onDiscard,
inferEnabled,
onInfer,
saveDiscardDisabled,
}) => (
<SaveToolbar
dirty={dirty}
saving={saving}
onSave={onSave}
error={error}
saveDiscardDisabled={saveDiscardDisabled}
onDiscard={onDiscard}
>
{inferEnabled && (
<button
type="button"
title="Infer index configuration from HEAD"
className="btn btn-link"
onClick={onInfer}
>
Infer index configuration from HEAD
</button>
)}
</SaveToolbar>
)

View File

@ -0,0 +1,29 @@
.grid {
display: grid;
grid-template-columns: [info] minmax(auto, 1fr) [state] min-content [caret] min-content [end];
row-gap: 1rem;
column-gap: 1rem;
align-items: center;
margin-bottom: 1rem;
@media (--sm-breakpoint-down) {
row-gap: 0.5rem;
column-gap: 0.5rem;
}
}
.separator {
// Make it full width in the current row.
grid-column: 1 / -1;
border-top: 1px solid var(--border-color-2);
@media (--xs-breakpoint-down) {
margin-top: 1rem;
padding-bottom: 1rem;
}
}
.name,
.button {
@media (--xs-breakpoint-down) {
grid-column: 1 / -1;
}
}

View File

@ -0,0 +1,98 @@
import classNames from 'classnames'
import * as H from 'history'
import PencilIcon from 'mdi-react/PencilIcon'
import TrashIcon from 'mdi-react/TrashIcon'
import React, { FunctionComponent } from 'react'
import { GitObjectType } from '@sourcegraph/shared/src/graphql/schema'
import { Button } from '@sourcegraph/wildcard'
import { CodeIntelligenceConfigurationPolicyFields } from '../../../graphql-operations'
import styles from './CodeIntelligencePolicyTable.module.scss'
import { IndexingPolicyDescription } from './IndexingPolicyDescription'
import { RetentionPolicyDescription } from './RetentionPolicyDescription'
export interface CodeIntelligencePolicyTableProps {
indexingEnabled: boolean
disabled: boolean
policies: CodeIntelligenceConfigurationPolicyFields[]
deletePolicy?: (id: string, name: string) => Promise<void>
history: H.History
}
export const CodeIntelligencePolicyTable: FunctionComponent<CodeIntelligencePolicyTableProps> = ({
indexingEnabled,
disabled,
policies,
deletePolicy,
history,
}) => (
<div className={classNames(styles.grid, 'mb-3')}>
{policies.map(policy => (
<React.Fragment key={policy.id}>
<span className={styles.separator} />
<div className={classNames(styles.name, 'd-flex flex-column')}>
<div className="m-0">
<h3 className="m-0 d-block d-md-inline">{policy.name}</h3>
</div>
<div>
<div className="mr-2 d-block d-mdinline-block">
Applied to{' '}
{policy.type === GitObjectType.GIT_COMMIT
? 'commits'
: policy.type === GitObjectType.GIT_TAG
? 'tags'
: policy.type === GitObjectType.GIT_TREE
? 'branches'
: ''}{' '}
matching <span className="text-monospace">{policy.pattern}</span>
</div>
<div>
{indexingEnabled && !policy.retentionEnabled && !policy.indexingEnabled ? (
<p className="text-muted mt-2">Data retention and auto-indexing disabled.</p>
) : (
<>
<p className="mt-2">
<RetentionPolicyDescription policy={policy} />
</p>
{indexingEnabled && (
<p className="mt-2">
<IndexingPolicyDescription policy={policy} />
</p>
)}
</>
)}
</div>
</div>
</div>
<span className={classNames(styles.button, 'd-none d-md-inline')}>
{deletePolicy && (
<Button
onClick={() => history.push(`./configuration/${policy.id}`)}
className="p-0"
disabled={disabled}
>
<PencilIcon className="icon-inline" />
</Button>
)}
</span>
<span className={classNames(styles.button, 'd-none d-md-inline')}>
{deletePolicy && (
<Button
onClick={() => deletePolicy(policy.id, policy.name)}
className="ml-2 p-0"
disabled={disabled}
>
<TrashIcon className="icon-inline text-danger" />
</Button>
)}
</span>
</React.Fragment>
))}
</div>
)

View File

@ -0,0 +1,136 @@
import * as H from 'history'
import { editor } from 'monaco-editor'
import React, { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { ThemeProps } from '@sourcegraph/shared/src/theme'
import { ErrorAlert } from '@sourcegraph/web/src/components/alerts'
import { SaveToolbarProps, SaveToolbarPropsGenerator } from '../../../components/SaveToolbar'
import { DynamicallyImportedMonacoSettingsEditor } from '../../../settings/DynamicallyImportedMonacoSettingsEditor'
import {
getConfigurationForRepository as defaultGetConfigurationForRepository,
getInferredConfigurationForRepository as defaultGetInferredConfigurationForRepository,
updateConfigurationForRepository as defaultUpdateConfigurationForRepository,
} from './backend'
import { IndexConfigurationSaveToolbar, IndexConfigurationSaveToolbarProps } from './IndexConfigurationSaveToolbar'
import allConfigSchema from './schema.json'
export interface ConfigurationEditorProps extends ThemeProps, TelemetryProps {
repoId: string
history: H.History
getConfigurationForRepository: typeof defaultGetConfigurationForRepository
getInferredConfigurationForRepository: typeof defaultGetInferredConfigurationForRepository
updateConfigurationForRepository: typeof defaultUpdateConfigurationForRepository
}
enum EditorState {
Idle,
Saving,
}
export const ConfigurationEditor: FunctionComponent<ConfigurationEditorProps> = ({
repoId,
isLightTheme,
telemetryService,
history,
getConfigurationForRepository,
getInferredConfigurationForRepository,
updateConfigurationForRepository,
}) => {
const [configuration, setConfiguration] = useState<string>()
const [inferredConfiguration, setInferredConfiguration] = useState<string>()
const [fetchError, setFetchError] = useState<Error>()
useEffect(() => {
const subscription = getConfigurationForRepository(repoId).subscribe(config => {
setConfiguration(config?.indexConfiguration?.configuration || '')
}, setFetchError)
return () => subscription.unsubscribe()
}, [repoId, getConfigurationForRepository])
useEffect(() => {
const subscription = getInferredConfigurationForRepository(repoId).subscribe(config => {
setInferredConfiguration(config?.indexConfiguration?.inferredConfiguration || '')
}, setFetchError)
return () => subscription.unsubscribe()
}, [repoId, getInferredConfigurationForRepository])
const [saveError, setSaveError] = useState<Error>()
const [state, setState] = useState(() => EditorState.Idle)
const save = useCallback(
async (content: string) => {
setState(EditorState.Saving)
setSaveError(undefined)
try {
await updateConfigurationForRepository(repoId, content).toPromise()
setDirty(false)
setConfiguration(content)
} catch (error) {
setSaveError(error)
} finally {
setState(EditorState.Idle)
}
},
[repoId, updateConfigurationForRepository]
)
const [dirty, setDirty] = useState<boolean>()
const [editor, setEditor] = useState<editor.ICodeEditor>()
const infer = useCallback(() => editor?.setValue(inferredConfiguration || ''), [editor, inferredConfiguration])
const customToolbar = useMemo<{
saveToolbar: React.FunctionComponent<SaveToolbarProps & IndexConfigurationSaveToolbarProps>
propsGenerator: SaveToolbarPropsGenerator<IndexConfigurationSaveToolbarProps>
}>(
() => ({
saveToolbar: IndexConfigurationSaveToolbar,
propsGenerator: props => {
const mergedProps = {
...props,
onInfer: infer,
loading: inferredConfiguration === undefined,
inferEnabled: !!inferredConfiguration && configuration !== inferredConfiguration,
}
mergedProps.willShowError = () => !mergedProps.saving
mergedProps.saveDiscardDisabled = () => mergedProps.saving || !dirty
return mergedProps
},
}),
[dirty, configuration, inferredConfiguration, infer]
)
return fetchError ? (
<ErrorAlert prefix="Error fetching index configuration" error={fetchError} />
) : (
<>
{saveError && <ErrorAlert prefix="Error saving index configuration" error={saveError} />}
{configuration === undefined ? (
<LoadingSpinner className="icon-inline" />
) : (
<DynamicallyImportedMonacoSettingsEditor
value={configuration}
jsonSchema={allConfigSchema}
canEdit={true}
onSave={save}
saving={state === EditorState.Saving}
height={600}
isLightTheme={isLightTheme}
history={history}
telemetryService={telemetryService}
customSaveToolbar={customToolbar}
onDirtyChange={setDirty}
onEditor={setEditor}
/>
)}
</>
)
}

View File

@ -0,0 +1,33 @@
import React, { FunctionComponent } from 'react'
import { defaultDurationValues } from './shared'
export interface DurationSelectProps {
id: string
value: string | null
disabled: boolean
onChange?: (value: number | null) => void
durationValues?: { value: number; displayText: string }[]
}
export const DurationSelect: FunctionComponent<DurationSelectProps> = ({
id,
value,
disabled,
onChange,
durationValues = defaultDurationValues,
}) => (
<select
id={id}
className="form-control"
value={value || undefined}
disabled={disabled}
onChange={event => onChange?.(!event.target.value ? null : Math.floor(parseInt(event.target.value, 10)))}
>
{durationValues.map(({ value, displayText }) => (
<option key={value} value={value || undefined}>
{displayText}
</option>
))}
</select>
)

View File

@ -0,0 +1,182 @@
import React, { FunctionComponent, useEffect, useState } from 'react'
import { ErrorAlert } from '@sourcegraph/web/src/components/alerts'
import { LoadingSpinner } from '@sourcegraph/wildcard'
import { GitObjectType } from '../../../graphql-operations'
import {
repoName as defaultRepoName,
searchGitBranches as defaultSearchGitBranches,
searchGitTags as defaultSearchGitTags,
} from './backend'
export interface GitObjectPreviewProps {
repoId: string
type: GitObjectType
pattern: string
repoName: typeof defaultRepoName
searchGitTags: typeof defaultSearchGitTags
searchGitBranches: typeof defaultSearchGitBranches
}
enum PreviewState {
Idle,
LoadingTags,
}
export const GitObjectPreview: FunctionComponent<GitObjectPreviewProps> = ({
repoId,
type,
pattern,
repoName,
searchGitTags,
searchGitBranches,
}) => {
const [state, setState] = useState(() => PreviewState.Idle)
const [commitPreview, setCommitPreview] = useState<GitObjectPreviewResult>()
const [commitPreviewFetchError, setCommitPreviewFetchError] = useState<Error>()
useEffect(() => {
async function updateCommitPreview(): Promise<void> {
setState(PreviewState.LoadingTags)
setCommitPreviewFetchError(undefined)
const resultFactories = [
{ type: GitObjectType.GIT_COMMIT, factory: () => resultFromCommit(repoId, pattern, repoName) },
{ type: GitObjectType.GIT_TAG, factory: () => resultFromTag(repoId, pattern, searchGitTags) },
{ type: GitObjectType.GIT_TREE, factory: () => resultFromBranch(repoId, pattern, searchGitBranches) },
]
try {
const match = resultFactories.find(({ type: match }) => match === type)
if (match) {
setCommitPreview(await match.factory())
}
} catch (error) {
setCommitPreviewFetchError(error)
} finally {
setState(PreviewState.Idle)
}
}
updateCommitPreview().catch(console.error)
}, [repoId, type, pattern, repoName, searchGitTags, searchGitBranches])
return (
<>
<h3>Preview of Git object filter</h3>
{type ? (
<>
<small>
{commitPreview?.preview.length === 0 ? (
<>Configuration policy does not match any known commits.</>
) : (
<>
Configuration policy will be applied to the following
{type === GitObjectType.GIT_COMMIT
? ' commit'
: type === GitObjectType.GIT_TAG
? ' tags'
: type === GitObjectType.GIT_TREE
? ' branches'
: ''}
.
</>
)}
</small>
{commitPreviewFetchError ? (
<ErrorAlert
prefix="Error fetching matching repository objects"
error={commitPreviewFetchError}
/>
) : (
<>
{commitPreview !== undefined && commitPreview.preview.length !== 0 && (
<div className="mt-2 p-2">
<div className="bg-dark text-light p-2">
{commitPreview.preview.map(tag => (
<p key={tag.revlike} className="text-monospace p-0 m-0">
<span className="search-filter-keyword">repo:</span>
<span>{tag.name}</span>
<span className="search-filter-keyword">@</span>
<span>{tag.revlike}</span>
</p>
))}
</div>
{commitPreview.preview.length < commitPreview.totalCount && (
<p className="pt-2">
...and {commitPreview.totalCount - commitPreview.preview.length} other
matches
</p>
)}
</div>
)}
{state === PreviewState.LoadingTags && <LoadingSpinner />}
</>
)}
</>
) : (
<small>Select a Git object type to preview matching commits.</small>
)}
</>
)
}
interface GitObjectPreviewResult {
preview: { name: string; revlike: string }[]
totalCount: number
}
const resultFromCommit = async (
repoId: string,
pattern: string,
repoName: typeof defaultRepoName
): Promise<GitObjectPreviewResult> => {
const result = await repoName(repoId).toPromise()
if (!result) {
return { preview: [], totalCount: 0 }
}
return { preview: [{ name: result.name, revlike: pattern }], totalCount: 1 }
}
const resultFromTag = async (
repoId: string,
pattern: string,
searchGitTags: typeof defaultSearchGitTags
): Promise<GitObjectPreviewResult> => {
const result = await searchGitTags(repoId, pattern).toPromise()
if (!result) {
return { preview: [], totalCount: 0 }
}
const { nodes, totalCount } = result.tags
return {
preview: nodes.map(node => ({ name: result.name, revlike: node.displayName })),
totalCount,
}
}
const resultFromBranch = async (
repoId: string,
pattern: string,
searchGitBranches: typeof defaultSearchGitBranches
): Promise<GitObjectPreviewResult> => {
const result = await searchGitBranches(repoId, pattern).toPromise()
if (!result) {
return { preview: [], totalCount: 0 }
}
const { nodes, totalCount } = result.branches
return {
preview: nodes.map(node => ({ name: result.name, revlike: node.displayName })),
totalCount,
}
}

View File

@ -0,0 +1,22 @@
import React, { FunctionComponent } from 'react'
import { GitObjectType } from '@sourcegraph/shared/src/graphql/schema'
import { CodeIntelligenceConfigurationPolicyFields } from '../../../graphql-operations'
export const GitObjectTargetDescription: FunctionComponent<{ policy: CodeIntelligenceConfigurationPolicyFields }> = ({
policy,
}) =>
policy.type === GitObjectType.GIT_COMMIT ? (
<>the matching commit</>
) : policy.type === GitObjectType.GIT_TAG ? (
<>the matching tags</>
) : policy.type === GitObjectType.GIT_TREE ? (
!policy.retainIntermediateCommits ? (
<>the tip of the matching branches</>
) : (
<>any commit on the matching branches</>
)
) : (
<></>
)

View File

@ -0,0 +1,53 @@
import * as H from 'history'
import React, { FunctionComponent } from 'react'
import { ErrorAlert } from '@sourcegraph/web/src/components/alerts'
import { Container } from '@sourcegraph/wildcard'
import { CodeIntelligenceConfigurationPolicyFields } from '../../../graphql-operations'
import { PoliciesList } from './PoliciesList'
import { PolicyListActions } from './PolicyListActions'
export interface GlobalPoliciesProps {
repo?: { id: string }
disabled: boolean
deleting: boolean
globalPolicies?: CodeIntelligenceConfigurationPolicyFields[]
deleteGlobalPolicy: (id: string, name: string) => Promise<void>
deleteError?: Error
indexingEnabled: boolean
history: H.History
}
export const GlobalPolicies: FunctionComponent<GlobalPoliciesProps> = ({
repo,
disabled,
deleting,
globalPolicies,
deleteGlobalPolicy,
deleteError,
indexingEnabled,
history,
}) => (
<Container>
<h3>Global policies</h3>
{repo === undefined && deleteError && (
<ErrorAlert prefix="Error deleting configuration policy" error={deleteError} />
)}
<PoliciesList
policies={globalPolicies}
deletePolicy={repo ? undefined : deleteGlobalPolicy}
disabled={disabled}
indexingEnabled={indexingEnabled}
buttonFragment={
repo === undefined ? (
<PolicyListActions disabled={disabled} deleting={deleting} history={history} />
) : undefined
}
history={history}
/>
</Container>
)

View File

@ -0,0 +1,35 @@
import React from 'react'
import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
import { Button } from '@sourcegraph/wildcard'
import { SaveToolbar, SaveToolbarProps } from '../../../components/SaveToolbar'
export interface IndexConfigurationSaveToolbarProps {
loading: boolean
inferEnabled: boolean
onInfer?: () => void
}
export const IndexConfigurationSaveToolbar: React.FunctionComponent<
SaveToolbarProps & IndexConfigurationSaveToolbarProps
> = ({ dirty, loading, saving, error, onSave, onDiscard, inferEnabled, onInfer, saveDiscardDisabled }) => (
<SaveToolbar
dirty={dirty}
saving={saving}
onSave={onSave}
error={error}
saveDiscardDisabled={saveDiscardDisabled}
onDiscard={onDiscard}
>
{loading ? (
<LoadingSpinner className="icon-inline mt-2 ml-2" />
) : (
inferEnabled && (
<Button type="button" title="Infer index configuration from HEAD" variant="link" onClick={onInfer}>
Infer index configuration from HEAD
</Button>
)
)}
</SaveToolbar>
)

View File

@ -0,0 +1,56 @@
import React, { FunctionComponent } from 'react'
import { Toggle } from '@sourcegraph/branded/src/components/Toggle'
import { Container } from '@sourcegraph/wildcard'
import { CodeIntelligenceConfigurationPolicyFields, GitObjectType } from '../../../graphql-operations'
import { DurationSelect } from './DurationSelect'
export interface IndexingSettingsProps {
policy: CodeIntelligenceConfigurationPolicyFields
setPolicy: (policy: CodeIntelligenceConfigurationPolicyFields) => void
}
export const IndexingSettings: FunctionComponent<IndexingSettingsProps> = ({ policy, setPolicy }) => (
<Container className="mt-2">
<h3>Auto-indexing</h3>
<div className="form-group">
<Toggle
id="indexing-enabled"
title="Enabled"
value={policy.indexingEnabled}
onToggle={value => setPolicy({ ...policy, indexingEnabled: value })}
/>
<label htmlFor="indexing-enabled" className="ml-2">
Enabled / disabled
</label>
</div>
<div className="form-group">
<label htmlFor="index-commit-max-age">Commit max age</label>
<DurationSelect
id="index-commit-max-age"
value={policy.indexCommitMaxAgeHours ? `${policy.indexCommitMaxAgeHours}` : null}
disabled={!policy.indexingEnabled}
onChange={value => setPolicy({ ...policy, indexCommitMaxAgeHours: value })}
/>
</div>
{policy.type === GitObjectType.GIT_TREE && (
<div className="form-group">
<Toggle
id="index-intermediate-commits"
title="Enabled"
value={policy.indexIntermediateCommits}
onToggle={value => setPolicy({ ...policy, indexIntermediateCommits: value })}
disabled={!policy.indexingEnabled}
/>
<label htmlFor="index-intermediate-commits" className="ml-2">
Index intermediate commits
</label>
</div>
)}
</Container>
)

View File

@ -0,0 +1,21 @@
import React, { FunctionComponent } from 'react'
import { CodeIntelligenceConfigurationPolicyFields } from '../../../graphql-operations'
import { GitObjectTargetDescription } from './GitObjectTargetDescription'
import { formatDurationValue } from './shared'
export const IndexingPolicyDescription: FunctionComponent<{ policy: CodeIntelligenceConfigurationPolicyFields }> = ({
policy,
}) =>
policy.indexingEnabled ? (
<>
<strong>Indexing policy:</strong> Auto-index <GitObjectTargetDescription policy={policy} />
{policy.indexCommitMaxAgeHours && (
<> if the target commit is no older than {formatDurationValue(policy.indexCommitMaxAgeHours)}</>
)}
.
</>
) : (
<span className="text-muted">Auto-indexing disabled.</span>
)

View File

@ -0,0 +1,31 @@
import * as H from 'history'
import React, { FunctionComponent } from 'react'
import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
import { CodeIntelligenceConfigurationPolicyFields } from '../../../graphql-operations'
import { CodeIntelligencePolicyTable } from './CodeIntelligencePolicyTable'
export interface PoliciesListProps {
policies?: CodeIntelligenceConfigurationPolicyFields[]
deletePolicy?: (id: string, name: string) => Promise<void>
disabled: boolean
indexingEnabled: boolean
buttonFragment?: JSX.Element
history: H.History
}
export const PoliciesList: FunctionComponent<PoliciesListProps> = ({ policies, buttonFragment, ...props }) =>
policies === undefined ? (
<LoadingSpinner className="icon-inline" />
) : (
<>
{policies.length === 0 ? (
<div>No policies have been defined.</div>
) : (
<CodeIntelligencePolicyTable {...props} policies={policies} />
)}
{buttonFragment}
</>
)

View File

@ -0,0 +1,30 @@
import * as H from 'history'
import React, { FunctionComponent } from 'react'
import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
import { Button } from '@sourcegraph/wildcard'
export interface PolicyListActionsProps {
disabled: boolean
deleting: boolean
history: H.History
}
export const PolicyListActions: FunctionComponent<PolicyListActionsProps> = ({ disabled, deleting, history }) => (
<>
<Button
className="mt-2"
variant="primary"
onClick={() => history.push('./configuration/new')}
disabled={disabled}
>
Create new policy
</Button>
{deleting && (
<span className="ml-2 mt-2">
<LoadingSpinner className="icon-inline" /> Deleting...
</span>
)}
</>
)

View File

@ -0,0 +1,90 @@
import * as H from 'history'
import React, { FunctionComponent } from 'react'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { ThemeProps } from '@sourcegraph/shared/src/theme'
import { Container, Tab, TabList, TabPanel, TabPanels, Tabs } from '@sourcegraph/wildcard'
import { CodeIntelligenceConfigurationPolicyFields } from '../../../graphql-operations'
import {
getConfigurationForRepository as defaultGetConfigurationForRepository,
getInferredConfigurationForRepository as defaultGetInferredConfigurationForRepository,
updateConfigurationForRepository as defaultUpdateConfigurationForRepository,
} from './backend'
import { ConfigurationEditor } from './ConfigurationEditor'
import { GlobalPolicies } from './GlobalPolicies'
import { RepositoryPolicies } from './RepositoryPolicies'
export interface RepositoryConfigurationProps extends ThemeProps, TelemetryProps {
repo: { id: string }
disabled: boolean
deleting: boolean
policies?: CodeIntelligenceConfigurationPolicyFields[]
deletePolicy: (id: string, name: string) => Promise<void>
globalPolicies?: CodeIntelligenceConfigurationPolicyFields[]
deleteGlobalPolicy: (id: string, name: string) => Promise<void>
deleteError?: Error
updateConfigurationForRepository: typeof defaultUpdateConfigurationForRepository
getConfigurationForRepository: typeof defaultGetConfigurationForRepository
getInferredConfigurationForRepository: typeof defaultGetInferredConfigurationForRepository
indexingEnabled: boolean
history: H.History
}
export const RepositoryConfiguration: FunctionComponent<RepositoryConfigurationProps> = ({
repo,
disabled,
deleting,
policies,
deletePolicy,
globalPolicies,
deleteGlobalPolicy,
deleteError,
indexingEnabled,
history,
...props
}) => (
<Tabs size="medium">
<TabList>
<Tab>Repository-specific policies</Tab>
<Tab>Global policies</Tab>
{indexingEnabled && <Tab>Index configuration</Tab>}
</TabList>
<TabPanels>
<TabPanel>
<RepositoryPolicies
disabled={disabled}
deleting={deleting}
policies={policies}
deletePolicy={deletePolicy}
indexingEnabled={indexingEnabled}
history={history}
/>
</TabPanel>
<TabPanel>
<GlobalPolicies
repo={repo}
disabled={disabled}
deleting={deleting}
globalPolicies={globalPolicies}
deleteGlobalPolicy={deleteGlobalPolicy}
indexingEnabled={indexingEnabled}
history={history}
/>
</TabPanel>
{indexingEnabled && (
<TabPanel>
<Container>
<h3>Auto-indexing configuration</h3>
<ConfigurationEditor repoId={repo.id} history={history} {...props} />
</Container>
</TabPanel>
)}
</TabPanels>
</Tabs>
)

View File

@ -0,0 +1,45 @@
import * as H from 'history'
import React, { FunctionComponent } from 'react'
import { ErrorAlert } from '@sourcegraph/web/src/components/alerts'
import { Container } from '@sourcegraph/wildcard'
import { CodeIntelligenceConfigurationPolicyFields } from '../../../graphql-operations'
import { PoliciesList } from './PoliciesList'
import { PolicyListActions } from './PolicyListActions'
export interface RepositoryPoliciesProps {
disabled: boolean
deleting: boolean
policies?: CodeIntelligenceConfigurationPolicyFields[]
deletePolicy: (id: string, name: string) => Promise<void>
deleteError?: Error
indexingEnabled: boolean
history: H.History
}
export const RepositoryPolicies: FunctionComponent<RepositoryPoliciesProps> = ({
disabled,
deleting,
policies,
deletePolicy,
deleteError,
indexingEnabled,
history,
}) => (
<Container>
<h3>Repository-specific policies</h3>
{deleteError && <ErrorAlert prefix="Error deleting configuration policy" error={deleteError} />}
<PoliciesList
policies={policies}
deletePolicy={deletePolicy}
disabled={disabled}
indexingEnabled={indexingEnabled}
buttonFragment={<PolicyListActions disabled={disabled} deleting={deleting} history={history} />}
history={history}
/>
</Container>
)

View File

@ -0,0 +1,25 @@
import React, { FunctionComponent } from 'react'
import { CodeIntelligenceConfigurationPolicyFields } from '../../../graphql-operations'
import { GitObjectTargetDescription } from './GitObjectTargetDescription'
import { formatDurationValue } from './shared'
export const RetentionPolicyDescription: FunctionComponent<{ policy: CodeIntelligenceConfigurationPolicyFields }> = ({
policy,
}) =>
policy.retentionEnabled ? (
<>
<strong>Retention policy:</strong>{' '}
<span>
Retain uploads used to resolve code intelligence queries for{' '}
<GitObjectTargetDescription policy={policy} />
{policy.retentionDurationHours && (
<> for at least {formatDurationValue(policy.retentionDurationHours)} after upload</>
)}
.
</span>
</>
) : (
<span className="text-muted">Data retention disabled.</span>
)

View File

@ -0,0 +1,57 @@
import React, { FunctionComponent } from 'react'
import { Toggle } from '@sourcegraph/branded/src/components/Toggle'
import { Container } from '@sourcegraph/wildcard'
import { CodeIntelligenceConfigurationPolicyFields, GitObjectType } from '../../../graphql-operations'
import { DurationSelect } from './DurationSelect'
export interface RetentionSettingsProps {
policy: CodeIntelligenceConfigurationPolicyFields
setPolicy: (policy: CodeIntelligenceConfigurationPolicyFields) => void
}
export const RetentionSettings: FunctionComponent<RetentionSettingsProps> = ({ policy, setPolicy }) => (
<Container className="mt-2">
<h3>Retention</h3>
<div className="form-group">
<Toggle
id="retention-enabled"
title="Enabled"
value={policy.retentionEnabled}
onToggle={value => setPolicy({ ...policy, retentionEnabled: value })}
/>
<label htmlFor="retention-enabled" className="ml-2">
Enabled / disabled
</label>
</div>
<div className="form-group">
<label htmlFor="retention-duration">Duration</label>
<DurationSelect
id="retention-duration"
value={policy.retentionDurationHours ? `${policy.retentionDurationHours}` : null}
onChange={value => setPolicy({ ...policy, retentionDurationHours: value })}
disabled={!policy.retentionEnabled}
/>
</div>
{policy.type === GitObjectType.GIT_TREE && (
<div className="form-group">
<Toggle
id="retain-intermediate-commits"
title="Enabled"
value={policy.retainIntermediateCommits}
onToggle={value => setPolicy({ ...policy, retainIntermediateCommits: value })}
disabled={!policy.retentionEnabled}
/>
<label htmlFor="retain-intermediate-commits" className="ml-2">
Retain intermediate commits
</label>
</div>
)}
</Container>
)

View File

@ -9,14 +9,205 @@ import {
import { requestGraphQL } from '../../../backend/graphql'
import {
CodeIntelligenceConfigurationPoliciesResult,
CodeIntelligenceConfigurationPoliciesVariables,
CodeIntelligenceConfigurationPolicyFields,
CodeIntelligenceConfigurationPolicyResult,
CodeIntelligenceConfigurationPolicyVariables,
CreateCodeIntelligenceConfigurationPolicyResult,
CreateCodeIntelligenceConfigurationPolicyVariables,
DeleteCodeIntelligenceConfigurationPolicyResult,
DeleteCodeIntelligenceConfigurationPolicyVariables,
IndexConfigurationResult,
IndexConfigurationVariables,
InferredIndexConfigurationResult,
InferredIndexConfigurationVariables,
RepositoryBranchesFields,
RepositoryIndexConfigurationFields,
RepositoryInferredIndexConfigurationFields,
RepositoryNameFields,
RepositoryNameResult,
RepositoryNameVariables,
RepositoryTagsFields,
SearchGitBranchesResult,
SearchGitBranchesVariables,
SearchGitTagsResult,
SearchGitTagsVariables,
UpdateCodeIntelligenceConfigurationPolicyResult,
UpdateCodeIntelligenceConfigurationPolicyVariables,
UpdateRepositoryIndexConfigurationResult,
UpdateRepositoryIndexConfigurationVariables,
} from '../../../graphql-operations'
export function getConfiguration({ id }: { id: string }): Observable<RepositoryIndexConfigurationFields | null> {
const codeIntelligenceConfigurationPolicyFieldsFragment = gql`
fragment CodeIntelligenceConfigurationPolicyFields on CodeIntelligenceConfigurationPolicy {
__typename
id
name
type
pattern
retentionEnabled
retentionDurationHours
retainIntermediateCommits
indexingEnabled
indexCommitMaxAgeHours
indexIntermediateCommits
}
`
export function getPolicies(repositoryId?: string): Observable<CodeIntelligenceConfigurationPolicyFields[]> {
const query = gql`
query CodeIntelligenceConfigurationPolicies($repositoryId: ID) {
codeIntelligenceConfigurationPolicies(repository: $repositoryId) {
...CodeIntelligenceConfigurationPolicyFields
}
}
${codeIntelligenceConfigurationPolicyFieldsFragment}
`
return requestGraphQL<CodeIntelligenceConfigurationPoliciesResult, CodeIntelligenceConfigurationPoliciesVariables>(
query,
{ repositoryId: repositoryId ?? null }
).pipe(
map(dataOrThrowErrors),
map(({ codeIntelligenceConfigurationPolicies }) => codeIntelligenceConfigurationPolicies)
)
}
export function getPolicyById(id: string): Observable<CodeIntelligenceConfigurationPolicyFields | undefined> {
const query = gql`
query CodeIntelligenceConfigurationPolicy($id: ID!) {
node(id: $id) {
...CodeIntelligenceConfigurationPolicyFields
}
}
${codeIntelligenceConfigurationPolicyFieldsFragment}
`
return requestGraphQL<CodeIntelligenceConfigurationPolicyResult, CodeIntelligenceConfigurationPolicyVariables>(
query,
{ id }
).pipe(
map(dataOrThrowErrors),
map(({ node }) => {
if (!node) {
throw new Error('No such CodeIntelligenceConfigurationPolicy')
}
return node
})
)
}
export function updatePolicy(
policy: CodeIntelligenceConfigurationPolicyFields,
repositoryId?: string
): Observable<void> {
if (policy.id) {
const query = gql`
mutation UpdateCodeIntelligenceConfigurationPolicy(
$id: ID!
$name: String!
$type: GitObjectType!
$pattern: String!
$retentionEnabled: Boolean!
$retentionDurationHours: Int
$retainIntermediateCommits: Boolean!
$indexingEnabled: Boolean!
$indexCommitMaxAgeHours: Int
$indexIntermediateCommits: Boolean!
) {
updateCodeIntelligenceConfigurationPolicy(
id: $id
name: $name
type: $type
pattern: $pattern
retentionEnabled: $retentionEnabled
retentionDurationHours: $retentionDurationHours
retainIntermediateCommits: $retainIntermediateCommits
indexingEnabled: $indexingEnabled
indexCommitMaxAgeHours: $indexCommitMaxAgeHours
indexIntermediateCommits: $indexIntermediateCommits
) {
alwaysNil
}
}
`
return requestGraphQL<
UpdateCodeIntelligenceConfigurationPolicyResult,
UpdateCodeIntelligenceConfigurationPolicyVariables
>(query, { ...policy }).pipe(
map(dataOrThrowErrors),
map(() => {
// no-op
})
)
}
const query = gql`
mutation CreateCodeIntelligenceConfigurationPolicy(
$repositoryId: ID
$name: String!
$type: GitObjectType!
$pattern: String!
$retentionEnabled: Boolean!
$retentionDurationHours: Int
$retainIntermediateCommits: Boolean!
$indexingEnabled: Boolean!
$indexCommitMaxAgeHours: Int
$indexIntermediateCommits: Boolean!
) {
createCodeIntelligenceConfigurationPolicy(
repository: $repositoryId
name: $name
type: $type
pattern: $pattern
retentionEnabled: $retentionEnabled
retentionDurationHours: $retentionDurationHours
retainIntermediateCommits: $retainIntermediateCommits
indexingEnabled: $indexingEnabled
indexCommitMaxAgeHours: $indexCommitMaxAgeHours
indexIntermediateCommits: $indexIntermediateCommits
) {
id
}
}
`
return requestGraphQL<
CreateCodeIntelligenceConfigurationPolicyResult,
CreateCodeIntelligenceConfigurationPolicyVariables
>(query, { ...policy, repositoryId: repositoryId ?? null }).pipe(
map(dataOrThrowErrors),
map(() => {
// no-op
})
)
}
export function deletePolicyById(id: string): Observable<void> {
const query = gql`
mutation DeleteCodeIntelligenceConfigurationPolicy($id: ID!) {
deleteCodeIntelligenceConfigurationPolicy(policy: $id) {
alwaysNil
}
}
`
return requestGraphQL<
DeleteCodeIntelligenceConfigurationPolicyResult,
DeleteCodeIntelligenceConfigurationPolicyVariables
>(query, { id }).pipe(
map(dataOrThrowErrors),
map(() => {
// no-op
})
)
}
export function getConfigurationForRepository(id: string): Observable<RepositoryIndexConfigurationFields | null> {
const query = gql`
query IndexConfiguration($id: ID!) {
node(id: $id) {
@ -28,7 +219,6 @@ export function getConfiguration({ id }: { id: string }): Observable<RepositoryI
__typename
indexConfiguration {
configuration
inferredConfiguration
}
}
`
@ -44,7 +234,36 @@ export function getConfiguration({ id }: { id: string }): Observable<RepositoryI
)
}
export function updateConfiguration({ id, content }: { id: string; content: string }): Observable<void> {
export function getInferredConfigurationForRepository(
id: string
): Observable<RepositoryInferredIndexConfigurationFields | null> {
const query = gql`
query InferredIndexConfiguration($id: ID!) {
node(id: $id) {
...RepositoryInferredIndexConfigurationFields
}
}
fragment RepositoryInferredIndexConfigurationFields on Repository {
__typename
indexConfiguration {
inferredConfiguration
}
}
`
return requestGraphQL<InferredIndexConfigurationResult, InferredIndexConfigurationVariables>(query, { id }).pipe(
map(dataOrThrowErrors),
map(({ node }) => {
if (!node) {
throw new Error('No such Repository')
}
return node
})
)
}
export function updateConfigurationForRepository(id: string, content: string): Observable<void> {
const query = gql`
mutation UpdateRepositoryIndexConfiguration($id: ID!, $content: String!) {
updateRepositoryIndexConfiguration(repository: $id, configuration: $content) {
@ -68,3 +287,92 @@ export function updateConfiguration({ id, content }: { id: string; content: stri
})
)
}
export function searchGitTags(id: string, term: string): Observable<RepositoryTagsFields | null> {
const query = gql`
query SearchGitTags($id: ID!, $query: String!) {
node(id: $id) {
...RepositoryTagsFields
}
}
fragment RepositoryTagsFields on Repository {
__typename
name
tags(query: $query, first: 10) {
nodes {
displayName
}
totalCount
}
}
`
return requestGraphQL<SearchGitTagsResult, SearchGitTagsVariables>(query, { id, query: term }).pipe(
map(dataOrThrowErrors),
map(({ node }) => {
if (!node) {
throw new Error('No such Repository')
}
return node
})
)
}
export function searchGitBranches(id: string, term: string): Observable<RepositoryBranchesFields | null> {
const query = gql`
query SearchGitBranches($id: ID!, $query: String!) {
node(id: $id) {
...RepositoryBranchesFields
}
}
fragment RepositoryBranchesFields on Repository {
__typename
name
branches(query: $query, first: 10) {
nodes {
displayName
}
totalCount
}
}
`
return requestGraphQL<SearchGitBranchesResult, SearchGitBranchesVariables>(query, { id, query: term }).pipe(
map(dataOrThrowErrors),
map(({ node }) => {
if (!node) {
throw new Error('No such Repository')
}
return node
})
)
}
export function repoName(id: string): Observable<RepositoryNameFields | null> {
const query = gql`
query RepositoryName($id: ID!) {
node(id: $id) {
...RepositoryNameFields
}
}
fragment RepositoryNameFields on Repository {
__typename
name
}
`
return requestGraphQL<RepositoryNameResult, RepositoryNameVariables>(query, { id }).pipe(
map(dataOrThrowErrors),
map(({ node }) => {
if (!node) {
throw new Error('No such Repository')
}
return node
})
)
}

View File

@ -0,0 +1,18 @@
export const defaultDurationValues = [
{ value: null, displayText: 'Forever' },
{ value: 168, displayText: '1 week' }, // 168 hours
{ value: 672, displayText: '1 month' }, // 168 hours * 4
{ value: 2016, displayText: '3 months' }, // 168 hours * 4 * 3
{ value: 4032, displayText: '6 months' }, // 168 hours * 4 * 6
{ value: 8064, displayText: '1 year' }, // 168 hours * 4 * 12
{ value: 40320, displayText: '5 years' }, // 168 hours * 4 * 12 * 5
]
export const formatDurationValue = (value: number): string => {
const match = defaultDurationValues.find(candidate => candidate.value === value)
if (!match) {
return `${value} hours`
}
return match.displayText
}

View File

@ -0,0 +1,28 @@
.grid {
display: grid;
grid-template-columns: [info] minmax(auto, 1fr) [state] min-content [caret] min-content [end];
row-gap: 1rem;
column-gap: 1rem;
align-items: center;
@media (--sm-breakpoint-down) {
row-gap: 0.5rem;
column-gap: 0.5rem;
}
}
.separator {
// Make it full width in the current row.
grid-column: 1 / -1;
border-top: 1px solid var(--border-color-2);
@media (--xs-breakpoint-down) {
margin-top: 1rem;
padding-bottom: 1rem;
}
}
.state,
.information {
@media (--xs-breakpoint-down) {
grid-column: 1 / -1;
}
}

View File

@ -0,0 +1,54 @@
import classNames from 'classnames'
import ChevronRightIcon from 'mdi-react/ChevronRightIcon'
import React, { FunctionComponent } from 'react'
import { Link } from 'react-router-dom'
import { LsifUploadFields } from '../../../graphql-operations'
import { CodeIntelState } from '../shared/CodeIntelState'
import { CodeIntelUploadOrIndexLastActivity } from '../shared/CodeIntelUploadOrIndexLastActivity'
import styles from './CodeIntelAssociatedIndex.module.scss'
export interface CodeIntelAssociatedIndexProps {
node: LsifUploadFields
now?: () => Date
}
export const CodeIntelAssociatedIndex: FunctionComponent<CodeIntelAssociatedIndexProps> = ({ node, now }) =>
node.associatedIndex && node.projectRoot ? (
<>
<div className="list-group position-relative">
<div className={classNames(styles.grid, 'mb-3')}>
<div className={classNames(styles.information, 'd-flex flex-column')}>
<div className="m-0">
<h3 className="m-0 d-block d-md-inline">This upload was created by an auto-indexing job</h3>
</div>
<div>
<small className="text-mute">
<CodeIntelUploadOrIndexLastActivity
node={{ ...node.associatedIndex, uploadedAt: null }}
now={now}
/>
</small>
</div>
</div>
<span className={classNames(styles.state, 'd-none d-md-inline')}>
<CodeIntelState node={node.associatedIndex} className="d-flex flex-column align-items-center" />
</span>
<span>
<Link
to={`/${node.projectRoot.repository.name}/-/settings/code-intelligence/indexes/${node.associatedIndex.id}`}
>
<ChevronRightIcon />
</Link>
</span>
<span className={styles.separator} />
</div>
</div>
</>
) : (
<></>
)

View File

@ -0,0 +1,28 @@
.grid {
display: grid;
grid-template-columns: [info] minmax(auto, 1fr) [state] min-content [caret] min-content [end];
row-gap: 1rem;
column-gap: 1rem;
align-items: center;
@media (--sm-breakpoint-down) {
row-gap: 0.5rem;
column-gap: 0.5rem;
}
}
.separator {
// Make it full width in the current row.
grid-column: 1 / -1;
border-top: 1px solid var(--border-color-2);
@media (--xs-breakpoint-down) {
margin-top: 1rem;
padding-bottom: 1rem;
}
}
.state,
.information {
@media (--xs-breakpoint-down) {
grid-column: 1 / -1;
}
}

View File

@ -0,0 +1,62 @@
import classNames from 'classnames'
import ChevronRightIcon from 'mdi-react/ChevronRightIcon'
import React, { FunctionComponent } from 'react'
import { Link } from '@sourcegraph/shared/src/components/Link'
import { Timestamp } from '../../../components/time/Timestamp'
import { LsifIndexFields } from '../../../graphql-operations'
import { CodeIntelState } from '../shared/CodeIntelState'
import { CodeIntelUploadOrIndexLastActivity } from '../shared/CodeIntelUploadOrIndexLastActivity'
import styles from './CodeIntelAssociatedUpload.module.scss'
export interface CodeIntelAssociatedUploadProps {
node: LsifIndexFields
now?: () => Date
}
export const CodeIntelAssociatedUpload: FunctionComponent<CodeIntelAssociatedUploadProps> = ({ node, now }) =>
node.associatedUpload && node.projectRoot ? (
<>
<div className="list-group position-relative">
<div className={styles.grid}>
<span className={styles.separator} />
<div className={classNames(styles.information, 'd-flex flex-column')}>
<div className="m-0">
<h3 className="m-0 d-block d-md-inline">
This job uploaded an index{' '}
<Timestamp date={node.associatedUpload.uploadedAt} now={now} />
</h3>
</div>
<div>
<small className="text-mute">
<CodeIntelUploadOrIndexLastActivity
node={{ ...node.associatedUpload, queuedAt: null }}
now={now}
/>
</small>
</div>
</div>
<span className={classNames(styles.state, 'd-none d-md-inline')}>
<CodeIntelState
node={node.associatedUpload}
className="d-flex flex-column align-items-center"
/>
</span>
<span>
<Link
to={`/${node.projectRoot.repository.name}/-/settings/code-intelligence/uploads/${node.associatedUpload.id}`}
>
<ChevronRightIcon />
</Link>
</span>
</div>
</div>
</>
) : (
<></>
)

View File

@ -0,0 +1,26 @@
import DeleteIcon from 'mdi-react/DeleteIcon'
import React, { FunctionComponent } from 'react'
import { ErrorLike } from '@sourcegraph/shared/src/util/errors'
import { Button } from '@sourcegraph/wildcard'
export interface CodeIntelDeleteIndexProps {
deleteIndex: () => Promise<void>
deletionOrError?: 'loading' | 'deleted' | ErrorLike
}
export const CodeIntelDeleteIndex: FunctionComponent<CodeIntelDeleteIndexProps> = ({
deleteIndex,
deletionOrError,
}) => (
<Button
type="button"
variant="danger"
onClick={deleteIndex}
disabled={deletionOrError === 'loading'}
aria-describedby="upload-delete-button-help"
data-tooltip="Deleting this index will remove it from the index queue."
>
<DeleteIcon className="icon-inline" /> Delete index
</Button>
)

View File

@ -0,0 +1,36 @@
import DeleteIcon from 'mdi-react/DeleteIcon'
import React, { FunctionComponent } from 'react'
import { LSIFUploadState } from '@sourcegraph/shared/src/graphql-operations'
import { ErrorLike } from '@sourcegraph/shared/src/util/errors'
import { Button } from '@sourcegraph/wildcard'
export interface CodeIntelDeleteUploadProps {
state: LSIFUploadState
deleteUpload: () => Promise<void>
deletionOrError?: 'loading' | 'deleted' | ErrorLike
}
export const CodeIntelDeleteUpload: FunctionComponent<CodeIntelDeleteUploadProps> = ({
state,
deleteUpload,
deletionOrError,
}) =>
state === LSIFUploadState.DELETING ? (
<></>
) : (
<Button
type="button"
variant="danger"
onClick={deleteUpload}
disabled={deletionOrError === 'loading'}
aria-describedby="upload-delete-button-help"
data-tooltip={
state === LSIFUploadState.COMPLETED
? 'Deleting this upload will make it unavailable to answer code intelligence queries the next time the repository commit graph is refreshed.'
: 'Delete this upload immediately'
}
>
<DeleteIcon className="icon-inline" /> Delete upload
</Button>
)

View File

@ -0,0 +1,36 @@
import React, { FunctionComponent } from 'react'
import { LsifIndexFields } from '../../../graphql-operations'
import { CodeIntelUploadOrIndexCommit } from '../shared/CodeIntelUploadOrIndexCommit'
import { CodeIntelUploadOrIndexRepository } from '../shared/CodeIntelUploadOrIndexerRepository'
import { CodeIntelUploadOrIndexIndexer } from '../shared/CodeIntelUploadOrIndexIndexer'
import { CodeIntelUploadOrIndexLastActivity } from '../shared/CodeIntelUploadOrIndexLastActivity'
import { CodeIntelUploadOrIndexRoot } from '../shared/CodeIntelUploadOrIndexRoot'
export interface CodeIntelIndexMetaProps {
node: LsifIndexFields
now?: () => Date
}
export const CodeIntelIndexMeta: FunctionComponent<CodeIntelIndexMetaProps> = ({ node, now }) => (
<div className="card">
<div className="card-body">
<div className="card border-0">
<div className="card-body">
<h3 className="card-title">
<CodeIntelUploadOrIndexRepository node={node} />
</h3>
<p className="card-subtitle mb-2 text-muted">
<CodeIntelUploadOrIndexLastActivity node={{ ...node, uploadedAt: null }} now={now} />
</p>
<p className="card-text">
Directory <CodeIntelUploadOrIndexRoot node={node} /> indexed at commit{' '}
<CodeIntelUploadOrIndexCommit node={node} /> by <CodeIntelUploadOrIndexIndexer node={node} />
</p>
</div>
</div>
</div>
</div>
)

View File

@ -1,41 +0,0 @@
.codeintel-associated-upload {
&__grid {
display: grid;
grid-template-columns: [info] minmax(auto, 1fr) [state] min-content [caret] min-content [end];
row-gap: 1rem;
column-gap: 1rem;
align-items: center;
@media (--sm-breakpoint-down) {
row-gap: 0.5rem;
column-gap: 0.5rem;
}
}
&__separator {
// Make it full width in the current row.
grid-column: 1 / -1;
border-top: 1px solid var(--border-color-2);
@media (--xs-breakpoint-down) {
margin-top: 1rem;
padding-bottom: 1rem;
}
}
&__state,
&__information {
@media (--xs-breakpoint-down) {
grid-column: 1 / -1;
}
}
}
.docker-command-spec {
display: flex;
flex-direction: row;
overflow: auto;
&__header {
width: 5rem;
flex-shrink: 0;
}
}

View File

@ -1,5 +1,5 @@
import { storiesOf } from '@storybook/react'
import React, { useCallback } from 'react'
import { Meta, Story } from '@storybook/react'
import React from 'react'
import { of } from 'rxjs'
import { LSIFUploadState } from '@sourcegraph/shared/src/graphql/schema'
@ -7,7 +7,7 @@ import { LSIFUploadState } from '@sourcegraph/shared/src/graphql/schema'
import { LsifIndexFields, LSIFIndexState, LsifIndexStepsFields } from '../../../graphql-operations'
import { EnterpriseWebStory } from '../../components/EnterpriseWebStory'
import { CodeIntelIndexPage } from './CodeIntelIndexPage'
import { CodeIntelIndexPage, CodeIntelIndexPageProps } from './CodeIntelIndexPage'
const trim = (value: string) => {
const firstSignificantLine = value
@ -390,40 +390,60 @@ const indexPrototype: Omit<LsifIndexFields, 'id' | 'state' | 'queuedAt' | 'steps
const now = () => new Date('2020-06-15T19:25:00+00:00')
const { add } = storiesOf('web/codeintel/detail/CodeIntelIndexPage', module)
.addDecorator(story => <div className="p-3 container">{story()}</div>)
.addParameters({
const story: Meta = {
title: 'web/codeintel/detail/CodeIntelIndexPage',
decorators: [story => <div className="p-3 container">{story()}</div>],
parameters: {
component: CodeIntelIndexPage,
chromatic: {
viewports: [320, 576, 978, 1440],
},
})
},
}
export default story
for (const { description, index } of [
{
description: 'Queued',
index: {
const Template: Story<CodeIntelIndexPageProps> = args => (
<EnterpriseWebStory>{props => <CodeIntelIndexPage {...props} {...args} />}</EnterpriseWebStory>
)
const defaults: Partial<CodeIntelIndexPageProps> = {
now,
deleteLsifIndex: () => of(),
}
export const Queued = Template.bind({})
Queued.args = {
...defaults,
fetchLsifIndex: () =>
of({
...indexPrototype,
id: '1',
state: LSIFIndexState.QUEUED,
queuedAt: '2020-06-15T17:50:01+00:00',
placeInQueue: 1,
steps: stepsPrototype,
},
},
{
description: 'Processing',
index: {
}),
}
export const Processing = Template.bind({})
Processing.args = {
...defaults,
fetchLsifIndex: () =>
of({
...indexPrototype,
id: '1',
state: LSIFIndexState.PROCESSING,
queuedAt: '2020-06-15T17:50:01+00:00',
startedAt: '2020-06-15T17:56:01+00:00',
steps: processingSteps,
},
},
{
description: 'Completed',
index: {
}),
}
export const Completed = Template.bind({})
Completed.args = {
...defaults,
fetchLsifIndex: () =>
of({
...indexPrototype,
id: '1',
state: LSIFIndexState.COMPLETED,
@ -431,11 +451,14 @@ for (const { description, index } of [
startedAt: '2020-06-15T17:56:01+00:00',
finishedAt: '2020-06-15T18:00:10+00:00',
steps: completedSteps,
},
},
{
description: 'Errored',
index: {
}),
}
export const Errored = Template.bind({})
Errored.args = {
...defaults,
fetchLsifIndex: () =>
of({
...indexPrototype,
id: '1',
state: LSIFIndexState.ERRORED,
@ -445,11 +468,14 @@ for (const { description, index } of [
failure:
'Upload failed to complete: dial tcp: lookup gitserver-8.gitserver on 10.165.0.10:53: no such host',
steps: failedSteps,
},
},
{
description: 'Associated upload',
index: {
}),
}
export const AssociatedUpload = Template.bind({})
AssociatedUpload.args = {
...defaults,
fetchLsifIndex: () =>
of({
...indexPrototype,
id: '1',
state: LSIFIndexState.COMPLETED,
@ -465,23 +491,5 @@ for (const { description, index } of [
finishedAt: '2020-06-15T18:10:00+00:00',
placeInQueue: null,
},
},
},
]) {
add(description, () => {
const fetchLsifIndex = useCallback(() => of(index), [])
return (
<EnterpriseWebStory>
{props => (
<CodeIntelIndexPage
{...props}
fetchLsifIndex={fetchLsifIndex}
deleteLsifIndex={() => of()}
now={now}
/>
)}
</EnterpriseWebStory>
)
})
}),
}

View File

@ -1,39 +1,25 @@
import { isArray } from 'lodash'
import CheckIcon from 'mdi-react/CheckIcon'
import ChevronRightIcon from 'mdi-react/ChevronRightIcon'
import DeleteIcon from 'mdi-react/DeleteIcon'
import ErrorIcon from 'mdi-react/ErrorIcon'
import ProgressClockIcon from 'mdi-react/ProgressClockIcon'
import TimerSandIcon from 'mdi-react/TimerSandIcon'
import React, { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
import { Redirect, RouteComponentProps } from 'react-router'
import { timer } from 'rxjs'
import { catchError, concatMap, delay, repeatWhen, takeWhile } from 'rxjs/operators'
import { LoadingSpinner } from '@sourcegraph/react-loading-spinner'
import { Link } from '@sourcegraph/shared/src/components/Link'
import { LSIFIndexState } from '@sourcegraph/shared/src/graphql-operations'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { asError, ErrorLike, isErrorLike } from '@sourcegraph/shared/src/util/errors'
import { isDefined } from '@sourcegraph/shared/src/util/types'
import { useObservable } from '@sourcegraph/shared/src/util/useObservable'
import { Container, PageHeader } from '@sourcegraph/wildcard'
import { ErrorAlert } from '../../../components/alerts'
import { ExecutionLogEntry } from '../../../components/ExecutionLogEntry'
import { PageTitle } from '../../../components/PageTitle'
import { Timestamp } from '../../../components/time/Timestamp'
import { Timeline, TimelineStage } from '../../../components/Timeline'
import { LsifIndexFields } from '../../../graphql-operations'
import { CodeIntelState } from '../shared/CodeIntelState'
import { CodeIntelStateBanner } from '../shared/CodeIntelStateBanner'
import { CodeIntelUploadOrIndexCommit } from '../shared/CodeIntelUploadOrIndexCommit'
import { CodeIntelUploadOrIndexRepository } from '../shared/CodeIntelUploadOrIndexerRepository'
import { CodeIntelUploadOrIndexIndexer } from '../shared/CodeIntelUploadOrIndexIndexer'
import { CodeIntelUploadOrIndexLastActivity } from '../shared/CodeIntelUploadOrIndexLastActivity'
import { CodeIntelUploadOrIndexRoot } from '../shared/CodeIntelUploadOrIndexRoot'
import { deleteLsifIndex as defaultDeleteLsifIndex, fetchLsifIndex as defaultFetchLsifIndex } from './backend'
import { CodeIntelAssociatedUpload } from './CodeIntelAssociatedUpload'
import { CodeIntelDeleteIndex } from './CodeIntelDeleteIndex'
import { CodeIntelIndexMeta } from './CodeIntelIndexMeta'
import { CodeIntelIndexTimeline } from './CodeIntelIndexTimeline'
export interface CodeIntelIndexPageProps extends RouteComponentProps<{ id: string }>, TelemetryProps {
fetchLsifIndex?: typeof defaultFetchLsifIndex
@ -158,243 +144,3 @@ const terminalStates = new Set([LSIFIndexState.COMPLETED, LSIFIndexState.ERRORED
function shouldReload(index: LsifIndexFields | ErrorLike | null | undefined): boolean {
return !isErrorLike(index) && !(index && terminalStates.has(index.state))
}
interface CodeIntelIndexMetaProps {
node: LsifIndexFields
now?: () => Date
}
const CodeIntelIndexMeta: FunctionComponent<CodeIntelIndexMetaProps> = ({ node, now }) => (
<div className="card">
<div className="card-body">
<div className="card border-0">
<div className="card-body">
<h3 className="card-title">
<CodeIntelUploadOrIndexRepository node={node} />
</h3>
<p className="card-subtitle mb-2 text-muted">
<CodeIntelUploadOrIndexLastActivity node={{ ...node, uploadedAt: null }} now={now} />
</p>
<p className="card-text">
Directory <CodeIntelUploadOrIndexRoot node={node} /> indexed at commit{' '}
<CodeIntelUploadOrIndexCommit node={node} /> by <CodeIntelUploadOrIndexIndexer node={node} />
</p>
</div>
</div>
</div>
</div>
)
interface CodeIntelIndexTimelineProps {
index: LsifIndexFields
now?: () => Date
className?: string
}
const CodeIntelIndexTimeline: FunctionComponent<CodeIntelIndexTimelineProps> = ({ index, now, className }) => {
const stages = useMemo(
() => [
{ icon: <TimerSandIcon />, text: 'Queued', date: index.queuedAt, className: 'bg-success' },
{ icon: <CheckIcon />, text: 'Began processing', date: index.startedAt, className: 'bg-success' },
indexSetupStage(index, now),
indexPreIndexStage(index, now),
indexIndexStage(index, now),
indexUploadStage(index, now),
indexTeardownStage(index, now),
index.state === LSIFIndexState.COMPLETED
? { icon: <CheckIcon />, text: 'Finished', date: index.finishedAt, className: 'bg-success' }
: { icon: <ErrorIcon />, text: 'Failed', date: index.finishedAt, className: 'bg-danger' },
],
[index, now]
)
return <Timeline stages={stages.filter(isDefined)} now={now} className={className} />
}
const indexSetupStage = (index: LsifIndexFields, now?: () => Date): TimelineStage | undefined =>
index.steps.setup.length === 0
? undefined
: {
text: 'Setup',
details: index.steps.setup.map(logEntry => (
<ExecutionLogEntry key={logEntry.key} logEntry={logEntry} now={now} />
)),
...genericStage(index.steps.setup),
}
const indexPreIndexStage = (index: LsifIndexFields, now?: () => Date): TimelineStage | undefined => {
const logEntries = index.steps.preIndex.map(step => step.logEntry).filter(isDefined)
return logEntries.length === 0
? undefined
: {
text: 'Pre Index',
details: index.steps.preIndex.map(
step =>
step.logEntry && (
<div key={`${step.image}${step.root}${step.commands.join(' ')}}`}>
<ExecutionLogEntry logEntry={step.logEntry} now={now}>
<ExecutionMetaInformation
{...{
image: step.image,
commands: step.commands,
root: step.root,
}}
/>
</ExecutionLogEntry>
</div>
)
),
...genericStage(logEntries),
}
}
const indexIndexStage = (index: LsifIndexFields, now?: () => Date): TimelineStage | undefined =>
!index.steps.index.logEntry
? undefined
: {
text: 'Index',
details: (
<>
<ExecutionLogEntry logEntry={index.steps.index.logEntry} now={now}>
<ExecutionMetaInformation
{...{
image: index.inputIndexer,
commands: index.steps.index.indexerArgs,
root: index.inputRoot,
}}
/>
</ExecutionLogEntry>
</>
),
...genericStage(index.steps.index.logEntry),
}
const indexUploadStage = (index: LsifIndexFields, now?: () => Date): TimelineStage | undefined =>
!index.steps.upload
? undefined
: {
text: 'Upload',
details: <ExecutionLogEntry logEntry={index.steps.upload} now={now} />,
...genericStage(index.steps.upload),
}
const indexTeardownStage = (index: LsifIndexFields, now?: () => Date): TimelineStage | undefined =>
index.steps.teardown.length === 0
? undefined
: {
text: 'Teardown',
details: index.steps.teardown.map(logEntry => (
<ExecutionLogEntry key={logEntry.key} logEntry={logEntry} now={now} />
)),
...genericStage(index.steps.teardown),
}
const genericStage = <E extends { startTime: string; exitCode: number | null }>(
value: E | E[]
): Pick<TimelineStage, 'icon' | 'date' | 'className' | 'expanded'> => {
const finished = isArray(value) ? value.every(logEntry => logEntry.exitCode !== null) : value.exitCode !== null
const success = isArray(value) ? value.every(logEntry => logEntry.exitCode === 0) : value.exitCode === 0
return {
icon: !finished ? <ProgressClockIcon /> : success ? <CheckIcon /> : <ErrorIcon />,
date: isArray(value) ? value[0].startTime : value.startTime,
className: success || !finished ? 'bg-success' : 'bg-danger',
expanded: !(success || !finished),
}
}
const ExecutionMetaInformation: React.FunctionComponent<{ image: string; commands: string[]; root: string }> = ({
image,
commands,
root,
}) => (
<div className="pt-3">
<div className="docker-command-spec py-2 border-top pl-2">
<strong className="docker-command-spec__header">Image</strong>
<div>{image}</div>
</div>
<div className="docker-command-spec py-2 border-top pl-2">
<strong className="docker-command-spec__header">Commands</strong>
<div>
<code>{commands.join(' ')}</code>
</div>
</div>
<div className="docker-command-spec py-2 border-top pl-2">
<strong className="docker-command-spec__header">Root</strong>
<div>/{root}</div>
</div>
</div>
)
const CodeIntelAssociatedUpload: FunctionComponent<CodeIntelAssociatedUploadProps> = ({ node, now }) =>
node.associatedUpload && node.projectRoot ? (
<>
<div className="list-group position-relative">
<div className="codeintel-associated-upload__grid">
<span className="codeintel-associated-upload__separator" />
<div className="d-flex flex-column codeintel-associated-upload__information">
<div className="m-0">
<h3 className="m-0 d-block d-md-inline">
This job uploaded an index{' '}
<Timestamp date={node.associatedUpload.uploadedAt} now={now} />
</h3>
</div>
<div>
<small className="text-mute">
<CodeIntelUploadOrIndexLastActivity
node={{ ...node.associatedUpload, queuedAt: null }}
now={now}
/>
</small>
</div>
</div>
<span className="d-none d-md-inline codeintel-associated-upload__state">
<CodeIntelState
node={node.associatedUpload}
className="d-flex flex-column align-items-center"
/>
</span>
<span>
<Link
to={`/${node.projectRoot.repository.name}/-/settings/code-intelligence/uploads/${node.associatedUpload.id}`}
>
<ChevronRightIcon />
</Link>
</span>
</div>
</div>
</>
) : (
<></>
)
interface CodeIntelDeleteIndexProps {
deleteIndex: () => Promise<void>
deletionOrError?: 'loading' | 'deleted' | ErrorLike
}
const CodeIntelDeleteIndex: FunctionComponent<CodeIntelDeleteIndexProps> = ({ deleteIndex, deletionOrError }) => (
<button
type="button"
className="btn btn-outline-danger"
onClick={deleteIndex}
disabled={deletionOrError === 'loading'}
aria-describedby="upload-delete-button-help"
data-tooltip="Deleting this index will remove it from the index queue."
>
<DeleteIcon className="icon-inline" /> Delete index
</button>
)
interface CodeIntelAssociatedUploadProps {
node: LsifIndexFields
now?: () => Date
}

View File

@ -0,0 +1,136 @@
import { isArray } from 'lodash'
import CheckIcon from 'mdi-react/CheckIcon'
import ErrorIcon from 'mdi-react/ErrorIcon'
import ProgressClockIcon from 'mdi-react/ProgressClockIcon'
import TimerSandIcon from 'mdi-react/TimerSandIcon'
import React, { FunctionComponent, useMemo } from 'react'
import { LSIFIndexState } from '@sourcegraph/shared/src/graphql-operations'
import { isDefined } from '@sourcegraph/shared/src/util/types'
import { ExecutionLogEntry } from '../../../components/ExecutionLogEntry'
import { Timeline, TimelineStage } from '../../../components/Timeline'
import { LsifIndexFields } from '../../../graphql-operations'
import { ExecutionMetaInformation } from './ExecutionMetaInformation'
export interface CodeIntelIndexTimelineProps {
index: LsifIndexFields
now?: () => Date
className?: string
}
export const CodeIntelIndexTimeline: FunctionComponent<CodeIntelIndexTimelineProps> = ({ index, now, className }) => {
const stages = useMemo(
() => [
{ icon: <TimerSandIcon />, text: 'Queued', date: index.queuedAt, className: 'bg-success' },
{ icon: <CheckIcon />, text: 'Began processing', date: index.startedAt, className: 'bg-success' },
indexSetupStage(index, now),
indexPreIndexStage(index, now),
indexIndexStage(index, now),
indexUploadStage(index, now),
indexTeardownStage(index, now),
index.state === LSIFIndexState.COMPLETED
? { icon: <CheckIcon />, text: 'Finished', date: index.finishedAt, className: 'bg-success' }
: { icon: <ErrorIcon />, text: 'Failed', date: index.finishedAt, className: 'bg-danger' },
],
[index, now]
)
return <Timeline stages={stages.filter(isDefined)} now={now} className={className} />
}
const indexSetupStage = (index: LsifIndexFields, now?: () => Date): TimelineStage | undefined =>
index.steps.setup.length === 0
? undefined
: {
text: 'Setup',
details: index.steps.setup.map(logEntry => (
<ExecutionLogEntry key={logEntry.key} logEntry={logEntry} now={now} />
)),
...genericStage(index.steps.setup),
}
const indexPreIndexStage = (index: LsifIndexFields, now?: () => Date): TimelineStage | undefined => {
const logEntries = index.steps.preIndex.map(step => step.logEntry).filter(isDefined)
return logEntries.length === 0
? undefined
: {
text: 'Pre Index',
details: index.steps.preIndex.map(
step =>
step.logEntry && (
<div key={`${step.image}${step.root}${step.commands.join(' ')}}`}>
<ExecutionLogEntry logEntry={step.logEntry} now={now}>
<ExecutionMetaInformation
{...{
image: step.image,
commands: step.commands,
root: step.root,
}}
/>
</ExecutionLogEntry>
</div>
)
),
...genericStage(logEntries),
}
}
const indexIndexStage = (index: LsifIndexFields, now?: () => Date): TimelineStage | undefined =>
!index.steps.index.logEntry
? undefined
: {
text: 'Index',
details: (
<>
<ExecutionLogEntry logEntry={index.steps.index.logEntry} now={now}>
<ExecutionMetaInformation
{...{
image: index.inputIndexer,
commands: index.steps.index.indexerArgs,
root: index.inputRoot,
}}
/>
</ExecutionLogEntry>
</>
),
...genericStage(index.steps.index.logEntry),
}
const indexUploadStage = (index: LsifIndexFields, now?: () => Date): TimelineStage | undefined =>
!index.steps.upload
? undefined
: {
text: 'Upload',
details: <ExecutionLogEntry logEntry={index.steps.upload} now={now} />,
...genericStage(index.steps.upload),
}
const indexTeardownStage = (index: LsifIndexFields, now?: () => Date): TimelineStage | undefined =>
index.steps.teardown.length === 0
? undefined
: {
text: 'Teardown',
details: index.steps.teardown.map(logEntry => (
<ExecutionLogEntry key={logEntry.key} logEntry={logEntry} now={now} />
)),
...genericStage(index.steps.teardown),
}
const genericStage = <E extends { startTime: string; exitCode: number | null }>(
value: E | E[]
): Pick<TimelineStage, 'icon' | 'date' | 'className' | 'expanded'> => {
const finished = isArray(value) ? value.every(logEntry => logEntry.exitCode !== null) : value.exitCode !== null
const success = isArray(value) ? value.every(logEntry => logEntry.exitCode === 0) : value.exitCode === 0
return {
icon: !finished ? <ProgressClockIcon /> : success ? <CheckIcon /> : <ErrorIcon />,
date: isArray(value) ? value[0].startTime : value.startTime,
className: success || !finished ? 'bg-success' : 'bg-danger',
expanded: !(success || !finished),
}
}

View File

@ -0,0 +1,36 @@
import React, { FunctionComponent } from 'react'
import { LsifUploadFields } from '../../../graphql-operations'
import { CodeIntelUploadOrIndexCommit } from '../shared/CodeIntelUploadOrIndexCommit'
import { CodeIntelUploadOrIndexRepository } from '../shared/CodeIntelUploadOrIndexerRepository'
import { CodeIntelUploadOrIndexIndexer } from '../shared/CodeIntelUploadOrIndexIndexer'
import { CodeIntelUploadOrIndexLastActivity } from '../shared/CodeIntelUploadOrIndexLastActivity'
import { CodeIntelUploadOrIndexRoot } from '../shared/CodeIntelUploadOrIndexRoot'
export interface CodeIntelUploadMetaProps {
node: LsifUploadFields
now?: () => Date
}
export const CodeIntelUploadMeta: FunctionComponent<CodeIntelUploadMetaProps> = ({ node, now }) => (
<div className="card">
<div className="card-body">
<div className="card border-0">
<div className="card-body">
<h3 className="card-title">
<CodeIntelUploadOrIndexRepository node={node} />
</h3>
<p className="card-subtitle mb-2 text-muted">
<CodeIntelUploadOrIndexLastActivity node={{ ...node, queuedAt: null }} now={now} />
</p>
<p className="card-text">
Directory <CodeIntelUploadOrIndexRoot node={node} /> indexed at commit{' '}
<CodeIntelUploadOrIndexCommit node={node} /> by <CodeIntelUploadOrIndexIndexer node={node} />
</p>
</div>
</div>
</div>
</div>
)

View File

@ -0,0 +1,11 @@
.grid {
display: grid;
grid-template-columns: [info] minmax(auto, 1fr) [state] min-content [caret] min-content [end];
row-gap: 1rem;
column-gap: 1rem;
align-items: center;
@media (--sm-breakpoint-down) {
row-gap: 0.5rem;
column-gap: 0.5rem;
}
}

View File

@ -1,49 +0,0 @@
.codeintel-associated-index {
&__grid {
display: grid;
grid-template-columns: [info] minmax(auto, 1fr) [state] min-content [caret] min-content [end];
row-gap: 1rem;
column-gap: 1rem;
align-items: center;
@media (--sm-breakpoint-down) {
row-gap: 0.5rem;
column-gap: 0.5rem;
}
}
&__separator {
// Make it full width in the current row.
grid-column: 1 / -1;
border-top: 1px solid var(--border-color-2);
@media (--xs-breakpoint-down) {
margin-top: 1rem;
padding-bottom: 1rem;
}
}
&__state,
&__information {
@media (--xs-breakpoint-down) {
grid-column: 1 / -1;
}
}
}
.codeintel-dependency-or-dependent-node {
&__separator {
// Make it full width in the current row.
grid-column: 1 / -1;
border-top: 1px solid var(--border-color-2);
@media (--xs-breakpoint-down) {
margin-top: 1rem;
padding-bottom: 1rem;
}
}
&__state,
&__information {
@media (--xs-breakpoint-down) {
grid-column: 1 / -1;
}
}
}

View File

@ -1,5 +1,5 @@
import { storiesOf } from '@storybook/react'
import React, { useCallback } from 'react'
import { Meta, Story } from '@storybook/react'
import React from 'react'
import { of } from 'rxjs'
import { LSIFIndexState } from '@sourcegraph/shared/src/graphql/schema'
@ -7,7 +7,7 @@ import { LSIFIndexState } from '@sourcegraph/shared/src/graphql/schema'
import { LsifUploadFields, LSIFUploadState } from '../../../graphql-operations'
import { EnterpriseWebStory } from '../../components/EnterpriseWebStory'
import { CodeIntelUploadPage } from './CodeIntelUploadPage'
import { CodeIntelUploadPage, CodeIntelUploadPageProps } from './CodeIntelUploadPage'
const uploadPrototype: Omit<LsifUploadFields, 'id' | 'state' | 'uploadedAt'> = {
__typename: 'LSIFUpload',
@ -96,58 +96,104 @@ const dependencies = [
const now = () => new Date('2020-06-15T15:25:00+00:00')
const { add } = storiesOf('web/codeintel/detail/CodeIntelUploadPage', module)
.addDecorator(story => <div className="p-3 container">{story()}</div>)
.addParameters({
const story: Meta = {
title: 'web/codeintel/detail/CodeIntelUploadPage',
decorators: [story => <div className="p-3 container">{story()}</div>],
parameters: {
component: CodeIntelUploadPage,
chromatic: {
viewports: [320, 576, 978, 1440],
},
})
},
}
export default story
for (const { description, upload } of [
{
description: 'Uploading',
upload: {
const Template: Story<CodeIntelUploadPageProps> = args => (
<EnterpriseWebStory>{props => <CodeIntelUploadPage {...props} {...args} />}</EnterpriseWebStory>
)
const defaults: Partial<CodeIntelUploadPageProps> = {
now,
deleteLsifUpload: () => of(),
fetchLsifUploads: ({ dependencyOf }: { dependencyOf?: string | null }) =>
dependencyOf === undefined
? of({
nodes: dependents,
totalCount: dependents.length,
pageInfo: {
__typename: 'PageInfo',
endCursor: null,
hasNextPage: false,
},
})
: of({
nodes: dependencies,
totalCount: dependencies.length,
pageInfo: {
__typename: 'PageInfo',
endCursor: null,
hasNextPage: false,
},
}),
}
export const Uploading = Template.bind({})
Uploading.args = {
...defaults,
fetchLsifUpload: () =>
of({
...uploadPrototype,
id: '1',
state: LSIFUploadState.UPLOADING,
uploadedAt: '2020-06-15T15:25:00+00:00',
},
},
{
description: 'Queued',
upload: {
}),
}
export const Queued = Template.bind({})
Queued.args = {
...defaults,
fetchLsifUpload: () =>
of({
...uploadPrototype,
id: '1',
state: LSIFUploadState.QUEUED,
uploadedAt: '2020-06-15T12:20:30+00:00',
placeInQueue: 1,
},
},
{
description: 'Processing',
upload: {
}),
}
export const Processing = Template.bind({})
Processing.args = {
...defaults,
fetchLsifUpload: () =>
of({
...uploadPrototype,
id: '1',
state: LSIFUploadState.PROCESSING,
uploadedAt: '2020-06-15T12:20:30+00:00',
startedAt: '2020-06-15T12:25:30+00:00',
},
},
{
description: 'Completed',
upload: {
}),
}
export const Completed = Template.bind({})
Completed.args = {
...defaults,
fetchLsifUpload: () =>
of({
...uploadPrototype,
id: '1',
state: LSIFUploadState.COMPLETED,
uploadedAt: '2020-06-14T12:20:30+00:00',
startedAt: '2020-06-14T12:25:30+00:00',
finishedAt: '2020-06-14T12:30:30+00:00',
},
},
{
description: 'Errored',
upload: {
}),
}
export const Errored = Template.bind({})
Errored.args = {
...defaults,
fetchLsifUpload: () =>
of({
...uploadPrototype,
id: '1',
state: LSIFUploadState.ERRORED,
@ -156,22 +202,28 @@ for (const { description, upload } of [
finishedAt: '2020-06-13T12:30:30+00:00',
failure:
'Upload failed to complete: dial tcp: lookup gitserver-8.gitserver on 10.165.0.10:53: no such host',
},
},
{
description: 'Deleting',
upload: {
}),
}
export const Deleting = Template.bind({})
Deleting.args = {
...defaults,
fetchLsifUpload: () =>
of({
...uploadPrototype,
id: '1',
state: LSIFUploadState.DELETING,
uploadedAt: '2020-06-14T12:20:30+00:00',
startedAt: '2020-06-14T12:25:30+00:00',
finishedAt: '2020-06-14T12:30:30+00:00',
},
},
{
description: 'Failed upload',
upload: {
}),
}
export const FailedUpload = Template.bind({})
FailedUpload.args = {
...defaults,
fetchLsifUpload: () =>
of({
...uploadPrototype,
id: '1',
state: LSIFUploadState.ERRORED,
@ -179,11 +231,14 @@ for (const { description, upload } of [
startedAt: null,
finishedAt: '2020-06-13T12:20:31+00:00',
failure: 'Upload failed to complete: object store error:\n * XMinioStorageFull etc etc',
},
},
{
description: 'Associated index',
upload: {
}),
}
export const AssociatedIndex = Template.bind({})
AssociatedIndex.args = {
...defaults,
fetchLsifUpload: () =>
of({
...uploadPrototype,
id: '1',
state: LSIFUploadState.PROCESSING,
@ -197,47 +252,5 @@ for (const { description, upload } of [
finishedAt: '2020-06-15T12:25:30+00:00',
placeInQueue: null,
},
},
},
]) {
add(description, () => {
const fetchLsifUpload = useCallback(() => of(upload), [])
const fetchLsifUploads = useCallback(
({ dependencyOf }: { dependencyOf?: string | null }) =>
dependencyOf === undefined
? of({
nodes: dependents,
totalCount: dependents.length,
pageInfo: {
__typename: 'PageInfo',
endCursor: null,
hasNextPage: false,
},
})
: of({
nodes: dependencies,
totalCount: dependencies.length,
pageInfo: {
__typename: 'PageInfo',
endCursor: null,
hasNextPage: false,
},
}),
[]
)
return (
<EnterpriseWebStory>
{props => (
<CodeIntelUploadPage
{...props}
fetchLsifUpload={fetchLsifUpload}
fetchLsifUploads={fetchLsifUploads}
deleteLsifUpload={() => of()}
now={now}
/>
)}
</EnterpriseWebStory>
)
})
}),
}

View File

@ -1,14 +1,7 @@
import CheckIcon from 'mdi-react/CheckIcon'
import ChevronRightIcon from 'mdi-react/ChevronRightIcon'
import DeleteIcon from 'mdi-react/DeleteIcon'
import ErrorIcon from 'mdi-react/ErrorIcon'
import FileUploadIcon from 'mdi-react/FileUploadIcon'
import classNames from 'classnames'
import InformationOutlineIcon from 'mdi-react/InformationOutlineIcon'
import MapSearchIcon from 'mdi-react/MapSearchIcon'
import ProgressClockIcon from 'mdi-react/ProgressClockIcon'
import React, { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
import { Redirect, RouteComponentProps } from 'react-router'
import { Link } from 'react-router-dom'
import { timer } from 'rxjs'
import { catchError, concatMap, delay, repeatWhen, takeWhile } from 'rxjs/operators'
@ -21,22 +14,23 @@ import {
FilteredConnection,
FilteredConnectionQueryArguments,
} from '@sourcegraph/web/src/components/FilteredConnection'
import { Timeline, TimelineStage } from '@sourcegraph/web/src/components/Timeline'
import { Container, PageHeader } from '@sourcegraph/wildcard'
import { Button, Container, PageHeader } from '@sourcegraph/wildcard'
import { ErrorAlert } from '../../../components/alerts'
import { PageTitle } from '../../../components/PageTitle'
import { LsifUploadFields } from '../../../graphql-operations'
import { fetchLsifUploads as defaultFetchLsifUploads } from '../shared/backend'
import { CodeIntelState } from '../shared/CodeIntelState'
import { CodeIntelStateBanner } from '../shared/CodeIntelStateBanner'
import { CodeIntelUploadOrIndexCommit } from '../shared/CodeIntelUploadOrIndexCommit'
import { CodeIntelUploadOrIndexRepository } from '../shared/CodeIntelUploadOrIndexerRepository'
import { CodeIntelUploadOrIndexIndexer } from '../shared/CodeIntelUploadOrIndexIndexer'
import { CodeIntelUploadOrIndexLastActivity } from '../shared/CodeIntelUploadOrIndexLastActivity'
import { CodeIntelUploadOrIndexRoot } from '../shared/CodeIntelUploadOrIndexRoot'
import { deleteLsifUpload as defaultDeleteLsifUpload, fetchLsifUpload as defaultFetchUpload } from './backend'
import { CodeIntelAssociatedIndex } from './CodeIntelAssociatedIndex'
import { CodeIntelDeleteUpload } from './CodeIntelDeleteUpload'
import { CodeIntelUploadMeta } from './CodeIntelUploadMeta'
import styles from './CodeIntelUploadPage.module.scss'
import { CodeIntelUploadTimeline } from './CodeIntelUploadTimeline'
import { DependencyOrDependentNode } from './DependencyOrDependentNode'
import { EmptyDependencies } from './EmptyDependencies'
import { EmptyDependents } from './EmptyDependents'
export interface CodeIntelUploadPageProps extends RouteComponentProps<{ id: string }>, TelemetryProps {
fetchLsifUpload?: typeof defaultFetchUpload
@ -205,26 +199,28 @@ export const CodeIntelUploadPage: FunctionComponent<CodeIntelUploadPageProps> =
{dependencyGraphState === DependencyGraphState.ShowDependencies ? (
<h3>
Dependencies
<button
<Button
type="button"
className="btn btn-link float-right p-0 mb-2"
className="float-right p-0 mb-2"
variant="link"
onClick={() => setDependencyGraphState(DependencyGraphState.ShowDependents)}
>
Show dependents
</button>
</Button>
</h3>
) : (
<h3>
Dependents
<button
<Button
type="button"
className="btn btn-link float-right p-0 mb-2"
className="float-right p-0 mb-2"
variant="link"
onClick={() =>
setDependencyGraphState(DependencyGraphState.ShowDependencies)
}
>
Show dependencies
</button>
</Button>
</h3>
)}
</div>
@ -232,7 +228,7 @@ export const CodeIntelUploadPage: FunctionComponent<CodeIntelUploadPageProps> =
{dependencyGraphState === DependencyGraphState.ShowDependencies ? (
<FilteredConnection
listComponent="div"
listClassName="codeintel-uploads__grid mb-3"
listClassName={classNames(styles.grid, 'mb-3')}
noun="dependency"
pluralNoun="dependencies"
nodeComponent={DependencyOrDependentNode}
@ -240,12 +236,12 @@ export const CodeIntelUploadPage: FunctionComponent<CodeIntelUploadPageProps> =
history={props.history}
location={props.location}
cursorPaging={true}
emptyElement={<EmptyDependenciesElement />}
emptyElement={<EmptyDependencies />}
/>
) : (
<FilteredConnection
listComponent="div"
listClassName="codeintel-uploads__grid mb-3"
listClassName={classNames(styles.grid, 'mb-3')}
noun="dependent"
pluralNoun="dependents"
nodeComponent={DependencyOrDependentNode}
@ -253,7 +249,7 @@ export const CodeIntelUploadPage: FunctionComponent<CodeIntelUploadPageProps> =
history={props.history}
location={props.location}
cursorPaging={true}
emptyElement={<EmptyDependentsElement />}
emptyElement={<EmptyDependents />}
/>
)}
</Container>
@ -269,245 +265,3 @@ const terminalStates = new Set([LSIFUploadState.COMPLETED, LSIFUploadState.ERROR
function shouldReload(upload: LsifUploadFields | ErrorLike | null | undefined): boolean {
return !isErrorLike(upload) && !(upload && terminalStates.has(upload.state))
}
interface CodeIntelUploadMetaProps {
node: LsifUploadFields
now?: () => Date
}
const CodeIntelUploadMeta: FunctionComponent<CodeIntelUploadMetaProps> = ({ node, now }) => (
<div className="card">
<div className="card-body">
<div className="card border-0">
<div className="card-body">
<h3 className="card-title">
<CodeIntelUploadOrIndexRepository node={node} />
</h3>
<p className="card-subtitle mb-2 text-muted">
<CodeIntelUploadOrIndexLastActivity node={{ ...node, queuedAt: null }} now={now} />
</p>
<p className="card-text">
Directory <CodeIntelUploadOrIndexRoot node={node} /> indexed at commit{' '}
<CodeIntelUploadOrIndexCommit node={node} /> by <CodeIntelUploadOrIndexIndexer node={node} />
</p>
</div>
</div>
</div>
</div>
)
interface CodeIntelUploadTimelineProps {
upload: LsifUploadFields
now?: () => Date
className?: string
}
enum FailedStage {
UPLOADING,
PROCESSING,
}
const CodeIntelUploadTimeline: FunctionComponent<CodeIntelUploadTimelineProps> = ({ upload, now, className }) => {
let failedStage: FailedStage | null = null
if (upload.state === LSIFUploadState.ERRORED && upload.startedAt === null) {
failedStage = FailedStage.UPLOADING
} else if (upload.state === LSIFUploadState.ERRORED && upload.startedAt !== null) {
failedStage = FailedStage.PROCESSING
}
const stages = useMemo(
() =>
[uploadStages, processingStages, terminalStages].flatMap(stageConstructor =>
stageConstructor(upload, failedStage)
),
[upload, failedStage]
)
return <Timeline stages={stages} now={now} className={className} />
}
const uploadStages = (upload: LsifUploadFields, failedStage: FailedStage | null): TimelineStage[] => [
{
icon: <FileUploadIcon />,
text:
upload.state === LSIFUploadState.UPLOADING ||
(LSIFUploadState.ERRORED && failedStage === FailedStage.UPLOADING)
? 'Upload started'
: 'Uploaded',
date: upload.uploadedAt,
className:
upload.state === LSIFUploadState.UPLOADING
? 'bg-primary'
: upload.state === LSIFUploadState.ERRORED
? failedStage === FailedStage.UPLOADING
? 'bg-danger'
: 'bg-success'
: 'bg-success',
},
]
const processingStages = (upload: LsifUploadFields, failedStage: FailedStage | null): TimelineStage[] => [
{
icon: <ProgressClockIcon />,
text:
upload.state === LSIFUploadState.PROCESSING ||
(LSIFUploadState.ERRORED && failedStage === FailedStage.PROCESSING)
? 'Processing started'
: 'Processed',
date: upload.startedAt,
className:
upload.state === LSIFUploadState.PROCESSING
? 'bg-primary'
: upload.state === LSIFUploadState.ERRORED
? 'bg-danger'
: 'bg-success',
},
]
const terminalStages = (upload: LsifUploadFields): TimelineStage[] =>
upload.state === LSIFUploadState.COMPLETED
? [
{
icon: <CheckIcon />,
text: 'Finished',
date: upload.finishedAt,
className: 'bg-success',
},
]
: upload.state === LSIFUploadState.ERRORED
? [
{
icon: <ErrorIcon />,
text: 'Failed',
date: upload.finishedAt,
className: 'bg-danger',
},
]
: []
const CodeIntelAssociatedIndex: FunctionComponent<CodeIntelAssociatedIndexProps> = ({ node, now }) =>
node.associatedIndex && node.projectRoot ? (
<>
<div className="list-group position-relative">
<div className="codeintel-associated-index__grid mb-3">
<div className="d-flex flex-column codeintel-associated-index__information">
<div className="m-0">
<h3 className="m-0 d-block d-md-inline">This upload was created by an auto-indexing job</h3>
</div>
<div>
<small className="text-mute">
<CodeIntelUploadOrIndexLastActivity
node={{ ...node.associatedIndex, uploadedAt: null }}
now={now}
/>
</small>
</div>
</div>
<span className="d-none d-md-inline codeintel-associated-index__state">
<CodeIntelState node={node.associatedIndex} className="d-flex flex-column align-items-center" />
</span>
<span>
<Link
to={`/${node.projectRoot.repository.name}/-/settings/code-intelligence/indexes/${node.associatedIndex.id}`}
>
<ChevronRightIcon />
</Link>
</span>
<span className="codeintel-associated-index__separator" />
</div>
</div>
</>
) : (
<></>
)
interface DependencyOrDependentNodeProps {
node: LsifUploadFields
now?: () => Date
}
const DependencyOrDependentNode: FunctionComponent<DependencyOrDependentNodeProps> = ({ node }) => (
<>
<span className="codeintel-dependency-or-dependent-node__separator" />
<div className="d-flex flex-column codeintel-dependency-or-dependent-node__information">
<div className="m-0">
<h3 className="m-0 d-block d-md-inline">
<CodeIntelUploadOrIndexRepository node={node} />
</h3>
</div>
<div>
<span className="mr-2 d-block d-mdinline-block">
Directory <CodeIntelUploadOrIndexRoot node={node} /> indexed at commit{' '}
<CodeIntelUploadOrIndexCommit node={node} /> by <CodeIntelUploadOrIndexIndexer node={node} />
</span>
</div>
</div>
<span className="d-none d-md-inline codeintel-dependency-or-dependent-node__state">
<CodeIntelState node={node} className="d-flex flex-column align-items-center" />
</span>
<span>
<Link to={`./${node.id}`}>
<ChevronRightIcon />
</Link>
</span>
</>
)
const EmptyDependenciesElement: React.FunctionComponent = () => (
<p className="text-muted text-center w-100 mb-0 mt-1">
<MapSearchIcon className="mb-2" />
<br />
This upload has no dependencies.
</p>
)
const EmptyDependentsElement: React.FunctionComponent = () => (
<p className="text-muted text-center w-100 mb-0 mt-1">
<MapSearchIcon className="mb-2" />
<br />
This upload has no dependents.
</p>
)
interface CodeIntelDeleteUploadProps {
state: LSIFUploadState
deleteUpload: () => Promise<void>
deletionOrError?: 'loading' | 'deleted' | ErrorLike
}
const CodeIntelDeleteUpload: FunctionComponent<CodeIntelDeleteUploadProps> = ({
state,
deleteUpload,
deletionOrError,
}) =>
state === LSIFUploadState.DELETING ? (
<></>
) : (
<button
type="button"
className="btn btn-outline-danger"
onClick={deleteUpload}
disabled={deletionOrError === 'loading'}
aria-describedby="upload-delete-button-help"
data-tooltip={
state === LSIFUploadState.COMPLETED
? 'Deleting this upload will make it unavailable to answer code intelligence queries the next time the repository commit graph is refreshed.'
: 'Delete this upload immediately'
}
>
<DeleteIcon className="icon-inline" /> Delete upload
</button>
)
interface CodeIntelAssociatedIndexProps {
node: LsifUploadFields
now?: () => Date
}

View File

@ -0,0 +1,103 @@
import CheckIcon from 'mdi-react/CheckIcon'
import ErrorIcon from 'mdi-react/ErrorIcon'
import FileUploadIcon from 'mdi-react/FileUploadIcon'
import ProgressClockIcon from 'mdi-react/ProgressClockIcon'
import React, { FunctionComponent, useMemo } from 'react'
import { LSIFUploadState } from '@sourcegraph/shared/src/graphql-operations'
import { Timeline, TimelineStage } from '@sourcegraph/web/src/components/Timeline'
import { LsifUploadFields } from '../../../graphql-operations'
export interface CodeIntelUploadTimelineProps {
upload: LsifUploadFields
now?: () => Date
className?: string
}
enum FailedStage {
UPLOADING,
PROCESSING,
}
export const CodeIntelUploadTimeline: FunctionComponent<CodeIntelUploadTimelineProps> = ({
upload,
now,
className,
}) => {
let failedStage: FailedStage | null = null
if (upload.state === LSIFUploadState.ERRORED && upload.startedAt === null) {
failedStage = FailedStage.UPLOADING
} else if (upload.state === LSIFUploadState.ERRORED && upload.startedAt !== null) {
failedStage = FailedStage.PROCESSING
}
const stages = useMemo(
() =>
[uploadStages, processingStages, terminalStages].flatMap(stageConstructor =>
stageConstructor(upload, failedStage)
),
[upload, failedStage]
)
return <Timeline stages={stages} now={now} className={className} />
}
const uploadStages = (upload: LsifUploadFields, failedStage: FailedStage | null): TimelineStage[] => [
{
icon: <FileUploadIcon />,
text:
upload.state === LSIFUploadState.UPLOADING ||
(LSIFUploadState.ERRORED && failedStage === FailedStage.UPLOADING)
? 'Upload started'
: 'Uploaded',
date: upload.uploadedAt,
className:
upload.state === LSIFUploadState.UPLOADING
? 'bg-primary'
: upload.state === LSIFUploadState.ERRORED
? failedStage === FailedStage.UPLOADING
? 'bg-danger'
: 'bg-success'
: 'bg-success',
},
]
const processingStages = (upload: LsifUploadFields, failedStage: FailedStage | null): TimelineStage[] => [
{
icon: <ProgressClockIcon />,
text:
upload.state === LSIFUploadState.PROCESSING ||
(LSIFUploadState.ERRORED && failedStage === FailedStage.PROCESSING)
? 'Processing started'
: 'Processed',
date: upload.startedAt,
className:
upload.state === LSIFUploadState.PROCESSING
? 'bg-primary'
: upload.state === LSIFUploadState.ERRORED
? 'bg-danger'
: 'bg-success',
},
]
const terminalStages = (upload: LsifUploadFields): TimelineStage[] =>
upload.state === LSIFUploadState.COMPLETED
? [
{
icon: <CheckIcon />,
text: 'Finished',
date: upload.finishedAt,
className: 'bg-success',
},
]
: upload.state === LSIFUploadState.ERRORED
? [
{
icon: <ErrorIcon />,
text: 'Failed',
date: upload.finishedAt,
className: 'bg-danger',
},
]
: []

View File

@ -0,0 +1,16 @@
.separator {
// Make it full width in the current row.
grid-column: 1 / -1;
border-top: 1px solid var(--border-color-2);
@media (--xs-breakpoint-down) {
margin-top: 1rem;
padding-bottom: 1rem;
}
}
.state,
.information {
@media (--xs-breakpoint-down) {
grid-column: 1 / -1;
}
}

View File

@ -0,0 +1,48 @@
import classNames from 'classnames'
import ChevronRightIcon from 'mdi-react/ChevronRightIcon'
import React, { FunctionComponent } from 'react'
import { Link } from 'react-router-dom'
import { LsifUploadFields } from '../../../graphql-operations'
import { CodeIntelState } from '../shared/CodeIntelState'
import { CodeIntelUploadOrIndexCommit } from '../shared/CodeIntelUploadOrIndexCommit'
import { CodeIntelUploadOrIndexRepository } from '../shared/CodeIntelUploadOrIndexerRepository'
import { CodeIntelUploadOrIndexIndexer } from '../shared/CodeIntelUploadOrIndexIndexer'
import { CodeIntelUploadOrIndexRoot } from '../shared/CodeIntelUploadOrIndexRoot'
import styles from './DependencyOrDependentNode.module.scss'
export interface DependencyOrDependentNodeProps {
node: LsifUploadFields
now?: () => Date
}
export const DependencyOrDependentNode: FunctionComponent<DependencyOrDependentNodeProps> = ({ node }) => (
<>
<span className={styles.separator} />
<div className={classNames(styles.information, 'd-flex flex-column')}>
<div className="m-0">
<h3 className="m-0 d-block d-md-inline">
<CodeIntelUploadOrIndexRepository node={node} />
</h3>
</div>
<div>
<span className="mr-2 d-block d-mdinline-block">
Directory <CodeIntelUploadOrIndexRoot node={node} /> indexed at commit{' '}
<CodeIntelUploadOrIndexCommit node={node} /> by <CodeIntelUploadOrIndexIndexer node={node} />
</span>
</div>
</div>
<span className={classNames(styles.state, 'd-none d-md-inline')}>
<CodeIntelState node={node} className="d-flex flex-column align-items-center" />
</span>
<span>
<Link to={`./${node.id}`}>
<ChevronRightIcon />
</Link>
</span>
</>
)

View File

@ -0,0 +1,10 @@
import MapSearchIcon from 'mdi-react/MapSearchIcon'
import React from 'react'
export const EmptyDependencies: React.FunctionComponent = () => (
<p className="text-muted text-center w-100 mb-0 mt-1">
<MapSearchIcon className="mb-2" />
<br />
This upload has no dependencies.
</p>
)

View File

@ -0,0 +1,10 @@
import MapSearchIcon from 'mdi-react/MapSearchIcon'
import React from 'react'
export const EmptyDependents: React.FunctionComponent = () => (
<p className="text-muted text-center w-100 mb-0 mt-1">
<MapSearchIcon className="mb-2" />
<br />
This upload has no dependents.
</p>
)

View File

@ -0,0 +1,10 @@
.docker-command-spec {
display: flex;
flex-direction: row;
overflow: auto;
}
.header {
width: 5rem;
flex-shrink: 0;
}

View File

@ -0,0 +1,33 @@
import classNames from 'classnames'
import React from 'react'
import styles from './ExecutionMetaInformation.module.scss'
export interface ExecutionMetaInformationProps {
image: string
commands: string[]
root: string
}
export const ExecutionMetaInformation: React.FunctionComponent<ExecutionMetaInformationProps> = ({
image,
commands,
root,
}) => (
<div className="pt-3">
<div className={classNames(styles.dockerCommandSpec, 'py-2 border-top pl-2')}>
<strong className={styles.header}>Image</strong>
<div>{image}</div>
</div>
<div className={classNames(styles.dockerCommandSpec, 'py-2 border-top pl-2')}>
<strong className={styles.header}>Commands</strong>
<div>
<code>{commands.join(' ')}</code>
</div>
</div>
<div className={classNames(styles.dockerCommandSpec, 'py-2 border-top pl-2')}>
<strong className={styles.header}>Root</strong>
<div>/{root}</div>
</div>
</div>
)

View File

@ -1,5 +0,0 @@
@import './detail/CodeIntelIndexPage';
@import './detail/CodeIntelUploadPage';
@import './list/CodeIntelIndexesPage';
@import './list/CodeIntelUploadsPage';
@import './shared/CodeIntelStateLabel';

View File

@ -0,0 +1,16 @@
.separator {
// Make it full width in the current row.
grid-column: 1 / -1;
border-top: 1px solid var(--border-color-2);
@media (--xs-breakpoint-down) {
margin-top: 1rem;
padding-bottom: 1rem;
}
}
.state,
.information {
@media (--xs-breakpoint-down) {
grid-column: 1 / -1;
}
}

View File

@ -0,0 +1,54 @@
import classNames from 'classnames'
import ChevronRightIcon from 'mdi-react/ChevronRightIcon'
import React, { FunctionComponent } from 'react'
import { Link } from '@sourcegraph/shared/src/components/Link'
import { LsifIndexFields } from '../../../graphql-operations'
import { CodeIntelState } from '../shared/CodeIntelState'
import { CodeIntelUploadOrIndexCommit } from '../shared/CodeIntelUploadOrIndexCommit'
import { CodeIntelUploadOrIndexRepository } from '../shared/CodeIntelUploadOrIndexerRepository'
import { CodeIntelUploadOrIndexIndexer } from '../shared/CodeIntelUploadOrIndexIndexer'
import { CodeIntelUploadOrIndexLastActivity } from '../shared/CodeIntelUploadOrIndexLastActivity'
import { CodeIntelUploadOrIndexRoot } from '../shared/CodeIntelUploadOrIndexRoot'
import styles from './CodeIntelIndexNode.module.scss'
export interface CodeIntelIndexNodeProps {
node: LsifIndexFields
now?: () => Date
}
export const CodeIntelIndexNode: FunctionComponent<CodeIntelIndexNodeProps> = ({ node, now }) => (
<>
<span className={styles.separator} />
<div className={classNames(styles.information, 'd-flex flex-column')}>
<div className="m-0">
<h3 className="m-0 d-block d-md-inline">
<CodeIntelUploadOrIndexRepository node={node} />
</h3>
</div>
<div>
<span className="mr-2 d-block d-mdinline-block">
Directory <CodeIntelUploadOrIndexRoot node={node} /> indexed at commit{' '}
<CodeIntelUploadOrIndexCommit node={node} /> by <CodeIntelUploadOrIndexIndexer node={node} />
</span>
<small className="text-mute">
<CodeIntelUploadOrIndexLastActivity node={{ ...node, uploadedAt: null }} now={now} />
</small>
</div>
</div>
<span className={classNames(styles.state, 'd-none d-md-inline')}>
<CodeIntelState node={node} className="d-flex flex-column align-items-center" />
</span>
<span>
<Link to={`./indexes/${node.id}`}>
<ChevronRightIcon />
</Link>
</span>
</>
)

View File

@ -0,0 +1,12 @@
.grid {
display: grid;
grid-template-columns: [info] minmax(auto, 1fr) [state] min-content [caret] min-content [end];
row-gap: 1rem;
column-gap: 1rem;
align-items: center;
margin-bottom: 1rem;
@media (--sm-breakpoint-down) {
row-gap: 0.5rem;
column-gap: 0.5rem;
}
}

View File

@ -1,33 +0,0 @@
.codeintel-indexes {
&__grid {
display: grid;
grid-template-columns: [info] minmax(auto, 1fr) [state] min-content [caret] min-content [end];
row-gap: 1rem;
column-gap: 1rem;
align-items: center;
margin-bottom: 1rem;
@media (--sm-breakpoint-down) {
row-gap: 0.5rem;
column-gap: 0.5rem;
}
}
}
.codeintel-index-node {
&__separator {
// Make it full width in the current row.
grid-column: 1 / -1;
border-top: 1px solid var(--border-color-2);
@media (--xs-breakpoint-down) {
margin-top: 1rem;
padding-bottom: 1rem;
}
}
&__state,
&__information {
@media (--xs-breakpoint-down) {
grid-column: 1 / -1;
}
}
}

View File

@ -1,11 +1,11 @@
import { storiesOf } from '@storybook/react'
import React, { useCallback } from 'react'
import { Meta, Story } from '@storybook/react'
import React from 'react'
import { of } from 'rxjs'
import { ExecutionLogEntryFields, LsifIndexFields, LSIFIndexState } from '../../../graphql-operations'
import { EnterpriseWebStory } from '../../components/EnterpriseWebStory'
import { CodeIntelIndexesPage } from './CodeIntelIndexesPage'
import { CodeIntelIndexesPage, CodeIntelIndexesPageProps } from './CodeIntelIndexesPage'
const executionLogPrototype: ExecutionLogEntryFields = {
key: 'log',
@ -91,86 +91,59 @@ const testIndexes: LsifIndexFields[] = [
const now = () => new Date('2020-06-15T15:25:00+00:00')
const { add } = storiesOf('web/codeintel/list/CodeIntelIndexesPage', module)
.addDecorator(story => <div className="p-3 container">{story()}</div>)
.addParameters({
const makeResponse = (indexes: LsifIndexFields[]) => ({
nodes: indexes,
totalCount: indexes.length,
pageInfo: {
__typename: 'PageInfo',
endCursor: null,
hasNextPage: false,
},
})
const story: Meta = {
title: 'web/codeintel/list/CodeIntelIndexesPage',
decorators: [story => <div className="p-3 container">{story()}</div>],
parameters: {
component: CodeIntelIndexesPage,
chromatic: {
viewports: [320, 576, 978, 1440],
},
})
},
}
export default story
add('Empty', () => {
const fetchLsifIndexes = useCallback(
() =>
of({
nodes: [],
totalCount: 0,
pageInfo: {
__typename: 'PageInfo',
endCursor: null,
hasNextPage: false,
},
}),
[]
)
const Template: Story<CodeIntelIndexesPageProps> = args => (
<EnterpriseWebStory>{props => <CodeIntelIndexesPage {...props} {...args} />}</EnterpriseWebStory>
)
return (
<EnterpriseWebStory>
{props => <CodeIntelIndexesPage {...props} now={now} fetchLsifIndexes={fetchLsifIndexes} />}
</EnterpriseWebStory>
)
})
const defaults: Partial<CodeIntelIndexesPageProps> = {
now,
fetchLsifIndexes: () => of(makeResponse([])),
}
add('SiteAdminPage', () => {
const fetchLsifIndexes = useCallback(
() =>
of({
nodes: testIndexes,
totalCount: testIndexes.length,
pageInfo: {
__typename: 'PageInfo',
endCursor: null,
hasNextPage: false,
},
}),
[]
)
export const EmptyGlobalPage = Template.bind({})
EmptyGlobalPage.args = {
...defaults,
}
return (
<EnterpriseWebStory>
{props => <CodeIntelIndexesPage {...props} now={now} fetchLsifIndexes={fetchLsifIndexes} />}
</EnterpriseWebStory>
)
})
export const GlobalPage = Template.bind({})
GlobalPage.args = {
...defaults,
fetchLsifIndexes: () => of(makeResponse(testIndexes)),
}
add('RepositoryPage', () => {
const fetchLsifIndexes = useCallback(
() =>
of({
nodes: testIndexes,
totalCount: testIndexes.length,
pageInfo: {
__typename: 'PageInfo',
endCursor: null,
hasNextPage: false,
},
}),
[]
)
export const EmptyRepositoryPage = Template.bind({})
EmptyRepositoryPage.args = {
...defaults,
repo: { id: 'sourcegraph' },
enqueueIndexJob: () => of([]),
}
const enqueueIndexJob = useCallback(() => of([]), [])
return (
<EnterpriseWebStory>
{props => (
<CodeIntelIndexesPage
{...props}
repo={{ id: 'sourcegraph' }}
now={now}
fetchLsifIndexes={fetchLsifIndexes}
enqueueIndexJob={enqueueIndexJob}
/>
)}
</EnterpriseWebStory>
)
})
export const RepositoryPage = Template.bind({})
RepositoryPage.args = {
...defaults,
repo: { id: 'sourcegraph' },
enqueueIndexJob: () => of([]),
fetchLsifIndexes: () => of(makeResponse(testIndexes)),
}

View File

@ -1,11 +1,9 @@
import ChevronRightIcon from 'mdi-react/ChevronRightIcon'
import React, { FunctionComponent, useCallback, useEffect, useMemo, useState } from 'react'
import classNames from 'classnames'
import React, { FunctionComponent, useCallback, useEffect, useMemo } from 'react'
import { RouteComponentProps } from 'react-router'
import { Subject } from 'rxjs'
import { Link } from '@sourcegraph/shared/src/components/Link'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { ErrorAlert } from '@sourcegraph/web/src/components/alerts'
import { Container, PageHeader } from '@sourcegraph/wildcard'
import {
@ -15,14 +13,11 @@ import {
} from '../../../components/FilteredConnection'
import { PageTitle } from '../../../components/PageTitle'
import { LsifIndexFields, LSIFIndexState } from '../../../graphql-operations'
import { CodeIntelState } from '../shared/CodeIntelState'
import { CodeIntelUploadOrIndexCommit } from '../shared/CodeIntelUploadOrIndexCommit'
import { CodeIntelUploadOrIndexRepository } from '../shared/CodeIntelUploadOrIndexerRepository'
import { CodeIntelUploadOrIndexIndexer } from '../shared/CodeIntelUploadOrIndexIndexer'
import { CodeIntelUploadOrIndexLastActivity } from '../shared/CodeIntelUploadOrIndexLastActivity'
import { CodeIntelUploadOrIndexRoot } from '../shared/CodeIntelUploadOrIndexRoot'
import { enqueueIndexJob as defaultEnqueueIndexJob, fetchLsifIndexes as defaultFetchLsifIndexes } from './backend'
import styles from './CodeIntelIndexesPage.module.scss'
import { CodeIntelIndexNode, CodeIntelIndexNodeProps } from './CodeIntelIndexNode'
import { EnqueueForm } from './EnqueueForm'
export interface CodeIntelIndexesPageProps extends RouteComponentProps<{}>, TelemetryProps {
repo?: { id: string }
@ -91,7 +86,12 @@ export const CodeIntelIndexesPage: FunctionComponent<CodeIntelIndexesPageProps>
return (
<div className="code-intel-indexes">
<PageTitle title="Auto-indexing jobs" />
<PageHeader headingElement="h2" path={[{ text: 'Auto-indexing jobs' }]} className="mb-3" />
<PageHeader
headingElement="h2"
path={[{ text: 'Auto-indexing jobs' }]}
description={`Auto-indexing jobs ${repo ? 'for this repository' : 'over all repositories'}.`}
className="mb-3"
/>
{repo && (
<Container className="mb-2">
@ -103,7 +103,7 @@ export const CodeIntelIndexesPage: FunctionComponent<CodeIntelIndexesPageProps>
<div className="list-group position-relative">
<FilteredConnection<LsifIndexFields, Omit<CodeIntelIndexNodeProps, 'node'>>
listComponent="div"
listClassName="codeintel-indexes__grid mb-3"
listClassName={classNames(styles.grid, 'mb-3')}
noun="index"
pluralNoun="indexes"
querySubject={querySubject}
@ -120,112 +120,3 @@ export const CodeIntelIndexesPage: FunctionComponent<CodeIntelIndexesPageProps>
</div>
)
}
interface CodeIntelIndexNodeProps {
node: LsifIndexFields
now?: () => Date
}
const CodeIntelIndexNode: FunctionComponent<CodeIntelIndexNodeProps> = ({ node, now }) => (
<>
<span className="codeintel-index-node__separator" />
<div className="d-flex flex-column codeintel-index-node__information">
<div className="m-0">
<h3 className="m-0 d-block d-md-inline">
<CodeIntelUploadOrIndexRepository node={node} />
</h3>
</div>
<div>
<span className="mr-2 d-block d-mdinline-block">
Directory <CodeIntelUploadOrIndexRoot node={node} /> indexed at commit{' '}
<CodeIntelUploadOrIndexCommit node={node} /> by <CodeIntelUploadOrIndexIndexer node={node} />
</span>
<small className="text-mute">
<CodeIntelUploadOrIndexLastActivity node={{ ...node, uploadedAt: null }} now={now} />
</small>
</div>
</div>
<span className="d-none d-md-inline codeintel-index-node__state">
<CodeIntelState node={node} className="d-flex flex-column align-items-center" />
</span>
<span>
<Link to={`./indexes/${node.id}`}>
<ChevronRightIcon />
</Link>
</span>
</>
)
enum State {
Idle,
Queueing,
Queued,
}
interface EnqueueFormProps {
repoId: string
querySubject: Subject<string>
enqueueIndexJob: typeof defaultEnqueueIndexJob
}
const EnqueueForm: FunctionComponent<EnqueueFormProps> = ({ repoId, querySubject, enqueueIndexJob }) => {
const [revlike, setRevlike] = useState('HEAD')
const [state, setState] = useState(() => State.Idle)
const [queueResult, setQueueResult] = useState<number>()
const [enqueueError, setEnqueueError] = useState<Error>()
const enqueue = useCallback(async () => {
setState(State.Queueing)
setEnqueueError(undefined)
setQueueResult(undefined)
try {
const indexes = await enqueueIndexJob(repoId, revlike).toPromise()
setQueueResult(indexes.length)
if (indexes.length > 0) {
querySubject.next(indexes[0].inputCommit)
}
} catch (error) {
setEnqueueError(error)
setQueueResult(undefined)
} finally {
setState(State.Queued)
}
}, [repoId, revlike, querySubject, enqueueIndexJob])
return (
<>
{enqueueError && <ErrorAlert prefix="Error enqueueing index job" error={enqueueError} />}
<div className="form-inline">
<label htmlFor="revlike">Git revlike</label>
<input
type="text"
id="revlike"
className="form-control ml-2"
value={revlike}
onChange={event => setRevlike(event.target.value)}
/>
<button
type="button"
title="Enqueue thing"
disabled={state === State.Queueing}
className="btn btn-primary ml-2"
onClick={enqueue}
>
Enqueue
</button>
</div>
{state === State.Queued && queueResult !== undefined && (
<div className="alert alert-success mt-3 mb-0">{queueResult} index jobs enqueued.</div>
)}
</>
)
}

View File

@ -0,0 +1,16 @@
.separator {
// Make it full width in the current row.
grid-column: 1 / -1;
border-top: 1px solid var(--border-color-2);
@media (--xs-breakpoint-down) {
margin-top: 1rem;
padding-bottom: 1rem;
}
}
.state,
.information {
@media (--xs-breakpoint-down) {
grid-column: 1 / -1;
}
}

View File

@ -0,0 +1,54 @@
import classNames from 'classnames'
import ChevronRightIcon from 'mdi-react/ChevronRightIcon'
import React, { FunctionComponent } from 'react'
import { Link } from '@sourcegraph/shared/src/components/Link'
import { LsifUploadFields } from '../../../graphql-operations'
import { CodeIntelState } from '../shared/CodeIntelState'
import { CodeIntelUploadOrIndexCommit } from '../shared/CodeIntelUploadOrIndexCommit'
import { CodeIntelUploadOrIndexRepository } from '../shared/CodeIntelUploadOrIndexerRepository'
import { CodeIntelUploadOrIndexIndexer } from '../shared/CodeIntelUploadOrIndexIndexer'
import { CodeIntelUploadOrIndexLastActivity } from '../shared/CodeIntelUploadOrIndexLastActivity'
import { CodeIntelUploadOrIndexRoot } from '../shared/CodeIntelUploadOrIndexRoot'
import styles from './CodeIntelUploadNode.module.scss'
export interface CodeIntelUploadNodeProps {
node: LsifUploadFields
now?: () => Date
}
export const CodeIntelUploadNode: FunctionComponent<CodeIntelUploadNodeProps> = ({ node, now }) => (
<>
<span className={styles.separator} />
<div className={classNames(styles.information, 'd-flex flex-column')}>
<div className="m-0">
<h3 className="m-0 d-block d-md-inline">
<CodeIntelUploadOrIndexRepository node={node} />
</h3>
</div>
<div>
<span className="mr-2 d-block d-mdinline-block">
Directory <CodeIntelUploadOrIndexRoot node={node} /> indexed at commit{' '}
<CodeIntelUploadOrIndexCommit node={node} /> by <CodeIntelUploadOrIndexIndexer node={node} />
</span>
<small className="text-mute">
<CodeIntelUploadOrIndexLastActivity node={{ ...node, queuedAt: null }} now={now} />
</small>
</div>
</div>
<span className={classNames(styles.state, 'd-none d-md-inline')}>
<CodeIntelState node={node} className="d-flex flex-column align-items-center" />
</span>
<span>
<Link to={`./uploads/${node.id}`}>
<ChevronRightIcon />
</Link>
</span>
</>
)

View File

@ -0,0 +1,12 @@
.grid {
display: grid;
grid-template-columns: [info] minmax(auto, 1fr) [state] min-content [caret] min-content [end];
row-gap: 1rem;
column-gap: 1rem;
align-items: center;
margin-bottom: 1rem;
@media (--sm-breakpoint-down) {
row-gap: 0.5rem;
column-gap: 0.5rem;
}
}

View File

@ -1,33 +0,0 @@
.codeintel-uploads {
&__grid {
display: grid;
grid-template-columns: [info] minmax(auto, 1fr) [state] min-content [caret] min-content [end];
row-gap: 1rem;
column-gap: 1rem;
align-items: center;
margin-bottom: 1rem;
@media (--sm-breakpoint-down) {
row-gap: 0.5rem;
column-gap: 0.5rem;
}
}
}
.codeintel-upload-node {
&__separator {
// Make it full width in the current row.
grid-column: 1 / -1;
border-top: 1px solid var(--border-color-2);
@media (--xs-breakpoint-down) {
margin-top: 1rem;
padding-bottom: 1rem;
}
}
&__state,
&__information {
@media (--xs-breakpoint-down) {
grid-column: 1 / -1;
}
}
}

View File

@ -1,11 +1,12 @@
import { storiesOf } from '@storybook/react'
import React, { useCallback } from 'react'
import { boolean } from '@storybook/addon-knobs'
import { Meta, Story } from '@storybook/react'
import React from 'react'
import { of } from 'rxjs'
import { LsifUploadFields, LSIFUploadState } from '../../../graphql-operations'
import { EnterpriseWebStory } from '../../components/EnterpriseWebStory'
import { CodeIntelUploadsPage } from './CodeIntelUploadsPage'
import { CodeIntelUploadsPage, CodeIntelUploadsPageProps } from './CodeIntelUploadsPage'
const uploadPrototype: Omit<LsifUploadFields, 'id' | 'state' | 'uploadedAt'> = {
__typename: 'LSIFUpload',
@ -83,91 +84,67 @@ const testUploads: LsifUploadFields[] = [
const now = () => new Date('2020-06-15T15:25:00+00:00')
const { add } = storiesOf('web/codeintel/list/CodeIntelUploadPage', module)
.addDecorator(story => <div className="p-3 container">{story()}</div>)
.addParameters({
const makeResponse = (uploads: LsifUploadFields[]) => ({
nodes: uploads,
totalCount: uploads.length,
pageInfo: {
__typename: 'PageInfo',
endCursor: null,
hasNextPage: false,
},
})
const story: Meta = {
title: 'web/codeintel/list/CodeIntelUploadPage',
decorators: [story => <div className="p-3 container">{story()}</div>],
parameters: {
component: CodeIntelUploadsPage,
chromatic: {
viewports: [320, 576, 978, 1440],
},
})
add('Empty', () => {
const fetchLsifUploads = useCallback(
() =>
of({
nodes: [],
totalCount: 0,
pageInfo: {
__typename: 'PageInfo',
endCursor: null,
hasNextPage: false,
},
}),
[]
)
return (
<EnterpriseWebStory>
{props => <CodeIntelUploadsPage {...props} now={now} fetchLsifUploads={fetchLsifUploads} />}
</EnterpriseWebStory>
)
})
add('SiteAdminPage', () => {
const fetchLsifUploads = useCallback(
() =>
of({
nodes: testUploads,
totalCount: testUploads.length,
pageInfo: {
__typename: 'PageInfo',
endCursor: null,
hasNextPage: false,
},
}),
[]
)
return (
<EnterpriseWebStory>
{props => <CodeIntelUploadsPage {...props} now={now} fetchLsifUploads={fetchLsifUploads} />}
</EnterpriseWebStory>
)
})
for (const { fresh, updated } of [
{ fresh: true, updated: true },
{ fresh: true, updated: false },
{ fresh: false, updated: true },
{ fresh: false, updated: false },
]) {
add(`${fresh ? 'Fresh' : 'Stale'}${updated ? '' : 'Unupdated'}RepositoryPage`, () => {
const fetchLsifUploads = useCallback(
() =>
of({
nodes: testUploads,
totalCount: testUploads.length,
pageInfo: {
__typename: 'PageInfo',
endCursor: null,
hasNextPage: false,
},
}),
[]
)
return (
<EnterpriseWebStory>
{props => (
<CodeIntelUploadsPage
{...props}
repo={{ id: 'sourcegraph' }}
now={now}
fetchLsifUploads={fetchLsifUploads}
fetchCommitGraphMetadata={() => of({ stale: !fresh, updatedAt: updated ? null : now() })}
/>
)}
</EnterpriseWebStory>
)
})
},
}
export default story
const Template: Story<CodeIntelUploadsPageProps> = args => {
const fetchCommitGraphMetadata = () =>
of({
stale: boolean('staleCommitGraph', false),
updatedAt: boolean('previouslyUpdatedCommitGraph', true) ? now() : null,
})
return (
<EnterpriseWebStory>
{props => <CodeIntelUploadsPage {...props} fetchCommitGraphMetadata={fetchCommitGraphMetadata} {...args} />}
</EnterpriseWebStory>
)
}
const defaults: Partial<CodeIntelUploadsPageProps> = {
now,
fetchLsifUploads: () => of(makeResponse([])),
}
export const EmptyGlobalPage = Template.bind({})
EmptyGlobalPage.args = {
...defaults,
}
export const GlobalPage = Template.bind({})
GlobalPage.args = {
...defaults,
fetchLsifUploads: () => of(makeResponse(testUploads)),
}
export const EmptyRepositoryPage = Template.bind({})
EmptyRepositoryPage.args = {
...defaults,
repo: { id: 'sourcegraph' },
}
export const RepositoryPage = Template.bind({})
RepositoryPage.args = {
...defaults,
repo: { id: 'sourcegraph' },
fetchLsifUploads: () => of(makeResponse(testUploads)),
}

View File

@ -1,14 +1,10 @@
import classNames from 'classnames'
import ChevronRightIcon from 'mdi-react/ChevronRightIcon'
import MapSearchIcon from 'mdi-react/MapSearchIcon'
import React, { FunctionComponent, useCallback, useEffect, useMemo } from 'react'
import { RouteComponentProps } from 'react-router'
import { of } from 'rxjs'
import { Link } from '@sourcegraph/shared/src/components/Link'
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
import { useObservable } from '@sourcegraph/shared/src/util/useObservable'
import { Timestamp } from '@sourcegraph/web/src/components/time/Timestamp'
import { Container, PageHeader } from '@sourcegraph/wildcard'
import {
@ -19,14 +15,12 @@ import {
import { PageTitle } from '../../../components/PageTitle'
import { LsifUploadFields, LSIFUploadState } from '../../../graphql-operations'
import { fetchLsifUploads as defaultFetchLsifUploads } from '../shared/backend'
import { CodeIntelState } from '../shared/CodeIntelState'
import { CodeIntelUploadOrIndexCommit } from '../shared/CodeIntelUploadOrIndexCommit'
import { CodeIntelUploadOrIndexRepository } from '../shared/CodeIntelUploadOrIndexerRepository'
import { CodeIntelUploadOrIndexIndexer } from '../shared/CodeIntelUploadOrIndexIndexer'
import { CodeIntelUploadOrIndexLastActivity } from '../shared/CodeIntelUploadOrIndexLastActivity'
import { CodeIntelUploadOrIndexRoot } from '../shared/CodeIntelUploadOrIndexRoot'
import { fetchCommitGraphMetadata as defaultFetchCommitGraphMetadata } from './backend'
import { CodeIntelUploadNode, CodeIntelUploadNodeProps } from './CodeIntelUploadNode'
import styles from './CodeIntelUploadsPage.module.scss'
import { CommitGraphMetadata } from './CommitGraphMetadata'
import { EmptyUploads } from './EmptyUploads'
export interface CodeIntelUploadsPageProps extends RouteComponentProps<{}>, TelemetryProps {
repo?: { id: string }
@ -118,7 +112,14 @@ export const CodeIntelUploadsPage: FunctionComponent<CodeIntelUploadsPageProps>
return (
<div className="code-intel-uploads">
<PageTitle title="Precise code intelligence uploads" />
<PageHeader headingElement="h2" path={[{ text: 'Precise code intelligence uploads' }]} className="mb-3" />
<PageHeader
headingElement="h2"
path={[{ text: 'Precise code intelligence uploads' }]}
description={`LSIF indexes uploaded to Sourcegraph from CI or from auto-indexing ${
repo ? 'for this repository' : 'over all repositories'
}.`}
className="mb-3"
/>
{repo && commitGraphMetadata && (
<Container className="mb-2">
@ -135,7 +136,7 @@ export const CodeIntelUploadsPage: FunctionComponent<CodeIntelUploadsPageProps>
<div className="list-group position-relative">
<FilteredConnection<LsifUploadFields, Omit<CodeIntelUploadNodeProps, 'node'>>
listComponent="div"
listClassName="codeintel-uploads__grid mb-3"
listClassName={classNames(styles.grid, 'mb-3')}
noun="upload"
pluralNoun="uploads"
nodeComponent={CodeIntelUploadNode}
@ -145,101 +146,10 @@ export const CodeIntelUploadsPage: FunctionComponent<CodeIntelUploadsPageProps>
location={props.location}
cursorPaging={true}
filters={filters}
emptyElement={<EmptyLSIFUploadsElement />}
emptyElement={<EmptyUploads />}
/>
</div>
</Container>
</div>
)
}
const EmptyLSIFUploadsElement: React.FunctionComponent = () => (
<p className="text-muted text-center w-100 mb-0 mt-1">
<MapSearchIcon className="mb-2" />
<br />
No uploads yet. Enable precise code intelligence by{' '}
<a
href="https://docs.sourcegraph.com/code_intelligence/explanations/precise_code_intelligence"
target="_blank"
rel="noreferrer noopener"
>
uploading LSIF data
</a>
.
</p>
)
interface CodeIntelUploadNodeProps {
node: LsifUploadFields
now?: () => Date
}
const CodeIntelUploadNode: FunctionComponent<CodeIntelUploadNodeProps> = ({ node, now }) => (
<>
<span className="codeintel-upload-node__separator" />
<div className="d-flex flex-column codeintel-upload-node__information">
<div className="m-0">
<h3 className="m-0 d-block d-md-inline">
<CodeIntelUploadOrIndexRepository node={node} />
</h3>
</div>
<div>
<span className="mr-2 d-block d-mdinline-block">
Directory <CodeIntelUploadOrIndexRoot node={node} /> indexed at commit{' '}
<CodeIntelUploadOrIndexCommit node={node} /> by <CodeIntelUploadOrIndexIndexer node={node} />
</span>
<small className="text-mute">
<CodeIntelUploadOrIndexLastActivity node={{ ...node, queuedAt: null }} now={now} />
</small>
</div>
</div>
<span className="d-none d-md-inline codeintel-upload-node__state">
<CodeIntelState node={node} className="d-flex flex-column align-items-center" />
</span>
<span>
<Link to={`./uploads/${node.id}`}>
<ChevronRightIcon />
</Link>
</span>
</>
)
interface CommitGraphMetadataProps {
stale: boolean
updatedAt: Date | null
className?: string
now?: () => Date
}
const CommitGraphMetadata: FunctionComponent<CommitGraphMetadataProps> = ({ stale, updatedAt, className, now }) => (
<>
<div className={classNames('alert', stale ? 'alert-primary' : 'alert-success', className)}>
{stale ? <StaleRepository /> : <FreshRepository />}{' '}
{updatedAt && <LastUpdated updatedAt={updatedAt} now={now} />}
</div>
</>
)
const FreshRepository: FunctionComponent<{}> = () => <>Repository commit graph is currently up to date.</>
const StaleRepository: FunctionComponent<{}> = () => (
<>
Repository commit graph is currently stale and is queued to be refreshed. Refreshing the commit graph updates
which uploads are visible from which commits.
</>
)
interface LastUpdatedProps {
updatedAt: Date
now?: () => Date
}
const LastUpdated: FunctionComponent<LastUpdatedProps> = ({ updatedAt, now }) => (
<>
Last refreshed <Timestamp date={updatedAt} now={now} />.
</>
)

View File

@ -0,0 +1,45 @@
import classNames from 'classnames'
import React, { FunctionComponent } from 'react'
import { Timestamp } from '@sourcegraph/web/src/components/time/Timestamp'
export interface CommitGraphMetadataProps {
stale: boolean
updatedAt: Date | null
className?: string
now?: () => Date
}
export const CommitGraphMetadata: FunctionComponent<CommitGraphMetadataProps> = ({
stale,
updatedAt,
className,
now,
}) => (
<>
<div className={classNames('alert', stale ? 'alert-primary' : 'alert-success', className)}>
{stale ? <StaleRepository /> : <FreshRepository />}{' '}
{updatedAt && <LastUpdated updatedAt={updatedAt} now={now} />}
</div>
</>
)
const FreshRepository: FunctionComponent<{}> = () => <>Repository commit graph is currently up to date.</>
const StaleRepository: FunctionComponent<{}> = () => (
<>
Repository commit graph is currently stale and is queued to be refreshed. Refreshing the commit graph updates
which uploads are visible from which commits.
</>
)
interface LastUpdatedProps {
updatedAt: Date
now?: () => Date
}
const LastUpdated: FunctionComponent<LastUpdatedProps> = ({ updatedAt, now }) => (
<>
Last refreshed <Timestamp date={updatedAt} now={now} />.
</>
)

View File

@ -0,0 +1,18 @@
import MapSearchIcon from 'mdi-react/MapSearchIcon'
import React from 'react'
export const EmptyUploads: React.FunctionComponent = () => (
<p className="text-muted text-center w-100 mb-0 mt-1">
<MapSearchIcon className="mb-2" />
<br />
No uploads yet. Enable precise code intelligence by{' '}
<a
href="https://docs.sourcegraph.com/code_intelligence/explanations/precise_code_intelligence"
target="_blank"
rel="noreferrer noopener"
>
uploading LSIF data
</a>
.
</p>
)

View File

@ -0,0 +1,78 @@
import React, { FunctionComponent, useCallback, useState } from 'react'
import { Subject } from 'rxjs'
import { ErrorAlert } from '@sourcegraph/web/src/components/alerts'
import { Button } from '@sourcegraph/wildcard'
import { enqueueIndexJob as defaultEnqueueIndexJob } from './backend'
export interface EnqueueFormProps {
repoId: string
querySubject: Subject<string>
enqueueIndexJob: typeof defaultEnqueueIndexJob
}
enum State {
Idle,
Queueing,
Queued,
}
export const EnqueueForm: FunctionComponent<EnqueueFormProps> = ({ repoId, querySubject, enqueueIndexJob }) => {
const [revlike, setRevlike] = useState('HEAD')
const [state, setState] = useState(() => State.Idle)
const [queueResult, setQueueResult] = useState<number>()
const [enqueueError, setEnqueueError] = useState<Error>()
const enqueue = useCallback(async () => {
setState(State.Queueing)
setEnqueueError(undefined)
setQueueResult(undefined)
try {
const indexes = await enqueueIndexJob(repoId, revlike).toPromise()
setQueueResult(indexes.length)
if (indexes.length > 0) {
querySubject.next(indexes[0].inputCommit)
}
} catch (error) {
setEnqueueError(error)
setQueueResult(undefined)
} finally {
setState(State.Queued)
}
}, [repoId, revlike, querySubject, enqueueIndexJob])
return (
<>
{enqueueError && <ErrorAlert prefix="Error enqueueing index job" error={enqueueError} />}
<div className="form-inline">
<label htmlFor="revlike">Git revlike</label>
<input
type="text"
id="revlike"
className="form-control ml-2"
value={revlike}
onChange={event => setRevlike(event.target.value)}
/>
<Button
type="button"
title="Enqueue thing"
disabled={state === State.Queueing}
className="ml-2"
variant="primary"
onClick={enqueue}
>
Enqueue
</Button>
</div>
{state === State.Queued && queueResult !== undefined && (
<div className="alert alert-success mt-3 mb-0">{queueResult} index jobs enqueued.</div>
)}
</>
)
}

View File

@ -10,7 +10,8 @@ import { BreadcrumbSetters } from '../../../components/Breadcrumbs'
import { RepositoryFields } from '../../../graphql-operations'
import { RouteDescriptor } from '../../../util/contributions'
import { lazyComponent } from '../../../util/lazyComponent'
import { CodeIntelIndexConfigurationPageProps } from '../configuration/CodeIntelIndexConfigurationPage'
import { CodeIntelConfigurationPageProps } from '../configuration/CodeIntelConfigurationPage'
import { CodeIntelConfigurationPolicyPageProps } from '../configuration/CodeIntelConfigurationPolicyPage'
import { CodeIntelIndexPageProps } from '../detail/CodeIntelIndexPage'
import { CodeIntelUploadPageProps } from '../detail/CodeIntelUploadPage'
import { CodeIntelIndexesPageProps } from '../list/CodeIntelIndexesPage'
@ -42,10 +43,15 @@ const CodeIntelIndexPage = lazyComponent<CodeIntelIndexPageProps, 'CodeIntelInde
'CodeIntelIndexPage'
)
const CodeIntelIndexConfigurationPage = lazyComponent<
CodeIntelIndexConfigurationPageProps,
'CodeIntelIndexConfigurationPage'
>(() => import('../../codeintel/configuration/CodeIntelIndexConfigurationPage'), 'CodeIntelIndexConfigurationPage')
const CodeIntelConfigurationPage = lazyComponent<CodeIntelConfigurationPageProps, 'CodeIntelConfigurationPage'>(
() => import('../../codeintel/configuration/CodeIntelConfigurationPage'),
'CodeIntelConfigurationPage'
)
const CodeIntelConfigurationPolicyPage = lazyComponent<
CodeIntelConfigurationPolicyPageProps,
'CodeIntelConfigurationPolicyPage'
>(() => import('../../codeintel/configuration/CodeIntelConfigurationPolicyPage'), 'CodeIntelConfigurationPolicyPage')
export const routes: readonly CodeIntelAreaRoute[] = [
{
@ -76,10 +82,14 @@ export const routes: readonly CodeIntelAreaRoute[] = [
condition: () => Boolean(window.context?.codeIntelAutoIndexingEnabled),
},
{
path: '/index-configuration',
path: '/configuration',
exact: true,
render: props => <CodeIntelIndexConfigurationPage {...props} />,
condition: () => Boolean(window.context?.codeIntelAutoIndexingEnabled),
render: props => <CodeIntelConfigurationPage {...props} />,
},
{
path: '/configuration/:id',
exact: true,
render: props => <CodeIntelConfigurationPolicyPage {...props} />,
},
]
@ -105,24 +115,19 @@ export interface RepositoryCodeIntelAreaPageProps
const sidebarRoutes: CodeIntelSideBarGroups = [
{
header: { label: 'Precise intelligence' },
header: { label: 'Code intelligence' },
items: [
{
to: '/uploads',
label: 'Uploads',
},
],
},
{
header: { label: 'Auto-indexing' },
condition: () => Boolean(window.context?.codeIntelAutoIndexingEnabled),
items: [
{
to: '/indexes',
label: 'Index jobs',
label: 'Auto indexing',
condition: () => Boolean(window.context?.codeIntelAutoIndexingEnabled),
},
{
to: '/index-configuration',
to: '/configuration',
label: 'Configuration',
},
],

View File

@ -0,0 +1,7 @@
.label {
text-align: center;
}
.block {
display: block;
}

View File

@ -1,9 +0,0 @@
.codeintel-state {
&__label {
text-align: center;
&--block {
display: block;
}
}
}

View File

@ -3,13 +3,15 @@ import React, { FunctionComponent } from 'react'
import { LSIFIndexState, LSIFUploadState } from '../../../graphql-operations'
import styles from './CodeIntelStateLabel.module.scss'
export interface CodeIntelStateLabelProps {
state: LSIFUploadState | LSIFIndexState
placeInQueue?: number | null
className?: string
}
const labelClassNames = 'codeintel-state__label text-muted'
const labelClassNames = classNames(styles.label, 'text-muted')
export const CodeIntelStateLabel: FunctionComponent<CodeIntelStateLabelProps> = ({ state, placeInQueue, className }) =>
state === LSIFUploadState.UPLOADING ? (
@ -35,4 +37,4 @@ export interface CodeIntelStateLabelPlaceInQueueProps {
}
const CodeIntelStateLabelPlaceInQueue: FunctionComponent<CodeIntelStateLabelPlaceInQueueProps> = ({ placeInQueue }) =>
placeInQueue ? <span className="codeintel-state__label--block">(#{placeInQueue} in line)</span> : <></>
placeInQueue ? <span className={styles.block}>(#{placeInQueue} in line)</span> : <></>

View File

@ -113,6 +113,24 @@ export const enterpriseSiteAdminAreaRoutes: readonly SiteAdminAreaRoute[] = [
condition: () => Boolean(window.context?.codeIntelAutoIndexingEnabled),
},
// Code intelligence configuration
{
path: '/code-intelligence/configuration',
render: lazyComponent(
() => import('../codeintel/configuration/CodeIntelConfigurationPage'),
'CodeIntelConfigurationPage'
),
exact: true,
},
{
path: '/code-intelligence/configuration/:id',
render: lazyComponent(
() => import('../codeintel/configuration/CodeIntelConfigurationPolicyPage'),
'CodeIntelConfigurationPolicyPage'
),
exact: true,
},
// Legacy routes
{
path: '/lsif-uploads/:id',

View File

@ -86,6 +86,10 @@ const codeIntelGroup: SiteAdminSideBarGroup = {
label: 'Auto indexing',
condition: () => Boolean(window.context?.codeIntelAutoIndexingEnabled),
},
{
to: '/site-admin/code-intelligence/configuration',
label: 'Configuration',
},
],
}

View File

@ -237,10 +237,10 @@ type CodeIntelConfigurationPolicy struct {
Type GitObjectType
Pattern string
RetentionEnabled bool
RetentionDurationHours int32
RetentionDurationHours *int32
RetainIntermediateCommits bool
IndexingEnabled bool
IndexCommitMaxAgeHours int32
IndexCommitMaxAgeHours *int32
IndexIntermediateCommits bool
}
@ -278,9 +278,9 @@ type CodeIntelligenceConfigurationPolicyResolver interface {
Type() (GitObjectType, error)
Pattern() string
RetentionEnabled() bool
RetentionDurationHours() int32
RetentionDurationHours() *int32
RetainIntermediateCommits() bool
IndexingEnabled() bool
IndexCommitMaxAgeHours() int32
IndexCommitMaxAgeHours() *int32
IndexIntermediateCommits() bool
}

View File

@ -13,10 +13,10 @@ extend type Mutation {
type: GitObjectType!
pattern: String!
retentionEnabled: Boolean!
retentionDurationHours: Int!
retentionDurationHours: Int
retainIntermediateCommits: Boolean!
indexingEnabled: Boolean!
indexCommitMaxAgeHours: Int!
indexCommitMaxAgeHours: Int
indexIntermediateCommits: Boolean!
): CodeIntelligenceConfigurationPolicy!
@ -29,10 +29,10 @@ extend type Mutation {
type: GitObjectType!
pattern: String!
retentionEnabled: Boolean!
retentionDurationHours: Int!
retentionDurationHours: Int
retainIntermediateCommits: Boolean!
indexingEnabled: Boolean!
indexCommitMaxAgeHours: Int!
indexCommitMaxAgeHours: Int
indexIntermediateCommits: Boolean!
): EmptyResponse
@ -191,7 +191,7 @@ type CodeIntelligenceConfigurationPolicy implements Node {
"""
The max age of data retained by this configuration policy.
"""
retentionDurationHours: Int!
retentionDurationHours: Int
"""
If the matching Git object is a branch, setting this value to true will also
@ -208,7 +208,7 @@ type CodeIntelligenceConfigurationPolicy implements Node {
"""
The max age of commits indexed by this configuration policy.
"""
indexCommitMaxAgeHours: Int!
indexCommitMaxAgeHours: Int
"""
If the matching Git object is a branch, setting this value to true will also

View File

@ -49,8 +49,8 @@ func (r *configurationPolicyResolver) RetentionEnabled() bool {
return r.configurationPolicy.RetentionEnabled
}
func (r *configurationPolicyResolver) RetentionDurationHours() int32 {
return int32(r.configurationPolicy.RetentionDuration / time.Hour)
func (r *configurationPolicyResolver) RetentionDurationHours() *int32 {
return toHours(r.configurationPolicy.RetentionDuration)
}
func (r *configurationPolicyResolver) RetainIntermediateCommits() bool {
@ -61,10 +61,19 @@ func (r *configurationPolicyResolver) IndexingEnabled() bool {
return r.configurationPolicy.IndexingEnabled
}
func (r *configurationPolicyResolver) IndexCommitMaxAgeHours() int32 {
return int32(r.configurationPolicy.IndexCommitMaxAge / time.Hour)
func (r *configurationPolicyResolver) IndexCommitMaxAgeHours() *int32 {
return toHours(r.configurationPolicy.IndexCommitMaxAge)
}
func (r *configurationPolicyResolver) IndexIntermediateCommits() bool {
return r.configurationPolicy.IndexIntermediateCommits
}
func toHours(duration *time.Duration) *int32 {
if duration == nil {
return nil
}
v := int32(*duration / time.Hour)
return &v
}

View File

@ -50,7 +50,7 @@ func (r *Resolver) NodeResolvers() map[string]gql.NodeByIDFunc {
return r.LSIFIndexByID(ctx, id)
},
"CodeIntelligenceConfigurationPolicy": func(ctx context.Context, id graphql.ID) (gql.Node, error) {
return r.ConfigurationPolicyResolverByID(ctx, id)
return r.ConfigurationPolicyByID(ctx, id)
},
}
}
@ -260,11 +260,7 @@ func (r *Resolver) GitBlobLSIFData(ctx context.Context, args *gql.GitBlobLSIFDat
return NewQueryResolver(resolver, r.locationResolver), nil
}
func (r *Resolver) ConfigurationPolicyResolverByID(ctx context.Context, id graphql.ID) (gql.CodeIntelligenceConfigurationPolicyResolver, error) {
if !autoIndexingEnabled() {
return nil, errAutoIndexingNotEnabled
}
func (r *Resolver) ConfigurationPolicyByID(ctx context.Context, id graphql.ID) (gql.CodeIntelligenceConfigurationPolicyResolver, error) {
// 🚨 SECURITY: Only site admins may configure code intelligence
if err := backend.CheckCurrentUserIsSiteAdmin(ctx, dbconn.Global); err != nil {
return nil, err
@ -339,10 +335,10 @@ func (r *Resolver) CreateCodeIntelligenceConfigurationPolicy(ctx context.Context
Type: string(args.Type),
Pattern: args.Pattern,
RetentionEnabled: args.RetentionEnabled,
RetentionDuration: time.Duration(args.RetentionDurationHours) * time.Hour,
RetentionDuration: toDuration(args.RetentionDurationHours),
RetainIntermediateCommits: args.RetainIntermediateCommits,
IndexingEnabled: args.IndexingEnabled,
IndexCommitMaxAge: time.Duration(args.IndexCommitMaxAgeHours) * time.Hour,
IndexCommitMaxAge: toDuration(args.IndexCommitMaxAgeHours),
IndexIntermediateCommits: args.IndexIntermediateCommits,
})
if err != nil {
@ -352,6 +348,15 @@ func (r *Resolver) CreateCodeIntelligenceConfigurationPolicy(ctx context.Context
return NewConfigurationPolicyResolver(configurationPolicy), nil
}
func toDuration(hours *int32) *time.Duration {
if hours == nil {
return nil
}
v := time.Duration(*hours) * time.Hour
return &v
}
func (r *Resolver) UpdateCodeIntelligenceConfigurationPolicy(ctx context.Context, args *gql.UpdateCodeIntelligenceConfigurationPolicyArgs) (*gql.EmptyResponse, error) {
// 🚨 SECURITY: Only site admins may configure code intelligence
if err := backend.CheckCurrentUserIsSiteAdmin(ctx, dbconn.Global); err != nil {
@ -373,10 +378,10 @@ func (r *Resolver) UpdateCodeIntelligenceConfigurationPolicy(ctx context.Context
Type: string(args.Type),
Pattern: args.Pattern,
RetentionEnabled: args.RetentionEnabled,
RetentionDuration: time.Duration(args.RetentionDurationHours) * time.Hour,
RetentionDuration: toDuration(args.RetentionDurationHours),
RetainIntermediateCommits: args.RetainIntermediateCommits,
IndexingEnabled: args.IndexingEnabled,
IndexCommitMaxAge: time.Duration(args.IndexCommitMaxAgeHours) * time.Hour,
IndexCommitMaxAge: toDuration(args.IndexCommitMaxAgeHours),
IndexIntermediateCommits: args.IndexIntermediateCommits,
}); err != nil {
return nil, err

View File

@ -19,10 +19,10 @@ type ConfigurationPolicy struct {
Type string
Pattern string
RetentionEnabled bool
RetentionDuration time.Duration
RetentionDuration *time.Duration
RetainIntermediateCommits bool
IndexingEnabled bool
IndexCommitMaxAge time.Duration
IndexCommitMaxAge *time.Duration
IndexIntermediateCommits bool
}
@ -36,7 +36,7 @@ func scanConfigurationPolicies(rows *sql.Rows, queryErr error) (_ []Configuratio
var configurationPolicies []ConfigurationPolicy
for rows.Next() {
var configurationPolicy ConfigurationPolicy
var retentionDurationHours, indexCommitMaxAgeHours int
var retentionDurationHours, indexCommitMaxAgeHours *int
if err := rows.Scan(
&configurationPolicy.ID,
@ -54,8 +54,14 @@ func scanConfigurationPolicies(rows *sql.Rows, queryErr error) (_ []Configuratio
return nil, err
}
configurationPolicy.RetentionDuration = time.Duration(retentionDurationHours) * time.Hour
configurationPolicy.IndexCommitMaxAge = time.Duration(indexCommitMaxAgeHours) * time.Hour
if retentionDurationHours != nil {
duration := time.Duration(*retentionDurationHours) * time.Hour
configurationPolicy.RetentionDuration = &duration
}
if indexCommitMaxAgeHours != nil {
duration := time.Duration(*indexCommitMaxAgeHours) * time.Hour
configurationPolicy.IndexCommitMaxAge = &duration
}
configurationPolicies = append(configurationPolicies, configurationPolicy)
}
@ -153,6 +159,18 @@ func (s *Store) CreateConfigurationPolicy(ctx context.Context, configurationPoli
ctx, endObservation := s.operations.createConfigurationPolicy.With(ctx, &err, observation.Args{})
defer endObservation(1, observation.Args{})
var retentionDurationHours *int
if configurationPolicy.RetentionDuration != nil {
duration := int(*configurationPolicy.RetentionDuration / time.Hour)
retentionDurationHours = &duration
}
var indexingCOmmitMaxAgeHours *int
if configurationPolicy.IndexCommitMaxAge != nil {
duration := int(*configurationPolicy.IndexCommitMaxAge / time.Hour)
indexingCOmmitMaxAgeHours = &duration
}
hydratedConfigurationPolicy, _, err := scanFirstConfigurationPolicy(s.Query(ctx, sqlf.Sprintf(
createConfigurationPolicyQuery,
configurationPolicy.RepositoryID,
@ -160,10 +178,10 @@ func (s *Store) CreateConfigurationPolicy(ctx context.Context, configurationPoli
configurationPolicy.Type,
configurationPolicy.Pattern,
configurationPolicy.RetentionEnabled,
int(configurationPolicy.RetentionDuration/time.Hour),
retentionDurationHours,
configurationPolicy.RetainIntermediateCommits,
configurationPolicy.IndexingEnabled,
int(configurationPolicy.IndexCommitMaxAge/time.Hour),
indexingCOmmitMaxAgeHours,
configurationPolicy.IndexIntermediateCommits,
)))
if err != nil {
@ -208,15 +226,27 @@ func (s *Store) UpdateConfigurationPolicy(ctx context.Context, policy Configurat
}})
defer endObservation(1, observation.Args{})
var retentionDuration *int
if policy.RetentionDuration != nil {
duration := int(*policy.RetentionDuration / time.Hour)
retentionDuration = &duration
}
var indexCommitMaxAge *int
if policy.IndexCommitMaxAge != nil {
duration := int(*policy.IndexCommitMaxAge / time.Hour)
indexCommitMaxAge = &duration
}
return s.Store.Exec(ctx, sqlf.Sprintf(updateConfigurationPolicyQuery,
policy.Name,
policy.Type,
policy.Pattern,
policy.RetentionEnabled,
int(policy.RetentionDuration/time.Hour),
retentionDuration,
policy.RetainIntermediateCommits,
policy.IndexingEnabled,
int(policy.IndexCommitMaxAge/time.Hour),
indexCommitMaxAge,
policy.IndexIntermediateCommits,
policy.ID,
))

View File

@ -49,6 +49,9 @@ func TestGetConfigurationPolicies(t *testing.T) {
t.Fatalf("unexpected error fetching configuration policies: %s", err)
}
d1 := time.Hour * 5
d2 := time.Hour * 6
expected := []ConfigurationPolicy{
{
ID: 4,
@ -57,10 +60,10 @@ func TestGetConfigurationPolicies(t *testing.T) {
Type: "GIT_COMMIT",
Pattern: "deadbeef",
RetentionEnabled: false,
RetentionDuration: time.Hour * 5,
RetentionDuration: &d1,
RetainIntermediateCommits: true,
IndexingEnabled: false,
IndexCommitMaxAge: time.Hour * 6,
IndexCommitMaxAge: &d2,
IndexIntermediateCommits: true,
},
{
@ -70,10 +73,10 @@ func TestGetConfigurationPolicies(t *testing.T) {
Type: "GIT_TAG",
Pattern: "3.0",
RetentionEnabled: false,
RetentionDuration: time.Hour * 6,
RetentionDuration: &d2,
RetainIntermediateCommits: false,
IndexingEnabled: true,
IndexCommitMaxAge: time.Hour * 6,
IndexCommitMaxAge: &d2,
IndexIntermediateCommits: false,
},
}
@ -92,6 +95,11 @@ func TestGetConfigurationPolicies(t *testing.T) {
t.Fatalf("unexpected error fetching configuration policies: %s", err)
}
d1 := time.Hour * 2
d2 := time.Hour * 3
d3 := time.Hour * 3
d4 := time.Hour * 4
expected := []ConfigurationPolicy{
{
ID: 1,
@ -100,10 +108,10 @@ func TestGetConfigurationPolicies(t *testing.T) {
Type: "GIT_TREE",
Pattern: "ab/",
RetentionEnabled: true,
RetentionDuration: time.Hour * 2,
RetentionDuration: &d1,
RetainIntermediateCommits: false,
IndexingEnabled: false,
IndexCommitMaxAge: time.Hour * 3,
IndexCommitMaxAge: &d2,
IndexIntermediateCommits: true,
},
{
@ -113,10 +121,10 @@ func TestGetConfigurationPolicies(t *testing.T) {
Type: "GIT_TREE",
Pattern: "nm/",
RetentionEnabled: false,
RetentionDuration: time.Hour * 3,
RetentionDuration: &d3,
RetainIntermediateCommits: true,
IndexingEnabled: false,
IndexCommitMaxAge: time.Hour * 4,
IndexCommitMaxAge: &d4,
IndexIntermediateCommits: false,
},
}
@ -150,16 +158,19 @@ func TestCreateConfigurationPolicy(t *testing.T) {
store := testStore(db)
repositoryID := 42
d1 := time.Hour * 5
d2 := time.Hour * 6
configurationPolicy := ConfigurationPolicy{
RepositoryID: &repositoryID,
Name: "name",
Type: "GIT_COMMIT",
Pattern: "deadbeef",
RetentionEnabled: false,
RetentionDuration: time.Hour * 5,
RetentionDuration: &d1,
RetainIntermediateCommits: true,
IndexingEnabled: false,
IndexCommitMaxAge: time.Hour * 6,
IndexCommitMaxAge: &d2,
IndexIntermediateCommits: true,
}
@ -196,16 +207,19 @@ func TestUpdateConfigurationPolicy(t *testing.T) {
store := testStore(db)
repositoryID := 42
d1 := time.Hour * 5
d2 := time.Hour * 6
configurationPolicy := ConfigurationPolicy{
RepositoryID: &repositoryID,
Name: "name",
Type: "GIT_COMMIT",
Pattern: "deadbeef",
RetentionEnabled: false,
RetentionDuration: time.Hour * 5,
RetentionDuration: &d1,
RetainIntermediateCommits: true,
IndexingEnabled: false,
IndexCommitMaxAge: time.Hour * 6,
IndexCommitMaxAge: &d2,
IndexIntermediateCommits: true,
}
@ -219,6 +233,9 @@ func TestUpdateConfigurationPolicy(t *testing.T) {
t.Fatalf("hydrated policy does not have an identifier")
}
d3 := time.Hour * 10
d4 := time.Hour * 15
newConfigurationPolicy := ConfigurationPolicy{
ID: hydratedConfigurationPolicy.ID,
RepositoryID: &repositoryID,
@ -226,11 +243,12 @@ func TestUpdateConfigurationPolicy(t *testing.T) {
Type: "GIT_TREE",
Pattern: "az/",
RetentionEnabled: true,
RetentionDuration: time.Hour * 10,
RetentionDuration: &d3,
RetainIntermediateCommits: false,
IndexingEnabled: true,
IndexCommitMaxAge: time.Hour * 15,
IndexIntermediateCommits: false,
IndexCommitMaxAge: &d4,
IndexIntermediateCommits: false,
}
if err := store.UpdateConfigurationPolicy(context.Background(), newConfigurationPolicy); err != nil {
@ -255,16 +273,19 @@ func TestDeleteConfigurationPolicyByID(t *testing.T) {
store := testStore(db)
repositoryID := 42
d1 := time.Hour * 5
d2 := time.Hour * 6
configurationPolicy := ConfigurationPolicy{
RepositoryID: &repositoryID,
Name: "name",
Type: "GIT_COMMIT",
Pattern: "deadbeef",
RetentionEnabled: false,
RetentionDuration: time.Hour * 5,
RetentionDuration: &d1,
RetainIntermediateCommits: true,
IndexingEnabled: false,
IndexCommitMaxAge: time.Hour * 6,
IndexCommitMaxAge: &d2,
IndexIntermediateCommits: true,
}

View File

@ -864,7 +864,8 @@ func (l *EventLogStore) codeIntelligenceSettingsPageViewCount(ctx context.Contex
"CodeIntelUploadPage",
"CodeIntelIndexesPage",
"CodeIntelIndexPage",
"CodeIntelIndexConfigurationPage",
"CodeIntelConfigurationPage",
"CodeIntelConfigurationPolicyPage",
}
names := make([]*sqlf.Query, 0, len(pageNames))

View File

@ -496,11 +496,11 @@ func TestEventLogs_CodeIntelligenceSettingsPageViewCounts(t *testing.T) {
names := []string{
"ViewBatchesConfiguration",
"ViewCodeIntelUploadsPage", // contributes 75 events
"ViewCodeIntelUploadPage", // contributes 75 events
"ViewCodeIntelIndexesPage", // contributes 75 events
"ViewCodeIntelIndexPage", // contributes 75 events
"ViewCodeIntelIndexConfigurationPage", // contributes 75 events
"ViewCodeIntelUploadsPage", // contributes 75 events
"ViewCodeIntelUploadPage", // contributes 75 events
"ViewCodeIntelIndexesPage", // contributes 75 events
"ViewCodeIntelIndexPage", // contributes 75 events
"ViewCodeIntelConfigurationPage", // contributes 75 events
}
// This unix timestamp is equivalent to `Friday, May 15, 2020 10:30:00 PM GMT` and is set to

View File

@ -825,10 +825,10 @@ Stores data points for a code insight that do not need to be queried directly, b
type | text | | not null |
pattern | text | | not null |
retention_enabled | boolean | | not null |
retention_duration_hours | integer | | not null |
retention_duration_hours | integer | | |
retain_intermediate_commits | boolean | | not null |
indexing_enabled | boolean | | not null |
index_commit_max_age_hours | integer | | not null |
index_commit_max_age_hours | integer | | |
index_intermediate_commits | boolean | | not null |
Indexes:
"lsif_configuration_policies_pkey" PRIMARY KEY, btree (id)
@ -836,7 +836,7 @@ Indexes:
```
**index_commit_max_age_hours**: The max age of commits indexed by this configuration policy.
**index_commit_max_age_hours**: The max age of commits indexed by this configuration policy. If null, the age is unbounded.
**index_intermediate_commits**: If the matching Git object is a branch, setting this value to true will also index all commits on the matching branches. Setting this value to false will only consider the tip of the branch.
@ -848,7 +848,7 @@ Indexes:
**retain_intermediate_commits**: If the matching Git object is a branch, setting this value to true will also retain all data used to resolve queries for any commit on the matching branches. Setting this value to false will only consider the tip of the branch.
**retention_duration_hours**: The max age of data retained by this configuration policy.
**retention_duration_hours**: The max age of data retained by this configuration policy. If null, the age is unbounded.
**retention_enabled**: Whether or not this configuration policy affects data retention rules.

View File

@ -7,10 +7,10 @@ CREATE TABLE lsif_configuration_policies (
type text NOT NULL,
pattern text NOT NULL,
retention_enabled boolean NOT NULL,
retention_duration_hours int NOT NULL,
retention_duration_hours int,
retain_intermediate_commits boolean NOT NULL,
indexing_enabled boolean NOT NULL,
index_commit_max_age_hours int NOT NULL,
index_commit_max_age_hours int,
index_intermediate_commits boolean NOT NULL
);
@ -20,8 +20,8 @@ COMMENT ON COLUMN lsif_configuration_policies.repository_id IS 'The identifier o
COMMENT ON COLUMN lsif_configuration_policies.type IS 'The type of Git object (e.g., COMMIT, BRANCH, TAG).';
COMMENT ON COLUMN lsif_configuration_policies.pattern IS 'A pattern used to match` names of the associated Git object type.';
COMMENT ON COLUMN lsif_configuration_policies.retention_enabled IS 'Whether or not this configuration policy affects data retention rules.';
COMMENT ON COLUMN lsif_configuration_policies.retention_duration_hours IS 'The max age of data retained by this configuration policy.';
COMMENT ON COLUMN lsif_configuration_policies.retention_duration_hours IS 'The max age of data retained by this configuration policy. If null, the age is unbounded.';
COMMENT ON COLUMN lsif_configuration_policies.retain_intermediate_commits IS 'If the matching Git object is a branch, setting this value to true will also retain all data used to resolve queries for any commit on the matching branches. Setting this value to false will only consider the tip of the branch.';
COMMENT ON COLUMN lsif_configuration_policies.indexing_enabled IS 'Whether or not this configuration policy affects auto-indexing schedules.';
COMMENT ON COLUMN lsif_configuration_policies.index_commit_max_age_hours IS 'The max age of commits indexed by this configuration policy.';
COMMENT ON COLUMN lsif_configuration_policies.index_commit_max_age_hours IS 'The max age of commits indexed by this configuration policy. If null, the age is unbounded.';
COMMENT ON COLUMN lsif_configuration_policies.index_intermediate_commits IS 'If the matching Git object is a branch, setting this value to true will also index all commits on the matching branches. Setting this value to false will only consider the tip of the branch.';