Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(web): pipeline event watchers #905

Merged
merged 7 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions web/crux-ui/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@
},

"pipelineStatuses": {
"unknown": "Unknown",
"queued": "Queued",
"running": "Running"
},
Expand Down
27 changes: 25 additions & 2 deletions web/crux-ui/locales/en/pipelines.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"new": "New pipeline",
"pipelinesName": "Pipelines - {{name}}",
"tips": "Pipelines are CI/CD jobs for your git repository.",
"noItems": "You haven't added a pipeline yet.",
"noPipelines": "You haven't added a pipeline yet.",
"repository": "Repository",
"repoBranchingIcon": "Code repository branching icon",
"trigger": "Trigger",
Expand All @@ -12,12 +12,35 @@
"pipelineName": "Pipeline name",
"areYouSureTriggerName": "Are you sure you want to trigger pipeline {{name}}?",
"triggerPipeline": "Trigger pipeline",
"noRuns": "You haven't triggered any runs yet.",
"noEventWatchers": "You haven't added an event watcher yet.",

"type": {
"gitlab": "Gitlab",
"github": "GitHub",
"azure": "Azure DevOps"
},

"finishedAt": "Finished at"
"triggeredBy": "Triggered by",
"finishedAt": "Finished at",
"runs": "Runs",

"newEventWatcher": "New event watcher",
"eventWatchers": "Event watchers",
"addEventWatcher": "Add event watcher",
"needV2Registry": "You will need a V2 registry to create an event watcher",

"triggerEvent": "Trigger event",

"triggerEvents": {
"image-push": "Image push",
"image-pull": "Image pull"
},

"templateTips": "Available templates: {{imageName}}, {{imageTag}}, {{label:<label-name>}}",
"areYouSureDeleteEventWatcher": "Are you sure you want to delete event watcher {{name}}?",

"filters": {
"imageNameStartsWith": "Image name starts with"
}
}
1 change: 1 addition & 0 deletions web/crux-ui/public/edit.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions web/crux-ui/quality-assurance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ export const QA_DIALOG_LABEL_DELETE_USER_TOKEN = 'deleteUserToken'
export const QA_DIALOG_LABEL_DELETE_USER = 'deleteUser'
export const QA_DIALOG_LABEL_REVOKE_REGISTRY_TOKEN = 'revokeRegistryToken'
export const QA_DIALOG_LABEL_TRIGGER_PIPELINE = 'triggerPipeline'
export const QA_DIALOG_LABEL_DELETE_PIPELINE_EVENT_WATCHER = 'deletePipelineEventWatcher'

export const QA_MODAL_LABEL_NODE_AUDIT_DETAILS = 'nodeAuditDetails'
export const QA_MODAL_LABEL_DEPLOYMENT_NOTE = 'deploymentNote'
Expand Down
195 changes: 195 additions & 0 deletions web/crux-ui/src/components/pipelines/edit-event-watcher-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import DyoButton from '@app/elements/dyo-button'
import { DyoCard } from '@app/elements/dyo-card'
import DyoChips, { chipsQALabelFromValue } from '@app/elements/dyo-chips'
import DyoForm from '@app/elements/dyo-form'
import { DyoHeading } from '@app/elements/dyo-heading'
import { DyoInput } from '@app/elements/dyo-input'
import { DyoLabel } from '@app/elements/dyo-label'
import DyoMessage from '@app/elements/dyo-message'
import { defaultApiErrorHandler } from '@app/errors'
import useDyoFormik from '@app/hooks/use-dyo-formik'
import useTeamRoutes from '@app/hooks/use-team-routes'
import {
CreatePipelineEventWatcher,
PIPELINE_TRIGGER_EVENT_VALUES,
PipelineDetails,
PipelineEventWatcher,
Registry,
UpdatePipelineEventWatcher,
} from '@app/models'
import { fetcher, sendForm } from '@app/utils'
import { upsertEventWatcherSchema } from '@app/validations'
import useTranslation from 'next-translate/useTranslation'
import { useEffect } from 'react'
import toast from 'react-hot-toast'
import useSWR from 'swr'
import { OFFLINE_EDITOR_STATE } from '../editor/use-item-editor-state'
import KeyValueInput from '../shared/key-value-input'

