mirror of
https://github.com/harness/drone.git
synced 2025-05-31 11:43:15 +00:00
Merge branch 'add-pipeline-editor' of _OKE5H2PQKOUfzFFDuD4FA/default/CODE/gitness (#431)
This commit is contained in:
commit
4138ebea1c
@ -221,6 +221,18 @@ module.exports = {
|
||||
'vb',
|
||||
'xml',
|
||||
'yaml'
|
||||
],
|
||||
globalAPI: true,
|
||||
filename: '[name].worker.[contenthash:6].js',
|
||||
customLanguages: [
|
||||
{
|
||||
label: 'yaml',
|
||||
entry: 'monaco-yaml',
|
||||
worker: {
|
||||
id: 'monaco-yaml/yamlWorker',
|
||||
entry: 'monaco-yaml/yaml.worker'
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
]
|
||||
|
@ -65,6 +65,7 @@
|
||||
"moment": "^2.25.3",
|
||||
"monaco-editor": "^0.40.0",
|
||||
"monaco-editor-webpack-plugin": "^7.1.0",
|
||||
"monaco-yaml": "^4.0.4",
|
||||
"qs": "^6.9.4",
|
||||
"react": "^17.0.2",
|
||||
"react-complex-tree": "^1.1.11",
|
||||
|
@ -46,7 +46,7 @@ export interface CODERoutes {
|
||||
toCODESpaceAccessControl: (args: Required<Pick<CODEProps, 'space'>>) => string
|
||||
toCODESpaceSettings: (args: Required<Pick<CODEProps, 'space'>>) => string
|
||||
toCODEPipelines: (args: Required<Pick<CODEProps, 'repoPath'>>) => string
|
||||
toCODEPipelinesNew: (args: Required<Pick<CODEProps, 'space'>>) => string
|
||||
toCODEPipelineEdit: (args: Required<Pick<CODEProps, 'repoPath' | 'pipeline'>>) => string
|
||||
toCODESecrets: (args: Required<Pick<CODEProps, 'space'>>) => string
|
||||
|
||||
toCODEGlobalSettings: () => string
|
||||
@ -97,7 +97,7 @@ export const routes: CODERoutes = {
|
||||
toCODESpaceAccessControl: ({ space }) => `/access-control/${space}`,
|
||||
toCODESpaceSettings: ({ space }) => `/settings/${space}`,
|
||||
toCODEPipelines: ({ repoPath }) => `/${repoPath}/pipelines`,
|
||||
toCODEPipelinesNew: ({ space }) => `/pipelines/${space}/new`,
|
||||
toCODEPipelineEdit: ({ repoPath, pipeline }) => `/${repoPath}/pipelines/${pipeline}/edit`,
|
||||
toCODESecrets: ({ space }) => `/secrets/${space}`,
|
||||
|
||||
toCODEGlobalSettings: () => '/settings',
|
||||
|
@ -31,7 +31,7 @@ import { useFeatureFlag } from 'hooks/useFeatureFlag'
|
||||
import ExecutionList from 'pages/ExecutionList/ExecutionList'
|
||||
import Execution from 'pages/Execution/Execution'
|
||||
import Secret from 'pages/Secret/Secret'
|
||||
import NewPipeline from 'pages/NewPipeline/NewPipeline'
|
||||
import AddUpdatePipeline from 'pages/AddUpdatePipeline/AddUpdatePipeline'
|
||||
|
||||
export const RouteDestinations: React.FC = React.memo(function RouteDestinations() {
|
||||
const { getString } = useStrings()
|
||||
@ -186,9 +186,9 @@ export const RouteDestinations: React.FC = React.memo(function RouteDestinations
|
||||
)}
|
||||
|
||||
{OPEN_SOURCE_PIPELINES && (
|
||||
<Route path={routes.toCODEPipelinesNew({ space: pathProps.space })} exact>
|
||||
<Route path={routes.toCODEPipelineEdit({ repoPath, pipeline: pathProps.pipeline })} exact>
|
||||
<LayoutWithSideNav title={getString('pageTitle.pipelines')}>
|
||||
<NewPipeline />
|
||||
<AddUpdatePipeline />
|
||||
</LayoutWithSideNav>
|
||||
</Route>
|
||||
)}
|
||||
|
@ -0,0 +1,17 @@
|
||||
.branchSelect {
|
||||
:global {
|
||||
.bp3-popover-wrapper {
|
||||
width: 100% !important;
|
||||
.bp3-popover-target {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
.bp3-button {
|
||||
justify-content: start;
|
||||
width: 100%;
|
||||
}
|
||||
.bp3-icon-chevron-down {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
3
web/src/components/NewPipelineModal/NewPipelineModal.module.scss.d.ts
vendored
Normal file
3
web/src/components/NewPipelineModal/NewPipelineModal.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const branchSelect: string
|
162
web/src/components/NewPipelineModal/NewPipelineModal.tsx
Normal file
162
web/src/components/NewPipelineModal/NewPipelineModal.tsx
Normal file
@ -0,0 +1,162 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
import { useMutate } from 'restful-react'
|
||||
import * as yup from 'yup'
|
||||
import { capitalize } from 'lodash'
|
||||
import {
|
||||
Button,
|
||||
ButtonVariation,
|
||||
Container,
|
||||
Dialog,
|
||||
FormInput,
|
||||
Formik,
|
||||
FormikForm,
|
||||
Layout,
|
||||
Text,
|
||||
useToaster
|
||||
} from '@harnessio/uicore'
|
||||
import { FontVariation } from '@harnessio/design-system'
|
||||
import { useModalHook } from 'hooks/useModalHook'
|
||||
import type { OpenapiCreatePipelineRequest, TypesPipeline, TypesRepository } from 'services/code'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { BranchTagSelect } from 'components/BranchTagSelect/BranchTagSelect'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { getErrorMessage } from 'utils/Utils'
|
||||
import { DEFAULT_YAML_PATH_PREFIX, DEFAULT_YAML_PATH_SUFFIX } from '../../pages/AddUpdatePipeline/Constants'
|
||||
|
||||
import css from './NewPipelineModal.module.scss'
|
||||
|
||||
interface FormData {
|
||||
name: string
|
||||
branch: string
|
||||
yamlPath: string
|
||||
}
|
||||
|
||||
const useNewPipelineModal = () => {
|
||||
const { routes } = useAppContext()
|
||||
const { getString } = useStrings()
|
||||
const history = useHistory()
|
||||
const { showError } = useToaster()
|
||||
const [repo, setRepo] = useState<TypesRepository | undefined>()
|
||||
const repoPath = useMemo(() => repo?.path || '', [repo])
|
||||
|
||||
const { mutate: savePipeline } = useMutate<TypesPipeline>({
|
||||
verb: 'POST',
|
||||
path: `/api/v1/repos/${repoPath}/+/pipelines`
|
||||
})
|
||||
|
||||
const handleCreatePipeline = (formData: FormData): void => {
|
||||
const { name, branch, yamlPath } = formData
|
||||
try {
|
||||
const payload: OpenapiCreatePipelineRequest = {
|
||||
config_path: yamlPath,
|
||||
default_branch: branch,
|
||||
uid: name
|
||||
}
|
||||
savePipeline(payload, { pathParams: { path: `/api/v1/repos/${repoPath}/+/pipelines` } })
|
||||
.then(() => {
|
||||
hideModal()
|
||||
history.push(routes.toCODEPipelineEdit({ repoPath, pipeline: name }))
|
||||
})
|
||||
.catch(error => {
|
||||
showError(getErrorMessage(error), 0, 'pipelines.failedToCreatePipeline')
|
||||
})
|
||||
} catch (exception) {
|
||||
showError(getErrorMessage(exception), 0, 'pipelines.failedToCreatePipeline')
|
||||
}
|
||||
}
|
||||
|
||||
const [openModal, hideModal] = useModalHook(() => {
|
||||
const onClose = () => {
|
||||
hideModal()
|
||||
}
|
||||
return (
|
||||
<Dialog isOpen enforceFocus={false} onClose={onClose} title={getString('pipelines.createNewPipeline')}>
|
||||
<Formik<FormData>
|
||||
initialValues={{ name: '', branch: repo?.default_branch || '', yamlPath: '' }}
|
||||
formName="createNewPipeline"
|
||||
enableReinitialize={true}
|
||||
validationSchema={yup.object().shape({
|
||||
name: yup
|
||||
.string()
|
||||
.trim()
|
||||
.required(`${getString('name')} ${getString('isRequired')}`),
|
||||
branch: yup
|
||||
.string()
|
||||
.trim()
|
||||
.required(`${getString('branch')} ${getString('isRequired')}`),
|
||||
yamlPath: yup
|
||||
.string()
|
||||
.trim()
|
||||
.required(`${getString('pipelines.yamlPath')} ${getString('isRequired')}`)
|
||||
})}
|
||||
validateOnChange
|
||||
validateOnBlur
|
||||
onSubmit={handleCreatePipeline}>
|
||||
{formik => {
|
||||
return (
|
||||
<FormikForm>
|
||||
<Layout.Vertical spacing="small">
|
||||
<Layout.Vertical spacing="small">
|
||||
<FormInput.Text
|
||||
name="name"
|
||||
label={getString('name')}
|
||||
placeholder={getString('pipelines.enterPipelineName')}
|
||||
inputGroup={{ autoFocus: true }}
|
||||
onChange={event => {
|
||||
const input = (event.target as HTMLInputElement)?.value
|
||||
formik?.setFieldValue('name', input)
|
||||
if (input) {
|
||||
// Keeping minimal validation for now, this could be much more exhaustive
|
||||
const path = input.trim().replace(/\s/g, '')
|
||||
formik?.setFieldValue(
|
||||
'yamlPath',
|
||||
DEFAULT_YAML_PATH_PREFIX.concat(path).concat(DEFAULT_YAML_PATH_SUFFIX)
|
||||
)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Layout.Vertical spacing="xsmall" padding={{ bottom: 'medium' }}>
|
||||
<Text font={{ variation: FontVariation.BODY }}>{capitalize(getString('branch'))}</Text>
|
||||
<Container className={css.branchSelect}>
|
||||
<BranchTagSelect
|
||||
gitRef={formik?.values?.branch || repo?.default_branch || ''}
|
||||
onSelect={(ref: string) => {
|
||||
formik?.setFieldValue('branch', ref)
|
||||
}}
|
||||
repoMetadata={repo || {}}
|
||||
disableBranchCreation
|
||||
disableViewAllBranches
|
||||
forBranchesOnly
|
||||
/>
|
||||
</Container>
|
||||
</Layout.Vertical>
|
||||
<FormInput.Text
|
||||
name="yamlPath"
|
||||
label={getString('pipelines.yamlPath')}
|
||||
placeholder={getString('pipelines.enterYAMLPath')}
|
||||
/>
|
||||
</Layout.Vertical>
|
||||
<Layout.Horizontal spacing="medium" width="100%">
|
||||
<Button variation={ButtonVariation.PRIMARY} text={getString('create')} type="submit" />
|
||||
<Button variation={ButtonVariation.SECONDARY} text={getString('cancel')} onClick={onClose} />
|
||||
</Layout.Horizontal>
|
||||
</Layout.Vertical>
|
||||
</FormikForm>
|
||||
)
|
||||
}}
|
||||
</Formik>
|
||||
</Dialog>
|
||||
)
|
||||
}, [repo])
|
||||
|
||||
return {
|
||||
openModal: ({ repoMetadata }: { repoMetadata?: TypesRepository }) => {
|
||||
setRepo(repoMetadata)
|
||||
openModal()
|
||||
},
|
||||
hideModal
|
||||
}
|
||||
}
|
||||
|
||||
export default useNewPipelineModal
|
53
web/src/components/PluginsPanel/PluginsPanel.module.scss
Normal file
53
web/src/components/PluginsPanel/PluginsPanel.module.scss
Normal file
@ -0,0 +1,53 @@
|
||||
.main {
|
||||
height: 100%;
|
||||
:global {
|
||||
.bp3-tabs {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mainTabPanel {
|
||||
&:global(.bp3-tab-panel[role='tabpanel']) {
|
||||
height: calc(100% - 30px);
|
||||
margin-top: 0 !important;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.pluginDetailsPanel {
|
||||
height: 100%;
|
||||
border-top: 1px solid var(--grey-100);
|
||||
}
|
||||
|
||||
.pluginIcon {
|
||||
background: var(--teal-200) !important;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.plugin {
|
||||
border: 1px solid var(--grey-100);
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.form {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
:global {
|
||||
.FormikForm--main {
|
||||
height: 100%;
|
||||
& > div {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.arrow {
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
9
web/src/components/PluginsPanel/PluginsPanel.module.scss.d.ts
vendored
Normal file
9
web/src/components/PluginsPanel/PluginsPanel.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const arrow: string
|
||||
export declare const form: string
|
||||
export declare const main: string
|
||||
export declare const mainTabPanel: string
|
||||
export declare const plugin: string
|
||||
export declare const pluginDetailsPanel: string
|
||||
export declare const pluginIcon: string
|
300
web/src/components/PluginsPanel/PluginsPanel.tsx
Normal file
300
web/src/components/PluginsPanel/PluginsPanel.tsx
Normal file
@ -0,0 +1,300 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react'
|
||||
import { Formik } from 'formik'
|
||||
import { capitalize, get } from 'lodash-es'
|
||||
import { useGet } from 'restful-react'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { Button, ButtonVariation, Container, FormInput, FormikForm, Layout, Tab, Tabs, Text } from '@harnessio/uicore'
|
||||
import { Color, FontVariation } from '@harnessio/design-system'
|
||||
import { Icon, type IconName } from '@harnessio/icons'
|
||||
import { LIST_FETCHING_LIMIT } from 'utils/Utils'
|
||||
import type { TypesPlugin } from 'services/code'
|
||||
import { YamlVersion } from 'pages/AddUpdatePipeline/Constants'
|
||||
|
||||
import css from './PluginsPanel.module.scss'
|
||||
|
||||
enum PluginCategory {
|
||||
Harness,
|
||||
Drone
|
||||
}
|
||||
|
||||
enum PluginPanelView {
|
||||
Category,
|
||||
Listing,
|
||||
Configuration
|
||||
}
|
||||
|
||||
interface PluginInterface {
|
||||
category: PluginCategory
|
||||
name: string
|
||||
description: string
|
||||
icon: IconName
|
||||
}
|
||||
|
||||
const PluginCategories: PluginInterface[] = [
|
||||
{
|
||||
category: PluginCategory.Harness,
|
||||
name: 'Run',
|
||||
description: 'Run a script on macOS, Linux, or Windows',
|
||||
icon: 'run-step'
|
||||
},
|
||||
{ category: PluginCategory.Drone, name: 'Drone', description: 'Run Drone plugins', icon: 'ci-infra' }
|
||||
]
|
||||
|
||||
const dronePluginSpecMockData = {
|
||||
inputs: {
|
||||
channel: {
|
||||
type: 'string'
|
||||
},
|
||||
token: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
steps: [
|
||||
{
|
||||
type: 'script',
|
||||
spec: {
|
||||
image: 'plugins/slack'
|
||||
},
|
||||
envs: {
|
||||
PLUGIN_CHANNEL: '<+inputs.channel>'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const runStepSpec = {
|
||||
inputs: {
|
||||
script: {
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface PluginsPanelInterface {
|
||||
version?: YamlVersion
|
||||
onPluginAddUpdate: (isUpdate: boolean, pluginFormData: Record<string, any>) => void
|
||||
}
|
||||
|
||||
export const PluginsPanel = ({ version = YamlVersion.V0, onPluginAddUpdate }: PluginsPanelInterface): JSX.Element => {
|
||||
const { getString } = useStrings()
|
||||
const [category, setCategory] = useState<PluginCategory>()
|
||||
const [panelView, setPanelView] = useState<PluginPanelView>(PluginPanelView.Category)
|
||||
const [plugin, setPlugin] = useState<TypesPlugin>()
|
||||
|
||||
const {
|
||||
data: plugins,
|
||||
loading,
|
||||
refetch: fetchPlugins
|
||||
} = useGet<TypesPlugin[]>({
|
||||
path: `/api/v1/plugins`,
|
||||
queryParams: {
|
||||
limit: LIST_FETCHING_LIMIT,
|
||||
page: 1
|
||||
},
|
||||
lazy: true
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (category === PluginCategory.Drone) {
|
||||
fetchPlugins()
|
||||
}
|
||||
}, [category])
|
||||
|
||||
const renderPluginCategories = (): JSX.Element => {
|
||||
return (
|
||||
<>
|
||||
{PluginCategories.map((item: PluginInterface) => {
|
||||
const { name, category, description, icon } = item
|
||||
return (
|
||||
<Layout.Horizontal
|
||||
onClick={() => {
|
||||
setCategory(category)
|
||||
if (category === PluginCategory.Drone) {
|
||||
setPanelView(PluginPanelView.Listing)
|
||||
} else if (category === PluginCategory.Harness) {
|
||||
setPlugin({ uid: getString('run') })
|
||||
setPanelView(PluginPanelView.Configuration)
|
||||
}
|
||||
}}
|
||||
key={category}
|
||||
padding={{ left: 'medium', right: 'medium', top: 'medium', bottom: 'medium' }}
|
||||
flex={{ justifyContent: 'flex-start' }}
|
||||
className={css.plugin}>
|
||||
<Container padding="small" className={css.pluginIcon}>
|
||||
<Icon name={icon} />
|
||||
</Container>
|
||||
<Layout.Vertical padding={{ left: 'small' }}>
|
||||
<Text color={Color.PRIMARY_7} font={{ variation: FontVariation.BODY2 }}>
|
||||
{name}
|
||||
</Text>
|
||||
<Text font={{ variation: FontVariation.SMALL }}>{description}</Text>
|
||||
</Layout.Vertical>
|
||||
</Layout.Horizontal>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const renderPlugins = useCallback((): JSX.Element => {
|
||||
return loading ? (
|
||||
<Container flex={{ justifyContent: 'center' }} padding="large">
|
||||
<Icon name="steps-spinner" color={Color.PRIMARY_7} size={25} />
|
||||
</Container>
|
||||
) : (
|
||||
<Layout.Vertical spacing="small" padding={{ top: 'small' }}>
|
||||
<Layout.Horizontal
|
||||
flex={{ justifyContent: 'flex-start', alignItems: 'center' }}
|
||||
spacing="small"
|
||||
padding={{ left: 'medium' }}>
|
||||
<Icon
|
||||
name="arrow-left"
|
||||
size={18}
|
||||
onClick={() => {
|
||||
setPanelView(PluginPanelView.Category)
|
||||
}}
|
||||
className={css.arrow}
|
||||
/>
|
||||
<Text font={{ variation: FontVariation.H5 }} flex={{ justifyContent: 'center' }}>
|
||||
{getString('plugins.addAPlugin', { category: PluginCategory[category as PluginCategory] })}
|
||||
</Text>
|
||||
</Layout.Horizontal>
|
||||
<Container>
|
||||
{plugins?.map((plugin: TypesPlugin) => {
|
||||
const { uid, description } = plugin
|
||||
return (
|
||||
<Layout.Horizontal
|
||||
flex={{ justifyContent: 'flex-start' }}
|
||||
padding={{ left: 'large', top: 'medium', bottom: 'medium', right: 'large' }}
|
||||
className={css.plugin}
|
||||
onClick={() => {
|
||||
setPanelView(PluginPanelView.Configuration)
|
||||
setPlugin(plugin)
|
||||
}}>
|
||||
<Icon name={'gear'} size={25} />
|
||||
<Layout.Vertical padding={{ left: 'small' }}>
|
||||
<Text font={{ variation: FontVariation.BODY2 }} color={Color.PRIMARY_7}>
|
||||
{uid}
|
||||
</Text>
|
||||
<Text font={{ variation: FontVariation.SMALL }}>{description}</Text>
|
||||
</Layout.Vertical>
|
||||
</Layout.Horizontal>
|
||||
)
|
||||
})}
|
||||
</Container>
|
||||
</Layout.Vertical>
|
||||
)
|
||||
}, [loading, plugins])
|
||||
|
||||
const renderPluginFormField = ({ name, type }: { name: string; type: 'string' }): JSX.Element => {
|
||||
return type === 'string' ? (
|
||||
<FormInput.Text
|
||||
name={name}
|
||||
label={<Text font={{ variation: FontVariation.FORM_INPUT_TEXT }}>{capitalize(name)}</Text>}
|
||||
style={{ width: '100%' }}
|
||||
key={name}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
}
|
||||
|
||||
const constructPayloadForYAMLInsertion = (isUpdate: boolean, pluginFormData: Record<string, any>) => {
|
||||
let constructedPayload = { ...pluginFormData }
|
||||
switch (category) {
|
||||
case PluginCategory.Drone:
|
||||
case PluginCategory.Harness:
|
||||
constructedPayload =
|
||||
version === YamlVersion.V1
|
||||
? { type: 'script', spec: constructedPayload }
|
||||
: { name: 'run step', commands: [get(constructedPayload, 'script', '')] }
|
||||
}
|
||||
onPluginAddUpdate?.(isUpdate, constructedPayload)
|
||||
}
|
||||
|
||||
const renderPluginConfigForm = useCallback((): JSX.Element => {
|
||||
// TODO obtain plugin input spec by parsing YAML
|
||||
const inputs = get(category === PluginCategory.Drone ? dronePluginSpecMockData : runStepSpec, 'inputs', {})
|
||||
return (
|
||||
<Layout.Vertical
|
||||
spacing="medium"
|
||||
margin={{ left: 'xxlarge', top: 'large', right: 'xxlarge', bottom: 'xxlarge' }}
|
||||
height="95%">
|
||||
<Layout.Horizontal spacing="small" flex={{ justifyContent: 'flex-start' }}>
|
||||
<Icon
|
||||
name="arrow-left"
|
||||
size={18}
|
||||
onClick={() => {
|
||||
setPlugin(undefined)
|
||||
if (category === PluginCategory.Drone) {
|
||||
setPanelView(PluginPanelView.Listing)
|
||||
} else if (category === PluginCategory.Harness) {
|
||||
setPanelView(PluginPanelView.Category)
|
||||
}
|
||||
}}
|
||||
className={css.arrow}
|
||||
/>
|
||||
{plugin?.uid ? (
|
||||
<Text font={{ variation: FontVariation.H5 }}>
|
||||
{getString('addLabel')} {plugin.uid} {getString('plugins.stepLabel')}
|
||||
</Text>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Layout.Horizontal>
|
||||
<Container className={css.form}>
|
||||
<Formik
|
||||
initialValues={{}}
|
||||
onSubmit={formData => {
|
||||
constructPayloadForYAMLInsertion(false, formData)
|
||||
}}>
|
||||
<FormikForm>
|
||||
<Layout.Vertical flex={{ alignItems: 'flex-start' }} height="100%">
|
||||
<Layout.Vertical width="100%">
|
||||
{Object.keys(inputs).map((field: string) => {
|
||||
const fieldType = get(inputs, `${field}.type`, '') as 'string'
|
||||
return renderPluginFormField({ name: field, type: fieldType })
|
||||
})}
|
||||
</Layout.Vertical>
|
||||
<Button variation={ButtonVariation.PRIMARY} text={getString('addLabel')} type="submit" />
|
||||
</Layout.Vertical>
|
||||
</FormikForm>
|
||||
</Formik>
|
||||
</Container>
|
||||
</Layout.Vertical>
|
||||
)
|
||||
}, [plugin, category])
|
||||
|
||||
const renderPluginsPanel = useCallback((): JSX.Element => {
|
||||
switch (panelView) {
|
||||
case PluginPanelView.Category:
|
||||
return renderPluginCategories()
|
||||
case PluginPanelView.Listing:
|
||||
return renderPlugins()
|
||||
case PluginPanelView.Configuration:
|
||||
return renderPluginConfigForm()
|
||||
default:
|
||||
return <></>
|
||||
}
|
||||
}, [loading, plugins, panelView, category])
|
||||
|
||||
return (
|
||||
<Container className={css.main}>
|
||||
<Tabs id={'pluginsPanel'} defaultSelectedTabId={'plugins'}>
|
||||
<Tab
|
||||
panelClassName={css.mainTabPanel}
|
||||
id="plugins"
|
||||
title={
|
||||
<Text
|
||||
font={{ variation: FontVariation.BODY2 }}
|
||||
padding={{ left: 'small', bottom: 'xsmall', top: 'xsmall' }}
|
||||
color={Color.PRIMARY_7}>
|
||||
{getString('plugins.title')}
|
||||
</Text>
|
||||
}
|
||||
panel={<Container className={css.pluginDetailsPanel}>{renderPluginsPanel()}</Container>}
|
||||
/>
|
||||
</Tabs>
|
||||
</Container>
|
||||
)
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
.branchSelect {
|
||||
:global {
|
||||
.bp3-popover-wrapper {
|
||||
width: 100% !important;
|
||||
.bp3-popover-target {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
.bp3-button {
|
||||
justify-content: start;
|
||||
width: 100%;
|
||||
}
|
||||
.bp3-icon-chevron-down {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
3
web/src/components/RunPipelineModal/RunPipelineModal.module.scss.d.ts
vendored
Normal file
3
web/src/components/RunPipelineModal/RunPipelineModal.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const branchSelect: string
|
130
web/src/components/RunPipelineModal/RunPipelineModal.tsx
Normal file
130
web/src/components/RunPipelineModal/RunPipelineModal.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { useHistory } from 'react-router'
|
||||
import { useMutate } from 'restful-react'
|
||||
import * as yup from 'yup'
|
||||
import { capitalize } from 'lodash'
|
||||
import { FontVariation } from '@harnessio/design-system'
|
||||
import {
|
||||
Button,
|
||||
ButtonVariation,
|
||||
Container,
|
||||
Dialog,
|
||||
Formik,
|
||||
FormikForm,
|
||||
Layout,
|
||||
Text,
|
||||
useToaster
|
||||
} from '@harnessio/uicore'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { useModalHook } from 'hooks/useModalHook'
|
||||
import type { CreateExecutionQueryParams, TypesExecution, TypesRepository } from 'services/code'
|
||||
import { getErrorMessage } from 'utils/Utils'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import { BranchTagSelect } from 'components/BranchTagSelect/BranchTagSelect'
|
||||
|
||||
import css from './RunPipelineModal.module.scss'
|
||||
|
||||
interface FormData {
|
||||
branch: string
|
||||
}
|
||||
|
||||
const useRunPipelineModal = () => {
|
||||
const { routes } = useAppContext()
|
||||
const { getString } = useStrings()
|
||||
const { showSuccess, showError, clear: clearToaster } = useToaster()
|
||||
const history = useHistory()
|
||||
const [repo, setRepo] = useState<TypesRepository>()
|
||||
const [pipeline, setPipeline] = useState<string>('')
|
||||
const repoPath = useMemo(() => repo?.path || '', [repo])
|
||||
|
||||
const { mutate: startExecution } = useMutate<TypesExecution>({
|
||||
verb: 'POST',
|
||||
path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}/executions`
|
||||
})
|
||||
|
||||
const runPipeline = (formData: FormData): void => {
|
||||
const { branch } = formData
|
||||
try {
|
||||
startExecution(
|
||||
{},
|
||||
{
|
||||
pathParams: { path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}/executions` },
|
||||
queryParams: { branch } as CreateExecutionQueryParams
|
||||
}
|
||||
)
|
||||
.then(response => {
|
||||
clearToaster()
|
||||
showSuccess(getString('pipelines.executionStarted'))
|
||||
if (response?.number && !isNaN(response.number)) {
|
||||
history.push(routes.toCODEExecution({ repoPath, pipeline, execution: response.number.toString() }))
|
||||
}
|
||||
hideModal()
|
||||
})
|
||||
.catch(error => {
|
||||
showError(getErrorMessage(error), 0, 'pipelines.executionCouldNotStart')
|
||||
})
|
||||
} catch (exception) {
|
||||
showError(getErrorMessage(exception), 0, 'pipelines.executionCouldNotStart')
|
||||
}
|
||||
}
|
||||
|
||||
const [openModal, hideModal] = useModalHook(() => {
|
||||
const onClose = () => {
|
||||
hideModal()
|
||||
}
|
||||
return (
|
||||
<Dialog isOpen enforceFocus={false} onClose={onClose} title={getString('pipelines.run')}>
|
||||
<Formik
|
||||
formName="run-pipeline-form"
|
||||
initialValues={{ branch: repo?.default_branch || '' }}
|
||||
validationSchema={yup.object().shape({
|
||||
branch: yup
|
||||
.string()
|
||||
.trim()
|
||||
.required(`${getString('branch')} ${getString('isRequired')}`)
|
||||
})}
|
||||
onSubmit={runPipeline}
|
||||
enableReinitialize>
|
||||
{formik => {
|
||||
return (
|
||||
<FormikForm>
|
||||
<Layout.Vertical spacing="medium">
|
||||
<Layout.Vertical spacing="xsmall" padding={{ bottom: 'medium' }}>
|
||||
<Text font={{ variation: FontVariation.BODY }}>{capitalize(getString('branch'))}</Text>
|
||||
<Container className={css.branchSelect}>
|
||||
<BranchTagSelect
|
||||
gitRef={formik?.values?.branch || repo?.default_branch || ''}
|
||||
onSelect={(ref: string) => {
|
||||
formik?.setFieldValue('branch', ref)
|
||||
}}
|
||||
repoMetadata={repo || {}}
|
||||
disableBranchCreation
|
||||
disableViewAllBranches
|
||||
forBranchesOnly
|
||||
/>
|
||||
</Container>
|
||||
</Layout.Vertical>
|
||||
<Layout.Horizontal spacing="medium">
|
||||
<Button variation={ButtonVariation.PRIMARY} type="submit" text={getString('pipelines.run')} />
|
||||
<Button variation={ButtonVariation.SECONDARY} text={getString('cancel')} onClick={onClose} />
|
||||
</Layout.Horizontal>
|
||||
</Layout.Vertical>
|
||||
</FormikForm>
|
||||
)
|
||||
}}
|
||||
</Formik>
|
||||
</Dialog>
|
||||
)
|
||||
}, [repo?.default_branch, pipeline])
|
||||
|
||||
return {
|
||||
openModal: ({ repoMetadata, pipeline }: { repoMetadata: TypesRepository; pipeline: string }) => {
|
||||
setRepo(repoMetadata)
|
||||
setPipeline(pipeline)
|
||||
openModal()
|
||||
},
|
||||
hideModal
|
||||
}
|
||||
}
|
||||
|
||||
export default useRunPipelineModal
|
@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react'
|
||||
import * as monaco from 'monaco-editor'
|
||||
import type monacoEditor from 'monaco-editor/esm/vs/editor/editor.api'
|
||||
import MonacoEditor, { MonacoDiffEditor } from 'react-monaco-editor'
|
||||
import { setDiagnosticsOptions } from 'monaco-yaml'
|
||||
import { noop } from 'lodash-es'
|
||||
import { SourceCodeEditorProps, PLAIN_TEXT } from 'utils/Utils'
|
||||
import { useEventListener } from 'hooks/useEventListener'
|
||||
@ -39,7 +40,8 @@ export default function MonacoSourceCodeEditor({
|
||||
height,
|
||||
autoHeight,
|
||||
wordWrap = true,
|
||||
onChange = noop
|
||||
onChange = noop,
|
||||
schema
|
||||
}: SourceCodeEditorProps) {
|
||||
const [editor, setEditor] = useState<monacoEditor.editor.IStandaloneCodeEditor>()
|
||||
const scrollbar = autoHeight ? 'hidden' : 'auto'
|
||||
@ -50,6 +52,24 @@ export default function MonacoSourceCodeEditor({
|
||||
monaco.languages.typescript?.typescriptDefaults?.setCompilerOptions?.(compilerOptions)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (language === 'yaml' && schema) {
|
||||
setDiagnosticsOptions({
|
||||
validate: true,
|
||||
enableSchemaRequest: false,
|
||||
hover: true,
|
||||
completion: true,
|
||||
schemas: [
|
||||
{
|
||||
fileMatch: ['*'],
|
||||
schema,
|
||||
uri: 'https://github.com/harness/harness-schema'
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
}, [language, schema])
|
||||
|
||||
useEventListener('resize', () => {
|
||||
editor?.layout({ width: 0, height: 0 })
|
||||
window.requestAnimationFrame(() => editor?.layout())
|
||||
|
@ -11,6 +11,7 @@ export interface StringsMap {
|
||||
add: string
|
||||
addComment: string
|
||||
addGitIgnore: string
|
||||
addLabel: string
|
||||
addLicense: string
|
||||
addMember: string
|
||||
addNewFile: string
|
||||
@ -190,6 +191,7 @@ export interface StringsMap {
|
||||
failedToCreateSpace: string
|
||||
failedToDeleteBranch: string
|
||||
failedToDeleteWebhook: string
|
||||
failedToSavePipeline: string
|
||||
fileDeleted: string
|
||||
fileTooLarge: string
|
||||
files: string
|
||||
@ -212,6 +214,7 @@ export interface StringsMap {
|
||||
'homepage.welcomeText': string
|
||||
in: string
|
||||
inactiveBranches: string
|
||||
isRequired: string
|
||||
leaveAComment: string
|
||||
license: string
|
||||
lineBreaks: string
|
||||
@ -318,11 +321,26 @@ export interface StringsMap {
|
||||
payloadUrl: string
|
||||
payloadUrlLabel: string
|
||||
pending: string
|
||||
'pipelines.createNewPipeline': string
|
||||
'pipelines.created': string
|
||||
'pipelines.editPipeline': string
|
||||
'pipelines.enterPipelineName': string
|
||||
'pipelines.enterYAMLPath': string
|
||||
'pipelines.executionCouldNotStart': string
|
||||
'pipelines.executionStarted': string
|
||||
'pipelines.failedToCreatePipeline': string
|
||||
'pipelines.lastExecution': string
|
||||
'pipelines.name': string
|
||||
'pipelines.newPipelineButton': string
|
||||
'pipelines.noData': string
|
||||
'pipelines.run': string
|
||||
'pipelines.saveAndRun': string
|
||||
'pipelines.time': string
|
||||
'pipelines.updated': string
|
||||
'pipelines.yamlPath': string
|
||||
'plugins.addAPlugin': string
|
||||
'plugins.stepLabel': string
|
||||
'plugins.title': string
|
||||
'pr.ableToMerge': string
|
||||
'pr.addDescription': string
|
||||
'pr.authorCommentedPR': string
|
||||
@ -455,6 +473,7 @@ export interface StringsMap {
|
||||
reviewerNotFound: string
|
||||
reviewers: string
|
||||
role: string
|
||||
run: string
|
||||
running: string
|
||||
samplePayloadUrl: string
|
||||
save: string
|
||||
|
@ -416,6 +416,7 @@ makeRequired: Make Required
|
||||
makeOptional: Make Optional
|
||||
remove: Remove
|
||||
required: Required
|
||||
isRequired: is required
|
||||
noneYet: None Yet
|
||||
noOptionalReviewers: No Optional Reviewers
|
||||
noRequiredReviewers: No Required Reviewers
|
||||
@ -487,6 +488,7 @@ showCommitHistory: Renamed from {file} - Show History
|
||||
noReviewers: No Reviewers
|
||||
assignPeople: Assign people
|
||||
add: Add +
|
||||
addLabel: Add
|
||||
users: Users
|
||||
findAUser: Find a user
|
||||
reviewerNotFound: 'Reviewer <strong>{{reviewer}}</strong> not found.'
|
||||
@ -586,6 +588,7 @@ spaceMemberships:
|
||||
memberAdded: Member added successfully.
|
||||
failedToCreateSpace: Failed to create Space. Please try again.
|
||||
failedToCreatePipeline: Failed to create Pipeline. Please try again.
|
||||
failedToSavePipeline: Failed to save Pipeline. Please try again.
|
||||
enterName: Enter the name
|
||||
createASpace: Create a space
|
||||
createSpace: Create Space
|
||||
@ -624,8 +627,20 @@ pipelines:
|
||||
noData: There are no pipelines :(
|
||||
newPipelineButton: New Pipeline
|
||||
name: Pipeline Name
|
||||
createNewPipeline: Create New Pipeline
|
||||
enterPipelineName: Enter pipeline name
|
||||
yamlPath: YAML Path
|
||||
enterYAMLPath: Enter YAML path
|
||||
failedToCreatePipeline: Failed to create pipeline
|
||||
saveAndRun: Save and Run
|
||||
editPipeline: Edit pipeline {{pipeline}}
|
||||
run: Run pipeline
|
||||
time: Time
|
||||
lastExecution: Last Execution
|
||||
created: Pipeline created successfully
|
||||
updated: Pipeline updated successfully
|
||||
executionStarted: Pipeline execution started successfully
|
||||
executionCouldNotStart: Failure while starting Pipeline execution
|
||||
executions:
|
||||
noData: There are no executions :(
|
||||
newExecutionButton: Run Pipeline
|
||||
@ -652,3 +667,8 @@ secrets:
|
||||
failedToDeleteSecret: Failed to delete Secret. Please try again.
|
||||
deleteSecret: Delete Secrets
|
||||
userUpdateSuccess: 'User updated successfully'
|
||||
run: Run
|
||||
plugins:
|
||||
title: Plugins
|
||||
addAPlugin: Add a {{category}} plugin
|
||||
stepLabel: step
|
||||
|
@ -0,0 +1,34 @@
|
||||
.main {
|
||||
--header-height: 96px;
|
||||
--heading-height: 58px;
|
||||
min-height: var(--page-height);
|
||||
background-color: var(--primary-bg) !important;
|
||||
|
||||
.layout {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.editorContainer {
|
||||
width: calc(100% - 30vw);
|
||||
height: calc(100vh - var(--header-height)) !important;
|
||||
}
|
||||
|
||||
.pluginsContainer {
|
||||
width: 30vw;
|
||||
}
|
||||
|
||||
.header {
|
||||
a {
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--primary-7);
|
||||
}
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.drawer {
|
||||
height: calc(100% - 40px);
|
||||
}
|
9
web/src/pages/AddUpdatePipeline/AddUpdatePipeline.module.scss.d.ts
vendored
Normal file
9
web/src/pages/AddUpdatePipeline/AddUpdatePipeline.module.scss.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const breadcrumb: string
|
||||
export declare const drawer: string
|
||||
export declare const editorContainer: string
|
||||
export declare const header: string
|
||||
export declare const layout: string
|
||||
export declare const main: string
|
||||
export declare const pluginsContainer: string
|
325
web/src/pages/AddUpdatePipeline/AddUpdatePipeline.tsx
Normal file
325
web/src/pages/AddUpdatePipeline/AddUpdatePipeline.tsx
Normal file
@ -0,0 +1,325 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useGet, useMutate } from 'restful-react'
|
||||
import { Link, useParams } from 'react-router-dom'
|
||||
import { get, isEmpty, isUndefined, set } from 'lodash'
|
||||
import { stringify } from 'yaml'
|
||||
import { Menu, PopoverPosition } from '@blueprintjs/core'
|
||||
import {
|
||||
Container,
|
||||
PageHeader,
|
||||
PageBody,
|
||||
Layout,
|
||||
ButtonVariation,
|
||||
Text,
|
||||
useToaster,
|
||||
SplitButton,
|
||||
Button
|
||||
} from '@harnessio/uicore'
|
||||
import { Icon } from '@harnessio/icons'
|
||||
import { Color, FontVariation } from '@harnessio/design-system'
|
||||
import type { OpenapiCommitFilesRequest, RepoCommitFilesResponse, RepoFileContent, TypesPipeline } from 'services/code'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
|
||||
import { useGetResourceContent } from 'hooks/useGetResourceContent'
|
||||
import MonacoSourceCodeEditor from 'components/SourceCodeEditor/MonacoSourceCodeEditor'
|
||||
import { PluginsPanel } from 'components/PluginsPanel/PluginsPanel'
|
||||
import useRunPipelineModal from 'components/RunPipelineModal/RunPipelineModal'
|
||||
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
|
||||
import { useAppContext } from 'AppContext'
|
||||
import type { CODEProps } from 'RouteDefinitions'
|
||||
import { getErrorMessage } from 'utils/Utils'
|
||||
import { decodeGitContent } from 'utils/GitUtils'
|
||||
import pipelineSchemaV1 from './schema/pipeline-schema-v1.json'
|
||||
import pipelineSchemaV0 from './schema/pipeline-schema-v0.json'
|
||||
import { YamlVersion } from './Constants'
|
||||
|
||||
import css from './AddUpdatePipeline.module.scss'
|
||||
|
||||
const StarterPipelineV1: Record<string, any> = {
|
||||
version: 1,
|
||||
stages: [
|
||||
{
|
||||
type: 'ci',
|
||||
spec: {
|
||||
steps: [
|
||||
{
|
||||
type: 'script',
|
||||
spec: {
|
||||
run: 'echo hello world'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const StarterPipelineV0: Record<string, any> = {
|
||||
kind: 'pipeline',
|
||||
type: 'docker',
|
||||
name: 'default',
|
||||
steps: [
|
||||
{
|
||||
name: 'test',
|
||||
image: 'alpine',
|
||||
commands: ['echo hello world']
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
enum PipelineSaveAndRunAction {
|
||||
SAVE,
|
||||
RUN,
|
||||
SAVE_AND_RUN
|
||||
}
|
||||
|
||||
interface PipelineSaveAndRunOption {
|
||||
title: string
|
||||
action: PipelineSaveAndRunAction
|
||||
}
|
||||
|
||||
const AddUpdatePipeline = (): JSX.Element => {
|
||||
const version = YamlVersion.V0
|
||||
const { routes } = useAppContext()
|
||||
const { getString } = useStrings()
|
||||
const { pipeline } = useParams<CODEProps>()
|
||||
const { repoMetadata } = useGetRepositoryMetadata()
|
||||
const { showError, showSuccess, clear: clearToaster } = useToaster()
|
||||
const [pipelineAsObj, setPipelineAsObj] = useState<Record<string, any>>(
|
||||
version === YamlVersion.V0 ? StarterPipelineV0 : StarterPipelineV1
|
||||
)
|
||||
const [pipelineAsYAML, setPipelineAsYaml] = useState<string>('')
|
||||
const { openModal: openRunPipelineModal } = useRunPipelineModal()
|
||||
const repoPath = useMemo(() => repoMetadata?.path || '', [repoMetadata])
|
||||
const [isExistingPipeline, setIsExistingPipeline] = useState<boolean>(false)
|
||||
const [isDirty, setIsDirty] = useState<boolean>(false)
|
||||
|
||||
const pipelineSaveOption: PipelineSaveAndRunOption = {
|
||||
title: getString('save'),
|
||||
action: PipelineSaveAndRunAction.SAVE
|
||||
}
|
||||
|
||||
const pipelineRunOption: PipelineSaveAndRunOption = {
|
||||
title: getString('run'),
|
||||
action: PipelineSaveAndRunAction.RUN
|
||||
}
|
||||
|
||||
const pipelineSaveAndRunOption: PipelineSaveAndRunOption = {
|
||||
title: getString('pipelines.saveAndRun'),
|
||||
action: PipelineSaveAndRunAction.SAVE_AND_RUN
|
||||
}
|
||||
|
||||
const pipelineSaveAndRunOptions: PipelineSaveAndRunOption[] = [pipelineSaveAndRunOption, pipelineSaveOption]
|
||||
|
||||
const [selectedOption, setSelectedOption] = useState<PipelineSaveAndRunOption>()
|
||||
|
||||
const { mutate, loading } = useMutate<RepoCommitFilesResponse>({
|
||||
verb: 'POST',
|
||||
path: `/api/v1/repos/${repoPath}/+/commits`
|
||||
})
|
||||
|
||||
// Fetch pipeline metadata to fetch pipeline YAML file content
|
||||
const { data: pipelineData, loading: fetchingPipeline } = useGet<TypesPipeline>({
|
||||
path: `/api/v1/repos/${repoPath}/+/pipelines/${pipeline}`,
|
||||
lazy: !repoMetadata
|
||||
})
|
||||
|
||||
const {
|
||||
data: pipelineYAMLFileContent,
|
||||
loading: fetchingPipelineYAMLFileContent,
|
||||
refetch: fetchPipelineYAMLFileContent
|
||||
} = useGetResourceContent({
|
||||
repoMetadata,
|
||||
gitRef: pipelineData?.default_branch || '',
|
||||
resourcePath: pipelineData?.config_path || ''
|
||||
})
|
||||
|
||||
const originalPipelineYAMLFileContent = useMemo(
|
||||
(): string => decodeGitContent((pipelineYAMLFileContent?.content as RepoFileContent)?.data),
|
||||
[pipelineYAMLFileContent?.content]
|
||||
)
|
||||
|
||||
// check if file already exists and has some content
|
||||
useEffect(() => {
|
||||
setIsExistingPipeline(!isEmpty(originalPipelineYAMLFileContent) && !isUndefined(originalPipelineYAMLFileContent))
|
||||
}, [originalPipelineYAMLFileContent])
|
||||
|
||||
// load initial content on the editor
|
||||
useEffect(() => {
|
||||
if (isExistingPipeline) {
|
||||
setPipelineAsYaml(originalPipelineYAMLFileContent)
|
||||
} else {
|
||||
// load with starter pipeline
|
||||
try {
|
||||
setPipelineAsYaml(stringify(pipelineAsObj))
|
||||
} catch (ex) {
|
||||
// ignore exception
|
||||
}
|
||||
}
|
||||
}, [isExistingPipeline, originalPipelineYAMLFileContent, pipelineAsObj])
|
||||
|
||||
// find if editor content was modified
|
||||
useEffect(() => {
|
||||
setIsDirty(originalPipelineYAMLFileContent !== pipelineAsYAML)
|
||||
}, [originalPipelineYAMLFileContent, pipelineAsYAML])
|
||||
|
||||
// set initial CTA title
|
||||
useEffect(() => {
|
||||
setSelectedOption(isDirty ? pipelineSaveAndRunOption : pipelineRunOption)
|
||||
}, [isDirty])
|
||||
|
||||
const handleSaveAndRun = (option: PipelineSaveAndRunOption): void => {
|
||||
if ([PipelineSaveAndRunAction.SAVE_AND_RUN, PipelineSaveAndRunAction.SAVE].includes(option?.action)) {
|
||||
try {
|
||||
const data: OpenapiCommitFilesRequest = {
|
||||
actions: [
|
||||
{
|
||||
action: isExistingPipeline ? 'UPDATE' : 'CREATE',
|
||||
path: pipelineData?.config_path,
|
||||
payload: pipelineAsYAML,
|
||||
sha: isExistingPipeline ? pipelineYAMLFileContent?.sha : ''
|
||||
}
|
||||
],
|
||||
branch: pipelineData?.default_branch || '',
|
||||
title: `${isExistingPipeline ? getString('updated') : getString('created')} pipeline ${pipeline}`,
|
||||
message: ''
|
||||
}
|
||||
|
||||
mutate(data)
|
||||
.then(() => {
|
||||
fetchPipelineYAMLFileContent()
|
||||
clearToaster()
|
||||
showSuccess(getString(isExistingPipeline ? 'pipelines.updated' : 'pipelines.created'))
|
||||
if (option?.action === PipelineSaveAndRunAction.SAVE_AND_RUN && repoMetadata && pipeline) {
|
||||
openRunPipelineModal({ repoMetadata, pipeline })
|
||||
}
|
||||
setSelectedOption(pipelineRunOption)
|
||||
})
|
||||
.catch(error => {
|
||||
showError(getErrorMessage(error), 0, 'pipelines.failedToSavePipeline')
|
||||
})
|
||||
} catch (exception) {
|
||||
showError(getErrorMessage(exception), 0, 'pipelines.failedToSavePipeline')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const updatePipeline = (payload: Record<string, any>): Record<string, any> => {
|
||||
const pipelineAsObjClone = { ...pipelineAsObj }
|
||||
const stepInsertPath = version === YamlVersion.V0 ? 'steps' : 'stages.0.spec.steps'
|
||||
let existingSteps: [unknown] = get(pipelineAsObjClone, stepInsertPath, [])
|
||||
if (existingSteps.length > 0) {
|
||||
existingSteps.push(payload)
|
||||
} else {
|
||||
existingSteps = [payload]
|
||||
}
|
||||
set(pipelineAsObjClone, stepInsertPath, existingSteps)
|
||||
return pipelineAsObjClone
|
||||
}
|
||||
|
||||
const addUpdatePluginToPipelineYAML = (_isUpdate: boolean, pluginFormData: Record<string, any>): void => {
|
||||
try {
|
||||
const updatedPipelineAsObj = updatePipeline(pluginFormData)
|
||||
setPipelineAsObj(updatedPipelineAsObj)
|
||||
setPipelineAsYaml(stringify(updatedPipelineAsObj))
|
||||
} catch (ex) {}
|
||||
}
|
||||
|
||||
const renderCTA = useCallback(() => {
|
||||
switch (selectedOption?.action) {
|
||||
case PipelineSaveAndRunAction.RUN:
|
||||
return (
|
||||
<Button
|
||||
variation={ButtonVariation.PRIMARY}
|
||||
text={getString('run')}
|
||||
onClick={() => {
|
||||
if (repoMetadata && pipeline) {
|
||||
openRunPipelineModal({ repoMetadata, pipeline })
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
case PipelineSaveAndRunAction.SAVE:
|
||||
case PipelineSaveAndRunAction.SAVE_AND_RUN:
|
||||
return isExistingPipeline ? (
|
||||
<Button
|
||||
variation={ButtonVariation.PRIMARY}
|
||||
text={getString('save')}
|
||||
onClick={() => {
|
||||
handleSaveAndRun(pipelineSaveOption)
|
||||
}}
|
||||
disabled={loading || !isDirty}
|
||||
/>
|
||||
) : (
|
||||
<SplitButton
|
||||
text={selectedOption?.title}
|
||||
disabled={loading || !isDirty}
|
||||
variation={ButtonVariation.PRIMARY}
|
||||
popoverProps={{
|
||||
interactionKind: 'click',
|
||||
usePortal: true,
|
||||
position: PopoverPosition.BOTTOM_RIGHT,
|
||||
transitionDuration: 1000
|
||||
}}
|
||||
intent="primary"
|
||||
onClick={() => handleSaveAndRun(selectedOption)}>
|
||||
{pipelineSaveAndRunOptions.map(option => {
|
||||
return (
|
||||
<Menu.Item
|
||||
key={option.title}
|
||||
text={
|
||||
<Text color={Color.BLACK} font={{ variation: FontVariation.SMALL_BOLD }}>
|
||||
{option.title}
|
||||
</Text>
|
||||
}
|
||||
onClick={() => {
|
||||
setSelectedOption(option)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</SplitButton>
|
||||
)
|
||||
default:
|
||||
return <></>
|
||||
}
|
||||
}, [loading, fetchingPipeline, isDirty, repoMetadata, pipeline, selectedOption, isExistingPipeline, pipelineAsYAML])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Container className={css.main}>
|
||||
<PageHeader
|
||||
title={getString('pipelines.editPipeline', { pipeline })}
|
||||
breadcrumbs={
|
||||
<Container className={css.header}>
|
||||
<Layout.Horizontal spacing="small" className={css.breadcrumb}>
|
||||
<Link to={routes.toCODEPipelines({ repoPath })}>{getString('pageTitle.pipelines')}</Link>
|
||||
<Icon name="main-chevron-right" size={8} color={Color.GREY_500} />
|
||||
<Text font={{ size: 'small' }}>{pipeline}</Text>
|
||||
</Layout.Horizontal>
|
||||
</Container>
|
||||
}
|
||||
content={<Layout.Horizontal flex={{ justifyContent: 'space-between' }}>{renderCTA()}</Layout.Horizontal>}
|
||||
/>
|
||||
<PageBody>
|
||||
<LoadingSpinner visible={fetchingPipeline || fetchingPipelineYAMLFileContent} />
|
||||
<Layout.Horizontal>
|
||||
<Container className={css.editorContainer}>
|
||||
<MonacoSourceCodeEditor
|
||||
language={'yaml'}
|
||||
schema={version === YamlVersion.V0 ? pipelineSchemaV0 : pipelineSchemaV1}
|
||||
source={pipelineAsYAML}
|
||||
onChange={(value: string) => setPipelineAsYaml(value)}
|
||||
/>
|
||||
</Container>
|
||||
<Container className={css.pluginsContainer}>
|
||||
<PluginsPanel onPluginAddUpdate={addUpdatePluginToPipelineYAML} />
|
||||
</Container>
|
||||
</Layout.Horizontal>
|
||||
</PageBody>
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default AddUpdatePipeline
|
7
web/src/pages/AddUpdatePipeline/Constants.ts
Normal file
7
web/src/pages/AddUpdatePipeline/Constants.ts
Normal file
@ -0,0 +1,7 @@
|
||||
export enum YamlVersion {
|
||||
V0,
|
||||
V1
|
||||
}
|
||||
|
||||
export const DEFAULT_YAML_PATH_PREFIX = '.harness/'
|
||||
export const DEFAULT_YAML_PATH_SUFFIX = '.yaml'
|
@ -0,0 +1,72 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": ["pipeline"]
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"trigger": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"event": {
|
||||
"type": "string"
|
||||
},
|
||||
"branch": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"path": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["event"]
|
||||
},
|
||||
"steps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"image": {
|
||||
"type": "string"
|
||||
},
|
||||
"commands": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"when": {
|
||||
"type": "string",
|
||||
"enum": ["on_success", "on_failure", "always"]
|
||||
},
|
||||
"depends_on": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"environment": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "commands"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["kind", "name", "steps"]
|
||||
}
|
2372
web/src/pages/AddUpdatePipeline/schema/pipeline-schema-v1.json
Normal file
2372
web/src/pages/AddUpdatePipeline/schema/pipeline-schema-v1.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,8 +0,0 @@
|
||||
.main {
|
||||
min-height: var(--page-height);
|
||||
background-color: var(--primary-bg) !important;
|
||||
|
||||
.layout {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
/* eslint-disable */
|
||||
// This is an auto-generated file
|
||||
export declare const layout: string
|
||||
export declare const main: string
|
@ -1,15 +0,0 @@
|
||||
import React from 'react'
|
||||
import { Container, PageHeader } from '@harnessio/uicore'
|
||||
import { useStrings } from 'framework/strings'
|
||||
import css from './NewPipeline.module.scss'
|
||||
|
||||
const NewPipeline = () => {
|
||||
const { getString } = useStrings()
|
||||
return (
|
||||
<Container className={css.main}>
|
||||
<PageHeader title={getString('pipelines.newPipelineButton')} />
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
export default NewPipeline
|
@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react'
|
||||
import { Classes, Menu, MenuItem, Popover, Position } from '@blueprintjs/core'
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
@ -33,6 +34,7 @@ import { RepositoryPageHeader } from 'components/RepositoryPageHeader/Repository
|
||||
import { ExecutionStatus, ExecutionState } from 'components/ExecutionStatus/ExecutionStatus'
|
||||
import { getStatus } from 'utils/PipelineUtils'
|
||||
import { PipeSeparator } from 'components/PipeSeparator/PipeSeparator'
|
||||
import useNewPipelineModal from 'components/NewPipelineModal/NewPipelineModal'
|
||||
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
|
||||
import usePipelineEventStream from 'hooks/usePipelineEventStream'
|
||||
import noPipelineImage from '../RepositoriesListing/no-repo.svg'
|
||||
@ -40,13 +42,13 @@ import css from './PipelineList.module.scss'
|
||||
|
||||
const PipelineList = () => {
|
||||
const { routes } = useAppContext()
|
||||
const space = useGetSpaceParam()
|
||||
const history = useHistory()
|
||||
const { getString } = useStrings()
|
||||
const [searchTerm, setSearchTerm] = useState<string | undefined>()
|
||||
const pageBrowser = useQueryParams<PageBrowserProps>()
|
||||
const pageInit = pageBrowser.page ? parseInt(pageBrowser.page) : 1
|
||||
const [page, setPage] = usePageIndex(pageInit)
|
||||
const space = useGetSpaceParam()
|
||||
|
||||
const { repoMetadata, error, loading, refetch } = useGetRepositoryMetadata()
|
||||
|
||||
@ -62,6 +64,7 @@ const PipelineList = () => {
|
||||
debounce: 500
|
||||
})
|
||||
|
||||
const { openModal } = useNewPipelineModal()
|
||||
//TODO - do not want to show load between refetchs - remove if/when we move to event stream method
|
||||
const [isInitialLoad, setIsInitialLoad] = useState(true)
|
||||
|
||||
@ -90,15 +93,17 @@ const PipelineList = () => {
|
||||
variation={ButtonVariation.PRIMARY}
|
||||
icon="plus"
|
||||
onClick={() => {
|
||||
history.push(routes.toCODEPipelinesNew({ space }))
|
||||
}}></Button>
|
||||
openModal({ repoMetadata })
|
||||
}}
|
||||
disabled={loading}
|
||||
/>
|
||||
)
|
||||
|
||||
const columns: Column<TypesPipeline>[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
Header: getString('pipelines.name'),
|
||||
width: 'calc(50% - 90px)',
|
||||
width: 'calc(100% - 210px)',
|
||||
Cell: ({ row }: CellProps<TypesPipeline>) => {
|
||||
const record = row.original
|
||||
return (
|
||||
@ -188,6 +193,46 @@ const PipelineList = () => {
|
||||
)
|
||||
},
|
||||
disableSortBy: true
|
||||
},
|
||||
{
|
||||
Header: ' ',
|
||||
width: '30px',
|
||||
Cell: ({ row }: CellProps<TypesPipeline>) => {
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const record = row.original
|
||||
const { uid } = record
|
||||
return (
|
||||
<Popover
|
||||
isOpen={menuOpen}
|
||||
onInteraction={nextOpenState => {
|
||||
setMenuOpen(nextOpenState)
|
||||
}}
|
||||
className={Classes.DARK}
|
||||
position={Position.BOTTOM_RIGHT}>
|
||||
<Button
|
||||
variation={ButtonVariation.ICON}
|
||||
icon="Options"
|
||||
data-testid={`menu-${record.uid}`}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
setMenuOpen(true)
|
||||
}}
|
||||
/>
|
||||
<Menu>
|
||||
<MenuItem
|
||||
icon="edit"
|
||||
text={getString('edit')}
|
||||
onClick={e => {
|
||||
e.stopPropagation()
|
||||
history.push(
|
||||
routes.toCODEPipelineEdit({ repoPath: repoMetadata?.path || '', pipeline: uid as string })
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Menu>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
}
|
||||
],
|
||||
[getString, repoMetadata?.path, routes, searchTerm]
|
||||
|
@ -88,6 +88,7 @@ export interface SourceCodeEditorProps {
|
||||
autoHeight?: boolean
|
||||
wordWrap?: boolean
|
||||
onChange?: (value: string) => void
|
||||
schema?: Record<string, any>
|
||||
}
|
||||
|
||||
// Monaco editor has a bug where when its value is set, the value
|
||||
|
@ -1387,6 +1387,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
|
||||
integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==
|
||||
|
||||
"@types/json-schema@^7.0.0":
|
||||
version "7.0.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
|
||||
integrity sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==
|
||||
|
||||
"@types/json5@^0.0.29":
|
||||
version "0.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
|
||||
@ -6809,6 +6814,11 @@ json5@^1.0.1, json5@^1.0.2:
|
||||
dependencies:
|
||||
minimist "^1.2.0"
|
||||
|
||||
jsonc-parser@^3.0.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76"
|
||||
integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==
|
||||
|
||||
jsonfile@^6.0.1:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae"
|
||||
@ -7718,7 +7728,7 @@ moment@^2.25.3:
|
||||
|
||||
monaco-editor-webpack-plugin@^7.1.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.npmjs.org/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-7.1.0.tgz#16f265c2b5dbb5fe08681b6b3b7d00d3c5b2ee97"
|
||||
resolved "https://registry.yarnpkg.com/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-7.1.0.tgz#16f265c2b5dbb5fe08681b6b3b7d00d3c5b2ee97"
|
||||
integrity sha512-ZjnGINHN963JQkFqjjcBtn1XBtUATDZBMgNQhDQwd78w2ukRhFXAPNgWuacaQiDZsUr4h1rWv5Mv6eriKuOSzA==
|
||||
dependencies:
|
||||
loader-utils "^2.0.2"
|
||||
@ -7728,6 +7738,32 @@ monaco-editor@^0.40.0:
|
||||
resolved "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.40.0.tgz#d10834e15ad50a15ec61fd01892e508464ebe2fe"
|
||||
integrity sha512-1wymccLEuFSMBvCk/jT1YDW/GuxMLYwnFwF9CDyYCxoTw2Pt379J3FUhwy9c43j51JdcxVPjwk0jm0EVDsBS2g==
|
||||
|
||||
monaco-marker-data-provider@^1.0.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/monaco-marker-data-provider/-/monaco-marker-data-provider-1.1.1.tgz#0ca69f367152f5aa12cec2bda95f32b7403e876f"
|
||||
integrity sha512-PGB7TJSZE5tmHzkxv/OEwK2RGNC2A7dcq4JRJnnj31CUAsfmw0Gl+1QTrH0W0deKhcQmQM0YVPaqgQ+0wCt8Mg==
|
||||
|
||||
monaco-worker-manager@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/monaco-worker-manager/-/monaco-worker-manager-2.0.1.tgz#f67c54dfca34ed4b225d5de84e77b24b4e36de8a"
|
||||
integrity sha512-kdPL0yvg5qjhKPNVjJoym331PY/5JC11aPJXtCZNwWRvBr6jhkIamvYAyiY5P1AWFmNOy0aRDRoMdZfa71h8kg==
|
||||
|
||||
monaco-yaml@^4.0.4:
|
||||
version "4.0.4"
|
||||
resolved "https://registry.yarnpkg.com/monaco-yaml/-/monaco-yaml-4.0.4.tgz#b05283a5539bf109d228982bd25772664651af96"
|
||||
integrity sha512-qbM36fY1twpDUs4lhhxoXDQGUPVyYAFCPJi3E0JKgLioD8wzsD/pawgauFFXSzpMa09z8wbt/DTLXjXEehnVFA==
|
||||
dependencies:
|
||||
"@types/json-schema" "^7.0.0"
|
||||
jsonc-parser "^3.0.0"
|
||||
monaco-marker-data-provider "^1.0.0"
|
||||
monaco-worker-manager "^2.0.0"
|
||||
path-browserify "^1.0.0"
|
||||
prettier "^2.0.0"
|
||||
vscode-languageserver-textdocument "^1.0.0"
|
||||
vscode-languageserver-types "^3.0.0"
|
||||
vscode-uri "^3.0.0"
|
||||
yaml "^2.0.0"
|
||||
|
||||
mri@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b"
|
||||
@ -8336,6 +8372,11 @@ pascalcase@^0.1.1:
|
||||
resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14"
|
||||
integrity sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==
|
||||
|
||||
path-browserify@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/path-browserify/-/path-browserify-1.0.1.tgz#d98454a9c3753d5790860f16f68867b9e46be1fd"
|
||||
integrity sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==
|
||||
|
||||
path-case@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz#9168645334eb942658375c56f80b4c0cb5f82c6f"
|
||||
@ -8589,6 +8630,11 @@ prepend-http@^1.0.1:
|
||||
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
|
||||
integrity sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==
|
||||
|
||||
prettier@^2.0.0:
|
||||
version "2.8.8"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da"
|
||||
integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==
|
||||
|
||||
prettier@^2.3.2, prettier@^2.6.2:
|
||||
version "2.8.7"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.8.7.tgz#bb79fc8729308549d28fe3a98fce73d2c0656450"
|
||||
@ -10979,6 +11025,21 @@ vfile@^5.0.0:
|
||||
unist-util-stringify-position "^3.0.0"
|
||||
vfile-message "^3.0.0"
|
||||
|
||||
vscode-languageserver-textdocument@^1.0.0:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz#9eae94509cbd945ea44bca8dcfe4bb0c15bb3ac0"
|
||||
integrity sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q==
|
||||
|
||||
vscode-languageserver-types@^3.0.0:
|
||||
version "3.17.3"
|
||||
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.17.3.tgz#72d05e47b73be93acb84d6e311b5786390f13f64"
|
||||
integrity sha512-SYU4z1dL0PyIMd4Vj8YOqFvHu7Hz/enbWtpfnVbJHU4Nd1YNYx8u0ennumc6h48GQNeOLxmwySmnADouT/AuZA==
|
||||
|
||||
vscode-uri@^3.0.0:
|
||||
version "3.0.7"
|
||||
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.7.tgz#6d19fef387ee6b46c479e5fb00870e15e58c1eb8"
|
||||
integrity sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==
|
||||
|
||||
w3c-hr-time@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd"
|
||||
@ -11417,6 +11478,11 @@ yaml@^1.10.0, yaml@^1.7.2, yaml@^1.8.3:
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b"
|
||||
integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==
|
||||
|
||||
yaml@^2.0.0:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.2.tgz#f522db4313c671a0ca963a75670f1c12ea909144"
|
||||
integrity sha512-N/lyzTPaJasoDmfV7YTrYCI0G/3ivm/9wdG0aHuheKowWQwGTsK0Eoiw6utmzAnI6pkJa0DUVygvp3spqqEKXg==
|
||||
|
||||
yargs-parser@20.x:
|
||||
version "20.2.9"
|
||||
resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee"
|
||||
|
Loading…
x
Reference in New Issue
Block a user