feat: [code-2337] add support for webhook execution logs (#2681)

* fix: [code-2337] address comments
* fix: [code-2337] increase drawer width to 50%
* fix: [code-2337] editor to scroll beyond last line
* fix: [code-2337] event title
* fix: [code-2337] remove commented code
* feat: [code-2337] add support for webhook execution logs
CODE-2402
Ritik Kapoor 2024-09-13 06:59:06 +00:00 committed by Harness
parent bdc01e85f7
commit 55c6cacbde
18 changed files with 997 additions and 61 deletions

View File

@ -108,9 +108,9 @@ const BranchProtectionForm = (props: {
const { data: statuses } = useGet<string[]>({
path: `/api/v1/repos/${repoMetadata?.path}/+/checks/recent`,
queryParams: {
query: searchStatusTerm,
debounce: 500
}
query: searchStatusTerm
},
debounce: 500
})
const statusOptions: SelectOption[] = useMemo(
() =>

View File

@ -41,6 +41,7 @@ export interface StringsMap {
ascending: string
assignPeople: string
at: string
atSubTitle: string
attachText: string
basedOn: string
behindDivergence: string
@ -358,6 +359,9 @@ export interface StringsMap {
enterUser: string
error: string
error404Text: string
event: string
executionHistory: string
executionId: string
'executions.canceledTime': string
'executions.completedTime': string
'executions.description': string
@ -565,6 +569,7 @@ export interface StringsMap {
'labels.updateLabel': string
'labels.updated': string
language: string
lastTriggeredAt: string
leaveAComment: string
license: string
lineBreaks: string
@ -626,6 +631,8 @@ export interface StringsMap {
noCommits: string
noCommitsMessage: string
noCommitsPR: string
noExecutionsFound: string
noExecutionsFoundForWebhook: string
noExpiration: string
noExpirationDate: string
noFilterResultMessage: string
@ -637,6 +644,7 @@ export interface StringsMap {
noWebHooks: string
none: string
noneYet: string
notRetriggerableMessage: string
off: string
ok: string
on: string
@ -855,6 +863,7 @@ export interface StringsMap {
pullRequestalreadyExists: string
pullRequests: string
quote: string
reTriggeredExecution: string
reactivate: string
readMe: string
reader: string
@ -903,6 +912,7 @@ export interface StringsMap {
repositoryName: string
reqChanges: string
requestChanges: string
requestPayload: string
required: string
resetZoom: string
resolve: string
@ -911,6 +921,7 @@ export interface StringsMap {
resolvedComments: string
restoreBranch: string
results: string
retriggerExecution: string
reviewProjectSettings: string
reviewerNotFound: string
reviewers: string
@ -973,6 +984,7 @@ export interface StringsMap {
selectToViewMore: string
selectUsers: string
'semanticSearch.sampleQueries': string
serverResponse: string
setAsAdmin: string
setting: string
settings: string
@ -1048,6 +1060,7 @@ export interface StringsMap {
token: string
tooltipRepoEdit: string
top: string
triggeredEvent: string
'triggers.actions': string
'triggers.createSuccess': string
'triggers.createTrigger': string
@ -1142,9 +1155,11 @@ export interface StringsMap {
webhookPRMerged: string
webhookPRReopened: string
webhookPRUpdated: string
webhookPage: string
webhookSelectAllEvents: string
webhookSelectIndividualEvents: string
webhookSelectPushEvents: string
webhookTabs: string
webhookTagCreated: string
webhookTagDeleted: string
webhookTagUpdated: string

View File

@ -420,7 +420,22 @@ webhookPRClosed: PR closed
webhookPRCommentCreated: PR comment created
webhookPRMerged: PR merged
nameYourWebhook: Name your webhook
noExecutionsFound: No Executions found
noExecutionsFoundForWebhook: No executions found for the given webhook
executionHistory: Execution History
serverResponse: Server Response
requestPayload: Request Payload
triggeredEvent: Triggered Event
event: Event
lastTriggeredAt: Last Triggered At
executionId: Execution ID
submitReview: Submit Review
webhookPage: Webhook page
webhookTabs: WebhookTabs
reTriggeredExecution: Re-triggered Execution
retriggerExecution: Re-trigger Execution
atSubTitle: At
notRetriggerableMessage: This webhook execution cannot be re-triggered
approve: Approve
requestChanges: Changes Requested
repoEmptyMarkdown: |

View File

@ -0,0 +1,102 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.main {
min-height: calc(var(--page-height) - 160px);
background-color: var(--primary-bg) !important;
width: 100%;
margin: var(--spacing-small);
:global {
.bp3-tab {
width: fit-content !important;
height: 34px;
}
.bp3-tab-panel {
width: 100%;
}
.bp3-tab {
margin-top: 20px;
margin-bottom: unset !important;
}
.bp3-tab-list .bp3-tab[aria-selected='true'] {
background-color: var(--grey-0);
-webkit-box-shadow: none;
box-shadow: none;
border-bottom: 2px solid var(--primary-7);
border-bottom-left-radius: 0px !important;
border-bottom-right-radius: 0px !important;
}
}
}
.tabsContainer {
flex-grow: 1;
display: flex;
background-color: var(--primary-bg) !important;
> div {
flex-grow: 1;
display: flex;
flex-direction: column;
}
> div > div[role='tablist'] {
background-color: var(--white) !important;
padding-left: var(--spacing-large) !important;
padding-right: var(--spacing-xlarge) !important;
border-bottom: 1px solid var(--grey-200) !important;
}
> div > div[role='tabpanel'] {
margin-top: 0;
flex-grow: 1;
display: flex;
flex-direction: column;
}
[aria-selected='true'] {
.tabTitle,
.tabTitle:hover {
color: var(--grey-900) !important;
font-weight: 600 !important;
}
}
.tabTitle {
font-weight: 500;
color: var(--grey-700);
display: flex;
align-items: center;
height: 24px;
margin-top: var(--spacing-8);
> svg {
display: inline-block;
margin-right: 5px;
}
}
.tabTitle:not:first-child {
margin-left: var(--spacing-8) !important;
}
}
.headerContainer {
border-bottom: unset !important;
}

View File

@ -0,0 +1,22 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const headerContainer: string
export declare const main: string
export declare const tabsContainer: string
export declare const tabTitle: string

View File

@ -14,52 +14,75 @@
* limitations under the License.
*/
import React from 'react'
import { Container, PageBody } from '@harnessio/uicore'
import { useGet } from 'restful-react'
import React, { useEffect } from 'react'
import cx from 'classnames'
import { PageBody, Container, Tabs } from '@harnessio/uicore'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import type { OpenapiWebhookType } from 'services/code'
import { useStrings } from 'framework/strings'
import { RepositoryPageHeader } from 'components/RepositoryPageHeader/RepositoryPageHeader'
import { WehookForm } from 'pages/WebhookNew/WehookForm'
import { useAppContext } from 'AppContext'
import { PageBrowserProps, getErrorMessage, voidFn } from 'utils/Utils'
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
import { WebhookTabs } from 'utils/GitUtils'
import WebhookDetailsTab from 'pages/WebhookDetailsTab/WebhookDetailsTab'
import WebhookExecutions from 'pages/WebhookExecutions/WebhookExecutions'
import { useUpdateQueryParams } from 'hooks/useUpdateQueryParams'
import { useQueryParams } from 'hooks/useQueryParams'
import css from './Webhook.module.scss'
export default function WebhookDetails() {
const { repoMetadata, error, loading, refetch, webhookId } = useGetRepositoryMetadata()
const queryParams = useQueryParams<PageBrowserProps>()
const { replaceQueryParams } = useUpdateQueryParams()
const { getString } = useStrings()
const { routes } = useAppContext()
const { repoMetadata, error, loading, webhookId, refetch: refreshMetadata } = useGetRepositoryMetadata()
const {
data,
loading: webhookLoading,
error: webhookError,
refetch: refetchWebhook
} = useGet<OpenapiWebhookType>({
path: `/api/v1/repos/${repoMetadata?.path}/+/webhooks/${webhookId}`,
lazy: !repoMetadata
useEffect(() => {
if (!queryParams.tab) {
replaceQueryParams({ ...queryParams, tab: WebhookTabs.DETAILS })
}
})
const tabListArray = [
{
id: WebhookTabs.DETAILS,
title: getString('details'),
panel: (
<Container padding={'large'}>
<WebhookDetailsTab />
</Container>
)
},
{
id: WebhookTabs.EXECUTIONS,
title: getString('pageTitle.executions'),
panel: <WebhookExecutions />
}
]
return (
<Container>
<Container className={css.main}>
<RepositoryPageHeader
className={css.headerContainer}
repoMetadata={repoMetadata}
title={getString('webhookDetails')}
dataTooltipId="webhookDetails"
extraBreadcrumbLinks={
repoMetadata && [
{
label: getString('webhooks'),
url: routes.toCODEWebhooks({ repoPath: repoMetadata.path as string })
}
]
}
title={`${getString('webhook')} : ${webhookId}`}
dataTooltipId={getString('webhookPage')}
/>
<PageBody
error={error || webhookError}
retryOnError={() => (repoMetadata ? refetchWebhook() : refreshMetadata())}>
<LoadingSpinner visible={loading || webhookLoading} withBorder={!!data && webhookLoading} />
{repoMetadata && data && <WehookForm isEdit webhook={data} repoMetadata={repoMetadata} />}
<PageBody error={getErrorMessage(error)} retryOnError={voidFn(refetch)}>
<LoadingSpinner visible={loading} />
{repoMetadata && (
<Container className={cx(css.main, css.tabsContainer)}>
<Tabs
id={getString('webhookTabs')}
large={false}
selectedTabId={queryParams.tab}
animate={false}
onChange={(id: WebhookTabs) => {
if (id === WebhookTabs.DETAILS) {
delete queryParams.page
}
replaceQueryParams({ ...queryParams, tab: id })
}}
tabList={tabListArray}></Tabs>
</Container>
)}
</PageBody>
</Container>
)

View File

@ -0,0 +1,47 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react'
import { Container, PageBody } from '@harnessio/uicore'
import { useGet } from 'restful-react'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import type { OpenapiWebhookType } from 'services/code'
import { WehookForm } from 'pages/WebhookNew/WehookForm'
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
export default function WebhookDetails() {
const { repoMetadata, error, loading, webhookId, refetch: refreshMetadata } = useGetRepositoryMetadata()
const {
data,
loading: webhookLoading,
error: webhookError,
refetch: refetchWebhook
} = useGet<OpenapiWebhookType>({
path: `/api/v1/repos/${repoMetadata?.path}/+/webhooks/${webhookId}`,
lazy: !repoMetadata
})
return (
<Container>
<PageBody
error={error || webhookError}
retryOnError={() => (repoMetadata ? refetchWebhook() : refreshMetadata())}>
<LoadingSpinner visible={loading || webhookLoading} withBorder={!!data && webhookLoading} />
{repoMetadata && data && <WehookForm isEdit webhook={data} repoMetadata={repoMetadata} />}
</PageBody>
</Container>
)
}

View File

@ -0,0 +1,130 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.main {
min-height: calc(var(--page-height) - 160px);
width: 100%;
:global {
.bp3-tab {
width: fit-content !important;
height: 34px;
}
.bp3-tab-panel {
width: 100%;
}
.bp3-tab {
margin-top: 20px;
margin-bottom: unset !important;
}
.bp3-tab-list .bp3-tab[aria-selected='true'] {
background-color: var(--grey-0);
-webkit-box-shadow: none;
box-shadow: none;
border-bottom: 2px solid var(--primary-7);
border-bottom-left-radius: 0px !important;
border-bottom-right-radius: 0px !important;
}
}
}
.tabsContainer {
flex-grow: 1;
display: flex;
> div {
flex-grow: 1;
display: flex;
flex-direction: column;
}
> div > div[role='tablist'] {
background-color: var(--white) !important;
padding-left: var(--spacing-large) !important;
padding-right: var(--spacing-xlarge) !important;
border-bottom: 1px solid var(--grey-200) !important;
}
> div > div[role='tabpanel'] {
margin-top: 0;
flex-grow: 1;
display: flex;
flex-direction: column;
}
[aria-selected='true'] {
.tabTitle,
.tabTitle:hover {
color: var(--grey-900) !important;
font-weight: 600 !important;
}
}
.tabTitle {
font-weight: 500;
color: var(--grey-700);
display: flex;
align-items: center;
height: 24px;
margin-top: var(--spacing-8);
> svg {
display: inline-block;
margin-right: 5px;
}
}
.tabTitle:not:first-child {
margin-left: var(--spacing-8) !important;
}
}
.pageBody {
min-height: 100% !important;
}
.executionContext {
padding: 2rem 1.8rem 1rem 1.8rem !important;
gap: 0.5rem;
}
.errorMessage {
display: flex;
align-items: flex-end;
}
.editor {
height: 100% !important;
:global(.bp3-drawer-header) {
margin: 0 !important;
padding-top: var(--spacing-5) !important;
padding-bottom: var(--spacing-5) !important;
}
}
.logsContainer {
position: relative;
}
.copyButton {
position: absolute;
cursor: pointer;
top: 20px;
right: 20px;
z-index: 34;
}

View File

@ -0,0 +1,27 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const copyButton: string
export declare const editor: string
export declare const errorMessage: string
export declare const executionContext: string
export declare const logsContainer: string
export declare const main: string
export declare const pageBody: string
export declare const tabsContainer: string
export declare const tabTitle: string

View File

@ -0,0 +1,234 @@
/*
* Copyright 2024 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useMemo, useState } from 'react'
import { Drawer, Position } from '@blueprintjs/core'
import cx from 'classnames'
import {
Button,
ButtonVariation,
Container,
Layout,
PageBody,
PageHeader,
Tabs,
Text,
useToaster
} from '@harnessio/uicore'
import { Color, FontVariation } from '@harnessio/design-system'
import MonacoEditor from 'react-monaco-editor'
import { defaultTo, isEmpty } from 'lodash-es'
import { useMutate } from 'restful-react'
import moment from 'moment'
import { Render } from 'react-jsx-match'
import { useStrings } from 'framework/strings'
import { CopyButton } from 'components/CopyButton/CopyButton'
import { getErrorMessage } from 'utils/Utils'
import { getConfig } from 'services/config'
import { DateTimeWithLocalContentInline } from 'utils/timePopoverLocal/TimePopoverWithLocal'
import { CodeIcon, ExecutionTabs, WebhookIndividualEvent, getEventDescription } from 'utils/GitUtils'
import type { RepoRepositoryOutput, TypesWebhookExecution, TypesWebhookExecutionResponse } from 'services/code'
import { useModalHook } from 'hooks/useModalHook'
import css from './useWebhookLogDrawer.module.scss'
interface LogViewerProps {
data?: string | TypesWebhookExecutionResponse
}
export function useWeebhookLogDrawer(refetchExecutionList: () => Promise<void>) {
const [executionData, setExecutionData] = useState<TypesWebhookExecution>()
const [activeTab, setActiveTab] = useState<string>(ExecutionTabs.PAYLOAD)
const [path, setPath] = useState('')
const { mutate: retriggerExection } = useMutate({
verb: 'POST',
base: getConfig('code/api/v1'),
path
})
const { getString } = useStrings()
const LogViewer = (props: LogViewerProps) => {
const { data } = props
return (
<Container padding={'medium'} className={css.logsContainer}>
<CopyButton
content={JSON.stringify(data, null, 2)}
className={css.copyButton}
icon={CodeIcon.Copy}
color={Color.PRIMARY_7}
iconProps={{ size: 20 }}
/>
<MonacoEditor
className={css.editor}
height={'100vh'}
language="json"
value={JSON.stringify(data, null, 2)}
data-testid="monaco-editor"
theme="vs-dark"
options={{
fontFamily: "'Roboto Mono', monospace",
fontSize: 13,
scrollBeyondLastLine: true,
minimap: {
enabled: false
},
unicodeHighlight: {
ambiguousCharacters: false
},
lineNumbers: 'on',
glyphMargin: true,
folding: false,
lineDecorationsWidth: 60,
wordWrap: 'on',
scrollbar: {
verticalScrollbarSize: 0
},
renderLineHighlight: 'none',
wordWrapBreakBeforeCharacters: '',
lineNumbersMinChars: 0,
wordBasedSuggestions: 'off',
readOnly: true
}}
/>
</Container>
)
}
const tabListArray = useMemo(
() => [
{
id: ExecutionTabs.PAYLOAD,
title: ExecutionTabs.PAYLOAD,
panel: <LogViewer data={JSON.parse(executionData?.request?.body ?? '{}')} />
},
{
id: ExecutionTabs.SERVER_RESPONSE,
title: ExecutionTabs.SERVER_RESPONSE,
panel: <LogViewer data={executionData?.response} />
}
],
[activeTab, executionData]
)
const { showSuccess, showError } = useToaster()
const [openModal, hideModal] = useModalHook(
() => (
<Drawer position={Position.RIGHT} isOpen={true} isCloseButtonShown={true} size={'50%'} onClose={hideModal}>
<PageHeader
title={
<Text icon={'execution'} iconProps={{ size: 24 }} font={{ variation: FontVariation.H4 }}>
{executionData?.id}
</Text>
}
content={
<Button
disabled={!executionData?.retriggerable}
tooltipProps={{
disabled: executionData?.retriggerable,
position: Position.TOP,
interactionKind: 'hover'
}}
tooltip={getString('notRetriggerableMessage')}
variation={ButtonVariation.SECONDARY}
onClick={() =>
retriggerExection({})
.then(() => {
showSuccess(getString('reTriggeredExecution'))
refetchExecutionList()
})
.catch(err => {
showError(getErrorMessage(err))
})
}>
{getString('retriggerExecution')}
</Button>
}
/>
<PageBody className={cx(css.pageBody)}>
<Layout.Vertical className={css.executionContext}>
<Layout.Horizontal spacing={'small'} flex={{ alignItems: 'center', justifyContent: 'flex-start' }}>
<Text font={{ variation: FontVariation.H6 }}>{getString('triggeredEvent')}: </Text>
<Text color={Color.GREY_600}>
{getEventDescription(executionData?.trigger_type as WebhookIndividualEvent)}
</Text>
</Layout.Horizontal>
<Layout.Horizontal spacing={'small'} flex={{ alignItems: 'center', justifyContent: 'flex-start' }}>
<Text font={{ variation: FontVariation.H6 }}>{getString('atSubTitle')}: </Text>
<Container>
<DateTimeWithLocalContentInline time={defaultTo(executionData?.created as number, 0)} />
</Container>
</Layout.Horizontal>
<Layout.Horizontal spacing={'small'}>
<Text font={{ variation: FontVariation.H6 }}>Duration:</Text>
<Text color={Color.GREY_600}>
{Math.ceil(
moment
.duration((executionData?.duration ? executionData?.duration / 1_000_000 : 0) as number)
.asSeconds()
)}
{'s'}
</Text>
</Layout.Horizontal>
</Layout.Vertical>
<Render when={!isEmpty(executionData?.error)}>
<Container
intent="danger"
background="red100"
border={{
color: 'red500'
}}
margin={{ top: 'small', right: 'medium', left: 'medium' }}>
<Text
className={css.errorMessage}
icon="error-outline"
iconProps={{ size: 16, margin: { right: 'small' } }}
padding={{ left: 'large', right: 'large', top: 'small', bottom: 'small' }}
color={Color.ERROR}>
{executionData?.error}
</Text>
</Container>
</Render>
<Container className={cx(css.main, css.tabsContainer)}>
<Tabs
id="WebhookExecutionLogs"
large={false}
defaultSelectedTabId={activeTab}
animate={false}
onChange={(id: string) => {
setActiveTab(id)
}}
tabList={tabListArray}></Tabs>
</Container>
</PageBody>
</Drawer>
),
[executionData, activeTab, path]
)
const openExecutionLogs = (
webhookExecution: TypesWebhookExecution,
logTab: ExecutionTabs,
repoMetadata?: RepoRepositoryOutput
) => {
setExecutionData(webhookExecution)
setActiveTab(logTab)
setPath(
`/repos/${repoMetadata?.path}/+/webhooks/${webhookExecution.webhook_id}/executions/${webhookExecution.id}/retrigger`
)
openModal()
}
return { openModal, hideModal, openExecutionLogs }
}

View File

@ -0,0 +1,31 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
.main {
.table {
.row {
height: fit-content !important;
display: flex;
justify-content: center;
}
div[class*='TableV2--row'] {
padding: 3px var(--spacing-medium);
justify-content: center;
min-height: 48px;
}
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable */
// This is an auto-generated file
export declare const main: string
export declare const row: string
export declare const table: string

View File

@ -0,0 +1,208 @@
/*
* Copyright 2023 Harness, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useEffect, useMemo } from 'react'
import { Container, TableV2, Text, Button, ButtonVariation, useToaster } from '@harnessio/uicore'
import type { CellProps, Column } from 'react-table'
import { useGet } from 'restful-react'
import { Color, FontVariation } from '@harnessio/design-system'
import { useHistory } from 'react-router-dom'
import { defaultTo, isEmpty } from 'lodash-es'
import { useQueryParams } from 'hooks/useQueryParams'
import { usePageIndex } from 'hooks/usePageIndex'
import { LIST_FETCHING_LIMIT, type PageBrowserProps } from 'utils/Utils'
import { ExecutionTabs, WebhookIndividualEvent, getEventDescription } from 'utils/GitUtils'
import { ResourceListingPagination } from 'components/ResourceListingPagination/ResourceListingPagination'
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
import { useStrings } from 'framework/strings'
import { useUpdateQueryParams } from 'hooks/useUpdateQueryParams'
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
import { getConfig } from 'services/config'
import { useGetRepositoryMetadata } from 'hooks/useGetRepositoryMetadata'
import type { TypesWebhookExecution } from 'services/code'
import { TimePopoverWithLocal } from 'utils/timePopoverLocal/TimePopoverWithLocal'
import { useWeebhookLogDrawer } from 'pages/WebhookExecutions/WebhookExecutionLogs/useWeebhookLogDrawer'
import { ExecutionStatusLabel } from 'components/ExecutionStatusLabel/ExecutionStatusLabel'
import css from './WebhookExecutions.module.scss'
const WebhookExecutions = () => {
const { repoMetadata, webhookId } = useGetRepositoryMetadata()
const { getString } = useStrings()
const { showError, showSuccess } = useToaster()
const history = useHistory()
const pageBrowser = useQueryParams<PageBrowserProps>()
const { updateQueryParams, replaceQueryParams } = useUpdateQueryParams()
const pageInit = pageBrowser.page ? parseInt(pageBrowser.page) : 1
const [page, setPage] = usePageIndex(pageInit)
useEffect(() => {
const params = {
...pageBrowser,
...(page > 1 && { page: page.toString() })
}
updateQueryParams(params, undefined, true)
if (page <= 1) {
const updateParams = { ...params }
delete updateParams.page
replaceQueryParams(updateParams, undefined, true)
}
}, [page]) // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (parseInt(pageBrowser.page ?? '1') !== page) {
setPage(parseInt(pageBrowser.page ?? '1'))
}
}, [pageBrowser])
const {
data: executionList,
loading: executionListLoading,
refetch: refetchExecutionList,
response
} = useGet<TypesWebhookExecution[]>({
base: getConfig('code/api/v1'),
path: `/repos/${repoMetadata?.path}/+/webhooks/${webhookId}/executions`,
queryParams: {
limit: LIST_FETCHING_LIMIT,
page: page
}
})
const { openExecutionLogs } = useWeebhookLogDrawer(refetchExecutionList)
const columns: Column<TypesWebhookExecution>[] = useMemo(
() => [
{
Header: getString('executionId'),
id: getString('executionId'),
sort: 'true',
width: '16.66%',
Cell: ({ row }: CellProps<TypesWebhookExecution>) => {
return (
<Text color={Color.GREY_900} icon={'execution'}>
{row.original.id}
</Text>
)
}
},
{
Header: getString('lastTriggeredAt'),
id: getString('lastTriggeredAt'),
sort: 'true',
width: '16.66%',
Cell: ({ row }: CellProps<TypesWebhookExecution>) => {
return (
<Text>
<TimePopoverWithLocal
time={defaultTo(row.original.created as number, 0)}
inline={false}
font={{ variation: FontVariation.BODY2_SEMI }}
color={Color.GREY_400}
/>
</Text>
)
}
},
{
Header: getString('event'),
id: getString('event'),
sort: 'true',
width: '16.66%',
Cell: ({ row }: CellProps<TypesWebhookExecution>) => {
return <Text>{getEventDescription(row.original.trigger_type as WebhookIndividualEvent)}</Text>
}
},
{
Header: getString('requestPayload'),
id: getString('requestPayload'),
sort: 'true',
width: '16.66%',
Cell: ({ row }: CellProps<TypesWebhookExecution>) => {
return (
<Button
variation={ButtonVariation.LINK}
font={{ variation: FontVariation.FORM_LABEL }}
text={'View'}
iconProps={{ size: 16 }}
icon={'file'}
onClick={() => openExecutionLogs(row.original, ExecutionTabs.PAYLOAD, repoMetadata)}
/>
)
}
},
{
Header: getString('serverResponse'),
id: getString('serverResponse'),
sort: 'true',
width: '16.66%',
Cell: ({ row }: CellProps<TypesWebhookExecution>) => {
return (
<Button
variation={ButtonVariation.LINK}
font={{ variation: FontVariation.FORM_LABEL }}
text={'View'}
iconProps={{ size: 18 }}
icon={'sto-dast'}
onClick={() => openExecutionLogs(row.original, ExecutionTabs.SERVER_RESPONSE, repoMetadata)}
/>
)
}
},
{
Header: getString('status'),
id: getString('status'),
sort: 'true',
width: '16.66%',
Cell: ({ row }: CellProps<TypesWebhookExecution>) => {
return (
<ExecutionStatusLabel
data={row.original.result === 'success' ? { state: 'success' } : { state: 'failed' }}
/>
)
}
}
], // eslint-disable-next-line react-hooks/exhaustive-deps
[history, getString, repoMetadata?.path, setPage, showError, showSuccess]
)
return (
<Container>
<Container className={css.main} padding={{ bottom: 'large', right: 'xlarge', left: 'xlarge' }}>
{executionList && !executionListLoading && executionList.length && (
<TableV2<TypesWebhookExecution>
className={css.table}
columns={columns}
data={executionList}
sortable
autoResetExpanded={true}
getRowClassName={() => css.row}
/>
)}
<LoadingSpinner visible={executionListLoading} />
<ResourceListingPagination response={response} page={page} setPage={setPage} />
</Container>
<NoResultCard
showWhen={() => !executionListLoading && isEmpty(executionList)}
forSearch={true}
title={getString('noExecutionsFound')}
emptySearchMessage={getString('noExecutionsFoundForWebhook')}
/>
</Container>
)
}
export default WebhookExecutions

View File

@ -35,33 +35,11 @@ import React from 'react'
import type { OpenapiUpdateWebhookRequest, EnumWebhookTrigger, OpenapiWebhookType } from 'services/code'
import { getErrorMessage, permissionProps } from 'utils/Utils'
import { useStrings } from 'framework/strings'
import type { GitInfoProps } from 'utils/GitUtils'
import { WebhookIndividualEvent, type GitInfoProps, WebhookEventType } from 'utils/GitUtils'
import { useAppContext } from 'AppContext'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import css from './WehookForm.module.scss'
enum WebhookEventType {
PUSH = 'push',
ALL = 'all',
INDIVIDUAL = 'individual'
}
enum WebhookIndividualEvent {
BRANCH_CREATED = 'branch_created',
BRANCH_UPDATED = 'branch_updated',
BRANCH_DELETED = 'branch_deleted',
TAG_CREATED = 'tag_created',
TAG_UPDATED = 'tag_updated',
TAG_DELETED = 'tag_deleted',
PR_CREATED = 'pullreq_created',
PR_UPDATED = 'pullreq_updated',
PR_REOPENED = 'pullreq_reopened',
PR_BRANCH_UPDATED = 'pullreq_branch_updated',
PR_CLOSED = 'pullreq_closed',
PR_COMMENT_CREATED = 'pullreq_comment_created',
PR_MERGED = 'pullreq_merged'
}
const SECRET_MASK = '********'
interface FormData {

View File

@ -50,7 +50,7 @@ import { ResourceListingPagination } from 'components/ResourceListingPagination/
import { LoadingSpinner } from 'components/LoadingSpinner/LoadingSpinner'
import { NoResultCard } from 'components/NoResultCard/NoResultCard'
import type { OpenapiWebhookType } from 'services/code'
import { formatTriggers } from 'utils/GitUtils'
import { WebhookTabs, formatTriggers } from 'utils/GitUtils'
import { useGetSpaceParam } from 'hooks/useGetSpaceParam'
import { WebhooksHeader } from './WebhooksHeader/WebhooksHeader'
import css from './Webhooks.module.scss'
@ -256,6 +256,20 @@ export default function Webhooks() {
}
})
}
},
{
hasIcon: true,
iconName: 'execution',
iconSize: 16,
text: getString('executionHistory'),
onClick: () => {
history.push(
`${routes.toCODEWebhookDetails({
repoPath: repoMetadata?.path as string,
webhookId: String(row.original?.identifier)
})}?tab=${WebhookTabs.EXECUTIONS}`
)
}
}
]}
/>

View File

@ -133,6 +133,16 @@ export enum SpaceSettingsTab {
labels = 'labels'
}
export enum WebhookTabs {
DETAILS = 'details',
EXECUTIONS = 'executions'
}
export enum ExecutionTabs {
PAYLOAD = 'Payload',
SERVER_RESPONSE = 'Server Response'
}
export enum VulnerabilityScanningType {
DETECT = 'detect',
BLOCK = 'block',
@ -579,3 +589,61 @@ export const dryMerge = (
})
}
}
export enum WebhookEventType {
PUSH = 'push',
ALL = 'all',
INDIVIDUAL = 'individual'
}
export enum WebhookIndividualEvent {
BRANCH_CREATED = 'branch_created',
BRANCH_UPDATED = 'branch_updated',
BRANCH_DELETED = 'branch_deleted',
TAG_CREATED = 'tag_created',
TAG_UPDATED = 'tag_updated',
TAG_DELETED = 'tag_deleted',
PR_CREATED = 'pullreq_created',
PR_UPDATED = 'pullreq_updated',
PR_REOPENED = 'pullreq_reopened',
PR_BRANCH_UPDATED = 'pullreq_branch_updated',
PR_CLOSED = 'pullreq_closed',
PR_COMMENT_CREATED = 'pullreq_comment_created',
PR_MERGED = 'pullreq_merged'
}
export enum WebhookEventMap {
BRANCH_CREATED = 'Branch created',
BRANCH_UPDATED = 'Branch updated',
BRANCH_DELETED = 'Branch deleted',
TAG_CREATED = 'Tag created',
TAG_UPDATED = 'Tag updated',
TAG_DELETED = 'Tag deleted',
PR_CREATED = 'PR created',
PR_UPDATED = 'PR updated',
PR_REOPENED = 'PR reopened',
PR_BRANCH_UPDATED = 'PR updated',
PR_CLOSED = 'PR closed',
PR_COMMENT_CREATED = 'PR created',
PR_MERGED = 'PR merged'
}
export const eventMapping: Record<WebhookIndividualEvent, WebhookEventMap> = {
[WebhookIndividualEvent.BRANCH_CREATED]: WebhookEventMap.BRANCH_CREATED,
[WebhookIndividualEvent.BRANCH_UPDATED]: WebhookEventMap.BRANCH_UPDATED,
[WebhookIndividualEvent.BRANCH_DELETED]: WebhookEventMap.BRANCH_DELETED,
[WebhookIndividualEvent.TAG_CREATED]: WebhookEventMap.TAG_CREATED,
[WebhookIndividualEvent.TAG_UPDATED]: WebhookEventMap.TAG_UPDATED,
[WebhookIndividualEvent.TAG_DELETED]: WebhookEventMap.TAG_DELETED,
[WebhookIndividualEvent.PR_CREATED]: WebhookEventMap.PR_CREATED,
[WebhookIndividualEvent.PR_UPDATED]: WebhookEventMap.PR_UPDATED,
[WebhookIndividualEvent.PR_REOPENED]: WebhookEventMap.PR_REOPENED,
[WebhookIndividualEvent.PR_BRANCH_UPDATED]: WebhookEventMap.PR_BRANCH_UPDATED,
[WebhookIndividualEvent.PR_CLOSED]: WebhookEventMap.PR_CLOSED,
[WebhookIndividualEvent.PR_COMMENT_CREATED]: WebhookEventMap.PR_COMMENT_CREATED,
[WebhookIndividualEvent.PR_MERGED]: WebhookEventMap.PR_MERGED
}
export function getEventDescription(event: WebhookIndividualEvent): string {
return eventMapping[event]
}

View File

@ -98,6 +98,7 @@ export const getErrorMessage = (error: Unknown): string | undefined =>
export interface PageBrowserProps {
page?: string
state?: string
tab?: string
}
export const extractInfoFromRuleViolationArr = (ruleViolationArr: TypesRuleViolations[]) => {

View File

@ -40,7 +40,7 @@ enum TimeZone {
export function DateTimeWithLocalContentInline({ time }: { time: number }): JSX.Element {
const { getString } = useStrings()
return (
<Layout.Vertical>
<Layout.Vertical margin={{ right: '4px' }}>
<Layout.Horizontal className={css.timeWrapper}>
<Text color={Color.GREY_600} className={css.time}>
{moment(time).format(DATE_PARSE_FORMAT)}