type EditEventWatcherCardProps = {
className?: string
pipeline: PipelineDetails
eventWatcher: PipelineEventWatcher
onEventWatcherEdited: (eventWatcher: PipelineEventWatcher) => void
onDiscard: VoidFunction
}

const EditEventWatcherCard = (props: EditEventWatcherCardProps) => {
const { className, pipeline, eventWatcher, onEventWatcherEdited, onDiscard } = props

const { t } = useTranslation('pipelines')

const routes = useTeamRoutes()

const handleApiError = defaultApiErrorHandler(t)

const editing = !!eventWatcher.id

const formik = useDyoFormik<CreatePipelineEventWatcher | UpdatePipelineEventWatcher>({
initialValues: {
...eventWatcher,
registryId: eventWatcher.registry?.id ?? null,
},
validationSchema: upsertEventWatcherSchema,
t,
onSubmit: async (values, { setFieldError }) => {
const body: CreatePipelineEventWatcher | UpdatePipelineEventWatcher = {
...values,
}

const res = !editing
? await sendForm('POST', routes.pipeline.api.eventWatchers(pipeline.id), body)
: await sendForm('PUT', routes.pipeline.api.eventWatcherDetails(pipeline.id, eventWatcher.id), body)

if (res.ok) {
const result =
res.status === 201
? ((await res.json()) as PipelineEventWatcher)
: {
...eventWatcher,
...values,
}

onEventWatcherEdited(result)
} else {
await handleApiError(res, setFieldError)
}
},
})

const { data: fetchedRegistries, error: fetchRegistriesError } = useSWR<Registry[]>(
routes.registry.api.list(),
fetcher,
)

const registries = fetchedRegistries?.filter(it => it.type === 'v2')

useEffect(() => {
if (registries && registries.length < 1) {
toast.error(t('needV2Registry'))
}
if (registries?.length === 1 && !formik.values.registryId) {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
formik.setFieldValue('registryId', registries[0].id)
}
}, [registries, t, formik])

return (
<DyoCard className={className}>
<div className="flex flex-row">
<DyoHeading element="h4" className="text-lg text-bright">
{editing ? t('common:editName', { name: eventWatcher.name }) : t('newEventWatcher')}
</DyoHeading>

<DyoButton outlined secondary className="ml-auto mr-2 px-10" onClick={onDiscard}>
{t('common:discard')}
</DyoButton>

<DyoButton outlined className="ml-2 px-10" onClick={() => formik.submitForm()}>
{editing ? t('common:save') : t('common:add')}
</DyoButton>
</div>

<DyoForm className="flex flex-col" onSubmit={formik.handleSubmit} onReset={formik.handleReset}>
<DyoInput
className="max-w-lg"
grow
name="name"
type="name"
required
label={t('common:name')}
onChange={formik.handleChange}
value={formik.values.name}
message={formik.errors.name}
/>

<DyoLabel textColor="mt-8 mb-2.5 text-light-eased">{t('common:event')}</DyoLabel>

<DyoChips
className="text-bright"
name="event"
choices={PIPELINE_TRIGGER_EVENT_VALUES}
selection={formik.values.event}
converter={it => t(`triggerEvents.${it}`)}
onSelectionChange={async (it): Promise<void> => {
await formik.setFieldValue('event', it, false)
}}
qaLabel={chipsQALabelFromValue}
/>

{fetchRegistriesError ? (
<DyoLabel>
{t('errors:fetchFailed', {
type: t('common:registries'),
})}
</DyoLabel>
) : !registries ? (
<DyoLabel>{t('common:loading')}</DyoLabel>
) : (
<div className="flex flex-col">
<DyoLabel className="mt-8 mb-2.5">{t('common:registries')}</DyoLabel>

<DyoChips
name="registries"
choices={registries ?? []}
converter={(it: Registry) => it.name}
selection={registries.find(it => it.id === formik.values.registryId)}
onSelectionChange={it => formik.setFieldValue('registryId', it.id)}
/>
{formik.errors.registryId && <DyoMessage message={formik.errors.registryId} messageType="error" />}
</div>
)}

<DyoInput
className="max-w-lg"
grow
name="filters.imageNameStartsWith"
type="text"
required
label={t('filters.imageNameStartsWith')}
onChange={formik.handleChange}
value={formik.values.filters.imageNameStartsWith}
message={formik.errors.filters?.imageNameStartsWith}
/>

<div className="mt-6">
<DyoLabel>{t('defaultInputs')}</DyoLabel>

<DyoMessage className="text-xs mt-2" message={t('templateTips')} messageType="info" />

<KeyValueInput
items={formik.values.triggerInputs}
onChange={async it => {
await formik.setFieldValue('triggerInputs', it)
}}
editorOptions={OFFLINE_EDITOR_STATE}
/>
</div>

<DyoButton className="hidden" type="submit" />
</DyoForm>
</DyoCard>
)
}

