mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 20:11:54 +00:00
codeintel: Add UI for data retention and index scheduling configuration (#24095)
This commit is contained in:
parent
6798b59c69
commit
895ffa20d2
@ -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';
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -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),
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -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' },
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
))
|
||||
@ -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>
|
||||
)
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -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</>
|
||||
)
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
@ -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>
|
||||
)
|
||||
@ -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>
|
||||
)
|
||||
@ -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>
|
||||
)
|
||||
@ -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>
|
||||
)
|
||||
@ -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}
|
||||
</>
|
||||
)
|
||||
@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
@ -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>
|
||||
)
|
||||
@ -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>
|
||||
)
|
||||
@ -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>
|
||||
)
|
||||
@ -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>
|
||||
)
|
||||
@ -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
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
18
client/web/src/enterprise/codeintel/configuration/shared.ts
Normal file
18
client/web/src/enterprise/codeintel/configuration/shared.ts
Normal 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
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
</>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
@ -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>
|
||||
)
|
||||
@ -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>
|
||||
)
|
||||
@ -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>
|
||||
)
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
})
|
||||
}),
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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),
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
})
|
||||
}),
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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',
|
||||
},
|
||||
]
|
||||
: []
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
</>
|
||||
)
|
||||
@ -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>
|
||||
)
|
||||
@ -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>
|
||||
)
|
||||
@ -0,0 +1,10 @@
|
||||
.docker-command-spec {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
width: 5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@ -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>
|
||||
)
|
||||
@ -1,5 +0,0 @@
|
||||
@import './detail/CodeIntelIndexPage';
|
||||
@import './detail/CodeIntelUploadPage';
|
||||
@import './list/CodeIntelIndexesPage';
|
||||
@import './list/CodeIntelUploadsPage';
|
||||
@import './shared/CodeIntelStateLabel';
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
</>
|
||||
)
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)),
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
</>
|
||||
)
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)),
|
||||
}
|
||||
|
||||
@ -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} />.
|
||||
</>
|
||||
)
|
||||
|
||||
@ -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} />.
|
||||
</>
|
||||
)
|
||||
18
client/web/src/enterprise/codeintel/list/EmptyUploads.tsx
Normal file
18
client/web/src/enterprise/codeintel/list/EmptyUploads.tsx
Normal 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>
|
||||
)
|
||||
78
client/web/src/enterprise/codeintel/list/EnqueueForm.tsx
Normal file
78
client/web/src/enterprise/codeintel/list/EnqueueForm.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -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',
|
||||
},
|
||||
],
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
.label {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.block {
|
||||
display: block;
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
.codeintel-state {
|
||||
&__label {
|
||||
text-align: center;
|
||||
|
||||
&--block {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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> : <></>
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -86,6 +86,10 @@ const codeIntelGroup: SiteAdminSideBarGroup = {
|
||||
label: 'Auto indexing',
|
||||
condition: () => Boolean(window.context?.codeIntelAutoIndexingEnabled),
|
||||
},
|
||||
{
|
||||
to: '/site-admin/code-intelligence/configuration',
|
||||
label: 'Configuration',
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
))
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
|
||||
@ -864,7 +864,8 @@ func (l *EventLogStore) codeIntelligenceSettingsPageViewCount(ctx context.Contex
|
||||
"CodeIntelUploadPage",
|
||||
"CodeIntelIndexesPage",
|
||||
"CodeIntelIndexPage",
|
||||
"CodeIntelIndexConfigurationPage",
|
||||
"CodeIntelConfigurationPage",
|
||||
"CodeIntelConfigurationPolicyPage",
|
||||
}
|
||||
|
||||
names := make([]*sqlf.Query, 0, len(pageNames))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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.';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user