mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 13:11:49 +00:00
Code Intel: Add inference script preview (#48898)
This commit is contained in:
parent
64cd22d127
commit
971891ad27
@ -12,6 +12,11 @@ export interface SaveToolbarProps {
|
||||
saving?: boolean
|
||||
error?: Error
|
||||
|
||||
/**
|
||||
* Determine if consumer children is added before or after the toolbar actions.
|
||||
*/
|
||||
childrenPosition?: 'start' | 'end'
|
||||
|
||||
onSave: () => void
|
||||
onDiscard: () => void
|
||||
/**
|
||||
@ -32,7 +37,17 @@ export type SaveToolbarPropsGenerator<T extends object> = (
|
||||
|
||||
export const SaveToolbar: React.FunctionComponent<
|
||||
React.PropsWithChildren<React.PropsWithChildren<SaveToolbarProps>>
|
||||
> = ({ dirty, saving, error, onSave, onDiscard, children, willShowError, saveDiscardDisabled }) => {
|
||||
> = ({
|
||||
dirty,
|
||||
saving,
|
||||
error,
|
||||
onSave,
|
||||
onDiscard,
|
||||
children,
|
||||
childrenPosition = 'end',
|
||||
willShowError,
|
||||
saveDiscardDisabled,
|
||||
}) => {
|
||||
const disabled = saveDiscardDisabled ? saveDiscardDisabled() : saving || !dirty
|
||||
let saveDiscardTitle: string | undefined
|
||||
if (saving) {
|
||||
@ -54,6 +69,7 @@ export const SaveToolbar: React.FunctionComponent<
|
||||
</div>
|
||||
)}
|
||||
<div className={classNames('mt-2', styles.actions)}>
|
||||
{childrenPosition === 'start' && children}
|
||||
<Button
|
||||
disabled={disabled}
|
||||
title={saveDiscardTitle || 'Save changes'}
|
||||
@ -72,7 +88,7 @@ export const SaveToolbar: React.FunctionComponent<
|
||||
>
|
||||
Discard changes
|
||||
</Button>
|
||||
{children}
|
||||
{childrenPosition === 'end' && children}
|
||||
{saving && (
|
||||
<span className={classNames(styles.item, styles.message)}>
|
||||
<LoadingSpinner /> Saving...
|
||||
|
||||
@ -1,114 +0,0 @@
|
||||
import { FunctionComponent, useCallback, useMemo, useState } from 'react'
|
||||
|
||||
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { useIsLightTheme } from '@sourcegraph/shared/src/theme'
|
||||
import { LoadingSpinner, PageHeader, screenReaderAnnounce, ErrorAlert } from '@sourcegraph/wildcard'
|
||||
|
||||
import { AuthenticatedUser } from '../../../../auth'
|
||||
import { PageTitle } from '../../../../components/PageTitle'
|
||||
import { SaveToolbar, SaveToolbarProps, SaveToolbarPropsGenerator } from '../../../../components/SaveToolbar'
|
||||
import { DynamicallyImportedMonacoSettingsEditor } from '../../../../settings/DynamicallyImportedMonacoSettingsEditor'
|
||||
import { INFERENCE_SCRIPT, useInferenceScript } from '../hooks/useInferenceScript'
|
||||
import { useUpdateInferenceScript } from '../hooks/useUpdateInferenceScript'
|
||||
|
||||
import styles from './CodeIntelConfigurationPageHeader.module.scss'
|
||||
|
||||
export interface InferenceScriptEditorProps extends TelemetryProps {
|
||||
authenticatedUser: AuthenticatedUser | null
|
||||
}
|
||||
|
||||
export const InferenceScriptEditor: FunctionComponent<InferenceScriptEditorProps> = ({
|
||||
authenticatedUser,
|
||||
telemetryService,
|
||||
}) => {
|
||||
const { inferenceScript, loadingScript, fetchError } = useInferenceScript()
|
||||
const { updateInferenceScript, isUpdating, updatingError } = useUpdateInferenceScript()
|
||||
|
||||
const save = useCallback(
|
||||
async (script: string) =>
|
||||
updateInferenceScript({
|
||||
variables: { script },
|
||||
refetchQueries: [INFERENCE_SCRIPT],
|
||||
}).then(() => {
|
||||
screenReaderAnnounce('Saved successfully')
|
||||
setDirty(false)
|
||||
}),
|
||||
[updateInferenceScript]
|
||||
)
|
||||
|
||||
const [dirty, setDirty] = useState<boolean>()
|
||||
const isLightTheme = useIsLightTheme()
|
||||
|
||||
const customToolbar = useMemo<{
|
||||
saveToolbar: FunctionComponent<SaveToolbarProps>
|
||||
propsGenerator: SaveToolbarPropsGenerator<{}>
|
||||
}>(
|
||||
() => ({
|
||||
saveToolbar: SaveToolbar,
|
||||
propsGenerator: props => {
|
||||
const mergedProps = {
|
||||
...props,
|
||||
loading: isUpdating,
|
||||
}
|
||||
mergedProps.willShowError = () => !mergedProps.saving
|
||||
mergedProps.saveDiscardDisabled = () => mergedProps.saving || !dirty
|
||||
|
||||
return mergedProps
|
||||
},
|
||||
}),
|
||||
[dirty, isUpdating]
|
||||
)
|
||||
|
||||
const title = (
|
||||
<>
|
||||
<PageTitle title="Code graph inference script" />
|
||||
<div className={styles.grid}>
|
||||
<PageHeader
|
||||
headingElement="h2"
|
||||
path={[
|
||||
{
|
||||
text: <>Code graph inference script</>,
|
||||
},
|
||||
]}
|
||||
description={`Lua script that emits complete and/or partial auto-indexing
|
||||
job specifications. `}
|
||||
className="mb-3"
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
if (fetchError) {
|
||||
return (
|
||||
<>
|
||||
{title}
|
||||
<ErrorAlert prefix="Error fetching inference script" error={fetchError} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{title}
|
||||
{updatingError && <ErrorAlert prefix="Error saving index configuration" error={updatingError} />}
|
||||
|
||||
{loadingScript ? (
|
||||
<LoadingSpinner />
|
||||
) : (
|
||||
<DynamicallyImportedMonacoSettingsEditor
|
||||
value={inferenceScript}
|
||||
language="lua"
|
||||
canEdit={authenticatedUser?.siteAdmin}
|
||||
readOnly={!authenticatedUser?.siteAdmin}
|
||||
onSave={save}
|
||||
saving={isUpdating}
|
||||
height={600}
|
||||
isLightTheme={isLightTheme}
|
||||
telemetryService={telemetryService}
|
||||
customSaveToolbar={authenticatedUser?.siteAdmin ? customToolbar : undefined}
|
||||
onDirtyChange={setDirty}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
.command-input {
|
||||
max-height: 20rem;
|
||||
}
|
||||
@ -0,0 +1,94 @@
|
||||
import React, { useMemo, useRef } from 'react'
|
||||
|
||||
import { defaultKeymap, history } from '@codemirror/commands'
|
||||
import { StreamLanguage, syntaxHighlighting, HighlightStyle } from '@codemirror/language'
|
||||
import { shell } from '@codemirror/legacy-modes/mode/shell'
|
||||
import { EditorState, Extension } from '@codemirror/state'
|
||||
import { EditorView, keymap } from '@codemirror/view'
|
||||
import { tags } from '@lezer/highlight'
|
||||
import classNames from 'classnames'
|
||||
|
||||
import { useCodeMirror, defaultSyntaxHighlighting } from '@sourcegraph/shared/src/components/CodeMirrorEditor'
|
||||
|
||||
import styles from './CommandInput.module.scss'
|
||||
|
||||
const shellHighlighting: Extension = [
|
||||
syntaxHighlighting(HighlightStyle.define([{ tag: [tags.keyword], class: 'hljs-keyword' }])),
|
||||
defaultSyntaxHighlighting,
|
||||
]
|
||||
|
||||
const staticExtensions: Extension = [
|
||||
keymap.of(defaultKeymap),
|
||||
history(),
|
||||
EditorView.theme({
|
||||
'&': {
|
||||
flex: 1,
|
||||
backgroundColor: 'var(--input-bg)',
|
||||
borderRadius: 'var(--border-radius)',
|
||||
borderColor: 'var(--border-color)',
|
||||
marginRight: '0.5rem',
|
||||
},
|
||||
'&.cm-editor.cm-focused': {
|
||||
outline: 'none',
|
||||
},
|
||||
'.cm-scroller': {
|
||||
overflowX: 'hidden',
|
||||
},
|
||||
'.cm-content': {
|
||||
caretColor: 'var(--search-query-text-color)',
|
||||
fontFamily: 'var(--code-font-family)',
|
||||
fontSize: 'var(--code-font-size)',
|
||||
},
|
||||
'.cm-content.focus-visible': {
|
||||
boxShadow: 'none',
|
||||
},
|
||||
'.cm-line': {
|
||||
padding: '0',
|
||||
},
|
||||
}),
|
||||
StreamLanguage.define(shell),
|
||||
shellHighlighting,
|
||||
]
|
||||
|
||||
interface CommandInputProps {
|
||||
value: string
|
||||
onChange?: (value: string) => void
|
||||
readOnly: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const CommandInput: React.FunctionComponent<CommandInputProps> = React.memo(function CodeMirrorComandInput({
|
||||
value,
|
||||
className,
|
||||
readOnly,
|
||||
onChange = () => {},
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null)
|
||||
const editorRef = useRef<EditorView | null>(null)
|
||||
|
||||
useCodeMirror(
|
||||
editorRef,
|
||||
containerRef,
|
||||
value,
|
||||
useMemo(
|
||||
() => [
|
||||
staticExtensions,
|
||||
EditorState.readOnly.of(readOnly),
|
||||
EditorView.updateListener.of(update => {
|
||||
if (update.docChanged) {
|
||||
onChange(update.state.sliceDoc())
|
||||
}
|
||||
}),
|
||||
],
|
||||
[onChange, readOnly]
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
data-editor="codemirror6"
|
||||
className={classNames('form-control', styles.commandInput, className)}
|
||||
/>
|
||||
)
|
||||
})
|
||||
@ -0,0 +1,14 @@
|
||||
.job-field {
|
||||
display: grid;
|
||||
grid-template-columns: [label] 15% [value] 85%;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.job-label {
|
||||
margin-top: 0.25rem;
|
||||
grid-area: label;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@ -0,0 +1,19 @@
|
||||
import { Label } from '@sourcegraph/wildcard'
|
||||
|
||||
import styles from './IndexJobLabel.module.scss'
|
||||
|
||||
interface IndexJobLabelProps {
|
||||
label: string
|
||||
}
|
||||
|
||||
export const IndexJobLabel: React.FunctionComponent<React.PropsWithChildren<IndexJobLabelProps>> = ({
|
||||
label,
|
||||
children,
|
||||
}) => (
|
||||
<>
|
||||
<li className={styles.jobField}>
|
||||
<Label className={styles.jobLabel}>{label}:</Label>
|
||||
{children}
|
||||
</li>
|
||||
</>
|
||||
)
|
||||
@ -0,0 +1,92 @@
|
||||
.job-anchor {
|
||||
display: block;
|
||||
position: relative;
|
||||
top: -4rem;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
// IndexJob
|
||||
.job {
|
||||
padding: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
.job-content {
|
||||
padding: 0.25rem;
|
||||
margin-bottom: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
// IndexJobStep
|
||||
.job-step-container {
|
||||
padding: 0 1rem;
|
||||
margin-top: 1rem;
|
||||
border: 3px solid var(--subtle-bg);
|
||||
}
|
||||
.job-step {
|
||||
padding: 1rem 0;
|
||||
border-bottom: 3px solid var(--subtle-bg);
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
.job-step-content {
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
// Misc
|
||||
.job-header,
|
||||
.job-step-header {
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
margin-top: -0.25rem;
|
||||
margin-left: -0.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.job-input {
|
||||
grid-area: value;
|
||||
// Ensure consistent height even when no value
|
||||
min-height: 2rem;
|
||||
height: auto;
|
||||
margin-bottom: 0.5rem;
|
||||
overflow: auto;
|
||||
|
||||
input[readonly] {
|
||||
// Override disabled bg-color
|
||||
background-color: var(--input-bg);
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.job-input-action {
|
||||
grid-area: value;
|
||||
min-height: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
max-width: 7rem;
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.job-command-container {
|
||||
grid-area: value;
|
||||
padding: 1rem;
|
||||
border: 3px solid var(--subtle-bg);
|
||||
}
|
||||
|
||||
.job-command {
|
||||
display: flex;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,248 @@
|
||||
import React from 'react'
|
||||
|
||||
import { mdiClose, mdiPlus } from '@mdi/js'
|
||||
import { uniqueId } from 'lodash'
|
||||
|
||||
import { Button, Container, H3, H4, Icon, Input } from '@sourcegraph/wildcard'
|
||||
|
||||
import { CommandInput } from './CommandInput'
|
||||
import { IndexJobLabel } from './IndexJobLabel'
|
||||
import { InferenceArrayValue, InferenceFormJob, InferenceFormJobStep } from './types'
|
||||
|
||||
import styles from './IndexJobNode.module.scss'
|
||||
|
||||
interface IndexJobNodeProps {
|
||||
job: InferenceFormJob
|
||||
jobNumber: number
|
||||
readOnly: boolean
|
||||
onChange: (name: keyof InferenceFormJob, value: unknown) => void
|
||||
onRemove: () => void
|
||||
}
|
||||
|
||||
export const IndexJobNode: React.FunctionComponent<IndexJobNodeProps> = ({
|
||||
job,
|
||||
jobNumber,
|
||||
readOnly,
|
||||
onChange,
|
||||
onRemove,
|
||||
}) => {
|
||||
const comparisonKey = job.meta.id
|
||||
|
||||
return (
|
||||
<Container id={comparisonKey} className={styles.job}>
|
||||
<div className={styles.jobHeader}>
|
||||
<H3 className="mb-0">Job #{jobNumber}</H3>
|
||||
{!readOnly && (
|
||||
<Button variant="icon" className="ml-2 text-danger" aria-label="Remove" onClick={onRemove}>
|
||||
<Icon svgPath={mdiClose} aria-hidden={true} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<ul className={styles.jobContent}>
|
||||
<IndexJobLabel label="Root">
|
||||
<Input
|
||||
value={job.root}
|
||||
onChange={event => onChange('root', event.target.value)}
|
||||
readOnly={readOnly}
|
||||
className={styles.jobInput}
|
||||
/>
|
||||
</IndexJobLabel>
|
||||
<IndexJobLabel label="Indexer">
|
||||
<CommandInput
|
||||
value={job.indexer}
|
||||
onChange={value => onChange('indexer', value)}
|
||||
readOnly={readOnly}
|
||||
className={styles.jobInput}
|
||||
/>
|
||||
</IndexJobLabel>
|
||||
<IndexJobLabel label="Indexer args">
|
||||
<IndexCommandNode
|
||||
commands={job.indexer_args}
|
||||
name="indexer_args"
|
||||
addLabel="arg"
|
||||
readOnly={readOnly}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</IndexJobLabel>
|
||||
<IndexJobLabel label="Requested env vars">
|
||||
<IndexCommandNode
|
||||
commands={job.requestedEnvVars ?? []}
|
||||
name="requestedEnvVars"
|
||||
addLabel="env var"
|
||||
readOnly={readOnly}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</IndexJobLabel>
|
||||
<IndexJobLabel label="Local steps">
|
||||
<IndexCommandNode
|
||||
commands={job.local_steps}
|
||||
name="local_steps"
|
||||
addLabel="local step"
|
||||
readOnly={readOnly}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</IndexJobLabel>
|
||||
<IndexJobLabel label="Outfile">
|
||||
<Input
|
||||
value={job.outfile}
|
||||
onChange={event => onChange('outfile', event.target.value)}
|
||||
readOnly={readOnly}
|
||||
className={styles.jobInput}
|
||||
/>
|
||||
</IndexJobLabel>
|
||||
{job.steps.length > 0 && (
|
||||
<Container className={styles.jobStepContainer} as="li">
|
||||
{job.steps.map((step, index) => (
|
||||
<div className={styles.jobStep} key={step.meta.id}>
|
||||
<div className={styles.jobStepHeader}>
|
||||
<H4 className="mb-0">Step #{index + 1}</H4>
|
||||
{!readOnly && (
|
||||
<Button
|
||||
variant="icon"
|
||||
className="ml-2 text-danger"
|
||||
aria-label="Remove"
|
||||
onClick={() => {
|
||||
const steps = [...job.steps]
|
||||
steps.splice(index, 1)
|
||||
onChange('steps', steps)
|
||||
}}
|
||||
>
|
||||
<Icon svgPath={mdiClose} aria-hidden={true} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<IndexStepNode
|
||||
step={step}
|
||||
readOnly={readOnly}
|
||||
onChange={(name, value) => {
|
||||
const steps = [...job.steps]
|
||||
steps[index] = { ...steps[index], [name]: value }
|
||||
onChange('steps', steps)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</Container>
|
||||
)}
|
||||
{!readOnly && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="d-block mt-2 ml-auto"
|
||||
onClick={() => {
|
||||
onChange('steps', [
|
||||
...job.steps,
|
||||
{ root: '', image: '', commands: [], meta: { id: uniqueId() } },
|
||||
])
|
||||
}}
|
||||
>
|
||||
<Icon svgPath={mdiPlus} aria-hidden={true} className="mr-1" />
|
||||
Add step
|
||||
</Button>
|
||||
)}
|
||||
</ul>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
interface IndexStepNodeProps {
|
||||
step: InferenceFormJobStep
|
||||
readOnly: boolean
|
||||
onChange: (name: keyof InferenceFormJobStep, value: unknown) => void
|
||||
}
|
||||
|
||||
const IndexStepNode: React.FunctionComponent<IndexStepNodeProps> = ({ step, readOnly, onChange }) => (
|
||||
<ul className={styles.jobStepContent}>
|
||||
<IndexJobLabel label="Root">
|
||||
<Input
|
||||
value={step.root}
|
||||
onChange={event => onChange('root', event.target.value)}
|
||||
readOnly={readOnly}
|
||||
className={styles.jobInput}
|
||||
/>
|
||||
</IndexJobLabel>
|
||||
<IndexJobLabel label="Image">
|
||||
<CommandInput
|
||||
value={step.image}
|
||||
onChange={value => onChange('image', value)}
|
||||
readOnly={readOnly}
|
||||
className={styles.jobInput}
|
||||
/>
|
||||
</IndexJobLabel>
|
||||
<IndexJobLabel label="Commands">
|
||||
<IndexCommandNode<keyof InferenceFormJobStep>
|
||||
commands={step.commands}
|
||||
name="commands"
|
||||
addLabel="command"
|
||||
readOnly={readOnly}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</IndexJobLabel>
|
||||
</ul>
|
||||
)
|
||||
|
||||
interface IndexCommandNodeProps<formKey = keyof InferenceFormJob> {
|
||||
name: formKey
|
||||
addLabel: string
|
||||
commands: InferenceArrayValue[]
|
||||
onChange: (name: formKey, value: unknown) => void
|
||||
readOnly: boolean
|
||||
}
|
||||
|
||||
const IndexCommandNode = <formKey,>({
|
||||
name,
|
||||
addLabel,
|
||||
commands,
|
||||
onChange,
|
||||
readOnly,
|
||||
}: IndexCommandNodeProps<formKey>): JSX.Element | null => (
|
||||
<div className={styles.jobCommandContainer}>
|
||||
{commands.map((command, index) => (
|
||||
<div className={styles.jobCommand} key={command.meta.id}>
|
||||
<CommandInput
|
||||
value={command.value}
|
||||
onChange={value => {
|
||||
const prevCommands = [...commands]
|
||||
prevCommands[index].value = value
|
||||
onChange(name, prevCommands)
|
||||
}}
|
||||
readOnly={readOnly}
|
||||
className={styles.jobInput}
|
||||
/>
|
||||
{!readOnly && (
|
||||
<Button
|
||||
variant="icon"
|
||||
className="ml-2 text-danger"
|
||||
aria-label="Remove"
|
||||
onClick={() => {
|
||||
const prevCommands = [...commands]
|
||||
prevCommands.splice(index, 1)
|
||||
onChange(name, prevCommands)
|
||||
}}
|
||||
>
|
||||
<Icon svgPath={mdiClose} aria-hidden={true} />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!readOnly && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
onChange(name, [
|
||||
...commands,
|
||||
{
|
||||
value: '',
|
||||
meta: {
|
||||
id: uniqueId(),
|
||||
},
|
||||
},
|
||||
])
|
||||
}}
|
||||
>
|
||||
<Icon svgPath={mdiPlus} aria-hidden={true} className="mr-1" />
|
||||
Add {addLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
@ -0,0 +1,103 @@
|
||||
import React, { useCallback, useState } from 'react'
|
||||
|
||||
import AJV from 'ajv'
|
||||
import addFormats from 'ajv-formats'
|
||||
|
||||
import { Button, Form } from '@sourcegraph/wildcard'
|
||||
|
||||
import { AutoIndexJobDescriptionFields } from '../../../../../graphql-operations'
|
||||
import schema from '../../schema.json'
|
||||
|
||||
import { autoIndexJobsToFormData } from './auto-index-to-form-job'
|
||||
import { formDataToSchema } from './form-data-to-schema'
|
||||
import { IndexJobNode } from './IndexJobNode'
|
||||
import { InferenceFormData, InferenceFormJob, SchemaCompatibleInferenceFormData } from './types'
|
||||
|
||||
const ajv = new AJV({ strict: false })
|
||||
addFormats(ajv)
|
||||
|
||||
interface InferenceFormProps {
|
||||
readOnly: boolean
|
||||
jobs: AutoIndexJobDescriptionFields[]
|
||||
onSubmit?: (data: SchemaCompatibleInferenceFormData) => void
|
||||
}
|
||||
|
||||
export const InferenceForm: React.FunctionComponent<InferenceFormProps> = ({ jobs, readOnly, onSubmit }) => {
|
||||
const [formData, setFormData] = useState<InferenceFormData>(autoIndexJobsToFormData(jobs))
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
|
||||
if (!onSubmit) {
|
||||
return
|
||||
}
|
||||
|
||||
const schemaCompatibleFormData = formDataToSchema(formData)
|
||||
|
||||
// Validate form data against JSONSchema
|
||||
const isValid = ajv.validate(schema, schemaCompatibleFormData)
|
||||
|
||||
if (isValid) {
|
||||
onSubmit(schemaCompatibleFormData)
|
||||
}
|
||||
},
|
||||
[formData, onSubmit]
|
||||
)
|
||||
|
||||
const getChangeHandler = useCallback(
|
||||
(id: string) => (name: keyof InferenceFormJob, value: unknown) => {
|
||||
setFormData(previous => {
|
||||
const index = previous.index_jobs.findIndex(job => job.meta.id === id)
|
||||
const job = previous.index_jobs[index]
|
||||
|
||||
return {
|
||||
index_jobs: [
|
||||
...previous.index_jobs.slice(0, index),
|
||||
{
|
||||
...job,
|
||||
[name]: value,
|
||||
},
|
||||
...previous.index_jobs.slice(index + 1),
|
||||
],
|
||||
}
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const getRemoveHandler = useCallback(
|
||||
(id: string) => () => {
|
||||
if (!window.confirm('Are you sure you want to remove this entire job?')) {
|
||||
return
|
||||
}
|
||||
|
||||
setFormData(previous => ({
|
||||
index_jobs: previous.index_jobs.filter(job => job.meta.id !== id),
|
||||
}))
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<>
|
||||
{formData.index_jobs.map((job, index) => (
|
||||
<IndexJobNode
|
||||
key={job.meta.id}
|
||||
job={job}
|
||||
jobNumber={index + 1}
|
||||
readOnly={readOnly}
|
||||
onChange={getChangeHandler(job.meta.id)}
|
||||
onRemove={getRemoveHandler(job.meta.id)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
{!readOnly && (
|
||||
<Button type="submit" variant="primary">
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,136 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`autoIndexToAutoIndexSchema should build form data as expected 1`] = `
|
||||
Object {
|
||||
"index_jobs": Array [
|
||||
Object {
|
||||
"indexer": "indexer-1@123",
|
||||
"indexer_args": Array [
|
||||
Object {
|
||||
"meta": Object {
|
||||
"id": "test-id",
|
||||
},
|
||||
"value": "arg-1",
|
||||
},
|
||||
Object {
|
||||
"meta": Object {
|
||||
"id": "test-id",
|
||||
},
|
||||
"value": "arg-2",
|
||||
},
|
||||
],
|
||||
"local_steps": Array [
|
||||
Object {
|
||||
"meta": Object {
|
||||
"id": "test-id",
|
||||
},
|
||||
"value": "command-1",
|
||||
},
|
||||
Object {
|
||||
"meta": Object {
|
||||
"id": "test-id",
|
||||
},
|
||||
"value": "command-2",
|
||||
},
|
||||
],
|
||||
"meta": Object {
|
||||
"id": "key-1",
|
||||
},
|
||||
"outfile": "outfile-1",
|
||||
"requestedEnvVars": Array [
|
||||
Object {
|
||||
"meta": Object {
|
||||
"id": "test-id",
|
||||
},
|
||||
"value": "ENV_VAR",
|
||||
},
|
||||
Object {
|
||||
"meta": Object {
|
||||
"id": "test-id",
|
||||
},
|
||||
"value": "ENV_VAR_2",
|
||||
},
|
||||
],
|
||||
"root": "root-1",
|
||||
"steps": Array [
|
||||
Object {
|
||||
"commands": Array [
|
||||
Object {
|
||||
"meta": Object {
|
||||
"id": "test-id",
|
||||
},
|
||||
"value": "pre-command-1",
|
||||
},
|
||||
Object {
|
||||
"meta": Object {
|
||||
"id": "test-id",
|
||||
},
|
||||
"value": "pre-command-2",
|
||||
},
|
||||
],
|
||||
"image": "indexer-1@123",
|
||||
"meta": Object {
|
||||
"id": "test-id",
|
||||
},
|
||||
"root": "pre-root-1",
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"indexer": "",
|
||||
"indexer_args": Array [],
|
||||
"local_steps": Array [],
|
||||
"meta": Object {
|
||||
"id": "key-2",
|
||||
},
|
||||
"outfile": "",
|
||||
"requestedEnvVars": Array [],
|
||||
"root": "",
|
||||
"steps": Array [],
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`autoIndexToAutoIndexSchema should build schema data as expected 1`] = `
|
||||
Object {
|
||||
"index_jobs": Array [
|
||||
Object {
|
||||
"indexer": "indexer-1@123",
|
||||
"indexer_args": Array [
|
||||
"arg-1",
|
||||
"arg-2",
|
||||
],
|
||||
"local_steps": Array [
|
||||
"command-1",
|
||||
"command-2",
|
||||
],
|
||||
"outfile": "outfile-1",
|
||||
"requestedEnvVars": Array [
|
||||
"ENV_VAR",
|
||||
"ENV_VAR_2",
|
||||
],
|
||||
"root": "root-1",
|
||||
"steps": Array [
|
||||
Object {
|
||||
"commands": Array [
|
||||
"pre-command-1",
|
||||
"pre-command-2",
|
||||
],
|
||||
"image": "indexer-1@123",
|
||||
"root": "pre-root-1",
|
||||
},
|
||||
],
|
||||
},
|
||||
Object {
|
||||
"indexer": "",
|
||||
"indexer_args": Array [],
|
||||
"local_steps": Array [],
|
||||
"outfile": "",
|
||||
"requestedEnvVars": Array [],
|
||||
"root": "",
|
||||
"steps": Array [],
|
||||
},
|
||||
],
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,51 @@
|
||||
import { uniqueId } from 'lodash'
|
||||
|
||||
import { AutoIndexJobDescriptionFields, AutoIndexLsifPreIndexFields } from '../../../../../graphql-operations'
|
||||
|
||||
import { InferenceFormData, InferenceFormJobStep, InferenceFormJob } from './types'
|
||||
|
||||
const autoIndexStepToFormStep = (step: AutoIndexLsifPreIndexFields): InferenceFormJobStep => ({
|
||||
root: step.root,
|
||||
image: step.image ?? '',
|
||||
commands: step.commands.map(arg => ({
|
||||
value: arg,
|
||||
meta: {
|
||||
id: uniqueId(),
|
||||
},
|
||||
})),
|
||||
meta: {
|
||||
id: uniqueId(),
|
||||
},
|
||||
})
|
||||
|
||||
const autoIndexJobToFormJob = (job: AutoIndexJobDescriptionFields): InferenceFormJob => ({
|
||||
root: job.root,
|
||||
indexer: job.indexer?.imageName ?? '',
|
||||
indexer_args: job.steps.index.indexerArgs.map(arg => ({
|
||||
value: arg,
|
||||
meta: {
|
||||
id: uniqueId(),
|
||||
},
|
||||
})),
|
||||
requestedEnvVars: (job.steps.index.requestedEnvVars ?? []).map(envVar => ({
|
||||
value: envVar,
|
||||
meta: {
|
||||
id: uniqueId(),
|
||||
},
|
||||
})),
|
||||
local_steps: job.steps.index.commands.map(command => ({
|
||||
value: command,
|
||||
meta: {
|
||||
id: uniqueId(),
|
||||
},
|
||||
})),
|
||||
outfile: job.steps.index.outfile ?? '',
|
||||
steps: job.steps.preIndex.map(autoIndexStepToFormStep),
|
||||
meta: {
|
||||
id: job.comparisonKey,
|
||||
},
|
||||
})
|
||||
|
||||
export const autoIndexJobsToFormData = (jobs: AutoIndexJobDescriptionFields[]): InferenceFormData => ({
|
||||
index_jobs: jobs.map(autoIndexJobToFormJob),
|
||||
})
|
||||
@ -0,0 +1,76 @@
|
||||
import AJV from 'ajv'
|
||||
import addFormats from 'ajv-formats'
|
||||
|
||||
import { AutoIndexJobDescriptionFields } from '../../../../../graphql-operations'
|
||||
import schema from '../../schema.json'
|
||||
|
||||
import { autoIndexJobsToFormData } from './auto-index-to-form-job'
|
||||
import { formDataToSchema } from './form-data-to-schema'
|
||||
|
||||
const ajv = new AJV({ strict: false })
|
||||
addFormats(ajv)
|
||||
|
||||
const mockAutoIndexJobs: AutoIndexJobDescriptionFields[] = [
|
||||
{
|
||||
comparisonKey: 'key-1',
|
||||
indexer: {
|
||||
imageName: 'indexer-1@123',
|
||||
name: 'indexer-1',
|
||||
url: 'https://example.com',
|
||||
key: 'key-indexer-1',
|
||||
},
|
||||
root: 'root-1',
|
||||
steps: {
|
||||
index: {
|
||||
commands: ['command-1', 'command-2'],
|
||||
indexerArgs: ['arg-1', 'arg-2'],
|
||||
requestedEnvVars: ['ENV_VAR', 'ENV_VAR_2'],
|
||||
outfile: 'outfile-1',
|
||||
},
|
||||
preIndex: [
|
||||
{
|
||||
commands: ['pre-command-1', 'pre-command-2'],
|
||||
root: 'pre-root-1',
|
||||
image: 'indexer-1@123',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
comparisonKey: 'key-2',
|
||||
indexer: null,
|
||||
root: '',
|
||||
steps: {
|
||||
index: {
|
||||
commands: [],
|
||||
indexerArgs: [],
|
||||
requestedEnvVars: null,
|
||||
outfile: null,
|
||||
},
|
||||
preIndex: [],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
describe('autoIndexToAutoIndexSchema', () => {
|
||||
it('should build form data as expected', () => {
|
||||
const formData = autoIndexJobsToFormData(mockAutoIndexJobs)
|
||||
expect(formData).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should build schema data as expected', () => {
|
||||
const formData = autoIndexJobsToFormData(mockAutoIndexJobs)
|
||||
const schemaData = formDataToSchema(formData)
|
||||
expect(schemaData).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it('should build valid schema data', () => {
|
||||
const formData = autoIndexJobsToFormData(mockAutoIndexJobs)
|
||||
const schemaData = formDataToSchema(formData)
|
||||
|
||||
// Validate form data against JSONSchema
|
||||
const isValid = ajv.validate(schema, schemaData)
|
||||
|
||||
expect(isValid).toBe(true)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,25 @@
|
||||
import { InferenceFormData, SchemaCompatibleInferenceFormData } from './types'
|
||||
|
||||
export const formDataToSchema = (formData: InferenceFormData): SchemaCompatibleInferenceFormData => {
|
||||
// Remove all meta information from the form data
|
||||
const cleanJobs = formData.index_jobs.map(job => {
|
||||
const { meta, ...cleanJob } = job
|
||||
return {
|
||||
...cleanJob,
|
||||
steps: job.steps.map(step => {
|
||||
const { meta, ...cleanStep } = step
|
||||
return {
|
||||
...cleanStep,
|
||||
commands: step.commands.map(command => command.value),
|
||||
}
|
||||
}),
|
||||
indexer_args: job.indexer_args.map(arg => arg.value),
|
||||
requestedEnvVars: job.requestedEnvVars.map(envVar => envVar.value),
|
||||
local_steps: job.local_steps.map(step => step.value),
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
index_jobs: cleanJobs,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
interface SchemaCompatibleInferenceFormStep {
|
||||
root: string
|
||||
image: string
|
||||
commands: string[]
|
||||
}
|
||||
|
||||
interface SchemaCompatibleInferenceFormJob {
|
||||
root: string
|
||||
indexer: string
|
||||
indexer_args: string[]
|
||||
requestedEnvVars: string[]
|
||||
local_steps: string[]
|
||||
outfile: string
|
||||
steps: SchemaCompatibleInferenceFormStep[]
|
||||
}
|
||||
|
||||
export interface SchemaCompatibleInferenceFormData {
|
||||
index_jobs: SchemaCompatibleInferenceFormJob[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Form data with additional metadata that is unrelated to submission
|
||||
*/
|
||||
interface MetaIdentifier {
|
||||
meta: {
|
||||
id: string
|
||||
}
|
||||
}
|
||||
|
||||
// InferenceJobs only return a unique ID for the actual job, not values within the job.
|
||||
// As we want to build a dynamic form, we need each array of values to have a unique id.
|
||||
export interface InferenceArrayValue extends MetaIdentifier {
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface InferenceFormJobStep extends Omit<SchemaCompatibleInferenceFormStep, 'commands'>, MetaIdentifier {
|
||||
commands: InferenceArrayValue[]
|
||||
}
|
||||
|
||||
export interface InferenceFormJob
|
||||
extends Omit<SchemaCompatibleInferenceFormJob, 'indexer_args' | 'requestedEnvVars' | 'local_steps' | 'steps'>,
|
||||
MetaIdentifier {
|
||||
indexer_args: InferenceArrayValue[]
|
||||
requestedEnvVars: InferenceArrayValue[]
|
||||
local_steps: InferenceArrayValue[]
|
||||
steps: InferenceFormJobStep[]
|
||||
}
|
||||
|
||||
export interface InferenceFormData {
|
||||
index_jobs: InferenceFormJob[]
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
import { FunctionComponent, useCallback, useMemo, useState } from 'react'
|
||||
|
||||
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { useIsLightTheme } from '@sourcegraph/shared/src/theme'
|
||||
import { screenReaderAnnounce, ErrorAlert, Button } from '@sourcegraph/wildcard'
|
||||
|
||||
import { AuthenticatedUser } from '../../../../../auth'
|
||||
import { SaveToolbar, SaveToolbarProps, SaveToolbarPropsGenerator } from '../../../../../components/SaveToolbar'
|
||||
import { DynamicallyImportedMonacoSettingsEditor } from '../../../../../settings/DynamicallyImportedMonacoSettingsEditor'
|
||||
import { INFERENCE_SCRIPT } from '../../hooks/useInferenceScript'
|
||||
import { useUpdateInferenceScript } from '../../hooks/useUpdateInferenceScript'
|
||||
|
||||
export interface InferenceScriptEditorProps extends TelemetryProps {
|
||||
script: string
|
||||
authenticatedUser: AuthenticatedUser | null
|
||||
setPreviewScript: (script: string) => void
|
||||
previewDisabled: boolean
|
||||
setTab: (index: number) => void
|
||||
}
|
||||
|
||||
export const InferenceScriptEditor: FunctionComponent<InferenceScriptEditorProps> = ({
|
||||
script: inferenceScript,
|
||||
setPreviewScript,
|
||||
previewDisabled,
|
||||
setTab,
|
||||
authenticatedUser,
|
||||
telemetryService,
|
||||
}) => {
|
||||
const { updateInferenceScript, isUpdating, updatingError } = useUpdateInferenceScript()
|
||||
|
||||
const save = useCallback(
|
||||
async (script: string) =>
|
||||
updateInferenceScript({
|
||||
variables: { script },
|
||||
refetchQueries: [INFERENCE_SCRIPT],
|
||||
}).then(() => {
|
||||
screenReaderAnnounce('Saved successfully')
|
||||
setDirty(false)
|
||||
}),
|
||||
[updateInferenceScript]
|
||||
)
|
||||
|
||||
const [dirty, setDirty] = useState<boolean>()
|
||||
const isLightTheme = useIsLightTheme()
|
||||
|
||||
const customToolbar = useMemo<{
|
||||
saveToolbar: FunctionComponent<SaveToolbarProps>
|
||||
propsGenerator: SaveToolbarPropsGenerator<{}>
|
||||
}>(
|
||||
() => ({
|
||||
saveToolbar: props => (
|
||||
<SaveToolbar childrenPosition="start" {...props}>
|
||||
<Button variant="success" className="mr-2" onClick={() => setTab(1)} disabled={previewDisabled}>
|
||||
Preview
|
||||
</Button>
|
||||
</SaveToolbar>
|
||||
),
|
||||
propsGenerator: props => {
|
||||
const mergedProps = {
|
||||
...props,
|
||||
loading: isUpdating,
|
||||
}
|
||||
mergedProps.willShowError = () => !mergedProps.saving
|
||||
mergedProps.saveDiscardDisabled = () => mergedProps.saving || !dirty
|
||||
|
||||
return mergedProps
|
||||
},
|
||||
}),
|
||||
[dirty, isUpdating, previewDisabled, setTab]
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
{updatingError && <ErrorAlert prefix="Error saving index configuration" error={updatingError} />}
|
||||
<DynamicallyImportedMonacoSettingsEditor
|
||||
value={inferenceScript}
|
||||
language="lua"
|
||||
canEdit={authenticatedUser?.siteAdmin}
|
||||
readOnly={!authenticatedUser?.siteAdmin}
|
||||
onSave={save}
|
||||
onChange={setPreviewScript}
|
||||
saving={isUpdating}
|
||||
height={600}
|
||||
isLightTheme={isLightTheme}
|
||||
telemetryService={telemetryService}
|
||||
customSaveToolbar={authenticatedUser?.siteAdmin ? customToolbar : undefined}
|
||||
onDirtyChange={setDirty}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
.container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.action-input {
|
||||
margin-right: 0.5rem;
|
||||
width: 25rem;
|
||||
flex: 1;
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
import React, { useEffect } from 'react'
|
||||
|
||||
import { useLazyQuery } from '@sourcegraph/http-client'
|
||||
import {
|
||||
LoadingSpinner,
|
||||
ErrorAlert,
|
||||
Input,
|
||||
Button,
|
||||
getDefaultInputProps,
|
||||
useField,
|
||||
useForm,
|
||||
Form,
|
||||
Label,
|
||||
} from '@sourcegraph/wildcard'
|
||||
|
||||
import {
|
||||
GetRepoIdResult,
|
||||
GetRepoIdVariables,
|
||||
InferAutoIndexJobsForRepoResult,
|
||||
InferAutoIndexJobsForRepoVariables,
|
||||
} from '../../../../../graphql-operations'
|
||||
import { RepositoryField } from '../../../../insights/components'
|
||||
import { InferenceForm } from '../inference-form/InferenceForm'
|
||||
|
||||
import { GET_REPO_ID, INFER_JOBS_SCRIPT } from './backend'
|
||||
|
||||
import styles from './InferenceScriptPreview.module.scss'
|
||||
|
||||
interface InferenceScriptPreviewFormValues {
|
||||
repository: string
|
||||
}
|
||||
|
||||
interface InferenceScriptPreviewProps {
|
||||
active: boolean
|
||||
script: string
|
||||
setTab: (index: number) => void
|
||||
}
|
||||
|
||||
export const InferenceScriptPreview: React.FunctionComponent<InferenceScriptPreviewProps> = ({ active, script }) => {
|
||||
const [getRepoId, repoData] = useLazyQuery<GetRepoIdResult, GetRepoIdVariables>(GET_REPO_ID, {})
|
||||
const [inferJobs, { data, loading, error }] = useLazyQuery<
|
||||
InferAutoIndexJobsForRepoResult,
|
||||
InferAutoIndexJobsForRepoVariables
|
||||
>(INFER_JOBS_SCRIPT, {})
|
||||
|
||||
const form = useForm<InferenceScriptPreviewFormValues>({
|
||||
initialValues: { repository: '' },
|
||||
onSubmit: async ({ repository }) => getRepoId({ variables: { name: repository } }),
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
const id = repoData?.data?.repository?.id
|
||||
|
||||
if (active && id) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
inferJobs({ variables: { repository: id, script, rev: null }, fetchPolicy: 'cache-first' })
|
||||
}
|
||||
}, [active, inferJobs, repoData, script])
|
||||
|
||||
const repository = useField({
|
||||
name: 'repository',
|
||||
formApi: form.formAPI,
|
||||
})
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<Form className={styles.actionContainer} ref={form.ref} noValidate={true} onSubmit={form.handleSubmit}>
|
||||
<Label id="preview-label">Run your script against a repository</Label>
|
||||
<div className="d-flex align-items-center">
|
||||
<Input
|
||||
as={RepositoryField}
|
||||
required={true}
|
||||
autoFocus={true}
|
||||
aria-label="Repository"
|
||||
placeholder="Example: github.com/sourcegraph/sourcegraph"
|
||||
{...getDefaultInputProps(repository)}
|
||||
className={styles.actionInput}
|
||||
/>
|
||||
|
||||
<Button variant="success" type="submit">
|
||||
Preview results
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
{loading ? (
|
||||
<LoadingSpinner className="d-block mx-auto mt-3" />
|
||||
) : error ? (
|
||||
<ErrorAlert error={error} />
|
||||
) : data ? (
|
||||
<InferenceForm jobs={data.inferAutoIndexJobsForRepo} readOnly={true} />
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
import { gql } from '@sourcegraph/http-client'
|
||||
|
||||
export const GET_REPO_ID = gql`
|
||||
query GetRepoId($name: String!) {
|
||||
repository(name: $name) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export const INFER_JOBS_SCRIPT = gql`
|
||||
query InferAutoIndexJobsForRepo($repository: ID!, $rev: String, $script: String) {
|
||||
inferAutoIndexJobsForRepo(repository: $repository, rev: $rev, script: $script) {
|
||||
...AutoIndexJobDescriptionFields
|
||||
}
|
||||
}
|
||||
|
||||
fragment AutoIndexJobDescriptionFields on AutoIndexJobDescription {
|
||||
comparisonKey
|
||||
root
|
||||
indexer {
|
||||
key
|
||||
imageName
|
||||
name
|
||||
url
|
||||
}
|
||||
steps {
|
||||
...AutoIndexLsifIndexStepsFields
|
||||
}
|
||||
}
|
||||
|
||||
fragment AutoIndexLsifIndexStepsFields on IndexSteps {
|
||||
preIndex {
|
||||
...AutoIndexLsifPreIndexFields
|
||||
}
|
||||
index {
|
||||
...AutoIndexLsifIndexFields
|
||||
}
|
||||
}
|
||||
|
||||
fragment AutoIndexLsifPreIndexFields on PreIndexStep {
|
||||
root
|
||||
image
|
||||
commands
|
||||
}
|
||||
|
||||
fragment AutoIndexLsifIndexFields on IndexStep {
|
||||
indexerArgs
|
||||
outfile
|
||||
commands
|
||||
requestedEnvVars
|
||||
}
|
||||
`
|
||||
@ -1,81 +0,0 @@
|
||||
import { ApolloError, gql, useQuery } from '@apollo/client'
|
||||
|
||||
import { InferAutoIndexJobsForRepoVariables, InferAutoIndexJobsForRepoResult } from '../../../../graphql-operations'
|
||||
|
||||
interface InferJobsScriptResult {
|
||||
data?: InferAutoIndexJobsForRepoResult
|
||||
loading: boolean
|
||||
error: ApolloError | undefined
|
||||
}
|
||||
|
||||
export const INFER_JOBS_SCRIPT = gql`
|
||||
query InferAutoIndexJobsForRepo($repository: ID!, $rev: String, $script: String) {
|
||||
inferAutoIndexJobsForRepo(repository: $repository, rev: $rev, script: $script) {
|
||||
...AutoIndexJobDescriptionFields
|
||||
}
|
||||
}
|
||||
|
||||
fragment AutoIndexJobDescriptionFields on AutoIndexJobDescription {
|
||||
root
|
||||
indexer {
|
||||
name
|
||||
url
|
||||
}
|
||||
steps {
|
||||
...LsifIndexStepsFields
|
||||
}
|
||||
}
|
||||
|
||||
fragment LsifIndexStepsFields on IndexSteps {
|
||||
setup {
|
||||
...ExecutionLogEntryFields
|
||||
}
|
||||
preIndex {
|
||||
root
|
||||
image
|
||||
commands
|
||||
logEntry {
|
||||
...ExecutionLogEntryFields
|
||||
}
|
||||
}
|
||||
index {
|
||||
indexerArgs
|
||||
outfile
|
||||
logEntry {
|
||||
...ExecutionLogEntryFields
|
||||
}
|
||||
}
|
||||
upload {
|
||||
...ExecutionLogEntryFields
|
||||
}
|
||||
teardown {
|
||||
...ExecutionLogEntryFields
|
||||
}
|
||||
}
|
||||
|
||||
fragment ExecutionLogEntryFields on ExecutionLogEntry {
|
||||
key
|
||||
command
|
||||
startTime
|
||||
exitCode
|
||||
out
|
||||
durationMilliseconds
|
||||
}
|
||||
`
|
||||
|
||||
export const useInferJobs = ({
|
||||
variables,
|
||||
}: {
|
||||
variables: InferAutoIndexJobsForRepoVariables
|
||||
}): InferJobsScriptResult => {
|
||||
const { data, loading, error } = useQuery<InferAutoIndexJobsForRepoResult>(INFER_JOBS_SCRIPT, {
|
||||
variables,
|
||||
nextFetchPolicy: 'cache-first',
|
||||
})
|
||||
|
||||
return {
|
||||
data,
|
||||
loading,
|
||||
error,
|
||||
}
|
||||
}
|
||||
@ -1,10 +1,13 @@
|
||||
import { FunctionComponent } from 'react'
|
||||
import { FunctionComponent, useState, useCallback } from 'react'
|
||||
|
||||
import { TelemetryProps } from '@sourcegraph/shared/src/telemetry/telemetryService'
|
||||
import { ErrorAlert, LoadingSpinner, PageHeader, Tab, TabList, TabPanel, TabPanels, Tabs } from '@sourcegraph/wildcard'
|
||||
|
||||
import { AuthenticatedUser } from '../../../../auth'
|
||||
import { PageTitle } from '../../../../components/PageTitle'
|
||||
import { InferenceScriptEditor } from '../components/InferenceScriptEditor'
|
||||
import { InferenceScriptEditor } from '../components/inference-script/InferenceScriptEditor'
|
||||
import { InferenceScriptPreview } from '../components/inference-script/InferenceScriptPreview'
|
||||
import { useInferenceScript } from '../hooks/useInferenceScript'
|
||||
|
||||
export interface CodeIntelInferenceConfigurationPageProps extends TelemetryProps {
|
||||
authenticatedUser: AuthenticatedUser | null
|
||||
@ -13,10 +16,59 @@ export interface CodeIntelInferenceConfigurationPageProps extends TelemetryProps
|
||||
export const CodeIntelInferenceConfigurationPage: FunctionComponent<CodeIntelInferenceConfigurationPageProps> = ({
|
||||
authenticatedUser,
|
||||
...props
|
||||
}) => (
|
||||
<>
|
||||
<PageTitle title="Code graph index configuration inference" />
|
||||
}) => {
|
||||
const [activeTabIndex, setActiveTabIndex] = useState<number>(0)
|
||||
const { inferenceScript, loadingScript, fetchError } = useInferenceScript()
|
||||
const [previewScript, setPreviewScript] = useState<string | null>(null)
|
||||
const setTab = useCallback((index: number) => {
|
||||
setActiveTabIndex(index)
|
||||
}, [])
|
||||
|
||||
<InferenceScriptEditor authenticatedUser={authenticatedUser} {...props} />
|
||||
</>
|
||||
)
|
||||
const inferencePreview = previewScript !== null ? previewScript : inferenceScript
|
||||
const previewDisabled = inferencePreview === ''
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageTitle title="Code graph inference script" />
|
||||
<PageHeader
|
||||
headingElement="h2"
|
||||
path={[
|
||||
{
|
||||
text: <>Code graph inference script</>,
|
||||
},
|
||||
]}
|
||||
description="Lua script that emits complete and/or partial auto-indexing job specifications."
|
||||
className="mb-3"
|
||||
/>
|
||||
{fetchError && <ErrorAlert prefix="Error fetching inference script" error={fetchError} />}
|
||||
{loadingScript && <LoadingSpinner />}
|
||||
<Tabs size="large" index={activeTabIndex} onChange={setTab}>
|
||||
<TabList>
|
||||
<Tab key="script">Script</Tab>
|
||||
<Tab key="preview" disabled={previewDisabled}>
|
||||
Preview
|
||||
</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<InferenceScriptEditor
|
||||
script={inferenceScript}
|
||||
authenticatedUser={authenticatedUser}
|
||||
setPreviewScript={setPreviewScript}
|
||||
previewDisabled={previewDisabled}
|
||||
setTab={setTab}
|
||||
{...props}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<InferenceScriptPreview
|
||||
active={activeTabIndex === 1}
|
||||
script={inferencePreview}
|
||||
setTab={setTab}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -338,6 +338,7 @@
|
||||
"@codemirror/lang-json": "^6.0.0",
|
||||
"@codemirror/lang-markdown": "^6.0.0",
|
||||
"@codemirror/language": "^6.2.0",
|
||||
"@codemirror/legacy-modes": "^6.3.1",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/search": "^6.0.1",
|
||||
"@codemirror/state": "^6.2.0",
|
||||
|
||||
@ -29,6 +29,7 @@ importers:
|
||||
'@codemirror/lang-json': ^6.0.0
|
||||
'@codemirror/lang-markdown': ^6.0.0
|
||||
'@codemirror/language': ^6.2.0
|
||||
'@codemirror/legacy-modes': ^6.3.1
|
||||
'@codemirror/lint': ^6.0.0
|
||||
'@codemirror/search': ^6.0.1
|
||||
'@codemirror/state': ^6.2.0
|
||||
@ -402,6 +403,7 @@ importers:
|
||||
'@codemirror/lang-json': 6.0.0
|
||||
'@codemirror/lang-markdown': 6.0.0
|
||||
'@codemirror/language': 6.2.0
|
||||
'@codemirror/legacy-modes': 6.3.1
|
||||
'@codemirror/lint': 6.0.0
|
||||
'@codemirror/search': 6.0.1
|
||||
'@codemirror/state': 6.2.0
|
||||
@ -3916,6 +3918,12 @@ packages:
|
||||
style-mod: 4.0.0
|
||||
dev: false
|
||||
|
||||
/@codemirror/legacy-modes/6.3.1:
|
||||
resolution: {integrity: sha512-icXmCs4Mhst2F8mE0TNpmG6l7YTj1uxam3AbZaFaabINH5oWAdg2CfR/PVi+d/rqxJ+TuTnvkKK5GILHrNThtw==}
|
||||
dependencies:
|
||||
'@codemirror/language': 6.2.0
|
||||
dev: false
|
||||
|
||||
/@codemirror/lint/6.0.0:
|
||||
resolution: {integrity: sha512-nUUXcJW1Xp54kNs+a1ToPLK8MadO0rMTnJB8Zk4Z8gBdrN0kqV7uvUraU/T2yqg+grDNR38Vmy/MrhQN/RgwiA==}
|
||||
dependencies:
|
||||
@ -13040,7 +13048,7 @@ packages:
|
||||
'@babel/core': 7.20.5
|
||||
find-cache-dir: 3.3.2
|
||||
schema-utils: 4.0.0
|
||||
webpack: 5.75.0_4wpfvhs5obqvjbkpkxtqpnn5oe
|
||||
webpack: 5.75.0_cf7cgeqdkm72g3fdehkr7aaod4
|
||||
dev: true
|
||||
|
||||
/babel-plugin-add-react-displayname/0.0.5:
|
||||
@ -15295,7 +15303,7 @@ packages:
|
||||
postcss-modules-values: 4.0.0_postcss@8.4.21
|
||||
postcss-value-parser: 4.2.0
|
||||
semver: 7.3.8
|
||||
webpack: 5.75.0_esbuild@0.17.8
|
||||
webpack: 5.75.0_cf7cgeqdkm72g3fdehkr7aaod4
|
||||
|
||||
/css-minimizer-webpack-plugin/4.2.2_6hyl5w2uqyeivowpusuiulbmoy:
|
||||
resolution: {integrity: sha512-s3Of/4jKfw1Hj9CxEO1E5oXhQAxlayuHO2y/ML+C6I9sQ7FdzfEV6QgMLN3vI+qFsjJGIAFLKtQK7t8BOXAIyA==}
|
||||
@ -20113,7 +20121,7 @@ packages:
|
||||
lodash: 4.17.21
|
||||
pretty-error: 4.0.0
|
||||
tapable: 2.2.1
|
||||
webpack: 5.75.0_yrajokeiryagdtuqucziuwdxti
|
||||
webpack: 5.75.0_cf7cgeqdkm72g3fdehkr7aaod4
|
||||
dev: true
|
||||
|
||||
/htmlparser2/6.1.0:
|
||||
@ -26920,7 +26928,7 @@ packages:
|
||||
klona: 2.0.5
|
||||
postcss: 8.4.21
|
||||
semver: 7.3.8
|
||||
webpack: 5.75.0_esbuild@0.17.8
|
||||
webpack: 5.75.0_cf7cgeqdkm72g3fdehkr7aaod4
|
||||
|
||||
/postcss-media-query-parser/0.2.3:
|
||||
resolution: {integrity: sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==}
|
||||
@ -29778,7 +29786,7 @@ packages:
|
||||
klona: 2.0.5
|
||||
neo-async: 2.6.2
|
||||
sass: 1.32.4
|
||||
webpack: 5.75.0_esbuild@0.17.8
|
||||
webpack: 5.75.0_cf7cgeqdkm72g3fdehkr7aaod4
|
||||
|
||||
/sass/1.32.4:
|
||||
resolution: {integrity: sha512-N0BT0PI/t3+gD8jKa83zJJUb7ssfQnRRfqN+GIErokW6U4guBpfYl8qYB+OFLEho+QvnV5ZH1R9qhUC/Z2Ch9w==}
|
||||
@ -31677,7 +31685,7 @@ packages:
|
||||
schema-utils: 3.1.1
|
||||
serialize-javascript: 6.0.0
|
||||
terser: 5.16.1
|
||||
webpack: 5.75.0_esbuild@0.17.8
|
||||
webpack: 5.75.0_cf7cgeqdkm72g3fdehkr7aaod4
|
||||
|
||||
/terser-webpack-plugin/5.3.6_oa2ac2s5skpozptxi7rtd3zsrm:
|
||||
resolution: {integrity: sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==}
|
||||
@ -31705,32 +31713,6 @@ packages:
|
||||
webpack: 5.75.0_yrajokeiryagdtuqucziuwdxti
|
||||
dev: true
|
||||
|
||||
/terser-webpack-plugin/5.3.6_tqhwwsvj4cnhuarwr7flkmuvna:
|
||||
resolution: {integrity: sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ==}
|
||||
engines: {node: '>= 10.13.0'}
|
||||
peerDependencies:
|
||||
'@swc/core': '*'
|
||||
esbuild: '*'
|
||||
uglify-js: '*'
|
||||
webpack: ^5.1.0
|
||||
peerDependenciesMeta:
|
||||
'@swc/core':
|
||||
optional: true
|
||||
esbuild:
|
||||
optional: true
|
||||
uglify-js:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.17
|
||||
'@swc/core': 1.3.28
|
||||
esbuild: 0.17.8
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 3.1.1
|
||||
serialize-javascript: 6.0.0
|
||||
terser: 5.16.1
|
||||
webpack: 5.75.0_4wpfvhs5obqvjbkpkxtqpnn5oe
|
||||
dev: true
|
||||
|
||||
/terser/4.8.1:
|
||||
resolution: {integrity: sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
@ -33759,46 +33741,6 @@ packages:
|
||||
resolution: {integrity: sha512-5NUqC2JquIL2pBAAo/VfBP6KuGkHIZQXW/lNKupLPfhViwh8wNsu0BObtl09yuKZszeEUfbXz8xhrHvSG16Nqw==}
|
||||
dev: true
|
||||
|
||||
/webpack/5.75.0_4wpfvhs5obqvjbkpkxtqpnn5oe:
|
||||
resolution: {integrity: sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
webpack-cli: '*'
|
||||
peerDependenciesMeta:
|
||||
webpack-cli:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@types/eslint-scope': 3.7.3
|
||||
'@types/estree': 0.0.51
|
||||
'@webassemblyjs/ast': 1.11.1
|
||||
'@webassemblyjs/wasm-edit': 1.11.1
|
||||
'@webassemblyjs/wasm-parser': 1.11.1
|
||||
acorn: 8.8.1
|
||||
acorn-import-assertions: 1.8.0_acorn@8.8.1
|
||||
browserslist: 4.21.4
|
||||
chrome-trace-event: 1.0.2
|
||||
enhanced-resolve: 5.10.0
|
||||
es-module-lexer: 0.9.3
|
||||
eslint-scope: 5.1.1
|
||||
events: 3.3.0
|
||||
glob-to-regexp: 0.4.1
|
||||
graceful-fs: 4.2.10
|
||||
json-parse-even-better-errors: 2.3.1
|
||||
loader-runner: 4.2.0
|
||||
mime-types: 2.1.35
|
||||
neo-async: 2.6.2
|
||||
schema-utils: 3.1.1
|
||||
tapable: 2.2.1
|
||||
terser-webpack-plugin: 5.3.6_tqhwwsvj4cnhuarwr7flkmuvna
|
||||
watchpack: 2.4.0
|
||||
webpack-sources: 3.2.3
|
||||
transitivePeerDependencies:
|
||||
- '@swc/core'
|
||||
- esbuild
|
||||
- uglify-js
|
||||
dev: true
|
||||
|
||||
/webpack/5.75.0_cf7cgeqdkm72g3fdehkr7aaod4:
|
||||
resolution: {integrity: sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
@ -33877,6 +33819,7 @@ packages:
|
||||
- '@swc/core'
|
||||
- esbuild
|
||||
- uglify-js
|
||||
dev: false
|
||||
|
||||
/webpack/5.75.0_yrajokeiryagdtuqucziuwdxti:
|
||||
resolution: {integrity: sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user