export default EditEventWatcherCard
6 changes: 3 additions & 3 deletions web/crux-ui/src/components/pipelines/edit-pipeline-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const DEFAULT_EDITABLE_PIPELINE: EditablePipeline = {
},
token: '',
audit: null,
runs: [],
eventWatchers: [],
}

type EditPipelineCardProps = Omit<DyoCardProps, 'children'> & {
Expand Down Expand Up @@ -239,8 +239,6 @@ const EditPipelineCard = (props: EditPipelineCardProps) => {
/>
</div>

<DyoButton className="hidden" type="submit" />

<div className="col-span-2">
<DyoLabel>{t('defaultInputs')}</DyoLabel>

Expand All @@ -252,6 +250,8 @@ const EditPipelineCard = (props: EditPipelineCardProps) => {
editorOptions={OFFLINE_EDITOR_STATE}
/>
</div>

<DyoButton className="hidden" type="submit" />
</DyoForm>
</DyoCard>
)
Expand Down
5 changes: 0 additions & 5 deletions web/crux-ui/src/components/pipelines/pipeline-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,12 @@ type PipelineCardProps = Omit<DyoCardProps, 'children'> & {
const statusOf = (
pipeline: (Pipeline | PipelineDetails) & {
lastRun?: PipelineRun
runs?: PipelineRun[]
},
): PipelineRunStatus => {
if (pipeline.lastRun) {
return pipeline.lastRun.status
}

if (pipeline.runs && pipeline.runs.length > 0) {
return pipeline.runs[0].status
}

return 'unknown'
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { DyoCard } from '@app/elements/dyo-card'
import DyoImgButton from '@app/elements/dyo-img-button'
import DyoTable, { DyoColumn } from '@app/elements/dyo-table'
import { PipelineEventWatcher } from '@app/models'
import { utcDateToLocale } from '@app/utils'
import useTranslation from 'next-translate/useTranslation'

type PipelineEventWatcherListProps = {
eventWatchers: PipelineEventWatcher[]
onEditEventWatcher: (eventWatcher: PipelineEventWatcher) => void
onDeleteEventWatcher: (eventWatcher: PipelineEventWatcher) => Promise<void>
}

const PipelineEventWatcherList = (props: PipelineEventWatcherListProps) => {
const { eventWatchers, onEditEventWatcher, onDeleteEventWatcher } = props

const { t } = useTranslation('pipelines')

return eventWatchers.length < 1 ? (
<span className="text-bright m-auto">{t('noEventWatchers')}</span>
) : (
<DyoCard className="mt-4">
<DyoTable data={eventWatchers} dataKey="id">
<DyoColumn header={t('common:name')} className="text-center" body={(it: PipelineEventWatcher) => it.name} />

<DyoColumn
header={t('common:event')}
className="w-2/12"
suppressHydrationWarning
body={(it: PipelineEventWatcher) => t(`triggerEvents.${it.event}`)}
/>

<DyoColumn
header={t('common:registry')}
className="w-2/12"
suppressHydrationWarning
body={(it: PipelineEventWatcher) => it.registry?.name}
/>

<DyoColumn
header={t('common:createdAt')}
className="w-2/12"
suppressHydrationWarning
body={(it: PipelineEventWatcher) => utcDateToLocale(it.createdAt)}
/>

<DyoColumn
header={t('common:actions')}
className="w-40 text-center"
bodyClassName="flex flex-row justify-center"
preventClickThrough
body={(it: PipelineEventWatcher) => (
<>
<DyoImgButton src="/edit.svg" alt={t('common:edit')} height={24} onClick={() => onEditEventWatcher(it)} />

<DyoImgButton
src="/trash-can.svg"
alt={t('common:delete')}
height={24}
onClick={async () => await onDeleteEventWatcher(it)}
/>
</>
)}
/>
</DyoTable>
</DyoCard>
)
}

export default PipelineEventWatcherList
Loading
Loading